Initial commit

This commit is contained in:
Ben 2019-03-01 19:41:40 +00:00
parent a29799503f
commit 9d7c437845
22 changed files with 2037 additions and 0 deletions

4
.gitignore vendored
View File

@ -9,6 +9,7 @@ __pycache__/
# Distribution / packaging # Distribution / packaging
.Python .Python
env/ env/
venv/
build/ build/
develop-eggs/ develop-eggs/
dist/ dist/
@ -56,3 +57,6 @@ docs/_build/
# PyBuilder # PyBuilder
target/ target/
# PyCharm
.idea/

11
Bootstrap.py Normal file
View 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
View 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
View 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
View File

@ -0,0 +1,4 @@
from gino import Gino
db = Gino()

View 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!")

View 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

View 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))

View File

View 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)

View File

View 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
View 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
View 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

View 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))

View File

159
Houdini/Spheniscidae.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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'