mirror of
https://github.com/solero/houdini.git
synced 2025-10-17 21:08:17 +00:00
Initial commit
This commit is contained in:
155
Houdini/Converters.py
Normal file
155
Houdini/Converters.py
Normal file
@@ -0,0 +1,155 @@
|
||||
import zope.interface
|
||||
from zope.interface import implementer
|
||||
|
||||
|
||||
class IConverter(zope.interface.Interface):
|
||||
|
||||
description = zope.interface.Attribute("""A short description of the purpose of the converter""")
|
||||
|
||||
async def convert(self):
|
||||
raise NotImplementedError('Converter must derive this class!')
|
||||
|
||||
|
||||
class Converter:
|
||||
|
||||
__slots__ = ['p', 'argument']
|
||||
|
||||
def __init__(self, p, argument):
|
||||
self.p = p
|
||||
self.argument = argument
|
||||
|
||||
|
||||
@implementer(IConverter)
|
||||
class CredentialsConverter(Converter):
|
||||
|
||||
description = """Used for obtaining login credentials from XML login data"""
|
||||
|
||||
async def convert(self):
|
||||
username = self.argument[0][0].text
|
||||
password = self.argument[0][1].text
|
||||
return username, password
|
||||
|
||||
|
||||
@implementer(IConverter)
|
||||
class VersionChkConverter(Converter):
|
||||
|
||||
description = """Used for checking the verChk version number"""
|
||||
|
||||
async def convert(self):
|
||||
return self.argument[0].get('v')
|
||||
|
||||
|
||||
@implementer(IConverter)
|
||||
class ConnectedPenguinConverter(Converter):
|
||||
|
||||
description = """Converts a penguin ID into a live penguin instance
|
||||
or none if the player is offline"""
|
||||
|
||||
async def convert(self):
|
||||
penguin_id = int(self.argument)
|
||||
if penguin_id in self.p.server.penguins_by_id:
|
||||
return self.p.server.penguins_by_id[penguin_id]
|
||||
return None
|
||||
|
||||
|
||||
@implementer(IConverter)
|
||||
class ConnectedIglooConverter(Converter):
|
||||
|
||||
description = """Converts a penguin ID into a live igloo instance or
|
||||
none if it's not available"""
|
||||
|
||||
async def convert(self):
|
||||
igloo_id = int(self.argument)
|
||||
if igloo_id in self.p.server.igloo_map:
|
||||
return self.p.server.igloo_map[igloo_id]
|
||||
return None
|
||||
|
||||
|
||||
@implementer(IConverter)
|
||||
class RoomConverter(Converter):
|
||||
|
||||
description = """Converts a room ID into a Houdini.Data.Room instance"""
|
||||
|
||||
async def convert(self):
|
||||
room_id = int(self.argument)
|
||||
if room_id in self.p.server.rooms:
|
||||
return self.p.server.rooms[room_id]
|
||||
return None
|
||||
|
||||
|
||||
@implementer(IConverter)
|
||||
class ItemConverter(Converter):
|
||||
|
||||
description = """Converts an item ID into a Houdini.Data.Item instance"""
|
||||
|
||||
async def convert(self):
|
||||
item_id = int(self.argument)
|
||||
if item_id in self.p.server.items:
|
||||
return self.p.server.items[item_id]
|
||||
return None
|
||||
|
||||
|
||||
@implementer(IConverter)
|
||||
class IglooConverter(Converter):
|
||||
|
||||
description = """Converts an igloo ID into a Houdini.Data.Igloo instance"""
|
||||
|
||||
async def convert(self):
|
||||
igloo_id = int(self.argument)
|
||||
if igloo_id in self.p.server.igloos:
|
||||
return self.p.server.igloos[igloo_id]
|
||||
return None
|
||||
|
||||
|
||||
@implementer(IConverter)
|
||||
class FurnitureConverter(Converter):
|
||||
|
||||
description = """Converts a furniture ID into a Houdini.Data.Furniture instance"""
|
||||
|
||||
async def convert(self):
|
||||
furniture_id = int(self.argument)
|
||||
if furniture_id in self.p.server.furniture:
|
||||
return self.p.server.furniture[furniture_id]
|
||||
return None
|
||||
|
||||
|
||||
@implementer(IConverter)
|
||||
class FlooringConverter(Converter):
|
||||
|
||||
description = """Converts a flooring ID into a Houdini.Data.Flooring instance"""
|
||||
|
||||
async def convert(self):
|
||||
flooring_id = int(self.argument)
|
||||
if flooring_id in self.p.server.flooring:
|
||||
return self.p.server.flooring[flooring_id]
|
||||
return None
|
||||
|
||||
|
||||
@implementer(IConverter)
|
||||
class StampConverter(Converter):
|
||||
|
||||
description = """Converts a stamp ID into a Houdini.Data.Stamp instance"""
|
||||
|
||||
async def convert(self):
|
||||
stamp_id = int(self.argument)
|
||||
if stamp_id in self.p.server.stamps:
|
||||
return self.p.server.stamps[stamp_id]
|
||||
return None
|
||||
|
||||
|
||||
@implementer(IConverter)
|
||||
class VerticalConverter(Converter):
|
||||
|
||||
description = """Converts vertically separated values into an int list"""
|
||||
|
||||
async def convert(self):
|
||||
return map(int, self.argument.split('|'))
|
||||
|
||||
|
||||
@implementer(IConverter)
|
||||
class CommaConverter(Converter):
|
||||
|
||||
description = """Converts comma separated values into an int list"""
|
||||
|
||||
async def convert(self):
|
||||
return map(int, self.argument.split(','))
|
38
Houdini/Crypto.py
Normal file
38
Houdini/Crypto.py
Normal file
@@ -0,0 +1,38 @@
|
||||
import hashlib
|
||||
from random import choice
|
||||
from string import ascii_letters, digits
|
||||
|
||||
|
||||
class Crypto:
|
||||
|
||||
@staticmethod
|
||||
def hash(string):
|
||||
if isinstance(string, int):
|
||||
string = str(string)
|
||||
|
||||
return hashlib.md5(string.encode("utf-8")).hexdigest()
|
||||
|
||||
@staticmethod
|
||||
def generate_random_key():
|
||||
character_selection = ascii_letters + digits
|
||||
|
||||
return "".join(choice(character_selection) for _ in range(16))
|
||||
|
||||
@staticmethod
|
||||
def encrypt_password(password, digest=True):
|
||||
if digest:
|
||||
password = Crypto.hash(password)
|
||||
|
||||
swapped_hash = password[16:32] + password[0:16]
|
||||
|
||||
return swapped_hash
|
||||
|
||||
@staticmethod
|
||||
def get_login_hash(password, rndk):
|
||||
key = Crypto.encrypt_password(password, False)
|
||||
key += rndk
|
||||
key += "Y(02.>'H}t\":E1"
|
||||
|
||||
login_hash = Crypto.encrypt_password(key)
|
||||
|
||||
return login_hash
|
4
Houdini/Data/__init__.py
Normal file
4
Houdini/Data/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from gino import Gino
|
||||
|
||||
db = Gino()
|
||||
|
79
Houdini/Events/HandlerFileEvent.py
Normal file
79
Houdini/Events/HandlerFileEvent.py
Normal file
@@ -0,0 +1,79 @@
|
||||
import sys
|
||||
import importlib
|
||||
from watchdog.events import FileSystemEventHandler
|
||||
|
||||
from Houdini.Handlers import listeners_from_module
|
||||
from Houdini.Events import evaluate_handler_file_event, remove_handlers_by_module
|
||||
|
||||
|
||||
class HandlerFileEventHandler(FileSystemEventHandler):
|
||||
|
||||
def __init__(self, server):
|
||||
self.logger = server.logger
|
||||
self.server = server
|
||||
|
||||
def on_created(self, event):
|
||||
handler_module_details = evaluate_handler_file_event(event)
|
||||
|
||||
if not handler_module_details:
|
||||
return
|
||||
|
||||
handler_module_path, handler_module = handler_module_details
|
||||
|
||||
if "__init__.py" in handler_module_path:
|
||||
return
|
||||
|
||||
self.logger.debug("New handler module detected %s", handler_module)
|
||||
|
||||
try:
|
||||
module = importlib.import_module(handler_module)
|
||||
listeners_from_module(self.server.xt_listeners, self.server.xml_listeners, module)
|
||||
except Exception as import_error:
|
||||
self.logger.error("%s detected in %s, not importing.", import_error.__class__.__name__, handler_module)
|
||||
|
||||
def on_deleted(self, event):
|
||||
handler_module_details = evaluate_handler_file_event(event)
|
||||
|
||||
if not handler_module_details:
|
||||
return
|
||||
|
||||
handler_module_path, handler_module = handler_module_details
|
||||
|
||||
if handler_module not in sys.modules:
|
||||
return
|
||||
|
||||
self.logger.debug("Deleting listeners registered by %s..", handler_module)
|
||||
|
||||
remove_handlers_by_module(self.server.xt_listeners, self.server.xml_listeners, handler_module_path)
|
||||
|
||||
def on_modified(self, event):
|
||||
handler_module_details = evaluate_handler_file_event(event)
|
||||
|
||||
if not handler_module_details:
|
||||
return
|
||||
|
||||
handler_module_path, handler_module = handler_module_details
|
||||
|
||||
if handler_module not in sys.modules:
|
||||
return False
|
||||
|
||||
self.logger.info("Reloading %s", handler_module)
|
||||
|
||||
xt_listeners, xml_listeners = remove_handlers_by_module(self.server.xt_listeners, self.server.xml_listeners,
|
||||
handler_module_path)
|
||||
|
||||
handler_module_object = sys.modules[handler_module]
|
||||
|
||||
try:
|
||||
module = importlib.reload(handler_module_object)
|
||||
listeners_from_module(self.server.xt_listeners, self.server.xml_listeners, module)
|
||||
|
||||
self.logger.info("Successfully reloaded %s!", handler_module)
|
||||
except Exception as rebuild_error:
|
||||
self.logger.error("%s detected in %s, not reloading.", rebuild_error.__class__.__name__, handler_module)
|
||||
self.logger.info("Restoring handler references...")
|
||||
|
||||
self.server.xt_listeners = xt_listeners
|
||||
self.server.xml_listeners = xml_listeners
|
||||
|
||||
self.logger.info("Handler references restored. Phew!")
|
54
Houdini/Events/__init__.py
Normal file
54
Houdini/Events/__init__.py
Normal file
@@ -0,0 +1,54 @@
|
||||
import os
|
||||
import copy
|
||||
|
||||
|
||||
def evaluate_handler_file_event(handler_file_event):
|
||||
# Ignore all directory events
|
||||
if handler_file_event.is_directory:
|
||||
return False
|
||||
|
||||
handler_module_path = handler_file_event.src_path[2:]
|
||||
|
||||
# Ignore non-Python files
|
||||
if handler_module_path[-3:] != ".py":
|
||||
return False
|
||||
|
||||
handler_module = handler_module_path.replace(os.path.sep, ".")[:-3]
|
||||
|
||||
return handler_module_path, handler_module
|
||||
|
||||
|
||||
def evaluate_plugin_file_event(plugin_file_event):
|
||||
# Ignore all directory events
|
||||
if plugin_file_event.is_directory:
|
||||
return False
|
||||
|
||||
handler_module_path = plugin_file_event.src_path[2:]
|
||||
|
||||
# Ignore non-Python files
|
||||
if handler_module_path[-3:] != ".py":
|
||||
return False
|
||||
|
||||
# Remove file extension and replace path separator with dots. Then make like a banana.. and split.
|
||||
handler_module_tokens = handler_module_path.replace(os.path.sep, ".")[:-3].split(".")
|
||||
|
||||
if handler_module_tokens.pop() == "__init__":
|
||||
return handler_module_path, ".".join(handler_module_tokens)
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def remove_handlers_by_module(xt_listeners, xml_listeners, handler_module_path):
|
||||
def remove_handlers(remove_handler_items):
|
||||
for handler_id, handler_listeners in remove_handler_items:
|
||||
for handler_listener in handler_listeners:
|
||||
if handler_listener.handler_file == handler_module_path:
|
||||
handler_listeners.remove(handler_listener)
|
||||
|
||||
xt_handler_collection = copy.copy(xt_listeners)
|
||||
remove_handlers(xt_listeners.items())
|
||||
|
||||
xml_handler_collection = copy.copy(xml_listeners)
|
||||
remove_handlers(xml_listeners.items())
|
||||
|
||||
return xt_handler_collection, xml_handler_collection
|
15
Houdini/Handlers/Login/Login.py
Normal file
15
Houdini/Handlers/Login/Login.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from Houdini import Handlers
|
||||
from Houdini.Handlers import XTPacket, XMLPacket
|
||||
from Houdini.Converters import CredentialsConverter, CommaConverter
|
||||
|
||||
|
||||
@Handlers.handler(XMLPacket('login'))
|
||||
async def handle_login(p, credentials: CredentialsConverter):
|
||||
username, password = credentials
|
||||
p.logger.info('{}:{} is logging in!'.format(username, password))
|
||||
|
||||
|
||||
@Handlers.handler(XTPacket('t', 'c'), pre_login=True)
|
||||
@Handlers.player_in_room(100)
|
||||
async def handle_test(p, numbers: CommaConverter):
|
||||
print(list(numbers))
|
0
Houdini/Handlers/Login/__init__.py
Normal file
0
Houdini/Handlers/Login/__init__.py
Normal file
12
Houdini/Handlers/Play/Navigation.py
Normal file
12
Houdini/Handlers/Play/Navigation.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from Houdini.Handlers import Handlers, XTPacket
|
||||
from Houdini.Converters import RoomConverter
|
||||
|
||||
|
||||
@Handlers.handler(XTPacket('j', 'js'))
|
||||
async def handle_join_world(p, is_moderator: bool, is_mascot: bool, is_member: bool):
|
||||
print(p, is_moderator, is_mascot, is_member)
|
||||
|
||||
|
||||
@Handlers.handler(XTPacket('j', 'jr'))
|
||||
async def handle_join_room(p, room: RoomConverter):
|
||||
print(room)
|
0
Houdini/Handlers/Play/__init__.py
Normal file
0
Houdini/Handlers/Play/__init__.py
Normal file
353
Houdini/Handlers/__init__.py
Normal file
353
Houdini/Handlers/__init__.py
Normal file
@@ -0,0 +1,353 @@
|
||||
import inspect
|
||||
import time
|
||||
import enum
|
||||
import os
|
||||
import asyncio
|
||||
from types import FunctionType
|
||||
|
||||
from Houdini.Converters import IConverter
|
||||
|
||||
|
||||
def get_relative_function_path(function_obj):
|
||||
abs_function_file = inspect.getfile(function_obj)
|
||||
rel_function_file = os.path.relpath(abs_function_file)
|
||||
|
||||
return rel_function_file
|
||||
|
||||
|
||||
def get_converter(component):
|
||||
if component.annotation is component.empty:
|
||||
return str
|
||||
return component.annotation
|
||||
|
||||
|
||||
async def do_conversion(converter, p, component_data):
|
||||
if IConverter.implementedBy(converter):
|
||||
converter_instance = converter(p, component_data)
|
||||
if asyncio.iscoroutinefunction(converter_instance.convert):
|
||||
return await converter_instance.convert()
|
||||
return converter_instance.convert()
|
||||
return converter(component_data)
|
||||
|
||||
|
||||
class _Packet:
|
||||
__slots__ = ['id']
|
||||
|
||||
def __init__(self):
|
||||
self.id = None
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.id == other.id
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.id)
|
||||
|
||||
|
||||
class XTPacket(_Packet):
|
||||
def __init__(self, *packet_id):
|
||||
super().__init__()
|
||||
self.id = '#'.join(packet_id)
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.id)
|
||||
|
||||
|
||||
class XMLPacket(_Packet):
|
||||
def __init__(self, packet_id):
|
||||
super().__init__()
|
||||
self.id = packet_id
|
||||
|
||||
|
||||
class Priority(enum.Enum):
|
||||
Override = 3
|
||||
High = 2
|
||||
Low = 1
|
||||
|
||||
|
||||
class BucketType(enum.Enum):
|
||||
Default = 1
|
||||
Penguin = 1
|
||||
Server = 2
|
||||
|
||||
|
||||
class _Cooldown:
|
||||
|
||||
__slots__ = ['rate', 'per', 'bucket_type', 'last',
|
||||
'_window', '_tokens']
|
||||
|
||||
def __init__(self, per, rate, bucket_type):
|
||||
self.per = float(per)
|
||||
self.rate = int(rate)
|
||||
self.bucket_type = bucket_type
|
||||
self.last = 0.0
|
||||
|
||||
self._window = 0.0
|
||||
self._tokens = self.rate
|
||||
|
||||
@property
|
||||
def is_cooling(self):
|
||||
current = time.time()
|
||||
self.last = current
|
||||
|
||||
if self._tokens == self.rate:
|
||||
self._window = current
|
||||
|
||||
if current > self._window + self.per:
|
||||
self._tokens = self.rate
|
||||
self._window = current
|
||||
|
||||
if self._tokens == 0:
|
||||
return self.per - (current - self._window)
|
||||
|
||||
self._tokens -= 1
|
||||
if self._tokens == 0:
|
||||
self._window = current
|
||||
|
||||
def reset(self):
|
||||
self._tokens = self.rate
|
||||
self.last = 0.0
|
||||
|
||||
def copy(self):
|
||||
return _Cooldown(self.per, self.rate, self.bucket_type)
|
||||
|
||||
|
||||
class _CooldownMapping:
|
||||
|
||||
__slots__ = ['_cooldown', '_cache']
|
||||
|
||||
def __init__(self, cooldown_object):
|
||||
self._cooldown = cooldown_object
|
||||
|
||||
self._cache = {}
|
||||
|
||||
def _get_bucket_key(self, p):
|
||||
if self._cooldown.bucket_type == BucketType.Default:
|
||||
return p
|
||||
return p.server
|
||||
|
||||
def _verify_cache_integrity(self):
|
||||
current = time.time()
|
||||
self._cache = {cache_key: bucket for cache_key, bucket in
|
||||
self._cache.items() if current < bucket.last + bucket.per}
|
||||
|
||||
def get_bucket(self, p):
|
||||
self._verify_cache_integrity()
|
||||
cache_key = self._get_bucket_key(p)
|
||||
if cache_key not in self._cache:
|
||||
bucket = self._cooldown.copy()
|
||||
self._cache[cache_key] = bucket
|
||||
else:
|
||||
bucket = self._cache[cache_key]
|
||||
return bucket
|
||||
|
||||
|
||||
class _Listener:
|
||||
|
||||
__slots__ = ['packet', 'components', 'handler', 'priority',
|
||||
'cooldown', 'pass_packet', 'handler_file',
|
||||
'overrides', 'pre_login', 'checklist', 'instance']
|
||||
|
||||
def __init__(self, packet, components, handler_function, **kwargs):
|
||||
self.packet = packet
|
||||
self.components = components
|
||||
self.handler = handler_function
|
||||
|
||||
self.priority = kwargs.get('priority', Priority.Low)
|
||||
self.overrides = kwargs.get('overrides', [])
|
||||
self.cooldown = kwargs.get('cooldown')
|
||||
self.pass_packet = kwargs.get('pass_packet', False)
|
||||
self.checklist = kwargs.get('checklist', [])
|
||||
|
||||
self.instance = None
|
||||
|
||||
if type(self.overrides) is not list:
|
||||
self.overrides = [self.overrides]
|
||||
|
||||
self.handler_file = get_relative_function_path(handler_function)
|
||||
|
||||
def _can_run(self, p):
|
||||
return True if not self.checklist else all(predicate(self.packet, p) for predicate in self.checklist)
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.__name__())
|
||||
|
||||
def __name__(self):
|
||||
return "{}.{}".format(self.handler.__module__, self.handler.__name__)
|
||||
|
||||
def __call__(self, p, packet_data):
|
||||
if self.cooldown is not None:
|
||||
bucket = self.cooldown.get_bucket(p)
|
||||
if bucket.is_cooling:
|
||||
raise RuntimeError('{} sent packet during cooldown'.format(p.peer_name))
|
||||
|
||||
if not self._can_run(p):
|
||||
raise RuntimeError('Could not handle packet due to checklist failure')
|
||||
|
||||
|
||||
class _XTListener(_Listener):
|
||||
|
||||
__slots__ = ['pre_login']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.pre_login = kwargs.get('pre_login')
|
||||
|
||||
async def __call__(self, p, packet_data):
|
||||
if not self.pre_login and not p.joined_world:
|
||||
p.logger.warn('{} tried sending XT packet before authentication!'.format(p.peer_name))
|
||||
await p.close()
|
||||
return
|
||||
|
||||
super().__call__(p, packet_data)
|
||||
|
||||
handler_call_arguments = [self.instance] if self.instance is not None else []
|
||||
handler_call_arguments += [self.packet, p] if self.pass_packet else [p]
|
||||
|
||||
arguments = iter(packet_data)
|
||||
for index, component in enumerate(self.components):
|
||||
if component.default is not component.empty:
|
||||
handler_call_arguments.append(component.default)
|
||||
next(arguments)
|
||||
elif component.kind == component.POSITIONAL_OR_KEYWORD:
|
||||
component_data = next(arguments)
|
||||
converter = get_converter(component)
|
||||
handler_call_arguments.append(await do_conversion(converter, p, component_data))
|
||||
elif component.kind == component.VAR_POSITIONAL:
|
||||
for component_data in arguments:
|
||||
converter = get_converter(component)
|
||||
handler_call_arguments.append(await do_conversion(converter, p, component_data))
|
||||
break
|
||||
return await self.handler(*handler_call_arguments)
|
||||
|
||||
|
||||
class _XMLListener(_Listener):
|
||||
__slots__ = ['pre_login']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
async def __call__(self, p, packet_data):
|
||||
super().__call__(p, packet_data)
|
||||
|
||||
handler_call_arguments = [self.instance] if self.instance is not None else []
|
||||
handler_call_arguments += [self.packet, p] if self.pass_packet else [p]
|
||||
|
||||
for index, component in enumerate(self.components):
|
||||
if component.default is not component.empty:
|
||||
handler_call_arguments.append(component.default)
|
||||
elif component.kind == component.POSITIONAL_OR_KEYWORD:
|
||||
converter = get_converter(component)
|
||||
handler_call_arguments.append(await do_conversion(converter, p, packet_data))
|
||||
return await self.handler(*handler_call_arguments)
|
||||
|
||||
|
||||
def handler(packet, **kwargs):
|
||||
def decorator(handler_function):
|
||||
if not asyncio.iscoroutinefunction(handler_function):
|
||||
raise TypeError('All handlers must be a coroutine.')
|
||||
|
||||
components = list(inspect.signature(handler_function).parameters.values())[1:]
|
||||
|
||||
if not issubclass(type(packet), _Packet):
|
||||
raise TypeError('All handlers can only listen for either XMLPacket or XTPacket.')
|
||||
|
||||
listener_class = _XTListener if isinstance(packet, XTPacket) else _XMLListener
|
||||
|
||||
try:
|
||||
cooldown_object = handler_function.cooldown
|
||||
del handler_function.cooldown
|
||||
except AttributeError:
|
||||
cooldown_object = None
|
||||
|
||||
try:
|
||||
checklist = handler_function.checks
|
||||
del handler_function.checks
|
||||
except AttributeError:
|
||||
checklist = []
|
||||
|
||||
listener_object = listener_class(packet, components, handler_function,
|
||||
cooldown=cooldown_object, checklist=checklist,
|
||||
**kwargs)
|
||||
return listener_object
|
||||
return decorator
|
||||
|
||||
|
||||
def listener_exists(xt_listeners, xml_listeners, packet):
|
||||
listener_collection = xt_listeners if isinstance(packet, XTPacket) else xml_listeners
|
||||
return packet in listener_collection
|
||||
|
||||
|
||||
def is_listener(listener):
|
||||
return issubclass(type(listener), _Listener)
|
||||
|
||||
|
||||
def listeners_from_module(xt_listeners, xml_listeners, module):
|
||||
listener_objects = inspect.getmembers(module, is_listener)
|
||||
for listener_name, listener_object in listener_objects:
|
||||
listener_collection = xt_listeners if type(listener_object) == _XTListener else xml_listeners
|
||||
if listener_object.packet not in listener_collection:
|
||||
listener_collection[listener_object.packet] = []
|
||||
|
||||
if listener_object not in listener_collection[listener_object.packet]:
|
||||
if listener_object.priority == Priority.High:
|
||||
listener_collection[listener_object.packet].insert(0, listener_object)
|
||||
elif listener_object.priority == Priority.Override:
|
||||
listener_collection[listener_object.packet] = [listener_object]
|
||||
else:
|
||||
listener_collection[listener_object.packet].append(listener_object)
|
||||
|
||||
for listener_name, listener_object in listener_objects:
|
||||
listener_collection = xt_listeners if type(listener_object) == _XTListener else xml_listeners
|
||||
for override in listener_object.overrides:
|
||||
listener_collection[override.packet].remove(override)
|
||||
|
||||
|
||||
def cooldown(per=1.0, rate=1, bucket_type=BucketType.Default):
|
||||
def decorator(handler_function):
|
||||
handler_function.cooldown = _CooldownMapping(_Cooldown(per, rate, bucket_type))
|
||||
return handler_function
|
||||
return decorator
|
||||
|
||||
|
||||
def check(predicate):
|
||||
def decorator(handler_function):
|
||||
if not hasattr(handler_function, 'checks'):
|
||||
handler_function.checks = []
|
||||
|
||||
if not type(predicate) == FunctionType:
|
||||
raise TypeError('All handler checks must be a function')
|
||||
|
||||
handler_function.checks.append(predicate)
|
||||
return handler_function
|
||||
return decorator
|
||||
|
||||
|
||||
def allow_once():
|
||||
def check_for_packet(packet, p):
|
||||
return packet not in p.received_packets
|
||||
return check(check_for_packet)
|
||||
|
||||
|
||||
def player_attribute(**attrs):
|
||||
def check_for_attributes(_, p):
|
||||
for attr, value in attrs.items():
|
||||
if not getattr(p, attr) == value:
|
||||
return False
|
||||
return True
|
||||
return check(check_for_attributes)
|
||||
|
||||
|
||||
def player_data_attribute(**attrs):
|
||||
def check_for_attributes(_, p):
|
||||
for attr, value in attrs.items():
|
||||
if not getattr(p.data, attr) == value:
|
||||
return False
|
||||
return True
|
||||
return check(check_for_attributes)
|
||||
|
||||
|
||||
def player_in_room(*room_ids):
|
||||
def check_room_id(_, p):
|
||||
return p.room.ID in room_ids
|
||||
return check(check_room_id)
|
181
Houdini/HoudiniFactory.py
Normal file
181
Houdini/HoudiniFactory.py
Normal file
@@ -0,0 +1,181 @@
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
import pkgutil
|
||||
import importlib
|
||||
|
||||
from Houdini.Spheniscidae import Spheniscidae
|
||||
from Houdini.Penguin import Penguin
|
||||
from Houdini import PenguinStringCompiler
|
||||
import config
|
||||
|
||||
from aiologger import Logger
|
||||
from aiologger.handlers.files import AsyncTimedRotatingFileHandler, RolloverInterval, AsyncFileHandler
|
||||
from aiologger.handlers.streams import AsyncStreamHandler
|
||||
|
||||
import logging
|
||||
from logging.handlers import RotatingFileHandler
|
||||
|
||||
import aioredis
|
||||
from aiocache import SimpleMemoryCache
|
||||
from watchdog.observers import Observer
|
||||
|
||||
from gino import Gino
|
||||
|
||||
try:
|
||||
import uvloop
|
||||
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
||||
except ImportError:
|
||||
uvloop = None
|
||||
|
||||
import Houdini.Handlers
|
||||
from Houdini.Handlers import listeners_from_module
|
||||
from Houdini.Events.HandlerFileEvent import HandlerFileEventHandler
|
||||
|
||||
|
||||
class HoudiniFactory:
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.server = None
|
||||
self.redis = None
|
||||
self.config = None
|
||||
self.cache = None
|
||||
self.db = Gino()
|
||||
self.peers_by_ip = {}
|
||||
|
||||
self.server_name = kwargs['server']
|
||||
self.server_config = None
|
||||
|
||||
self.logger = None
|
||||
|
||||
self.client_class = Spheniscidae
|
||||
self.penguin_string_compiler = None
|
||||
|
||||
self.penguins_by_id = {}
|
||||
self.penguins_by_username = {}
|
||||
|
||||
self.xt_listeners, self.xml_listeners = {}, {}
|
||||
|
||||
async def start(self):
|
||||
self.config = config
|
||||
|
||||
self.server_config = self.config.servers[self.server_name]
|
||||
|
||||
self.server = await asyncio.start_server(
|
||||
self.client_connected, self.server_config['Address'],
|
||||
self.server_config['Port']
|
||||
)
|
||||
|
||||
await self.db.set_bind('postgresql://{}:{}@{}/{}'.format(
|
||||
self.config.database['Username'], self.config.database['Password'],
|
||||
self.config.database['Address'],
|
||||
self.config.database['Name']))
|
||||
|
||||
general_log_directory = os.path.dirname(self.server_config["Logging"]["General"])
|
||||
errors_log_directory = os.path.dirname(self.server_config["Logging"]["Errors"])
|
||||
|
||||
if not os.path.exists(general_log_directory):
|
||||
os.mkdir(general_log_directory)
|
||||
|
||||
if not os.path.exists(errors_log_directory):
|
||||
os.mkdir(errors_log_directory)
|
||||
|
||||
if sys.platform != 'win32':
|
||||
self.logger = Logger.with_default_handlers(name='Houdini')
|
||||
universal_handler = AsyncTimedRotatingFileHandler(
|
||||
filename=self.server_config['Logging']['General'],
|
||||
backup_count=3,
|
||||
when=RolloverInterval.HOURS
|
||||
)
|
||||
error_handler = AsyncFileHandler(filename=self.server_config['Logging']['General'])
|
||||
console_handler = AsyncStreamHandler(stream=sys.stdout)
|
||||
else:
|
||||
self.logger = logging.getLogger('Houdini')
|
||||
universal_handler = RotatingFileHandler(self.server_config['Logging']['General'],
|
||||
maxBytes=2097152, backupCount=3, encoding='utf-8')
|
||||
|
||||
error_handler = logging.FileHandler(self.server_config['Logging']['Errors'])
|
||||
console_handler = logging.StreamHandler(stream=sys.stdout)
|
||||
|
||||
log_formatter = logging.Formatter('%(asctime)s [%(levelname)-5.5s] %(message)s')
|
||||
error_handler.setLevel(logging.ERROR)
|
||||
|
||||
universal_handler.setFormatter(log_formatter)
|
||||
console_handler.setFormatter(log_formatter)
|
||||
|
||||
self.logger.addHandler(universal_handler)
|
||||
self.logger.addHandler(console_handler)
|
||||
|
||||
level = logging.getLevelName(self.server_config['Logging']['Level'])
|
||||
self.logger.setLevel(level)
|
||||
|
||||
self.logger.info('Houdini module instantiated')
|
||||
|
||||
if self.server_config['World']:
|
||||
self.redis = await aioredis.create_redis_pool('redis://{}:{}'.format(
|
||||
self.config.redis['Address'], self.redis['Port']),
|
||||
minsize=5, maxsize=10)
|
||||
|
||||
await self.redis.delete('{}.players'.format(self.server_name))
|
||||
await self.redis.delete('{}.population'.format(self.server_name))
|
||||
|
||||
self.cache = SimpleMemoryCache(namespace='houdini', ttl=self.server_config['CacheExpiry'])
|
||||
|
||||
self.client_class = Penguin
|
||||
self.penguin_string_compiler = PenguinStringCompiler()
|
||||
|
||||
self.load_handler_modules(exclude_load="Houdini.Handlers.Login.Login")
|
||||
self.logger.info('World server started')
|
||||
else:
|
||||
self.load_handler_modules("Houdini.Handlers.Login.Login")
|
||||
self.logger.info('Login server started')
|
||||
|
||||
handlers_path = './Houdini{}Handlers'.format(os.path.sep)
|
||||
plugins_path = './Houdini{}Plugins'.format(os.path.sep)
|
||||
self.configure_obvservers([handlers_path, HandlerFileEventHandler])
|
||||
|
||||
self.logger.info('Listening on {}:{}'.format(self.server_config['Address'], self.server_config['Port']))
|
||||
|
||||
async with self.server:
|
||||
await self.server.serve_forever()
|
||||
|
||||
async def client_connected(self, reader, writer):
|
||||
client_object = self.client_class(self, reader, writer)
|
||||
await client_object.run()
|
||||
|
||||
def load_handler_modules(self, strict_load=None, exclude_load=None):
|
||||
for handler_module in self.get_package_modules(Houdini.Handlers):
|
||||
if not (strict_load and handler_module not in strict_load or exclude_load and handler_module in exclude_load):
|
||||
if handler_module not in sys.modules.keys():
|
||||
module = importlib.import_module(handler_module)
|
||||
listeners_from_module(self.xt_listeners, self.xml_listeners, module)
|
||||
|
||||
self.logger.info("Handler modules loaded")
|
||||
|
||||
def get_package_modules(self, package):
|
||||
package_modules = []
|
||||
|
||||
for importer, module_name, is_package in pkgutil.iter_modules(package.__path__):
|
||||
full_module_name = "{0}.{1}".format(package.__name__, module_name)
|
||||
|
||||
if is_package:
|
||||
subpackage_object = importlib.import_module(full_module_name, package=package.__path__)
|
||||
subpackage_object_directory = dir(subpackage_object)
|
||||
|
||||
if "Plugin" in subpackage_object_directory:
|
||||
package_modules.append((subpackage_object, module_name))
|
||||
continue
|
||||
|
||||
sub_package_modules = self.get_package_modules(subpackage_object)
|
||||
|
||||
package_modules = package_modules + sub_package_modules
|
||||
else:
|
||||
package_modules.append(full_module_name)
|
||||
|
||||
return package_modules
|
||||
|
||||
def configure_obvservers(self, *observer_settings):
|
||||
for observer_path, observer_class in observer_settings:
|
||||
event_observer = Observer()
|
||||
event_observer.schedule(observer_class(self), observer_path, recursive=True)
|
||||
event_observer.start()
|
36
Houdini/Penguin.py
Normal file
36
Houdini/Penguin.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from Houdini.Spheniscidae import Spheniscidae
|
||||
|
||||
|
||||
class Penguin(Spheniscidae):
|
||||
|
||||
__slots__ = ['x', 'y', 'room', 'waddle', 'table', 'data']
|
||||
|
||||
def __init__(self, *args):
|
||||
super().__init__(*args)
|
||||
|
||||
self.x, self.y = (0, 0)
|
||||
self.room = None
|
||||
self.waddle = None
|
||||
self.table = None
|
||||
|
||||
self.data = None
|
||||
|
||||
self.logger.debug('New penguin created')
|
||||
|
||||
async def add_inventory(self, item):
|
||||
pass
|
||||
|
||||
async def add_igloo(self, igloo):
|
||||
pass
|
||||
|
||||
async def add_furniture(self, furniture):
|
||||
pass
|
||||
|
||||
async def add_flooring(self, flooring):
|
||||
pass
|
||||
|
||||
async def add_buddy(self, buddy):
|
||||
pass
|
||||
|
||||
async def add_inbox(self, postcard):
|
||||
pass
|
11
Houdini/Plugins/Commands/__init__.py
Normal file
11
Houdini/Plugins/Commands/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from Houdini.Handlers import Handlers, XTPacket
|
||||
|
||||
|
||||
class Commands:
|
||||
|
||||
def __init__(self, server):
|
||||
self.server = server
|
||||
|
||||
@Handlers.handler(XTPacket('s', 'sm'))
|
||||
async def handle_send_message(self, message: str):
|
||||
print('Do stuff with {}'.format(message))
|
0
Houdini/Plugins/__init__.py
Normal file
0
Houdini/Plugins/__init__.py
Normal file
159
Houdini/Spheniscidae.py
Normal file
159
Houdini/Spheniscidae.py
Normal file
@@ -0,0 +1,159 @@
|
||||
from Houdini import Handlers
|
||||
from Houdini.Handlers import XMLPacket, XTPacket
|
||||
|
||||
from asyncio import IncompleteReadError
|
||||
|
||||
import defusedxml.cElementTree as Et
|
||||
from xml.etree.cElementTree import Element, SubElement, tostring
|
||||
|
||||
|
||||
class Spheniscidae:
|
||||
|
||||
__slots__ = ['__reader', '__writer', 'server', 'logger',
|
||||
'peer_name', 'received_packets', 'joined_world']
|
||||
|
||||
Delimiter = b'\x00'
|
||||
|
||||
def __init__(self, server, reader, writer):
|
||||
self.__reader = reader
|
||||
self.__writer = writer
|
||||
|
||||
self.server = server
|
||||
self.logger = server.logger
|
||||
|
||||
self.peer_name = writer.get_extra_info('peername')
|
||||
self.server.peers_by_ip[self.peer_name] = self
|
||||
|
||||
self.joined_world = False
|
||||
|
||||
self.received_packets = set()
|
||||
|
||||
async def send_error_and_disconnect(self, error):
|
||||
await self.send_xt('e', error)
|
||||
await self.close()
|
||||
|
||||
async def send_error(self, error):
|
||||
await self.send_xt('e', error)
|
||||
|
||||
async def send_policy_file(self):
|
||||
await self.send_line('<cross-domain-policy><allow-access-from domain="*" to-ports="{}" /></cross-domain-policy>'
|
||||
.format(self.server.server_config['Port']))
|
||||
await self.close()
|
||||
|
||||
async def send_xt(self, *data):
|
||||
data = list(data)
|
||||
|
||||
handler_id = data.pop(0)
|
||||
internal_id = -1
|
||||
|
||||
mapped_data = map(str, data)
|
||||
|
||||
xt_data = '%'.join(mapped_data)
|
||||
line = '%xt%{0}%{1}%{2}%'.format(handler_id, internal_id, xt_data)
|
||||
await self.send_line(line)
|
||||
|
||||
async def send_xml(self, xml_dict):
|
||||
data_root = Element('msg')
|
||||
data_root.set('t', 'sys')
|
||||
|
||||
sub_element_parent = data_root
|
||||
for sub_element, sub_element_attribute in xml_dict.iteritems():
|
||||
sub_element_object = SubElement(sub_element_parent, sub_element)
|
||||
|
||||
if type(xml_dict[sub_element]) is dict:
|
||||
for sub_element_attribute_key, sub_element_attribute_value in xml_dict[sub_element].iteritems():
|
||||
sub_element_object.set(sub_element_attribute_key, sub_element_attribute_value)
|
||||
else:
|
||||
sub_element_object.text = xml_dict[sub_element]
|
||||
|
||||
sub_element_parent = sub_element_object
|
||||
|
||||
xml_data = tostring(data_root)
|
||||
await self.send_line(xml_data)
|
||||
|
||||
async def send_line(self, data):
|
||||
self.logger.debug('Outgoing data: %s', data)
|
||||
self.__writer.write(data.encode() + Spheniscidae.Delimiter)
|
||||
|
||||
async def close(self):
|
||||
self.__writer.close()
|
||||
|
||||
async def __handle_xt_data(self, data):
|
||||
self.logger.debug("Received XT data: {0}".format(data))
|
||||
parsed_data = data.split("%")[1:-1]
|
||||
|
||||
packet_id = parsed_data[2]
|
||||
packet = XTPacket(packet_id)
|
||||
|
||||
if Handlers.listener_exists(self.server.xt_listeners, self.server.xml_listeners, packet):
|
||||
xt_listeners = self.server.xt_listeners[packet]
|
||||
packet_data = parsed_data[4:]
|
||||
|
||||
for listener in xt_listeners:
|
||||
await listener(self, packet_data)
|
||||
self.received_packets.add(packet)
|
||||
else:
|
||||
self.logger.debug("Handler for {0} doesn't exist!".format(packet_id))
|
||||
|
||||
async def __handle_xml_data(self, data):
|
||||
self.logger.debug("Received XML data: {0}".format(data))
|
||||
|
||||
element_tree = Et.fromstring(data)
|
||||
|
||||
if element_tree.tag == "policy-file-request":
|
||||
await self.send_policy_file()
|
||||
|
||||
elif element_tree.tag == "msg":
|
||||
self.logger.debug("Received valid XML data")
|
||||
|
||||
try:
|
||||
body_tag = element_tree[0]
|
||||
action = body_tag.get("action")
|
||||
packet = XMLPacket(action)
|
||||
|
||||
if Handlers.listener_exists(self.server.xt_listeners, self.server.xml_listeners, packet):
|
||||
xml_listeners = self.server.xml_listeners[packet]
|
||||
|
||||
for listener in xml_listeners:
|
||||
await listener(self, body_tag)
|
||||
|
||||
self.received_packets.add(packet)
|
||||
else:
|
||||
self.logger.warn("Packet did not contain a valid action attribute!")
|
||||
|
||||
except IndexError:
|
||||
self.logger.warn("Received invalid XML data (didn't contain a body tag)")
|
||||
else:
|
||||
self.logger.warn("Received invalid XML data!")
|
||||
|
||||
async def __client_connected(self):
|
||||
self.logger.info('Client %s connected', self.peer_name)
|
||||
|
||||
async def __client_disconnected(self):
|
||||
del self.server.peers_by_ip[self.peer_name]
|
||||
|
||||
self.logger.info('Client %s disconnected', self.peer_name)
|
||||
|
||||
async def __data_received(self, data):
|
||||
data = data.decode()[:-1]
|
||||
if data.startswith('<'):
|
||||
await self.__handle_xml_data(data)
|
||||
else:
|
||||
await self.__handle_xt_data(data)
|
||||
|
||||
async def run(self):
|
||||
await self.__client_connected()
|
||||
while not self.__writer.is_closing():
|
||||
try:
|
||||
data = await self.__reader.readuntil(
|
||||
separator=Spheniscidae.Delimiter)
|
||||
if data:
|
||||
await self.__data_received(data)
|
||||
else:
|
||||
self.__writer.close()
|
||||
await self.__writer.drain()
|
||||
except IncompleteReadError:
|
||||
self.__writer.close()
|
||||
except ConnectionResetError:
|
||||
self.__writer.close()
|
||||
await self.__client_disconnected()
|
40
Houdini/__init__.py
Normal file
40
Houdini/__init__.py
Normal file
@@ -0,0 +1,40 @@
|
||||
from collections import OrderedDict
|
||||
from aiocache import cached
|
||||
from types import FunctionType
|
||||
import asyncio
|
||||
|
||||
|
||||
class PenguinStringCompiler(OrderedDict):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def __setitem__(self, key, compiler_method):
|
||||
assert type(compiler_method) == FunctionType
|
||||
super().__setitem__(key, compiler_method)
|
||||
|
||||
@cached(namespace='houdini')
|
||||
async def compile(self, p):
|
||||
compiler_method_results = []
|
||||
|
||||
for compiler_method in self.values():
|
||||
if asyncio.iscoroutinefunction(compiler_method):
|
||||
compiler_method_result = await compiler_method(p)
|
||||
else:
|
||||
compiler_method_result = compiler_method(p)
|
||||
compiler_method_results.append(str(compiler_method_result))
|
||||
|
||||
compiler_result = '|'.join(compiler_method_results)
|
||||
return compiler_result
|
||||
|
||||
@classmethod
|
||||
def attribute_by_name(cls, attribute_name):
|
||||
async def attribute_method(p):
|
||||
return getattr(p, attribute_name)
|
||||
return attribute_method
|
||||
|
||||
@classmethod
|
||||
def data_attribute_by_name(cls, attribute_name):
|
||||
async def attribute_method(p):
|
||||
return getattr(p.data, attribute_name)
|
||||
return attribute_method
|
Reference in New Issue
Block a user