restructured treepuncher, reworked addons system
Moved most of Treepuncher logic into 'game/*' files, making it more modular. Changed a little bit events (but will need much more work). Added SQLite storage and ini config file, for persistance. Moved the launcher inside __main__, making it a launchable python module. There still is some confusion between Addons and game logic components.
This commit is contained in:
parent
e20141f437
commit
4b1b508be9
19 changed files with 624 additions and 327 deletions
|
@ -1,2 +1,5 @@
|
||||||
|
setproctitle
|
||||||
|
termcolor
|
||||||
apscheduler
|
apscheduler
|
||||||
aiocraft
|
aiocraft
|
||||||
|
aioconsole
|
||||||
|
|
|
@ -0,0 +1,133 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from importlib import import_module
|
||||||
|
from typing import get_type_hints
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from setproctitle import setproctitle
|
||||||
|
|
||||||
|
from .treepuncher import Treepuncher, Addon, ConfigObject
|
||||||
|
from .helpers import configure_logging
|
||||||
|
|
||||||
|
def main():
|
||||||
|
root = Path(os.getcwd())
|
||||||
|
# TODO would be cool if it was possible to configure addons path, but we need to load addons before doing argparse so we can do helptext
|
||||||
|
# addon_path = Path(args.addon_path) if args.addon_path else ( root/'addons' )
|
||||||
|
addon_path = root/'addons'
|
||||||
|
addons : List[Type[Addon]] = []
|
||||||
|
|
||||||
|
for path in sorted(addon_path.rglob('*.py')):
|
||||||
|
m = import_module(path)
|
||||||
|
for obj_name in vars(m).keys():
|
||||||
|
obj = getattr(m, obj_name)
|
||||||
|
if issubclass(obj, Addon):
|
||||||
|
addons.append(obj)
|
||||||
|
|
||||||
|
class ChatLogger(Addon):
|
||||||
|
"""This addon will print (optionally colored) game chat to terminal"""
|
||||||
|
REMOVE_COLOR_FORMATS = re.compile(r"§[0-9a-z]")
|
||||||
|
@dataclass
|
||||||
|
class Options(ConfigObject):
|
||||||
|
color : bool = True
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
@self.on_packet(PacketChatMessage)
|
||||||
|
async def print_chat_colored_to_terminal(packet:PacketChatMessage):
|
||||||
|
print(self.REMOVE_COLOR_FORMATS.sub("", parse_chat(packet.message, ansi_color=self.config.color)))
|
||||||
|
|
||||||
|
addons.append(ChatLogger)
|
||||||
|
|
||||||
|
class ChatInput(Addon):
|
||||||
|
task : asyncio.Task
|
||||||
|
running : bool
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.task = None
|
||||||
|
self.running = False
|
||||||
|
|
||||||
|
async def initialize(self):
|
||||||
|
async def aio_input():
|
||||||
|
while self.running:
|
||||||
|
try:
|
||||||
|
await self.client.chat(await asyncio.wait_for(ainput(""), 1))
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
pass
|
||||||
|
except Exception:
|
||||||
|
client._logger.exception("Exception processing input from keyboard")
|
||||||
|
self.running = True
|
||||||
|
self.task = asyncio.get_event_loop().create_task(aio_input())
|
||||||
|
|
||||||
|
async def cleanup(self, force:bool=False):
|
||||||
|
self.running = False
|
||||||
|
if self.task and not force:
|
||||||
|
await self.task
|
||||||
|
|
||||||
|
addons.append(ChatInput)
|
||||||
|
|
||||||
|
help_text = '\n\naddons:\n' + str.join( # TODO do this iteratively to make it readable!
|
||||||
|
'\n', (
|
||||||
|
f" {addon.__name__}: {addon.__doc__ or '-no description-'}\n " + str.join('\n ',
|
||||||
|
(f"- {name} ({clazz.__name__}) {'[!]' if not hasattr(addon.Options, name) else ''}" for (name, clazz) in get_type_hints(addon.Options).items())
|
||||||
|
) for addon in addons
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
prog='python -m treepuncher',
|
||||||
|
description='Treepuncher | Block Game automation framework',
|
||||||
|
epilog=help_text,
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument('name', help='name to use for this client session')
|
||||||
|
parser.add_argument('server', help='server to connect to')
|
||||||
|
parser.add_argument('--ms-client-id', dest='client_id', default='c63ef189-23cb-453b-8060-13800b85d2dc', help='Azure application client_id')
|
||||||
|
parser.add_argument('--ms-client-secret', dest='client_secret', default='N2e7Q~ybYA0IO39KB1mFD4GmoYzISRaRNyi59', help='Azure application client_secret')
|
||||||
|
parser.add_argument('--ms-redirect-uri', dest='redirect_uri', default='https://fantabos.co/msauth', help='Azure application redirect_uri')
|
||||||
|
parser.add_argument('--addon-path', dest='addon-path', default='', help='Path for loading addons')
|
||||||
|
parser.add_argument('--chat-log', dest='chat_log', action='store_const', const=True, default=False, help="print (colored) chat to terminal")
|
||||||
|
parser.add_argument('--chat-input', dest='chat_input', action='store_const', const=True, default=False, help="read input from stdin and send it to chat")
|
||||||
|
parser.add_argument('--debug', dest='_debug', action='store_const', const=True, default=False, help="enable debug logs")
|
||||||
|
parser.add_argument('--no-packet-whitelist', dest='use_packet_whitelist', action='store_const', const=False, default=True, help="disable packet whitelist")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
configure_logging(args.name, level=logging.DEBUG if args._debug else logging.INFO)
|
||||||
|
setproctitle(f"treepuncher[{args.name}]")
|
||||||
|
|
||||||
|
code = input(f"-> Go to 'https://fantabos.co/msauth?client_id={args.client_id}&state=hardcoded', click 'Auth' and login, then copy here the code you received\n--> ")
|
||||||
|
|
||||||
|
client = Treepuncher(
|
||||||
|
args.name,
|
||||||
|
args.server,
|
||||||
|
use_packet_whitelist=use_packet_whitelist,
|
||||||
|
notifier=notifier,
|
||||||
|
client_id=args.client_id,
|
||||||
|
client_secret=args.client_secret,
|
||||||
|
redirect_uri="https://fantabos.co/msauth"
|
||||||
|
)
|
||||||
|
|
||||||
|
for addon in addons:
|
||||||
|
client.install(addon)
|
||||||
|
|
||||||
|
if args.chat_log:
|
||||||
|
client.install(ChatLogger)
|
||||||
|
|
||||||
|
if args.chat_input:
|
||||||
|
from aioconsole import ainput
|
||||||
|
client.install(ChatInput)
|
||||||
|
|
||||||
|
client.run()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
||||||
|
|
|
@ -1 +1,3 @@
|
||||||
from .chat import ChatEvent
|
from .chat import ChatEvent
|
||||||
|
from .join_game import JoinGameEvent
|
||||||
|
from .death import DeathEvent
|
||||||
|
|
2
treepuncher/events/base.py
Normal file
2
treepuncher/events/base.py
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
class BaseEvent:
|
||||||
|
pass
|
|
@ -5,6 +5,8 @@ from enum import Enum
|
||||||
|
|
||||||
from aiocraft.util.helpers import parse_chat
|
from aiocraft.util.helpers import parse_chat
|
||||||
|
|
||||||
|
from .base import BaseEvent
|
||||||
|
|
||||||
CHAT_MESSAGE_MATCHER = re.compile(r"<(?P<usr>[A-Za-z0-9_]+)> (?P<msg>.+)")
|
CHAT_MESSAGE_MATCHER = re.compile(r"<(?P<usr>[A-Za-z0-9_]+)> (?P<msg>.+)")
|
||||||
REMOVE_COLOR_FORMATS = re.compile(r"§[0-9a-z]")
|
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)
|
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)
|
||||||
|
@ -19,7 +21,7 @@ class MessageType(Enum):
|
||||||
LEAVE = "leave"
|
LEAVE = "leave"
|
||||||
SYSTEM = "system"
|
SYSTEM = "system"
|
||||||
|
|
||||||
class ChatEvent:
|
class ChatEvent(BaseEvent):
|
||||||
text : str
|
text : str
|
||||||
type : MessageType
|
type : MessageType
|
||||||
user : str
|
user : str
|
||||||
|
|
4
treepuncher/events/death.py
Normal file
4
treepuncher/events/death.py
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
from .base import BaseEvent
|
||||||
|
|
||||||
|
class DeathEvent(BaseEvent):
|
||||||
|
SENTINEL = object()
|
10
treepuncher/events/join_game.py
Normal file
10
treepuncher/events/join_game.py
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
from aiocraft.mc.definitions import Dimension, Difficulty, Gamemode
|
||||||
|
|
||||||
|
from .base import BaseEvent
|
||||||
|
|
||||||
|
class JoinGameEvent(BaseEvent):
|
||||||
|
SENTINEL = object()
|
||||||
|
|
||||||
|
dimension : Dimension
|
||||||
|
difficulty : Difficulty
|
||||||
|
gamemode : Gamemode
|
5
treepuncher/game/__init__.py
Normal file
5
treepuncher/game/__init__.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from .state import GameState
|
||||||
|
from .inventory import GameInventory
|
||||||
|
from .tablist import GameTablist
|
||||||
|
from .chat import GameChat
|
||||||
|
from .world import GameWorld
|
36
treepuncher/game/chat.py
Normal file
36
treepuncher/game/chat.py
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
from aiocraft.client import MinecraftClient
|
||||||
|
from aiocraft.mc.proto.play.clientbound import PacketChat as PacketChatMessage
|
||||||
|
from aiocraft.mc.proto.play.serverbound import PacketChat
|
||||||
|
|
||||||
|
from ..events.chat import ChatEvent, MessageType
|
||||||
|
|
||||||
|
class GameChat(MinecraftClient):
|
||||||
|
|
||||||
|
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)
|
||||||
|
self.register(PacketChatMessage, process_chat_packet)
|
||||||
|
return fun
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
async def chat(self, message:str, whisper:str=None, wait:bool=False):
|
||||||
|
if whisper:
|
||||||
|
message = f"/w {whisper} {message}"
|
||||||
|
await self.dispatcher.write(
|
||||||
|
PacketChat(
|
||||||
|
self.dispatcher.proto,
|
||||||
|
message=message
|
||||||
|
),
|
||||||
|
wait=wait
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
38
treepuncher/game/inventory.py
Normal file
38
treepuncher/game/inventory.py
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from aiocraft.client import MinecraftClient
|
||||||
|
from aiocraft.mc.definitions import Item
|
||||||
|
from aiocraft.mc.proto.play.clientbound import PacketSetSlot, PacketHeldItemSlot as PacketHeldItemChange
|
||||||
|
from aiocraft.mc.proto.play.serverbound import PacketHeldItemSlot
|
||||||
|
|
||||||
|
class GameInventory(MinecraftClient):
|
||||||
|
slot : int
|
||||||
|
inventory : List[Item]
|
||||||
|
# TODO inventory
|
||||||
|
|
||||||
|
async def set_slot(self, slot:int):
|
||||||
|
self.slot = slot
|
||||||
|
await self.dispatcher.write(PacketHeldItemSlot(self.dispatcher.proto, slotId=slot))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hotbar(self) -> List[Item]:
|
||||||
|
return self.inventory[36:45]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def selected(self) -> Item:
|
||||||
|
return self.hotbar[self.slot]
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
self.slot = 0
|
||||||
|
self.inventory = [ Item() for _ in range(46) ]
|
||||||
|
|
||||||
|
@self.on_packet(PacketSetSlot)
|
||||||
|
async def on_set_slot(packet:PacketSetSlot):
|
||||||
|
if packet.windowId == 0: # player inventory
|
||||||
|
self.inventory[packet.slot] = packet.item
|
||||||
|
|
||||||
|
@self.on_packet(PacketHeldItemChange)
|
||||||
|
async def on_held_item_change(packet:PacketHeldItemChange):
|
||||||
|
self.slot = packet.slot
|
132
treepuncher/game/state.py
Normal file
132
treepuncher/game/state.py
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
import asyncio
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from aiocraft.client import MinecraftClient
|
||||||
|
from aiocraft.mc.definitions import Gamemode, Dimension, Difficulty
|
||||||
|
from aiocraft.mc.proto import PacketRespawn, PacketLogin, PacketUpdateHealth, PacketExperience, PacketSettings, PacketClientCommand
|
||||||
|
|
||||||
|
from ..events import JoinGameEvent, DeathEvent
|
||||||
|
|
||||||
|
class GameState(MinecraftClient):
|
||||||
|
hp : float
|
||||||
|
food : float
|
||||||
|
xp : float
|
||||||
|
lvl : int
|
||||||
|
total_xp : int
|
||||||
|
|
||||||
|
# TODO player abilities
|
||||||
|
# walk_speed : float
|
||||||
|
# fly_speed : float
|
||||||
|
# flags : int
|
||||||
|
|
||||||
|
in_game : bool
|
||||||
|
gamemode : Gamemode
|
||||||
|
dimension : Dimension
|
||||||
|
difficulty : Difficulty
|
||||||
|
join_time : datetime.datetime
|
||||||
|
|
||||||
|
def on_death(self):
|
||||||
|
def decorator(fun):
|
||||||
|
@functool.wraps(fun)
|
||||||
|
async def wrapper():
|
||||||
|
event = DeathEvent()
|
||||||
|
return await fun(event)
|
||||||
|
return self.register(DeathEvent.SENTINEL, fun)
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
def on_joined_world(self):
|
||||||
|
def decorator(fun):
|
||||||
|
@functool.wraps(fun)
|
||||||
|
async def wrapper():
|
||||||
|
event = JoinGameEvent()
|
||||||
|
return await fun(event)
|
||||||
|
return self.register(JoinGameEvent.SENTINEL, callback)
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
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.join_time = datetime.datetime(2011, 11, 18)
|
||||||
|
|
||||||
|
self.hp = 20.0
|
||||||
|
self.food = 20.0
|
||||||
|
self.xp = 0.0
|
||||||
|
self.lvl = 0
|
||||||
|
|
||||||
|
@self.on_disconnected()
|
||||||
|
async def disconnected_cb():
|
||||||
|
self.in_game = False
|
||||||
|
|
||||||
|
@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)
|
||||||
|
self.join_time = datetime.datetime.now()
|
||||||
|
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.run_callbacks(JoinGameEvent, self.dimension, self.difficulty, self.gamemode)
|
||||||
|
await self.dispatcher.write(
|
||||||
|
PacketSettings(
|
||||||
|
self.dispatcher.proto,
|
||||||
|
locale="en_US",
|
||||||
|
viewDistance=4,
|
||||||
|
chatFlags=0,
|
||||||
|
chatColors=True,
|
||||||
|
skinParts=0xF,
|
||||||
|
mainHand=0,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await self.dispatcher.write(PacketClientCommand(self.dispatcher.proto, actionId=0))
|
||||||
|
|
||||||
|
@self.on_packet(PacketUpdateHealth)
|
||||||
|
async def player_hp_cb(packet:PacketUpdateHealth):
|
||||||
|
died = packet.health != self.hp and packet.health <= 0
|
||||||
|
self.hp = packet.health
|
||||||
|
self.food = packet.food + packet.foodSaturation
|
||||||
|
if died:
|
||||||
|
self.run_callbacks(DeathEvent.SENTINEL)
|
||||||
|
self._logger.info("Dead, respawning...")
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
await self.dispatcher.write(
|
||||||
|
PacketClientCommand(self.dispatcher.proto, actionId=0) # respawn
|
||||||
|
)
|
||||||
|
|
||||||
|
@self.on_packet(PacketExperience)
|
||||||
|
async def player_xp_cb(packet:PacketExperience):
|
||||||
|
if packet.level != self.lvl:
|
||||||
|
self._logger.info("Level up : %d", packet.level)
|
||||||
|
self.xp = packet.experienceBar
|
||||||
|
self.lvl = packet.level
|
||||||
|
self.total_xp = packet.totalExperience
|
||||||
|
|
41
treepuncher/game/tablist.py
Normal file
41
treepuncher/game/tablist.py
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import uuid
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from typing import Dict, List
|
||||||
|
|
||||||
|
from aiocraft.client import MinecraftClient
|
||||||
|
from aiocraft.mc.definitions import Item
|
||||||
|
from aiocraft.mc.proto import PacketPlayerInfo
|
||||||
|
|
||||||
|
class GameTablist(MinecraftClient):
|
||||||
|
tablist : Dict[uuid.UUID, dict]
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
self.tablist = {}
|
||||||
|
|
||||||
|
@self.on_connected()
|
||||||
|
async def connected_cb():
|
||||||
|
self.tablist.clear()
|
||||||
|
|
||||||
|
@self.on_packet(PacketPlayerInfo)
|
||||||
|
async def tablist_update(packet:PacketPlayerInfo):
|
||||||
|
for record in packet.data:
|
||||||
|
uid = record['UUID']
|
||||||
|
if packet.action != 0 and uid not in self.tablist:
|
||||||
|
continue # TODO this happens kinda often but doesn't seem to be an issue?
|
||||||
|
if packet.action == 0:
|
||||||
|
self.tablist[uid] = record
|
||||||
|
self.tablist[uid]['joinTime'] = datetime.datetime.now()
|
||||||
|
elif packet.action == 1:
|
||||||
|
self.tablist[uid]['gamemode'] = record['gamemode']
|
||||||
|
elif packet.action == 2:
|
||||||
|
self.tablist[uid]['ping'] = record['ping']
|
||||||
|
elif packet.action == 3:
|
||||||
|
self.tablist[uid]['displayName'] = record['displayName']
|
||||||
|
elif packet.action == 4:
|
||||||
|
self.tablist.pop(uid, None)
|
||||||
|
|
||||||
|
|
||||||
|
|
35
treepuncher/game/world.py
Normal file
35
treepuncher/game/world.py
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import uuid
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from typing import Dict, List
|
||||||
|
|
||||||
|
from aiocraft.client import MinecraftClient
|
||||||
|
from aiocraft.mc.definitions import BlockPos
|
||||||
|
from aiocraft.mc.proto import PacketPosition, PacketTeleportConfirm
|
||||||
|
|
||||||
|
class GameWorld(MinecraftClient):
|
||||||
|
position : BlockPos
|
||||||
|
# TODO world
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
self.position = BlockPos(0, 0, 0)
|
||||||
|
|
||||||
|
@self.on_connected()
|
||||||
|
async def connected_cb():
|
||||||
|
self.tablist.clear()
|
||||||
|
|
||||||
|
@self.on_packet(PacketPosition)
|
||||||
|
async def player_rubberband_cb(packet:PacketPosition):
|
||||||
|
self.position = BlockPos(packet.x, packet.y, packet.z)
|
||||||
|
self._logger.info(
|
||||||
|
"Position synchronized : (x:%.0f,y:%.0f,z:%.0f)",
|
||||||
|
self.position.x, self.position.y, self.position.z
|
||||||
|
)
|
||||||
|
await self.dispatcher.write(
|
||||||
|
PacketTeleportConfirm(
|
||||||
|
self.dispatcher.proto,
|
||||||
|
teleportId=packet.teleportId
|
||||||
|
)
|
||||||
|
)
|
49
treepuncher/helpers.py
Normal file
49
treepuncher/helpers.py
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
from termcolor import colored
|
||||||
|
|
||||||
|
def configure_logging(name:str, level=logging.INFO, color:bool = True):
|
||||||
|
import os
|
||||||
|
from logging.handlers import RotatingFileHandler
|
||||||
|
|
||||||
|
if not os.path.isdir("debug"):
|
||||||
|
os.mkdir("debug")
|
||||||
|
|
||||||
|
class ColorFormatter(logging.Formatter):
|
||||||
|
def __init__(self, fmt:str):
|
||||||
|
self.fmt : str = fmt
|
||||||
|
self.formatters : Dict[int, logging.Formatter] = {
|
||||||
|
logging.DEBUG: logging.Formatter(colored(fmt, color='grey')),
|
||||||
|
logging.INFO: logging.Formatter(colored(fmt)),
|
||||||
|
logging.WARNING: logging.Formatter(colored(fmt, color='yellow')),
|
||||||
|
logging.ERROR: logging.Formatter(colored(fmt, color='red')),
|
||||||
|
logging.CRITICAL: logging.Formatter(colored(fmt, color='red', attrs=['bold'])),
|
||||||
|
}
|
||||||
|
|
||||||
|
def format(self, record:logging.LogRecord) -> str:
|
||||||
|
if record.exc_text: # jank way to color the stacktrace but will do for now
|
||||||
|
record.exc_text = colored(record.exc_text, color='grey', attrs=['bold'])
|
||||||
|
return self.formatters[record.levelno].format(record)
|
||||||
|
|
||||||
|
logger = logging.getLogger()
|
||||||
|
logger.setLevel(level)
|
||||||
|
# create file handler which logs even debug messages
|
||||||
|
fh = RotatingFileHandler(f'data/{name}.log', maxBytes=1048576, backupCount=5) # 1MB files
|
||||||
|
fh.setLevel(logging.DEBUG)
|
||||||
|
# create console handler with a higher log level
|
||||||
|
ch = logging.StreamHandler()
|
||||||
|
ch.setLevel(logging.DEBUG)
|
||||||
|
# create formatter and add it to the handlers
|
||||||
|
file_formatter = logging.Formatter("[%(asctime)s] [%(name)s] [%(levelname)s] %(message)s", "%b %d %Y %H:%M:%S")
|
||||||
|
print_formatter : logging.Formatter
|
||||||
|
if color:
|
||||||
|
print_formatter = ColorFormatter("├🮥 %(message)s")
|
||||||
|
else:
|
||||||
|
print_formatter = logging.Formatter("├🮥 %(message)s")
|
||||||
|
fh.setFormatter(file_formatter)
|
||||||
|
ch.setFormatter(print_formatter)
|
||||||
|
# add the handlers to the logger
|
||||||
|
logger.addHandler(fh)
|
||||||
|
logger.addHandler(ch)
|
|
@ -1 +0,0 @@
|
||||||
from .module import LogicModule
|
|
|
@ -1,81 +0,0 @@
|
||||||
from ..treepuncher import Treepuncher, TreepuncherEvents
|
|
||||||
from .module import LogicModule
|
|
||||||
|
|
||||||
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 aiocraft.mc.definitions import Difficulty, Dimension, Gamemode, Position
|
|
||||||
|
|
||||||
class CoreLogic(LogicModule):
|
|
||||||
def register(self, client:Treepuncher):
|
|
||||||
@client.on_disconnected()
|
|
||||||
async def on_disconnected():
|
|
||||||
client.in_game = False
|
|
||||||
|
|
||||||
@client.on_packet(PacketRespawn)
|
|
||||||
async def on_player_respawning(packet:PacketRespawn):
|
|
||||||
client.gamemode = Gamemode(packet.gamemode)
|
|
||||||
client.dimension = Dimension(packet.dimension)
|
|
||||||
client.difficulty = Difficulty(packet.difficulty)
|
|
||||||
if client.difficulty != Difficulty.PEACEFUL \
|
|
||||||
and client.gamemode != Gamemode.SPECTATOR:
|
|
||||||
client.in_game = True
|
|
||||||
else:
|
|
||||||
client.in_game = False
|
|
||||||
client._logger.info(
|
|
||||||
"Reloading world: %s (%s) in %s",
|
|
||||||
client.dimension.name,
|
|
||||||
client.difficulty.name,
|
|
||||||
client.gamemode.name
|
|
||||||
)
|
|
||||||
|
|
||||||
@client.on_packet(PacketLogin)
|
|
||||||
async def player_joining_cb(packet:PacketLogin):
|
|
||||||
client.gamemode = Gamemode(packet.gameMode)
|
|
||||||
client.dimension = Dimension(packet.dimension)
|
|
||||||
client.difficulty = Difficulty(packet.difficulty)
|
|
||||||
if client.difficulty != Difficulty.PEACEFUL \
|
|
||||||
and client.gamemode != Gamemode.SPECTATOR:
|
|
||||||
client.in_game = True
|
|
||||||
else:
|
|
||||||
client.in_game = False
|
|
||||||
client._logger.info(
|
|
||||||
"Joined world: %s (%s) in %s",
|
|
||||||
client.dimension.name,
|
|
||||||
client.difficulty.name,
|
|
||||||
client.gamemode.name
|
|
||||||
)
|
|
||||||
client.run_callbacks(TreepuncherEvents.IN_GAME)
|
|
||||||
|
|
||||||
@client.on_packet(PacketPosition)
|
|
||||||
async def player_rubberband_cb(packet:PacketPosition):
|
|
||||||
client._logger.info("Position synchronized")
|
|
||||||
client.position = Position(packet.x, packet.y, packet.z)
|
|
||||||
await client.dispatcher.write(
|
|
||||||
PacketTeleportConfirm(
|
|
||||||
client.dispatcher.proto,
|
|
||||||
teleportId=packet.teleportId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
@client.on_packet(PacketUpdateHealth)
|
|
||||||
async def player_hp_cb(packet:PacketUpdateHealth):
|
|
||||||
if packet.health != client.hp and packet.health <= 0:
|
|
||||||
client._logger.info("Dead, respawning...")
|
|
||||||
await client.dispatcher.write(
|
|
||||||
PacketClientCommand(client.dispatcher.proto, actionId=0) # respawn
|
|
||||||
)
|
|
||||||
client.run_callbacks(TreepuncherEvents.DIED)
|
|
||||||
client.hp = packet.health
|
|
||||||
client.food = packet.food
|
|
||||||
|
|
||||||
@client.on_packet(PacketExperience)
|
|
||||||
async def player_xp_cb(packet:PacketExperience):
|
|
||||||
if packet.level != client.lvl:
|
|
||||||
client._logger.info("Level up : %d", packet.level)
|
|
||||||
client.xp = packet.experienceBar
|
|
||||||
client.lvl = packet.level
|
|
||||||
client.total_xp = packet.totalExperience
|
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
|
|
||||||
class LogicModule:
|
|
||||||
def register(self, client:'Treepuncher') -> None:
|
|
||||||
pass # override to register callbacks on client
|
|
||||||
|
|
||||||
async def initialize(self, client:'Treepuncher') -> None:
|
|
||||||
pass # override to register stuff on client start
|
|
||||||
|
|
||||||
async def cleanup(self, client:'Treepuncher') -> None:
|
|
||||||
pass # override to register stuff on client stop
|
|
||||||
|
|
60
treepuncher/storage.py
Normal file
60
treepuncher/storage.py
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
import json
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Optional, Any
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SystemState:
|
||||||
|
name : str
|
||||||
|
token : str
|
||||||
|
start_time : int
|
||||||
|
|
||||||
|
class Storage:
|
||||||
|
name : str
|
||||||
|
db : sqlite3.Connection
|
||||||
|
|
||||||
|
def __init__(self, name:str):
|
||||||
|
self.name = name
|
||||||
|
self.db = sqlite3.connect(f'{name}.session')
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
self.db.close()
|
||||||
|
|
||||||
|
def _init_db(self):
|
||||||
|
cur = self.db.cursor()
|
||||||
|
cur.execute('CREATE TABLE system (name TEXT, token TEXT, start_time LONG)')
|
||||||
|
cur.execute('CREATE TABLE documents (name TEXT, value TEXT)')
|
||||||
|
self.db.commit()
|
||||||
|
|
||||||
|
def _set_state(self, state:SystemState):
|
||||||
|
cur = self.db.cursor()
|
||||||
|
cur.execute('DELETE FROM system')
|
||||||
|
cur.execute('INSERT INTO system VALUES (?, ?, ?)', (state.name, state.token, state.start_time))
|
||||||
|
self.db.commit()
|
||||||
|
|
||||||
|
def system(self) -> SystemState:
|
||||||
|
cur = self.db.cursor()
|
||||||
|
val = cur.execute('SELECT * FROM system').fetchall()
|
||||||
|
if not val:
|
||||||
|
raise ValueError("No system state set")
|
||||||
|
return SystemState(
|
||||||
|
name=val[0][0],
|
||||||
|
token=val[0][1],
|
||||||
|
start_time=val[0][2]
|
||||||
|
)
|
||||||
|
|
||||||
|
def get(self, key:str) -> Optional[Any]:
|
||||||
|
cur = self.db.cursor()
|
||||||
|
val = cur.execute("SELECT * FROM documents WHERE name = ?", (key,)).fetchall()
|
||||||
|
return json.loads(val[0][1]) if val else None
|
||||||
|
|
||||||
|
def put(self, key:str, val:Any) -> None:
|
||||||
|
cur = self.db.cursor()
|
||||||
|
cur.execute("DELETE FROM documents WHERE name = ?", (key,))
|
||||||
|
cur.execute("INSERT INTO documents VALUES (?, ?)", (key, json.dumps(val)))
|
||||||
|
self.db.commit()
|
||||||
|
|
|
@ -4,113 +4,108 @@ import asyncio
|
||||||
import datetime
|
import datetime
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from typing import List, Dict, Union, Optional, Any, Type
|
from typing import List, Dict, Tuple, Union, Optional, Any, Type, get_type_hints
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from configparser import ConfigParser
|
||||||
|
|
||||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||||
|
|
||||||
from aiocraft.client import MinecraftClient
|
from aiocraft.client import MinecraftClient
|
||||||
from aiocraft.mc.packet import Packet
|
from aiocraft.mc.packet import Packet
|
||||||
from aiocraft.mc.definitions import Difficulty, Dimension, Gamemode, BlockPos, Item
|
|
||||||
|
|
||||||
from aiocraft.mc.proto.play.clientbound import (
|
|
||||||
PacketRespawn, PacketLogin, PacketPosition, PacketUpdateHealth, PacketExperience, PacketSetSlot,
|
|
||||||
PacketAbilities, PacketPlayerInfo, PacketChat as PacketChatMessage, PacketHeldItemSlot as PacketHeldItemChange
|
|
||||||
)
|
|
||||||
from aiocraft.mc.proto.play.serverbound import (
|
|
||||||
PacketTeleportConfirm, PacketClientCommand, PacketSettings, PacketChat,
|
|
||||||
PacketHeldItemSlot
|
|
||||||
)
|
|
||||||
|
|
||||||
|
from .storage import Storage
|
||||||
from .notifier import Notifier
|
from .notifier import Notifier
|
||||||
from .events import ChatEvent
|
from .game import GameState, GameChat, GameInventory, GameTablist, GameWorld
|
||||||
from .events.chat import MessageType
|
|
||||||
from .modules.module import LogicModule
|
|
||||||
|
|
||||||
REMOVE_COLOR_FORMATS = re.compile(r"§[0-9a-z]")
|
REMOVE_COLOR_FORMATS = re.compile(r"§[0-9a-z]")
|
||||||
|
|
||||||
class TreepuncherEvents(Enum):
|
class ConfigObject:
|
||||||
DIED = 0
|
def __getitem__(self, key:str) -> Any:
|
||||||
IN_GAME = 1
|
return getattr(self, key)
|
||||||
|
|
||||||
class Treepuncher(MinecraftClient):
|
class Addon:
|
||||||
in_game : bool
|
name : str
|
||||||
gamemode : Gamemode
|
config : ConfigObject
|
||||||
dimension : Dimension
|
_client : 'Treepuncher'
|
||||||
difficulty : Difficulty
|
|
||||||
join_time : datetime.datetime
|
|
||||||
|
|
||||||
hp : float
|
@dataclass(frozen=True)
|
||||||
food : float
|
class Options(ConfigObject):
|
||||||
xp : float
|
pass
|
||||||
lvl : int
|
|
||||||
total_xp : int
|
|
||||||
|
|
||||||
slot : int
|
@property
|
||||||
inventory : List[Item]
|
def client(self) -> 'Treepuncher':
|
||||||
# TODO inventory
|
return self._client
|
||||||
|
|
||||||
position : BlockPos
|
def __init__(self, client:'Treepuncher'):
|
||||||
# TODO world
|
self._client = client
|
||||||
|
self.name = type(self).__name__
|
||||||
|
cfg = self._client.config
|
||||||
|
kwargs : Dict[str, Any] = {}
|
||||||
|
for name, clazz in get_type_hints(self.Options).items():
|
||||||
|
default = getattr(self.Options, name, None)
|
||||||
|
if cfg.has_option(self.name, name):
|
||||||
|
if clazz is bool:
|
||||||
|
kwargs[name] = self._client.config[self.name].getboolean(name)
|
||||||
|
else:
|
||||||
|
kwargs[name] = clazz(self._client.config[self.name].get(name))
|
||||||
|
elif default is None:
|
||||||
|
raise ValueError(f"Missing required value '{name}' of type '{clazz.__name__}' in section '{self.name}'")
|
||||||
|
else: # not really necessary since it's a dataclass but whatever
|
||||||
|
kwargs[name] = default
|
||||||
|
self.config = self.Options(**kwargs)
|
||||||
|
|
||||||
tablist : Dict[uuid.UUID, dict]
|
async def initialize(self):
|
||||||
|
pass
|
||||||
|
|
||||||
# TODO player abilities
|
async def cleanup(self):
|
||||||
# walk_speed : float
|
pass
|
||||||
# fly_speed : float
|
|
||||||
# flags : int
|
class Treepuncher(
|
||||||
|
GameState,
|
||||||
|
GameChat,
|
||||||
|
GameInventory,
|
||||||
|
GameTablist,
|
||||||
|
GameWorld
|
||||||
|
):
|
||||||
|
name : str
|
||||||
|
config : ConfigParser
|
||||||
|
storage : Storage
|
||||||
|
|
||||||
notifier : Notifier
|
notifier : Notifier
|
||||||
scheduler : AsyncIOScheduler
|
scheduler : AsyncIOScheduler
|
||||||
modules : List[LogicModule]
|
modules : List[Addon]
|
||||||
ctx : Dict[Any, Any]
|
ctx : Dict[Any, Any]
|
||||||
|
|
||||||
def __init__(self, *args, notifier:Notifier=None, **kwargs):
|
def __init__(self, name:str, *args, config_file:str=None, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.ctx = dict()
|
self.ctx = dict()
|
||||||
|
|
||||||
self.in_game = False
|
self.name = name
|
||||||
self.gamemode = Gamemode.SURVIVAL
|
self.config = ConfigParser()
|
||||||
self.dimension = Dimension.OVERWORLD
|
config_path = config_file or f'config-{self.name}.ini'
|
||||||
self.difficulty = Difficulty.HARD
|
self.config.read(config_path)
|
||||||
self.join_time = datetime.datetime(2011, 11, 18)
|
|
||||||
|
|
||||||
self.hp = 20.0
|
self.storage = Storage(self.name)
|
||||||
self.food = 20.0
|
|
||||||
self.xp = 0.0
|
|
||||||
self.lvl = 0
|
|
||||||
|
|
||||||
self.slot = 0
|
|
||||||
self.inventory = [ Item() for _ in range(46) ]
|
|
||||||
|
|
||||||
self.position = BlockPos(0, 0, 0)
|
|
||||||
|
|
||||||
self.tablist = {}
|
|
||||||
|
|
||||||
self._register_handlers()
|
|
||||||
self.modules = []
|
self.modules = []
|
||||||
|
|
||||||
self.notifier = notifier or Notifier()
|
# self.notifier = notifier or Notifier()
|
||||||
tz = datetime.datetime.now(datetime.timezone.utc).astimezone().tzname() # APScheduler will complain if I don't specify a timezone...
|
tz = datetime.datetime.now(datetime.timezone.utc).astimezone().tzname() # APScheduler will complain if I don't specify a timezone...
|
||||||
self.scheduler = AsyncIOScheduler(timezone=tz)
|
self.scheduler = AsyncIOScheduler(timezone=tz)
|
||||||
logging.getLogger('apscheduler.executors.default').setLevel(logging.WARNING) # So it's way less spammy
|
logging.getLogger('apscheduler.executors.default').setLevel(logging.WARNING) # So it's way less spammy
|
||||||
self.scheduler.start(paused=True)
|
self.scheduler.start(paused=True)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self) -> str:
|
def playerName(self) -> str:
|
||||||
if self.online_mode and self.token:
|
if self.online_mode:
|
||||||
return self.token.selectedProfile.name
|
if self._authenticator and self._authenticator.selectedProfile:
|
||||||
if not self.online_mode and self.username:
|
return self._authenticator.selectedProfile.name
|
||||||
return self.username
|
raise ValueError("Username unknown: client not authenticated")
|
||||||
raise ValueError("No token or username given")
|
else:
|
||||||
|
if self._username:
|
||||||
@property
|
return self._username
|
||||||
def hotbar(self) -> List[Item]:
|
raise ValueError("No username configured for offline mode")
|
||||||
return self.inventory[36:45]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def selected(self) -> Item:
|
|
||||||
return self.hotbar[self.slot]
|
|
||||||
|
|
||||||
async def start(self):
|
async def start(self):
|
||||||
await self.notifier.initialize(self)
|
await self.notifier.initialize(self)
|
||||||
|
@ -123,170 +118,13 @@ class Treepuncher(MinecraftClient):
|
||||||
self.scheduler.pause()
|
self.scheduler.pause()
|
||||||
await super().stop(force=force)
|
await super().stop(force=force)
|
||||||
for m in self.modules:
|
for m in self.modules:
|
||||||
await m.cleanup(self)
|
await m.cleanup()
|
||||||
await self.notifier.cleanup(self)
|
await self.notifier.cleanup(self)
|
||||||
|
|
||||||
def add(self, module:LogicModule):
|
def install(self, module:Type[Addon]) -> Type[Addon]:
|
||||||
module.register(self)
|
self.modules.append(module(self))
|
||||||
self.modules.append(module)
|
return module
|
||||||
|
|
||||||
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)
|
|
||||||
self.register(PacketChatMessage, process_chat_packet)
|
|
||||||
return fun
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
def on_death(self):
|
|
||||||
def wrapper(fun):
|
|
||||||
return self.register(TreepuncherEvents.DIED, fun)
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
def on_joined_world(self):
|
|
||||||
def wrapper(fun):
|
|
||||||
return self.register(TreepuncherEvents.IN_GAME, fun)
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
async def write(self, packet:Packet, wait:bool=False):
|
async def write(self, packet:Packet, wait:bool=False):
|
||||||
await self.dispatcher.write(packet, wait)
|
await self.dispatcher.write(packet, wait)
|
||||||
|
|
||||||
async def chat(self, message:str, whisper:str=None, wait:bool=False):
|
|
||||||
if whisper:
|
|
||||||
message = f"/w {whisper} {message}"
|
|
||||||
await self.dispatcher.write(
|
|
||||||
PacketChat(
|
|
||||||
self.dispatcher.proto,
|
|
||||||
message=message
|
|
||||||
),
|
|
||||||
wait=wait
|
|
||||||
)
|
|
||||||
|
|
||||||
async def set_slot(self, slot:int):
|
|
||||||
self.slot = slot
|
|
||||||
await self.dispatcher.write(PacketHeldItemSlot(self.dispatcher.proto, slotId=slot))
|
|
||||||
|
|
||||||
def _register_handlers(self):
|
|
||||||
@self.on_disconnected()
|
|
||||||
async def disconnected_cb():
|
|
||||||
self.in_game = False
|
|
||||||
|
|
||||||
@self.on_connected()
|
|
||||||
async def connected_cb():
|
|
||||||
self.tablist.clear()
|
|
||||||
|
|
||||||
@self.on_packet(PacketSetSlot)
|
|
||||||
async def on_set_slot(packet:PacketSetSlot):
|
|
||||||
if packet.windowId == 0: # player inventory
|
|
||||||
self.inventory[packet.slot] = packet.item
|
|
||||||
|
|
||||||
@self.on_packet(PacketHeldItemChange)
|
|
||||||
async def on_held_item_change(packet:PacketHeldItemChange):
|
|
||||||
self.slot = packet.slot
|
|
||||||
|
|
||||||
@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)
|
|
||||||
self.join_time = datetime.datetime.now()
|
|
||||||
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.run_callbacks(TreepuncherEvents.IN_GAME)
|
|
||||||
await self.write(
|
|
||||||
PacketSettings(
|
|
||||||
self.dispatcher.proto,
|
|
||||||
locale="en_US",
|
|
||||||
viewDistance=4,
|
|
||||||
chatFlags=0,
|
|
||||||
chatColors=True,
|
|
||||||
skinParts=0xF,
|
|
||||||
mainHand=0,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
await self.write(PacketClientCommand(self.dispatcher.proto, actionId=0))
|
|
||||||
|
|
||||||
@self.on_packet(PacketPosition)
|
|
||||||
async def player_rubberband_cb(packet:PacketPosition):
|
|
||||||
self.position = BlockPos(packet.x, packet.y, packet.z)
|
|
||||||
self._logger.info(
|
|
||||||
"Position synchronized : (x:%.0f,y:%.0f,z:%.0f)",
|
|
||||||
self.position.x, self.position.y, self.position.z
|
|
||||||
)
|
|
||||||
await self.dispatcher.write(
|
|
||||||
PacketTeleportConfirm(
|
|
||||||
self.dispatcher.proto,
|
|
||||||
teleportId=packet.teleportId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
@self.on_packet(PacketUpdateHealth)
|
|
||||||
async def player_hp_cb(packet:PacketUpdateHealth):
|
|
||||||
died = packet.health != self.hp and packet.health <= 0
|
|
||||||
self.hp = packet.health
|
|
||||||
self.food = packet.food + packet.foodSaturation
|
|
||||||
if died:
|
|
||||||
self.run_callbacks(TreepuncherEvents.DIED)
|
|
||||||
self._logger.info("Dead, respawning...")
|
|
||||||
await asyncio.sleep(0.5)
|
|
||||||
await self.dispatcher.write(
|
|
||||||
PacketClientCommand(self.dispatcher.proto, actionId=0) # respawn
|
|
||||||
)
|
|
||||||
|
|
||||||
@self.on_packet(PacketExperience)
|
|
||||||
async def player_xp_cb(packet:PacketExperience):
|
|
||||||
if packet.level != self.lvl:
|
|
||||||
self._logger.info("Level up : %d", packet.level)
|
|
||||||
self.xp = packet.experienceBar
|
|
||||||
self.lvl = packet.level
|
|
||||||
self.total_xp = packet.totalExperience
|
|
||||||
|
|
||||||
@self.on_packet(PacketPlayerInfo)
|
|
||||||
async def tablist_update(packet:PacketPlayerInfo):
|
|
||||||
for record in packet.data:
|
|
||||||
uid = record['UUID']
|
|
||||||
if packet.action != 0 and uid not in self.tablist:
|
|
||||||
continue # TODO this happens kinda often but doesn't seem to be an issue?
|
|
||||||
if packet.action == 0:
|
|
||||||
self.tablist[uid] = record
|
|
||||||
self.tablist[uid]['joinTime'] = datetime.datetime.now()
|
|
||||||
elif packet.action == 1:
|
|
||||||
self.tablist[uid]['gamemode'] = record['gamemode']
|
|
||||||
elif packet.action == 2:
|
|
||||||
self.tablist[uid]['ping'] = record['ping']
|
|
||||||
elif packet.action == 3:
|
|
||||||
self.tablist[uid]['displayName'] = record['displayName']
|
|
||||||
elif packet.action == 4:
|
|
||||||
self.tablist.pop(uid, None)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue