mirror of
				https://github.com/solero/houdini.git
				synced 2025-10-31 12:41:56 +00:00 
			
		
		
		
	Initial commit
This commit is contained in:
		
							
								
								
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -9,6 +9,7 @@ __pycache__/ | ||||
| # Distribution / packaging | ||||
| .Python | ||||
| env/ | ||||
| venv/ | ||||
| build/ | ||||
| develop-eggs/ | ||||
| dist/ | ||||
| @@ -56,3 +57,6 @@ docs/_build/ | ||||
|  | ||||
| # PyBuilder | ||||
| target/ | ||||
|  | ||||
| # PyCharm | ||||
| .idea/ | ||||
							
								
								
									
										11
									
								
								Bootstrap.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								Bootstrap.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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()) | ||||
							
								
								
									
										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 | ||||
							
								
								
									
										25
									
								
								Test.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								Test.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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('<msg t="sys"><body action="login" r="0"><login z="w1"><nick><![CDATA[benny]]></nick><pword><![CDATA[pword]]></pword></login></body></msg>\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() | ||||
							
								
								
									
										87
									
								
								config.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								config.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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} | ||||
|         ]} | ||||
|     ] | ||||
| } | ||||
							
								
								
									
										762
									
								
								houdini.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										762
									
								
								houdini.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -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?'; | ||||
							
								
								
									
										11
									
								
								requirements.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								requirements.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -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' | ||||
		Reference in New Issue
	
	Block a user
	 Ben
					Ben