initial work, ported some basic logic out of aiocraft
This commit is contained in:
parent
d2fa232ee9
commit
15db4bc210
8 changed files with 257 additions and 0 deletions
2
requirements.txt
Normal file
2
requirements.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
apscheduler
|
||||||
|
aiocraft
|
27
setup.py
Normal file
27
setup.py
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
|
with open("requirements.txt") as f:
|
||||||
|
requirements = f.read().split("\n")
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name='treepuncher',
|
||||||
|
version='0.0.1',
|
||||||
|
description='An hackable Minecraft client, built with aiocraft',
|
||||||
|
url='https://github.com/alemidev/treepuncher',
|
||||||
|
author='alemi',
|
||||||
|
author_email='me@alemi.dev',
|
||||||
|
license='MIT',
|
||||||
|
packages=find_packages(),
|
||||||
|
package_data = {
|
||||||
|
'treepuncher': ['py.typed'],
|
||||||
|
},
|
||||||
|
install_requires=requirements,
|
||||||
|
classifiers=[
|
||||||
|
'Development Status :: 1 - Planning',
|
||||||
|
'Intended Audience :: Developers',
|
||||||
|
'License :: OSI Approved :: MIT License',
|
||||||
|
'Operating System :: POSIX :: Linux',
|
||||||
|
'Programming Language :: Python :: 3',
|
||||||
|
'Programming Language :: Python :: 3.8',
|
||||||
|
],
|
||||||
|
)
|
1
treepuncher/__init__.py
Normal file
1
treepuncher/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
from .treepuncher import Treepuncher
|
0
treepuncher/__main__.py
Normal file
0
treepuncher/__main__.py
Normal file
1
treepuncher/events/__init__.py
Normal file
1
treepuncher/events/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
from .chat import ChatEvent
|
54
treepuncher/events/chat.py
Normal file
54
treepuncher/events/chat.py
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
import re
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
from aiocraft.util.helpers import parse_chat
|
||||||
|
|
||||||
|
CHAT_MESSAGE_MATCHER = re.compile(r"<(?P<usr>[A-Za-z0-9_]+)> (?P<msg>.+)")
|
||||||
|
REMOVE_COLOR_FORMATS = re.compile(r"§[0-9a-z]")
|
||||||
|
WHISPER_MATCHER = re.compile(r"(?:to (?P<touser>[A-Za-z0-9_]+)( |):|(?P<fromuser>[A-Za-z0-9_]+) whispers( |):|from (?P<from9b>[A-Za-z0-9_]+):) (?P<txt>.+)", flags=re.IGNORECASE)
|
||||||
|
JOIN_LEAVE_MATCHER = re.compile(r"(?P<usr>[A-Za-z0-9_]+) (?P<action>joined|left)( the game|)$", flags=re.IGNORECASE)
|
||||||
|
|
||||||
|
class MessageType(Enum):
|
||||||
|
CHAT = "chat"
|
||||||
|
WHISPER = "whisper"
|
||||||
|
#COMMAND = "cmd"
|
||||||
|
#DEATH = "death" # ???? TODO!
|
||||||
|
JOIN = "join"
|
||||||
|
LEAVE = "leave"
|
||||||
|
SYSTEM = "system"
|
||||||
|
|
||||||
|
class ChatEvent:
|
||||||
|
text : str
|
||||||
|
type : MessageType
|
||||||
|
user : str
|
||||||
|
message : str
|
||||||
|
|
||||||
|
def __init__(self, text:str):
|
||||||
|
self.text = REMOVE_COLOR_FORMATS.sub("", parse_chat(text))
|
||||||
|
self.user = ""
|
||||||
|
self.message= ""
|
||||||
|
self.type = MessageType.SYSTEM
|
||||||
|
self._parse()
|
||||||
|
|
||||||
|
def _parse(self):
|
||||||
|
match = CHAT_MESSAGE_MATCHER.search(self.text)
|
||||||
|
if match:
|
||||||
|
self.type = MessageType.CHAT
|
||||||
|
self.user = match["usr"]
|
||||||
|
self.message = match["msg"]
|
||||||
|
return
|
||||||
|
|
||||||
|
match = WHISPER_MATCHER.search(self.text)
|
||||||
|
if match:
|
||||||
|
self.type = MessageType.WHISPER
|
||||||
|
self.user = match["touser"] or match["fromuser"] or match["from9b"]
|
||||||
|
self.message = match["txt"]
|
||||||
|
return
|
||||||
|
|
||||||
|
match = JOIN_LEAVE_MATCHER.search(self.text)
|
||||||
|
if match:
|
||||||
|
self.type = MessageType.JOIN if match["action"] == "join" else MessageType.LEAVE
|
||||||
|
self.user = match["usr"]
|
||||||
|
return
|
0
treepuncher/py.typed
Normal file
0
treepuncher/py.typed
Normal file
172
treepuncher/treepuncher.py
Normal file
172
treepuncher/treepuncher.py
Normal file
|
@ -0,0 +1,172 @@
|
||||||
|
import re
|
||||||
|
|
||||||
|
from typing import List, Dict, Union, Optional
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||||
|
|
||||||
|
from aiocraft.client import MinecraftClient
|
||||||
|
from aiocraft.mc.packet import Packet
|
||||||
|
from aiocraft.mc.definitions import Difficulty, Dimension, Gamemode, Position
|
||||||
|
|
||||||
|
from aiocraft.mc.proto.play.clientbound import (
|
||||||
|
PacketRespawn, PacketLogin, PacketPosition, PacketUpdateHealth, PacketExperience,
|
||||||
|
PacketAbilities, PacketChat as PacketChatMessage
|
||||||
|
)
|
||||||
|
from aiocraft.mc.proto.play.serverbound import PacketTeleportConfirm, PacketClientCommand, PacketChat
|
||||||
|
|
||||||
|
from .events import ChatEvent
|
||||||
|
from .events.chat import MessageType
|
||||||
|
|
||||||
|
REMOVE_COLOR_FORMATS = re.compile(r"§[0-9a-z]")
|
||||||
|
|
||||||
|
class BotEvents(Enum):
|
||||||
|
DIED = 0
|
||||||
|
|
||||||
|
class Treepuncher(MinecraftClient):
|
||||||
|
in_game : bool
|
||||||
|
gamemode : Gamemode
|
||||||
|
dimension : Dimension
|
||||||
|
difficulty : Difficulty
|
||||||
|
|
||||||
|
hp : float
|
||||||
|
food : float
|
||||||
|
xp : float
|
||||||
|
lvl : int
|
||||||
|
total_xp : int
|
||||||
|
|
||||||
|
slot : int
|
||||||
|
# TODO inventory
|
||||||
|
|
||||||
|
position : Position
|
||||||
|
# TODO world
|
||||||
|
|
||||||
|
# TODO player abilities
|
||||||
|
# walk_speed : float
|
||||||
|
# fly_speed : float
|
||||||
|
# flags : int
|
||||||
|
|
||||||
|
scheduler : AsyncIOScheduler
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
self.in_game = False
|
||||||
|
self.gamemode = Gamemode.SURVIVAL
|
||||||
|
self.dimension = Dimension.OVERWORLD
|
||||||
|
self.difficulty = Difficulty.HARD
|
||||||
|
|
||||||
|
self.hp = 20.0
|
||||||
|
self.food = 20.0
|
||||||
|
self.xp = 0.0
|
||||||
|
self.lvl = 0
|
||||||
|
|
||||||
|
self.slot = 0
|
||||||
|
|
||||||
|
self.position = Position(0, 0, 0)
|
||||||
|
|
||||||
|
self._register_handlers()
|
||||||
|
|
||||||
|
self.scheduler = AsyncIOScheduler()
|
||||||
|
# self.scheduler.add_job(job, "interval", seconds=3)
|
||||||
|
self.scheduler.start(paused=True)
|
||||||
|
|
||||||
|
async def start(self):
|
||||||
|
await super().start()
|
||||||
|
self.scheduler.resume()
|
||||||
|
|
||||||
|
async def stop(self, force:bool=False):
|
||||||
|
self.scheduler.pause()
|
||||||
|
await super().stop(force=force)
|
||||||
|
|
||||||
|
def on_chat(self, msg_type:Union[str, MessageType] = None):
|
||||||
|
if isinstance(msg_type, str):
|
||||||
|
msg_type = MessageType(msg_type)
|
||||||
|
def wrapper(fun):
|
||||||
|
async def process_chat_packet(packet:PacketChatMessage):
|
||||||
|
msg = ChatEvent(packet.message)
|
||||||
|
if not msg_type or msg.type == msg_type:
|
||||||
|
return await fun(msg)
|
||||||
|
return self.register(PacketChatMessage, process_chat_packet)
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
def on_death(self):
|
||||||
|
def wrapper(fun):
|
||||||
|
return self.register(BotEvents.DIED, fun)
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
async def write(self, packet:Packet, wait:bool=False):
|
||||||
|
await self.dispatcher.write(packet, wait)
|
||||||
|
|
||||||
|
async def chat(self, message:str, wait:bool=False):
|
||||||
|
await self.dispatcher.write(
|
||||||
|
PacketChat(
|
||||||
|
self.dispatcher.proto,
|
||||||
|
message=message
|
||||||
|
),
|
||||||
|
wait=wait
|
||||||
|
)
|
||||||
|
|
||||||
|
def _register_handlers(self):
|
||||||
|
@self.on_packet(PacketRespawn)
|
||||||
|
async def on_player_respawning(packet:PacketRespawn):
|
||||||
|
self.gamemode = Gamemode(packet.gamemode)
|
||||||
|
self.dimension = Dimension(packet.dimension)
|
||||||
|
self.difficulty = Difficulty(packet.difficulty)
|
||||||
|
if self.difficulty != Difficulty.PEACEFUL \
|
||||||
|
and self.gamemode != Gamemode.SPECTATOR:
|
||||||
|
self.in_game = True
|
||||||
|
else:
|
||||||
|
self.in_game = False
|
||||||
|
self._logger.info(
|
||||||
|
"Reloading world: %s (%s) in %s",
|
||||||
|
self.dimension.name,
|
||||||
|
self.difficulty.name,
|
||||||
|
self.gamemode.name
|
||||||
|
)
|
||||||
|
|
||||||
|
@self.on_packet(PacketLogin)
|
||||||
|
async def player_joining_cb(packet:PacketLogin):
|
||||||
|
self.gamemode = Gamemode(packet.gameMode)
|
||||||
|
self.dimension = Dimension(packet.dimension)
|
||||||
|
self.difficulty = Difficulty(packet.difficulty)
|
||||||
|
if self.difficulty != Difficulty.PEACEFUL \
|
||||||
|
and self.gamemode != Gamemode.SPECTATOR:
|
||||||
|
self.in_game = True
|
||||||
|
else:
|
||||||
|
self.in_game = False
|
||||||
|
self._logger.info(
|
||||||
|
"Joined world: %s (%s) in %s",
|
||||||
|
self.dimension.name,
|
||||||
|
self.difficulty.name,
|
||||||
|
self.gamemode.name
|
||||||
|
)
|
||||||
|
|
||||||
|
@self.on_packet(PacketPosition)
|
||||||
|
async def player_rubberband_cb(packet:PacketPosition):
|
||||||
|
self._logger.info("Position synchronized")
|
||||||
|
self.position = Position(packet.x, packet.y, packet.z)
|
||||||
|
await self.dispatcher.write(
|
||||||
|
PacketTeleportConfirm(
|
||||||
|
self.dispatcher.proto,
|
||||||
|
teleportId=packet.teleportId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@self.on_packet(PacketUpdateHealth)
|
||||||
|
async def player_hp_cb(packet:PacketUpdateHealth):
|
||||||
|
if packet.health != self.hp and packet.health <= 0:
|
||||||
|
self._logger.info("Dead, respawning...")
|
||||||
|
await self.dispatcher.write(
|
||||||
|
PacketClientCommand(self.dispatcher.proto, actionId=0) # respawn
|
||||||
|
)
|
||||||
|
self.run_callbacks(BotEvents.DIED)
|
||||||
|
self.hp = packet.health
|
||||||
|
self.food = packet.food
|
||||||
|
|
||||||
|
@self.on_packet(PacketExperience)
|
||||||
|
async def player_hp_cb(packet:PacketExperience):
|
||||||
|
self.xp = packet.experienceBar
|
||||||
|
self.lvl = packet.level
|
||||||
|
self.total_xp = packet.totalExperience
|
||||||
|
|
Loading…
Reference in a new issue