diff --git a/treepuncher/__main__.py b/treepuncher/__main__.py index 5b82a6c..00e00de 100644 --- a/treepuncher/__main__.py +++ b/treepuncher/__main__.py @@ -36,6 +36,8 @@ def main(): for addon in addons: help_text += f"\n {addon.__name__} \t{addon.__doc__ or ''}" cfg_clazz = get_type_hints(addon)['config'] + if cfg_clazz is ConfigObject: + continue # it's the superclass type hint for name, field in cfg_clazz.__dataclass_fields__.items(): default = field.default if field.default is not MISSING \ else field.default_factory() if field.default_factory is not MISSING \ @@ -61,6 +63,8 @@ def main(): 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-filter', dest='use_packet_whitelist', action='store_const', const=False, default=True, help="disable packet whitelist, will decrease performance") + parser.add_argument('--addons', dest='add', nargs='+', type=str, default=[a.__name__ for a in addons], help='specify addons to enable, defaults to all') + # TODO find a better way to specify which addons are enabled args = parser.parse_args() @@ -74,15 +78,18 @@ def main(): client = Treepuncher( args.name, args.server, - use_packet_whitelist=use_packet_whitelist, + use_packet_whitelist=args.use_packet_whitelist, login_code=code, client_id=args.cid, client_secret=args.secret, redirect_uri=args.uri ) + enabled_addons = set(a.lower() for a in args.add) for addon in addons: - client.install(addon) + if addon.__name__.lower() in enabled_addons: + logging.info("Installing '%s'", addon.__name__) + client.install(addon) client.run() diff --git a/treepuncher/game/state.py b/treepuncher/game/state.py index 27b1a9f..da5ae57 100644 --- a/treepuncher/game/state.py +++ b/treepuncher/game/state.py @@ -1,5 +1,6 @@ import asyncio import datetime +import functools from aiocraft.client import MinecraftClient from aiocraft.mc.definitions import Gamemode, Dimension, Difficulty @@ -27,20 +28,20 @@ class GameState(MinecraftClient): def on_death(self): def decorator(fun): - @functool.wraps(fun) + @functools.wraps(fun) async def wrapper(): event = DeathEvent() return await fun(event) - return self.register(DeathEvent.SENTINEL, fun) + return self.register(DeathEvent.SENTINEL, wrapper) return decorator def on_joined_world(self): def decorator(fun): - @functool.wraps(fun) + @functools.wraps(fun) async def wrapper(): event = JoinGameEvent() return await fun(event) - return self.register(JoinGameEvent.SENTINEL, callback) + return self.register(JoinGameEvent.SENTINEL, wrapper) return decorator def __init__(self, *args, **kwargs): diff --git a/treepuncher/notifier.py b/treepuncher/notifier.py index 13a56a0..8e7073b 100644 --- a/treepuncher/notifier.py +++ b/treepuncher/notifier.py @@ -1,6 +1,6 @@ from typing import Callable, List -class Notifier: +class Notifier: # TODO this should be an Addon too! _report_functions : List[Callable] def __init__(self): @@ -16,9 +16,9 @@ class Notifier: def notify(self, text, log:bool = False, **kwargs): print(text) - async def initialize(self, _client:'Treepuncher'): + async def initialize(self): pass - async def cleanup(self, _client:'Treepuncher'): + async def cleanup(self): pass diff --git a/treepuncher/storage.py b/treepuncher/storage.py index d018e11..1a292a3 100644 --- a/treepuncher/storage.py +++ b/treepuncher/storage.py @@ -1,3 +1,4 @@ +import os import json import sqlite3 @@ -16,7 +17,10 @@ class Storage: def __init__(self, name:str): self.name = name + init = not os.path.isfile(f"{name}.session") self.db = sqlite3.connect(f'{name}.session') + if init: + self._init_db() def __del__(self): self.close() diff --git a/treepuncher/treepuncher.py b/treepuncher/treepuncher.py index e96652f..458e14c 100644 --- a/treepuncher/treepuncher.py +++ b/treepuncher/treepuncher.py @@ -7,7 +7,8 @@ import uuid from typing import List, Dict, Tuple, Union, Optional, Any, Type, get_type_hints from enum import Enum -from dataclasses import dataclass +from time import time +from dataclasses import dataclass, MISSING from configparser import ConfigParser from apscheduler.schedulers.asyncio import AsyncIOScheduler @@ -27,12 +28,12 @@ class ConfigObject: class Addon: name : str + config : ConfigObject _client : 'Treepuncher' @dataclass(frozen=True) class Options(ConfigObject): pass - config : Options @property def client(self) -> 'Treepuncher': @@ -43,21 +44,25 @@ class Addon: self.name = type(self).__name__ cfg = self._client.config opts : 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: - opts[name] = self._client.config[self.name].getboolean(name) - else: - opts[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 - opts[name] = default + cfg_clazz = get_type_hints(type(self))['config'] + if cfg_clazz is not ConfigObject: + for name, field in cfg_clazz.__dataclass_fields__.items(): + default = field.default if field.default is not MISSING \ + else field.default_factory() if field.default_factory is not MISSING \ + else MISSING + if cfg.has_option(self.name, name): + if field.type is bool: + opts[name] = self._client.config[self.name].getboolean(name) + else: + opts[name] = field.type(self._client.config[self.name].get(name)) + elif default is MISSING: + raise ValueError(f"Missing required value '{name}' of type '{field.type.__name__}' in section '{self.name}'") + else: # not really necessary since it's a dataclass but whatever + opts[name] = default self.config = self.Options(**opts) - self.register(client) + self.register() - def register(self, client:'Treepuncher'): + def register(self): pass async def initialize(self): @@ -82,25 +87,26 @@ class Treepuncher( modules : List[Addon] ctx : Dict[Any, Any] - def __init__(self, name:str, *args, config_file:str=None, **kwargs): + def __init__(self, name:str, *args, config_file:str=None, notifier:Notifier=None, **kwargs): super().__init__(*args, **kwargs) self.ctx = dict() self.name = name self.config = ConfigParser() - config_path = config_file or f'config-{self.name}.ini' + config_path = config_file or f'{self.name}.ini' self.config.read(config_path) self.storage = Storage(self.name) prev = self.storage.system() # if this isn't 1st time, this won't be None. Load token from there if prev: if self.name != prev.name: - self._logger.warning("Saved token session name differs from current") + self._logger.warning("Saved credentials are not from this session") self._authenticator.deserialize(json.loads(prev.token)) + self._logger.info("Loaded credentials") 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... self.scheduler = AsyncIOScheduler(timezone=tz) logging.getLogger('apscheduler.executors.default').setLevel(logging.WARNING) # So it's way less spammy @@ -127,9 +133,9 @@ class Treepuncher( self.storage._set_state(state) async def start(self): - await self.notifier.initialize(self) + await self.notifier.initialize() for m in self.modules: - await m.initialize(self) + await m.initialize() await super().start() self.scheduler.resume() @@ -138,12 +144,8 @@ class Treepuncher( await super().stop(force=force) for m in self.modules: await m.cleanup() - await self.notifier.cleanup(self) + await self.notifier.cleanup() def install(self, module:Type[Addon]) -> Type[Addon]: self.modules.append(module(self)) return module - - async def write(self, packet:Packet, wait:bool=False): - await self.dispatcher.write(packet, wait) -