diff --git a/.gitignore b/.gitignore index a65d046..a8b8dc8 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ __pycache__/ # Distribution / packaging .Python env/ +venv/ build/ develop-eggs/ dist/ @@ -56,3 +57,6 @@ docs/_build/ # PyBuilder target/ + +# PyCharm +.idea/ \ No newline at end of file diff --git a/Bootstrap.py b/Bootstrap.py new file mode 100644 index 0000000..d43f980 --- /dev/null +++ b/Bootstrap.py @@ -0,0 +1,11 @@ +import asyncio +import sys +from Houdini.HoudiniFactory import HoudiniFactory + +if __name__ == '__main__': + if sys.platform == 'win32': + loop = asyncio.ProactorEventLoop() + asyncio.set_event_loop(loop) + + factory_instance = HoudiniFactory(server='Login') + asyncio.run(factory_instance.start()) diff --git a/Houdini/Converters.py b/Houdini/Converters.py new file mode 100644 index 0000000..72e4535 --- /dev/null +++ b/Houdini/Converters.py @@ -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(',')) diff --git a/Houdini/Crypto.py b/Houdini/Crypto.py new file mode 100644 index 0000000..dfc2831 --- /dev/null +++ b/Houdini/Crypto.py @@ -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 diff --git a/Houdini/Data/__init__.py b/Houdini/Data/__init__.py new file mode 100644 index 0000000..0a70952 --- /dev/null +++ b/Houdini/Data/__init__.py @@ -0,0 +1,4 @@ +from gino import Gino + +db = Gino() + diff --git a/Houdini/Events/HandlerFileEvent.py b/Houdini/Events/HandlerFileEvent.py new file mode 100644 index 0000000..99f48f6 --- /dev/null +++ b/Houdini/Events/HandlerFileEvent.py @@ -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!") diff --git a/Houdini/Events/__init__.py b/Houdini/Events/__init__.py new file mode 100644 index 0000000..3a7dcc3 --- /dev/null +++ b/Houdini/Events/__init__.py @@ -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 diff --git a/Houdini/Handlers/Login/Login.py b/Houdini/Handlers/Login/Login.py new file mode 100644 index 0000000..5a916d4 --- /dev/null +++ b/Houdini/Handlers/Login/Login.py @@ -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)) diff --git a/Houdini/Handlers/Login/__init__.py b/Houdini/Handlers/Login/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Houdini/Handlers/Play/Navigation.py b/Houdini/Handlers/Play/Navigation.py new file mode 100644 index 0000000..43ca83b --- /dev/null +++ b/Houdini/Handlers/Play/Navigation.py @@ -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) diff --git a/Houdini/Handlers/Play/__init__.py b/Houdini/Handlers/Play/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Houdini/Handlers/__init__.py b/Houdini/Handlers/__init__.py new file mode 100644 index 0000000..71b322b --- /dev/null +++ b/Houdini/Handlers/__init__.py @@ -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) diff --git a/Houdini/HoudiniFactory.py b/Houdini/HoudiniFactory.py new file mode 100644 index 0000000..1187740 --- /dev/null +++ b/Houdini/HoudiniFactory.py @@ -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() diff --git a/Houdini/Penguin.py b/Houdini/Penguin.py new file mode 100644 index 0000000..2594815 --- /dev/null +++ b/Houdini/Penguin.py @@ -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 diff --git a/Houdini/Plugins/Commands/__init__.py b/Houdini/Plugins/Commands/__init__.py new file mode 100644 index 0000000..5999eb8 --- /dev/null +++ b/Houdini/Plugins/Commands/__init__.py @@ -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)) diff --git a/Houdini/Plugins/__init__.py b/Houdini/Plugins/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Houdini/Spheniscidae.py b/Houdini/Spheniscidae.py new file mode 100644 index 0000000..ca89a8f --- /dev/null +++ b/Houdini/Spheniscidae.py @@ -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('' + .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() diff --git a/Houdini/__init__.py b/Houdini/__init__.py new file mode 100644 index 0000000..4ae8ce1 --- /dev/null +++ b/Houdini/__init__.py @@ -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 diff --git a/Test.py b/Test.py new file mode 100644 index 0000000..c50b4a9 --- /dev/null +++ b/Test.py @@ -0,0 +1,25 @@ +import asyncio + + +async def tcp_echo_client(message): + reader, writer = await asyncio.open_connection( + '127.0.0.1', 6112) + + print(f'Send: {message!r}') + writer.write(message.encode()) + + data = await reader.read(100) + print(f'Received: {data.decode()!r}') + + print('Close the connection') + writer.close() + await writer.wait_closed() + +loop = asyncio.ProactorEventLoop() +asyncio.set_event_loop(loop) + +for x in range(1): + # asyncio.ensure_future(tcp_echo_client('\0')) + asyncio.ensure_future(tcp_echo_client('%xt%s%t#c%-1%1,2,3,4%\0%xt%s%t#c%-1%1,2,3,4%\0')) + +loop.run_forever() diff --git a/config.py b/config.py new file mode 100644 index 0000000..3b7fb97 --- /dev/null +++ b/config.py @@ -0,0 +1,87 @@ +database = { + "Address": "localhost", + "Username": "postgres", + "Password": "password", + "Name": "houdini", +} + +redis = { + "Address": "127.0.0.1", + "Port": 6379 +} + +servers = { + "Login": { + "Address": "0.0.0.0", + "Port": 6112, + "World": False, + "Plugins": [ + "Example" + ], + "Logging": { + "General": "logs/login.log", + "Errors": "logs/login-errors.log", + "Level": "DEBUG" + }, + "LoginFailureLimit": 5, + "LoginFailureTimer": 3600 + }, + "Wind": { + "Id": "100", + "Address": "0.0.0.0", + "Port": 9875, + "World": True, + "Capacity": 200, + "CacheExpiry": 3600, + "Plugins": [ + "Commands", + "Bot", + "Rank" + ], + "Logging": { + "General": "logs/wind.log", + "Errors": "logs/wind-errors.log", + "Level": "INFO" + } + } +} + +tables = { + "Four": [ + {"RoomId": 220, "Tables": [205, 206, 207]}, + {"RoomId": 221, "Tables": [200, 201, 202, 203, 204]} + ], + "Mancala": [ + {"RoomId": 111, "Tables": [100, 101, 102, 103, 104]} + ], + "Treasure": [ + {"RoomId": 422, "Tables": [300, 301, 302, 303, 304, 305, 306, 307]} + ] +} + +waddles = { + "Sled": [ + {"RoomId": 230, "Waddles": [ + {"Id": 100, "Seats": 4}, + {"Id": 101, "Seats": 3}, + {"Id": 102, "Seats": 2}, + {"Id": 103, "Seats": 2} + ]} + ], + "Card": [ + {"RoomId": 320, "Waddles": [ + {"Id": 200, "Seats": 2}, + {"Id": 201, "Seats": 2}, + {"Id": 202, "Seats": 2}, + {"Id": 203, "Seats": 2} + ]} + ], + "CardFire": [ + {"RoomId": 812, "Waddles": [ + {"Id": 300, "Seats": 2}, + {"Id": 301, "Seats": 2}, + {"Id": 302, "Seats": 3}, + {"Id": 303, "Seats": 4} + ]} + ] +} diff --git a/houdini.sql b/houdini.sql new file mode 100644 index 0000000..d06c44d --- /dev/null +++ b/houdini.sql @@ -0,0 +1,762 @@ +DROP TABLE IF EXISTS item; +CREATE TABLE item ( + "ID" SMALLINT NOT NULL, + "Name" VARCHAR(30), + "Type" SMALLINT NOT NULL DEFAULT 1, + "Cost" SMALLINT NOT NULL DEFAULT 0, + "Member" BOOLEAN NOT NULL DEFAULT FALSE, + "Bait" BOOLEAN NOT NULL DEFAULT FALSE, + "Patched" BOOLEAN NOT NULL DEFAULT FALSE, + "EPF" BOOLEAN NOT NULL DEFAULT FALSE, + "Tour" BOOLEAN NOT NULL DEFAULT FALSE, + "ReleaseDate" DATE NOT NULL, + PRIMARY KEY ("ID") +); + +ALTER TABLE item ALTER COLUMN "ReleaseDate" SET DEFAULT now(); + +COMMENT ON TABLE item IS 'Server item crumbs'; + +COMMENT ON COLUMN item."ID" IS 'Unique item ID'; +COMMENT ON COLUMN item."Name" IS 'Item name'; +COMMENT ON COLUMN item."Type" IS 'Item clothing type'; +COMMENT ON COLUMN item."Cost" IS 'Cost of item'; +COMMENT ON COLUMN item."Member" IS 'Is member-only?'; +COMMENT ON COLUMN item."Bait" IS 'Is bait item?'; +COMMENT ON COLUMN item."Patched" IS 'Is item patched?'; +COMMENT ON COLUMN item."EPF" IS 'Is EPF item?'; +COMMENT ON COLUMN item."Tour" IS 'Gives tour status?'; + +DROP TABLE IF EXISTS igloo; +CREATE TABLE igloo ( + "ID" SMALLINT NOT NULL, + "Name" VARCHAR(30) NOT NULL, + "Cost" SMALLINT NOT NULL DEFAULT 0, + PRIMARY KEY("ID") +); + +COMMENT ON TABLE igloo IS 'Server igloo crumbs'; + +COMMENT ON COLUMN igloo."ID" IS 'Unique igloo ID'; +COMMENT ON COLUMN igloo."Name" IS 'Igloo name'; +COMMENT ON COLUMN igloo."Cost" IS 'Cost of igloo'; + +DROP TABLE IF EXISTS location; +CREATE TABLE location ( + "ID" SMALLINT NOT NULL, + "Name" VARCHAR(30) NOT NULL, + "Cost" SMALLINT NOT NULL DEFAULT 0, + PRIMARY KEY ("ID") +); + +COMMENT ON TABLE location IS 'Server location crumbs'; + +COMMENT ON COLUMN location."ID" IS 'Unique location ID'; +COMMENT ON COLUMN location."Name" IS 'Location name'; +COMMENT ON COLUMN location."Cost" IS 'Cost of location'; + +DROP TABLE IF EXISTS furniture; +CREATE TABLE furniture ( + "ID" SMALLINT NOT NULL, + "Name" VARCHAR(30) NOT NULL, + "Type" SMALLINT NOT NULL DEFAULT 1, + "Sort" SMALLINT NOT NULL DEFAULT 1, + "Cost" SMALLINT NOT NULL DEFAULT 0, + "Member" BOOLEAN NOT NULL DEFAULT FALSE, + "MaxQuantity" SMALLINT NOT NULL DEFAULT 100, + PRIMARY KEY("ID") +); + +COMMENT ON TABLE furniture IS 'Server furniture crumbs'; + +COMMENT ON COLUMN furniture."ID" IS 'Unique furniture ID'; +COMMENT ON COLUMN furniture."Type" IS 'Furniture type ID'; +COMMENT ON COLUMN furniture."Sort" IS 'Furniture sort ID'; +COMMENT ON COLUMN furniture."Cost" IS 'Cost of furniture'; +COMMENT ON COLUMN furniture."Member" IS 'Is member-only?'; +COMMENT ON COLUMN furniture."MaxQuantity" IS 'Max inventory quantity'; + +DROP TABLE IF EXISTS flooring; +CREATE TABLE flooring ( + "ID" SMALLINT NOT NULL, + "Name" VARCHAR(30), + "Cost" SMALLINT NOT NULL DEFAULT 0, + PRIMARY KEY ("ID") +); + +COMMENT ON TABLE flooring IS 'Server flooring crumbs'; + +COMMENT ON COLUMN flooring."ID" IS 'Unique flooring ID'; +COMMENT ON COLUMN flooring."Name" IS 'Flooring name'; +COMMENT ON COLUMN flooring."Cost" IS 'Cost of flooring'; + +CREATE TYPE card_element AS ENUM ('s', 'w', 'f'); +CREATE TYPE card_color AS ENUM ('b', 'g', 'o', 'p', 'r', 'y'); + +DROP TABLE IF EXISTS card; +CREATE TABLE card ( + "ID" SMALLINT NOT NULL, + "Name" VARCHAR(30) NOT NULL, + "SetID" SMALLINT NOT NULL DEFAULT 1, + "PowerID" SMALLINT NOT NULL DEFAULT 0, + "Element" card_element NOT NULL DEFAULT 's', + "Color" card_color NOT NULL DEFAULT 'b', + "Value" SMALLINT NOT NULL DEFAULT 2, + "Description" VARCHAR(50) NOT NULL DEFAULT '', + PRIMARY KEY ("ID") +); + +COMMENT ON TABLE card IS 'Server jitsu card crumbs'; + +COMMENT ON COLUMN card."ID" IS 'Unique card ID'; +COMMENT ON COLUMN card."Name" IS 'Card name'; +COMMENT ON COLUMN card."SetID" IS 'Card set ID'; +COMMENT ON COLUMN card."PowerID" IS 'Card power ID'; +COMMENT ON COLUMN card."Element" IS 'Card element'; +COMMENT ON COLUMN card."Color" IS 'Card color'; +COMMENT ON COLUMN card."Value" IS 'Value of card'; +COMMENT ON COLUMN card."Description" IS 'Play description'; + +DROP TABLE IF EXISTS room; +CREATE TABLE room ( + "ID" SMALLINT NOT NULL, + "InternalID" SERIAL NOT NULL, + "Name" VARCHAR(30) NOT NULL, + "Member" BOOLEAN NOT NULL DEFAULT FALSE, + "MaxUsers" SMALLINT NOT NULL DEFAULT 80, + "RequiredItem" SMALLINT, + PRIMARY KEY("ID", "InternalID"), + CONSTRAINT room_ibfk_1 FOREIGN KEY ("RequiredItem") REFERENCES item ("ID") ON DELETE CASCADE ON UPDATE CASCADE +); + +COMMENT ON TABLE room IS 'Server room crumbs'; + +COMMENT ON COLUMN room."ID" IS 'Unique room ID'; +COMMENT ON COLUMN room."InternalID" IS 'Internal room key'; +COMMENT ON COLUMN room."Name" IS 'Room name'; +COMMENT ON COLUMN room."Member" IS 'Is member-only?'; +COMMENT ON COLUMN room."MaxUsers" IS 'Maximum room users'; +COMMENT ON COLUMN room."RequiredItem" IS 'Required inventory item'; + +DROP TABLE IF EXISTS stamp; +CREATE TABLE stamp ( + "ID" SMALLINT NOT NULL, + "Name" VARCHAR(30) NOT NULL, + "GroupID" SMALLINT NOT NULL DEFAULT 0, + "Member" BOOLEAN NOT NULL DEFAULT FALSE, + "Rank" SMALLINT NOT NULL DEFAULT 1, + "Description" VARCHAR(50) NOT NULL DEFAULT '', + PRIMARY KEY("ID") +); + +COMMENT ON TABLE stamp IS 'Server stamp crumbs'; + +COMMENT ON COLUMN stamp."ID" IS 'Unique stamp ID'; +COMMENT ON COLUMN stamp."Name" IS 'Stamp name'; +COMMENT ON COLUMN stamp."GroupID" IS 'Stamp group ID'; +COMMENT ON COLUMN stamp."Member" IS 'Is member-only?'; +COMMENT ON COLUMN stamp."Rank" IS 'Stamp difficulty ranking'; +COMMENT ON COLUMN stamp."Description" IS 'Stamp description'; + +DROP TABLE IF EXISTS puffle_care_item; +CREATE TABLE puffle_care_item ( + "ID" SMALLINT NOT NULL, + "Name" VARCHAR(30) NOT NULL DEFAULT '', + "Cost" SMALLINT NOT NULL DEFAULT 0, + "Quantity" SMALLINT NOT NULL DEFAULT 1, + "Member" BOOLEAN NOT NULL DEFAULT FALSE, + "FoodEffect" SMALLINT NOT NULL DEFAULT 0, + "RestEffect" SMALLINT NOT NULL DEFAULT 0, + "PlayEffect" SMALLINT NOT NULL DEFAULT 0, + "CleanEffect" SMALLINT NOT NULL DEFAULT 0, + PRIMARY KEY ("ID") +); + +COMMENT ON TABLE puffle_care_item IS 'Server puffle care item crumbs'; + +COMMENT ON COLUMN puffle_care_item."ID" IS 'Unique care item ID'; +COMMENT ON COLUMN puffle_care_item."Name" IS 'Care item name'; +COMMENT ON COLUMN puffle_care_item."Cost" IS 'Cost of care item'; +COMMENT ON COLUMN puffle_care_item."Quantity" IS 'Base quantity of purchase'; +COMMENT ON COLUMN puffle_care_item."Member" IS 'Is member-only?'; +COMMENT ON COLUMN puffle_care_item."FoodEffect" IS 'Effect on puffle food level'; +COMMENT ON COLUMN puffle_care_item."RestEffect" IS 'Effect on puffle rest level'; +COMMENT ON COLUMN puffle_care_item."PlayEffect" IS 'Effect on puffle play level'; +COMMENT ON COLUMN puffle_care_item."CleanEffect" IS 'Effect on puffle clean level'; + +DROP TABLE IF EXISTS puffle; +CREATE TABLE puffle ( + "ID" SMALLINT NOT NULL, + "ParentID" SMALLINT NOT NULL, + "Name" VARCHAR(30) NOT NULL DEFAULT '', + "Member" BOOLEAN NOT NULL DEFAULT FALSE, + "FavouriteFood" SMALLINT NOT NULL, + "RunawayPostcard" SMALLINT NOT NULL DEFAULT 100, + "MaxFood" SMALLINT NOT NULL DEFAULT 100, + "MaxRest" SMALLINT NOT NULL DEFAULT 100, + "MaxClean" SMALLINT NOT NULL DEFAULT 100, + PRIMARY KEY ("ID"), + CONSTRAINT puffle_ibfk_1 FOREIGN KEY ("ParentID") REFERENCES puffle ("ID") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT puffle_ibfk_2 FOREIGN KEY ("FavouriteFood") REFERENCES puffle_care_item ("ID") ON DELETE CASCADE ON UPDATE CASCADE +); + +COMMENT ON TABLE puffle IS 'Server puffle crumbs'; + +COMMENT ON COLUMN puffle."ID" IS 'Unique puffle ID'; +COMMENT ON COLUMN puffle."ParentID" IS 'Base color puffle ID'; +COMMENT ON COLUMN puffle."Name" IS 'Puffle name'; +COMMENT ON COLUMN puffle."Member" IS 'Is member-only?'; +COMMENT ON COLUMN puffle."FavouriteFood" IS 'Favourite puffle-care item'; +COMMENT ON COLUMN puffle."RunawayPostcard" IS 'Runaway postcard ID'; +COMMENT ON COLUMN puffle."MaxFood" IS 'Maximum food level'; +COMMENT ON COLUMN puffle."MaxRest" IS 'Maximum rest level'; +COMMENT ON COLUMN puffle."MaxClean" IS 'Maximum clean level'; + +DROP TABLE IF EXISTS penguin; +CREATE TABLE penguin ( + "ID" SERIAL, + "Username" VARCHAR(12) NOT NULL, + "Nickname" VARCHAR(30) NOT NULL, + "Approval" BOOLEAN NOT NULL DEFAULT FALSE, + "Password" CHAR(255) NOT NULL, + "LoginKey" CHAR(255) DEFAULT '', + "Email" VARCHAR(255) NOT NULL, + "RegistrationDate" TIMESTAMP NOT NULL, + "Active" BOOLEAN NOT NULL DEFAULT FALSE, + "LastPaycheck" TIMESTAMP NOT NULL, + "MinutesPlayed" INT NOT NULL DEFAULT 0, + "Moderator" BOOLEAN NOT NULL DEFAULT FALSE, + "Member" BOOLEAN NOT NULL DEFAULT TRUE, + "MascotStamp" SMALLINT DEFAULT NULL, + "Coins" INT NOT NULL DEFAULT 500, + "Color" SMALLINT DEFAULT NULL, + "Head" SMALLINT DEFAULT NULL, + "Face" SMALLINT DEFAULT NULL, + "Neck" SMALLINT DEFAULT NULL, + "Body" SMALLINT DEFAULT NULL, + "Hand" SMALLINT DEFAULT NULL, + "Feet" SMALLINT DEFAULT NULL, + "Photo" SMALLINT DEFAULT NULL, + "Flag" SMALLINT DEFAULT NULL, + "Permaban" SMALLINT NOT NULL DEFAULT 0, + "BookModified" SMALLINT NOT NULL DEFAULT 0, + "BookColor" SMALLINT NOT NULL DEFAULT 1, + "BookHighlight" SMALLINT NOT NULL DEFAULT 1, + "BookPattern" SMALLINT NOT NULL DEFAULT 0, + "BookIcon" SMALLINT NOT NULL DEFAULT 1, + "AgentStatus" SMALLINT NOT NULL DEFAULT 0, + "FieldOpStatus" SMALLINT NOT NULL DEFAULT 0, + "CareerMedals" INT NOT NULL DEFAULT 0, + "AgentMedals" INT NOT NULL DEFAULT 0, + "LastFieldOp" TIMESTAMP NOT NULL, + "NinjaRank" SMALLINT NOT NULL DEFAULT 0, + "NinjaProgress" SMALLINT NOT NULL DEFAULT 0, + "FireNinjaRank" SMALLINT NOT NULL DEFAULT 0, + "FireNinjaProgress" SMALLINT NOT NULL DEFAULT 0, + "WaterNinjaRank" SMALLINT NOT NULL DEFAULT 0, + "WaterNinjaProgress" SMALLINT NOT NULL DEFAULT 0, + "NinjaMatchesWon" INT NOT NULL DEFAULT 0, + "FireMatchesWon" INT NOT NULL DEFAULT 0, + "WaterMatchesWon" INT NOT NULL DEFAULT 0, + PRIMARY KEY ("ID"), + CONSTRAINT penguin_ibfk_1 FOREIGN KEY ("Color") REFERENCES item ("ID") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT penguin_ibfk_2 FOREIGN KEY ("Head") REFERENCES item ("ID") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT penguin_ibfk_3 FOREIGN KEY ("Face") REFERENCES item ("ID") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT penguin_ibfk_4 FOREIGN KEY ("Neck") REFERENCES item ("ID") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT penguin_ibfk_5 FOREIGN KEY ("Body") REFERENCES item ("ID") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT penguin_ibfk_6 FOREIGN KEY ("Hand") REFERENCES item ("ID") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT penguin_ibfk_7 FOREIGN KEY ("Feet") REFERENCES item ("ID") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT penguin_ibfk_8 FOREIGN KEY ("Photo") REFERENCES item ("ID") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT penguin_ibfk_9 FOREIGN KEY ("Flag") REFERENCES item ("ID") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT penguin_ibfk_10 FOREIGN KEY ("MascotStamp") REFERENCES stamp ("ID") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE INDEX "Email" ON Penguin("Email"); +CREATE UNIQUE INDEX "Username" ON Penguin("Username"); + +ALTER TABLE penguin ALTER COLUMN "RegistrationDate" SET DEFAULT now(); +ALTER TABLE penguin ALTER COLUMN "LastPaycheck" SET DEFAULT now(); +ALTER TABLE penguin ALTER COLUMN "LastFieldOp" SET DEFAULT now(); + +COMMENT ON TABLE penguin IS 'Penguins'; + +COMMENT ON COLUMN penguin."ID" IS 'Unique penguin ID'; +COMMENT ON COLUMN penguin."Username" IS 'Penguin login name'; +COMMENT ON COLUMN penguin."Nickname" IS 'Penguin display name'; +COMMENT ON COLUMN penguin."Approval" IS 'Username approval'; +COMMENT ON COLUMN penguin."Password" IS 'Password hash'; +COMMENT ON COLUMN penguin."LoginKey" IS 'Temporary login key'; +COMMENT ON COLUMN penguin."Email" IS 'User Email address'; +COMMENT ON COLUMN penguin."RegistrationDate" IS 'Date of registration'; +COMMENT ON COLUMN penguin."Active" IS '"Email" activated'; +COMMENT ON COLUMN penguin."LastPaycheck" IS 'EPF previous paycheck'; +COMMENT ON COLUMN penguin."MinutesPlayed" IS 'Total minutes connected'; +COMMENT ON COLUMN penguin."Moderator" IS 'Is user moderator?'; +COMMENT ON COLUMN penguin."Member" IS 'Is user member?'; +COMMENT ON COLUMN penguin."MascotStamp" IS 'Mascot stamp ID'; +COMMENT ON COLUMN penguin."Coins" IS 'Penguin coins'; +COMMENT ON COLUMN penguin."Color" IS 'Penguin color ID'; +COMMENT ON COLUMN penguin."Head" IS 'Penguin head item ID'; +COMMENT ON COLUMN penguin."Face" IS 'Penguin face item ID'; +COMMENT ON COLUMN penguin."Neck" IS 'Penguin neck item ID'; +COMMENT ON COLUMN penguin."Body" IS 'Penguin body item ID'; +COMMENT ON COLUMN penguin."Hand" IS 'Penguin hand item ID'; +COMMENT ON COLUMN penguin."Feet" IS 'Penguin feet item ID'; +COMMENT ON COLUMN penguin."Photo" IS 'Penguin background ID'; +COMMENT ON COLUMN penguin."Flag" IS 'Penguin pin ID'; +COMMENT ON COLUMN penguin."Permaban" IS 'Is penguin banned forever?'; +COMMENT ON COLUMN penguin."BookModified" IS 'Is book cover modified?'; +COMMENT ON COLUMN penguin."BookColor" IS 'Stampbook cover color'; +COMMENT ON COLUMN penguin."BookHighlight" IS 'Stampbook highlight color'; +COMMENT ON COLUMN penguin."BookPattern" IS 'Stampbook cover pattern'; +COMMENT ON COLUMN penguin."BookIcon" IS 'Stampbook cover icon'; +COMMENT ON COLUMN penguin."AgentStatus" IS 'Is penguin EPF agent?'; +COMMENT ON COLUMN penguin."FieldOpStatus" IS 'Is field op complete?'; +COMMENT ON COLUMN penguin."CareerMedals" IS 'Total career medals'; +COMMENT ON COLUMN penguin."AgentMedals" IS 'Current medals'; +COMMENT ON COLUMN penguin."LastFieldOp" IS 'Date of last field op'; +COMMENT ON COLUMN penguin."NinjaRank" IS 'Ninja rank'; +COMMENT ON COLUMN penguin."NinjaProgress" IS 'Ninja progress'; +COMMENT ON COLUMN penguin."FireNinjaRank" IS 'Fire ninja rank'; +COMMENT ON COLUMN penguin."FireNinjaProgress" IS 'Fire ninja progress'; +COMMENT ON COLUMN penguin."WaterNinjaRank" IS 'Water ninja rank'; +COMMENT ON COLUMN penguin."WaterNinjaProgress" IS 'Water ninja progress'; +COMMENT ON COLUMN penguin."NinjaMatchesWon" IS 'CardJitsu matches won'; +COMMENT ON COLUMN penguin."FireMatchesWon" IS 'JitsuFire matches won'; +COMMENT ON COLUMN penguin."WaterMatchesWon" IS 'JitsuWater matces won'; + + +DROP TABLE IF EXISTS activation_key; +CREATE TABLE activation_key ( + "PenguinID" INT NOT NULL, + "ActivationKey" CHAR(255) NOT NULL, + PRIMARY KEY ("PenguinID", "ActivationKey") +); + +COMMENT ON TABLE activation_key IS 'Penguin activation keys'; + +COMMENT ON COLUMN activation_key."PenguinID" IS 'Penguin ID'; +COMMENT ON COLUMN activation_key."ActivationKey" IS 'Penguin activation key'; + +DROP TABLE IF EXISTS ban; +CREATE TABLE ban ( + "PenguinID" INT NOT NULL, + "Issued" TIMESTAMP NOT NULL, + "Expires" TIMESTAMP NOT NULL, + "ModeratorID" INT DEFAULT NULL, + "Reason" SMALLINT NOT NULL, + "Comment" text DEFAULT NULL, + PRIMARY KEY ("PenguinID", "Issued", "Expires"), + CONSTRAINT ban_ibfk_1 FOREIGN KEY ("PenguinID") REFERENCES penguin ("ID") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT ban_ibfk_2 FOREIGN KEY ("ModeratorID") REFERENCES penguin ("ID") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE INDEX "ModeratorID" ON ban ("ModeratorID"); + +ALTER TABLE ban ALTER COLUMN "Issued" SET DEFAULT now(); +ALTER TABLE ban ALTER COLUMN "Expires" SET DEFAULT now(); + +COMMENT ON TABLE ban IS 'Penguin ban records'; + +COMMENT ON COLUMN ban."PenguinID" IS 'Banned penguin ID'; +COMMENT ON COLUMN ban."Issued" IS 'Issue date'; +COMMENT ON COLUMN ban."Expires" IS 'Expiry date'; +COMMENT ON COLUMN ban."ModeratorID" IS 'Moderator penguin ID'; +COMMENT ON COLUMN ban."Reason" IS 'Ban reason'; +COMMENT ON COLUMN ban."Comment" IS 'Ban comment'; + +DROP TABLE IF EXISTS buddy_list; +CREATE TABLE buddy_list ( + "PenguinID" INT NOT NULL, + "BuddyID" INT NOT NULL, + PRIMARY KEY ("PenguinID","BuddyID"), + CONSTRAINT buddy_list_ibfk_1 FOREIGN KEY ("PenguinID") REFERENCES penguin ("ID") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT buddy_list_ibfk_2 FOREIGN KEY ("BuddyID") REFERENCES penguin ("ID") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE INDEX "BuddyID" ON buddy_list ("BuddyID"); + +COMMENT ON TABLE buddy_list IS 'Penguin buddy relationships'; + +DROP TABLE IF EXISTS buddy_request; +CREATE TABLE buddy_request ( + "PenguinID" INT NOT NULL, + "RequesterID" INT NOT NULL, + PRIMARY KEY ("PenguinID", "RequesterID"), + CONSTRAINT buddy_request_ibfk_1 FOREIGN KEY ("PenguinID") REFERENCES penguin ("ID") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT buddy_request_ibfk_2 FOREIGN KEY ("RequesterID") REFERENCES penguin ("ID") ON DELETE CASCADE ON UPDATE CASCADE +); + +COMMENT ON TABLE buddy_request IS 'Penguin buddy requests'; + +DROP TABLE IF EXISTS best_buddy; +CREATE TABLE best_buddy ( + "PenguinID" INT NOT NULL, + "BuddyID" INT NOT NULL, + PRIMARY KEY ("PenguinID", "BuddyID"), + CONSTRAINT best_buddy_ibfk_1 FOREIGN KEY ("PenguinID") REFERENCES penguin ("ID") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT best_buddy_ibfk_2 FOREIGN KEY ("BuddyID") REFERENCES penguin ("ID") ON DELETE CASCADE ON UPDATE CASCADE +); + +COMMENT ON TABLE best_buddy IS 'Penguin best buddies'; + +DROP TABLE IF EXISTS cover_stamps; +CREATE TABLE cover_stamps ( + "PenguinID" INT NOT NULL, + "StampID" SMALLINT NOT NULL, + "ItemID" SMALLINT NOT NULL, + "X" SMALLINT NOT NULL DEFAULT 0, + "Y" SMALLINT NOT NULL DEFAULT 0, + "Type" SMALLINT NOT NULL DEFAULT 0, + "Rotation" SMALLINT NOT NULL DEFAULT 0, + "Depth" SMALLINT NOT NULL DEFAULT 0, + PRIMARY KEY ("PenguinID", "StampID"), + CONSTRAINT cover_stamps_ibfk_1 FOREIGN KEY ("PenguinID") REFERENCES penguin ("ID") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT cover_stamps_ibfk_2 FOREIGN KEY ("StampID") REFERENCES stamp ("ID") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT cover_stamps_ibfk_3 FOREIGN KEY ("ItemID") REFERENCES item ("ID") ON DELETE CASCADE ON UPDATE CASCADE +); + +COMMENT ON TABLE cover_stamps IS 'Stamps placed on book cover'; + +COMMENT ON COLUMN cover_stamps."PenguinID" IS 'Unique penguin ID'; +COMMENT ON COLUMN cover_stamps."StampID" IS 'Cover stamp or item ID'; +COMMENT ON COLUMN cover_stamps."X" IS 'Cover X position'; +COMMENT ON COLUMN cover_stamps."Y" IS 'Cover Y position'; +COMMENT ON COLUMN cover_stamps."Type" IS 'Cover item type'; +COMMENT ON COLUMN cover_stamps."Rotation" IS 'Stamp cover rotation'; +COMMENT ON COLUMN cover_stamps."Depth" IS 'Stamp cover depth'; + +DROP TABLE IF EXISTS penguin_card; +CREATE TABLE penguin_card ( + "PenguinID" INT NOT NULL, + "CardID" SMALLINT NOT NULL, + "Quantity" SMALLINT NOT NULL DEFAULT 1, + PRIMARY KEY ("PenguinID", "CardID"), + CONSTRAINT penguin_card_ibfk_1 FOREIGN KEY ("PenguinID") REFERENCES penguin ("ID") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT penguin_card_ibfk_2 FOREIGN KEY ("CardID") REFERENCES card ("ID") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE INDEX "PenguinID" ON penguin_card("PenguinID"); + +COMMENT ON TABLE penguin_card IS 'Penguin Card Jitsu decks'; + +COMMENT ON COLUMN penguin_card."PenguinID" IS 'Owner penguin ID'; +COMMENT ON COLUMN penguin_card."CardID" IS 'Card type ID'; +COMMENT ON COLUMN penguin_card."Quantity" IS 'Quantity owned'; + +DROP TABLE IF EXISTS penguin_furniture; +CREATE TABLE penguin_furniture ( + "PenguinID" INT NOT NULL, + "FurnitureID" SMALLINT NOT NULL, + "Quantity" SMALLINT NOT NULL DEFAULT 1, + PRIMARY KEY ("PenguinID", "FurnitureID"), + CONSTRAINT penguin_furniture_ibfk_1 FOREIGN KEY ("PenguinID") REFERENCES penguin ("ID") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT penguin_furniture_ibfk_2 FOREIGN KEY ("FurnitureID") REFERENCES furniture ("ID") ON DELETE CASCADE ON UPDATE CASCADE +); + +COMMENT ON TABLE penguin_furniture IS 'Penguin owned furniture'; + +COMMENT ON COLUMN penguin_furniture."PenguinID" IS 'Owner penguin ID'; +COMMENT ON COLUMN penguin_furniture."FurnitureID" IS 'Furniture item ID'; +COMMENT ON COLUMN penguin_furniture."Quantity" IS 'Quantity owned'; + +DROP TABLE IF EXISTS penguin_igloo; +CREATE TABLE penguin_igloo ( + "ID" SERIAL, + "PenguinID" INT NOT NULL, + "Type" SMALLINT NOT NULL, + "Flooring" SMALLINT NOT NULL DEFAULT 0, + "Music" SMALLINT NOT NULL DEFAULT 0, + "Locked" BOOLEAN NOT NULL DEFAULT FALSE, + PRIMARY KEY ("ID"), + CONSTRAINT igloo_ibfk_1 FOREIGN KEY ("PenguinID") REFERENCES penguin ("ID") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT igloo_ibfk_2 FOREIGN KEY ("Type") REFERENCES igloo ("ID") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT igloo_ibfk_3 FOREIGN KEY ("Flooring") REFERENCES flooring ("ID") ON DELETE CASCADE ON UPDATE CASCADE +); + +COMMENT ON TABLE penguin_igloo IS 'Penguin igloo settings'; + +COMMENT ON COLUMN penguin_igloo."ID" IS 'Unique igloo ID'; +COMMENT ON COLUMN penguin_igloo."PenguinID" IS 'Owner penguin ID'; +COMMENT ON COLUMN penguin_igloo."Type" IS 'Igloo type ID'; +COMMENT ON COLUMN penguin_igloo."Floor" IS 'Igloo flooring ID'; +COMMENT ON COLUMN penguin_igloo."Music" IS 'Igloo music ID'; +COMMENT ON COLUMN penguin_igloo."Locked" IS 'Is igloo locked?'; + +DROP TABLE IF EXISTS igloo_like; +CREATE TABLE igloo_like ( + "IglooID" INT NOT NULL, + "OwnerID" INT NOT NULL, + "PlayerID" INT NOT NULL, + "Count" SMALLiNT NOT NULL, + "Date" DATE NOT NULL, + PRIMARY KEY ("IglooID", "OwnerID", "PlayerID"), + CONSTRAINT igloo_like_ibfk_1 FOREIGN KEY ("IglooID") REFERENCES penguin_igloo ("ID") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT igloo_like_ibfk_2 FOREIGN KEY ("OwnerID") REFERENCES penguin ("ID") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT igloo_like_ibfk_3 FOREIGN KEY ("PlayerID") REFERENCES penguin ("ID") ON DELETE CASCADE ON UPDATE CASCADE +); + +ALTER TABLE igloo_like ALTER COLUMN "Date" SET DEFAULT now(); + +COMMENT ON TABLE igloo_like IS 'Player igloo likes'; + +COMMENT ON COLUMN igloo_like."IglooID" IS 'Igloo unique ID'; +COMMENT ON COLUMN igloo_like."OwnerID" IS 'Owner unique ID'; +COMMENT ON COLUMN igloo_like."PlayerID" IS 'Liker unique ID'; +COMMENT ON COLUMN igloo_like."Count" IS 'Number of likes'; +COMMENT ON COLUMN igloo_like."Date" IS 'Date of like'; + + +DROP TABLE IF EXISTS penguin_location; +CREATE TABLE penguin_location ( + "PenguinID" INT NOT NULL, + "LocationID" SMALLINT NOT NULL, + PRIMARY KEY ("PenguinID", "LocationID"), + CONSTRAINT penguin_location_ibfk_1 FOREIGN KEY ("PenguinID") REFERENCES penguin ("ID") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT penguin_location_ibfk_2 FOREIGN KEY ("LocationID") REFERENCES location ("ID") ON DELETE CASCADE ON UPDATE CASCADE +); + +COMMENT ON TABLE penguin_location IS 'Penguin owned locations'; + +COMMENT ON COLUMN penguin_location."PenguinID" IS 'Owner penguin ID'; +COMMENT ON COLUMN penguin_location."LocationID" IS 'Location ID'; + +DROP TABLE IF EXISTS igloo_furniture; +CREATE TABLE igloo_furniture ( + "IglooID" INT NOT NULL, + "FurnitureID" SMALLINT NOT NULL, + "X" SMALLINT NOT NULL DEFAULT 0, + "Y" SMALLINT NOT NULL DEFAULT 0, + "Frame" SMALLINT NOT NULL DEFAULT 0, + "Rotation" SMALLINT NOT NULL DEFAULT 0, + PRIMARY KEY ("IglooID", "FurnitureID", "X", "Y", "Frame", "Rotation"), + CONSTRAINT igloo_furniture_ibfk_1 FOREIGN KEY ("IglooID") REFERENCES penguin_igloo ("ID") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT igloo_furniture_ibfk_2 FOREIGN KEY ("FurnitureID") REFERENCES furniture ("ID") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE INDEX IglooID ON igloo_furniture("IglooID"); + +COMMENT ON TABLE igloo_furniture IS 'Furniture placed inside igloos'; + +COMMENT ON COLUMN igloo_furniture."IglooID" IS 'Furniture igloo ID'; +COMMENT ON COLUMN igloo_furniture."FurnitureID" IS 'Furniture item ID'; +COMMENT ON COLUMN igloo_furniture."X" IS 'Igloo X position'; +COMMENT ON COLUMN igloo_furniture."Y" IS 'Igloo Y position'; +COMMENT ON COLUMN igloo_furniture."Frame" IS 'Furniture frame ID'; +COMMENT ON COLUMN igloo_furniture."Rotation" IS 'Furniture rotation ID'; + +DROP TABLE IF EXISTS igloo_inventory; +CREATE TABLE igloo_inventory ( + "PenguinID" INT NOT NULL DEFAULT 0, + "IglooID" SMALLINT NOT NULL, + PRIMARY KEY ("PenguinID", "IglooID"), + CONSTRAINT igloo_inventory_ibfk_1 FOREIGN KEY ("PenguinID") REFERENCES penguin ("ID") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT igloo_inventory_ibfk_2 FOREIGN KEY ("IglooID") REFERENCES igloo ("ID") ON DELETE CASCADE ON UPDATE CASCADE +); + +COMMENT ON TABLE igloo_inventory IS 'Penguin owned igloos'; + +COMMENT ON COLUMN igloo_inventory."PenguinID" IS 'Owner penguin ID'; +COMMENT ON COLUMN igloo_inventory."IglooID" IS 'Igloo ID'; + +DROP TABLE IF EXISTS ignore_list; +CREATE TABLE ignore_list ( + "PenguinID" INT NOT NULL, + "IgnoreID" INT NOT NULL, + PRIMARY KEY ("PenguinID", "IgnoreID"), + CONSTRAINT ignore_list_ibfk_1 FOREIGN KEY ("PenguinID") REFERENCES penguin ("ID") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT ignore_list_ibfk_2 FOREIGN KEY ("IgnoreID") REFERENCES penguin ("ID") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE INDEX IgnoreID ON ignore_list("IgnoreID"); + +COMMENT ON TABLE ignore_list IS 'Penguin ignore relationships'; + +DROP TABLE IF EXISTS penguin_item; +CREATE TABLE penguin_item ( + "PenguinID" INT NOT NULL, + "ItemID" SMALLINT NOT NULL DEFAULT 0, + PRIMARY KEY ("PenguinID", "ItemID"), + CONSTRAINT penguin_item_ibfk_1 FOREIGN KEY ("PenguinID") REFERENCES penguin ("ID") ON DELETE CASCADE ON UPDATE CASCADE +); + +COMMENT ON TABLE penguin_item IS 'Penguin owned clothing items'; + +COMMENT ON COLUMN penguin_item."PenguinID" IS 'Owner penguin ID'; +COMMENT ON COLUMN penguin_item."ItemID" IS 'Clothing item ID'; + +DROP TABLE IF EXISTS login; +CREATE TABLE login ( + "ID" SERIAL, + "PenguinID" INT NOT NULL, + "Date" TIMESTAMP NOT NULL, + "IPAddress" char(255) NOT NULL, + PRIMARY KEY ("ID"), + CONSTRAINT login_ibfk_1 FOREIGN KEY ("PenguinID") REFERENCES penguin ("ID") ON DELETE CASCADE ON UPDATE CASCADE +); + +ALTER TABLE login ALTER COLUMN "Date" SET DEFAULT now(); + +COMMENT ON TABLE login IS 'Penguin login records'; + +COMMENT ON COLUMN login."ID" IS 'Unique login ID'; +COMMENT ON COLUMN login."PenguinID" IS 'Login penguin ID'; +COMMENT ON COLUMN login."Date" IS 'Login date'; +COMMENT ON COLUMN login."IPAddress" IS 'Connection IP address'; + +DROP TABLE IF EXISTS postcard; +CREATE TABLE postcard ( + "ID" SERIAL, + "SenderID" INT DEFAULT NULL, + "RecipientID" INT NOT NULL, + "Type" SMALLINT NOT NULL, + "SendDate" TIMESTAMP NOT NULL, + "Details" char(255) NOT NULL DEFAULT '', + "HasRead" BOOLEAN NOT NULL DEFAULT FALSE, + PRIMARY KEY ("ID"), + CONSTRAINT postcard_ibfk_1 FOREIGN KEY ("SenderID") REFERENCES penguin ("ID") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT postcard_ibfk_2 FOREIGN KEY ("RecipientID") REFERENCES penguin ("ID") ON DELETE CASCADE ON UPDATE CASCADE +); + +ALTER TABLE postcard ALTER COLUMN "SendDate" SET DEFAULT now(); + +CREATE INDEX "SenderID" ON postcard("SenderID"); +CREATE INDEX "RecipientID" ON postcard("RecipientID"); + +COMMENT ON TABLE postcard IS 'Sent postcards'; + +COMMENT ON COLUMN postcard."ID" IS 'Unique postcard ID'; +COMMENT ON COLUMN postcard."SenderID" IS 'Sender penguin ID'; +COMMENT ON COLUMN postcard."RecipientID" IS 'Postcard type ID'; +COMMENT ON COLUMN postcard."Type" IS 'Postcard type ID'; +COMMENT ON COLUMN postcard."SendDate" IS 'Postcard type ID'; +COMMENT ON COLUMN postcard."Details" IS 'Postcard details'; +COMMENT ON COLUMN postcard."HasRead" IS 'Is read?'; + +DROP TABLE IF EXISTS penguin_puffle; +CREATE TABLE penguin_puffle ( + "ID" SERIAL, + "PenguinID" INT NOT NULL, + "Name" varchar(16) NOT NULL, + "AdoptionDate" TIMESTAMP NOT NULL, + "Type" SMALLINT NOT NULL, + "Food" SMALLINT NOT NULL DEFAULT 100, + "Play" SMALLINT NOT NULL DEFAULT 100, + "Rest" SMALLINT NOT NULL DEFAULT 100, + "Clean" SMALLINT NOT NULL DEFAULT 100, + "Walking" BOOLEAN DEFAULT FALSE, + "Hat" SMALLINT NOT NULL, + "Backyard" BOOLEAN DEFAULT FALSE, + "HasDug" BOOLEAN DEFAULT FALSE, + PRIMARY KEY ("ID"), + CONSTRAINT penguin_puffle_ibfk_1 FOREIGN KEY ("PenguinID") REFERENCES penguin ("ID") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT penguin_puffle_ibfk_2 FOREIGN KEY ("Type") REFERENCES puffle ("ID") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT penguin_puffle_ibfk_3 FOREIGN KEY ("Hat") REFERENCES puffle_care_item ("ID") ON DELETE CASCADE ON UPDATE CASCADE +); + +ALTER TABLE penguin_puffle ALTER COLUMN "AdoptionDate" SET DEFAULT now(); + +COMMENT ON TABLE penguin_puffle IS 'Adopted puffles'; + +COMMENT ON COLUMN penguin_puffle."ID" IS 'Unique puffle ID'; +COMMENT ON COLUMN penguin_puffle."PenguinID" IS 'Owner penguin ID'; +COMMENT ON COLUMN penguin_puffle."Name" IS 'Puffle name'; +COMMENT ON COLUMN penguin_puffle."AdoptionDate" IS 'Date of adoption'; +COMMENT ON COLUMN penguin_puffle."Type" IS 'Puffle type ID'; +COMMENT ON COLUMN penguin_puffle."Food" IS 'Puffle health %'; +COMMENT ON COLUMN penguin_puffle."Play" IS 'Puffle hunger %'; +COMMENT ON COLUMN penguin_puffle."Rest" IS 'Puffle rest %'; +COMMENT ON COLUMN penguin_puffle."Clean" IS 'Puffle clean %'; +COMMENT ON COLUMN penguin_puffle."Walking" IS 'Is being walked?'; +COMMENT ON COLUMN penguin_puffle."Hat" IS 'Puffle hat item ID'; +COMMENT ON COLUMN penguin_puffle."Backyard" IS 'Is in backyard?'; +COMMENT ON COLUMN penguin_puffle."HasDug" IS 'Has dug?'; + +DROP TABLE IF EXISTS puffle_quest; +CREATE TABLE puffle_quest ( + "PenguinID" SMALLINT NOT NULL, + "TaskID" SMALLINT NOT NULL, + "CompletionDate" TIMESTAMP DEFAULT NULL, + "ItemCollected" BOOLEAN NOT NULL DEFAULT FALSE, + "CoinsCollected" BOOLEAN NOT NULL DEFAULT FALSE, + PRIMARY KEY ("PenguinID", "TaskID"), + CONSTRAINT puffle_quest_ibfk_1 FOREIGN KEY ("PenguinID") REFERENCES penguin ("ID") ON DELETE CASCADE ON UPDATE CASCADE +); + +COMMENT ON TABLE puffle_quest IS 'Puffle quest progress'; + +COMMENT ON COLUMN puffle_quest."PenguinID" IS 'Quest penguin ID'; +COMMENT ON COLUMN puffle_quest."TaskID" IS 'Quest task ID'; +COMMENT ON COLUMN puffle_quest."CompletionDate" IS 'Time of completion'; +COMMENT ON COLUMN puffle_quest."ItemCollected" IS 'Item collection status'; +COMMENT ON COLUMN puffle_quest."CoinsCollected" IS 'Coins collection status'; + +CREATE TYPE redemption_type AS ENUM ('DS','BLANKET','CARD','GOLDEN','CAMPAIGN'); + +DROP TABLE IF EXISTS redemption_code; +CREATE TABLE redemption_code ( + "ID" SERIAL, + "Code" varchar(16) NOT NULL, + "Type" redemption_type NOT NULL DEFAULT 'BLANKET', + "Coins" INT NOT NULL DEFAULT 0, + "Expires" TIMESTAMP DEFAULT NULL, + PRIMARY KEY ("ID") +); + +COMMENT ON TABLE redemption_code IS 'Redemption codes'; + +COMMENT ON COLUMN redemption_code."ID" IS 'Unique code ID'; +COMMENT ON COLUMN redemption_code."Code" IS 'Redemption code'; +COMMENT ON COLUMN redemption_code."Type" IS 'Code type'; +COMMENT ON COLUMN redemption_code."Coins" IS 'Code coins amount'; +COMMENT ON COLUMN redemption_code."Expires" IS 'Expiry date'; + +DROP TABLE IF EXISTS penguin_redemption; +CREATE TABLE penguin_redemption ( + "PenguinID" INT NOT NULL DEFAULT 0, + "CodeID" INT NOT NULL DEFAULT 0, + PRIMARY KEY ("PenguinID", "CodeID"), + CONSTRAINT penguin_redemption_ibfk_1 FOREIGN KEY ("PenguinID") REFERENCES penguin ("ID") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT penguin_redemption_ibfk_2 FOREIGN KEY ("CodeID") REFERENCES redemption_code ("ID") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE INDEX "CodeID" ON penguin_redemption("CodeID"); + +COMMENT ON TABLE penguin_redemption IS 'Redeemed codes'; + +COMMENT ON COLUMN penguin_redemption."PenguinID" IS 'Unique penguin ID'; +COMMENT ON COLUMN penguin_redemption."CodeID" IS 'Unique code ID'; + + +DROP TABLE IF EXISTS redemption_award; +CREATE TABLE redemption_award ( + "CodeID" INT NOT NULL DEFAULT 0, + "CardID" SMALLINT DEFAULT NULL, + "ItemID" SMALLINT DEFAULT NULL, + PRIMARY KEY ("CodeID", "CardID", "ItemID"), + CONSTRAINT redemption_award_ibfk_1 FOREIGN KEY ("CodeID") REFERENCES redemption_code ("ID") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT redemption_award_ibfk_2 FOREIGN KEY ("CardID") REFERENCES card ("ID") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT redemption_award_ibfk_3 FOREIGN KEY ("ItemID") REFERENCES item ("ID") ON DELETE CASCADE ON UPDATE CASCADE +); + +COMMENT ON TABLE redemption_award IS 'Redemption code awards'; + +COMMENT ON COLUMN redemption_award."CodeID" IS 'Unique code ID'; +COMMENT ON COLUMN redemption_award."CardID" IS 'Code card ID'; +COMMENT ON COLUMN redemption_award."ItemID" IS 'Code item ID'; + +DROP TABLE IF EXISTS penguin_stamp; +CREATE TABLE penguin_stamp ( + "PenguinID" INT NOT NULL, + "StampID" SMALLINT NOT NULL, + "Recent" BOOLEAN NOT NULL DEFAULT TRUE, + PRIMARY KEY ("PenguinID", "StampID"), + CONSTRAINT stamp_ibfk_1 FOREIGN KEY ("PenguinID") REFERENCES penguin ("ID") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT stamp_ibfk_2 FOREIGN KEY ("StampID") REFERENCES stamp ("ID") ON DELETE CASCADE ON UPDATE CASCADE +); + +COMMENT ON TABLE penguin_stamp IS 'Penguin earned stamps'; + +COMMENT ON COLUMN penguin_stamp."PenguinID" IS 'Stamp penguin ID'; +COMMENT ON COLUMN penguin_stamp."StampID" IS 'Stamp ID'; +COMMENT ON COLUMN penguin_stamp."Recent" IS 'Is recently earned?'; diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f92b62b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,11 @@ +asyncio +aioredis +gino +aiologger==0.4.0rc1 +aiologger[aiofiles] +aiocache +ujson +watchdog +defusedxml +zope.interface +uvloop; sys_platform == 'linux2' or sys_platform == 'darwin' \ No newline at end of file