mirror of
https://github.com/solero/houdini.git
synced 2024-11-21 21:17:21 +00:00
Initial commit
This commit is contained in:
parent
a29799503f
commit
9d7c437845
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'
|
Loading…
Reference in New Issue
Block a user