From 6421a373e1f8e541c28e458c3a7930f5fa54af75 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 21 Jun 2020 15:32:31 +0200 Subject: [PATCH] Webhost Update introduce a very WIP tracker Server will try to reuse port and also try to only use one port --- MultiServer.py | 3 +- Utils.py | 10 ++- WebHost/__init__.py | 132 ++++++++++++++++++++++------- WebHost/customserver.py | 27 +++++- WebHost/models.py | 13 +-- WebHost/requirements.txt | 3 +- WebHost/static/itemnames.json | 140 +++++++++++++++++++++++++++++++ WebHost/templates/host_room.html | 5 ++ WebHost/templates/tracker.html | 32 +++++++ 9 files changed, 316 insertions(+), 49 deletions(-) create mode 100644 WebHost/static/itemnames.json create mode 100644 WebHost/templates/tracker.html diff --git a/MultiServer.py b/MultiServer.py index 86fbf9bb..368a57f9 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -1202,8 +1202,7 @@ async def main(args: argparse.Namespace): await ctx.shutdown_task if __name__ == '__main__': - loop = asyncio.get_event_loop() try: - loop.run_until_complete(main(parse_args())) + asyncio.run(main(parse_args())) except asyncio.exceptions.CancelledError: pass diff --git a/Utils.py b/Utils.py index c8234efc..944d23a1 100644 --- a/Utils.py +++ b/Utils.py @@ -1,12 +1,18 @@ from __future__ import annotations +import typing + + +def tuplize_version(version: str) -> typing.Tuple[int, ...]: + return tuple(int(piece, 10) for piece in version.split(".")) + __version__ = "2.3.3" -_version_tuple = tuple(int(piece, 10) for piece in __version__.split(".")) +_version_tuple = tuplize_version(__version__) import os import subprocess import sys -import typing + import functools from yaml import load, dump diff --git a/WebHost/__init__.py b/WebHost/__init__.py index c1ee0e74..1afaeefe 100644 --- a/WebHost/__init__.py +++ b/WebHost/__init__.py @@ -1,14 +1,16 @@ +import json import os import logging import typing import multiprocessing import threading -import json import zlib +import collections -from pony.orm import db_session, commit from pony.flask import Pony -from flask import Flask, flash, request, redirect, url_for, render_template, Response, session +from flask import Flask, request, redirect, url_for, render_template, Response, session, abort, flash +from flask_caching import Cache +from pony.orm import commit from .models import * @@ -20,9 +22,9 @@ os.makedirs(LOGS_FOLDER, exist_ok=True) def allowed_file(filename): return filename.endswith('multidata') - app = Flask(__name__) Pony(app) + app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER app.config['MAX_CONTENT_LENGTH'] = 1 * 1024 * 1024 # 1 megabyte limit # if you want persistent sessions on your server, make sure you make this a constant in your config.yaml @@ -33,6 +35,9 @@ app.config["PONY"] = { 'filename': os.path.abspath('db.db3'), 'create_db': True } +app.config["CACHE_TYPE"] = "simple" + +cache = Cache(app) multiworlds = {} @@ -66,36 +71,14 @@ class MultiworldInstance(): self.process.terminate() self.process = None -@app.route('/', methods=['GET', 'POST']) -def upload_multidata(): - if request.method == 'POST': - # check if the post request has the file part - if 'file' not in request.files: - flash('No file part') - else: - file = request.files['file'] - # if user does not select file, browser also - # submit an empty part without filename - if file.filename == '': - flash('No selected file') - elif file and allowed_file(file.filename): - try: - multidata = json.loads(zlib.decompress(file.read()).decode("utf-8-sig")) - except: - flash("Could not load multidata. File may be corrupted or incompatible.") - else: - seed = Seed(multidata=multidata) - commit() # place into DB and generate ids - return redirect(url_for("view_seed", seed=seed.id)) - else: - flash("Not recognized file format. Awaiting a .multidata file.") - return render_template("upload_multidata.html") - @app.route('/seed/') def view_seed(seed: int): seed = Seed.get(id=seed) - return render_template("view_seed.html", seed=seed) + if seed: + return render_template("view_seed.html", seed=seed) + else: + abort(404) @app.route('/new_room/') @@ -143,4 +126,93 @@ def host_room(room: int): return render_template("host_room.html", room=room) +@app.route('/tracker/') +@cache.memoize(timeout=60 * 5) # update every 5 minutes +def get_tracker(room: int): + # This more WIP than the rest + import Items + def get_id(item_name): + return Items.item_table[item_name][3] + + room = Room.get(id=room) + if not room: + abort(404) + if room.allow_tracker: + multidata = room.seed.multidata + locations = {tuple(k): tuple(v) for k, v in multidata['locations']} + + links = {"Bow": "Progressive Bow", + "Silver Arrows": "Progressive Bow", + "Bottle (Red Potion)": "Bottle", + "Bottle (Green Potion)": "Bottle", + "Bottle (Blue Potion)": "Bottle", + "Bottle (Fairy)": "Bottle", + "Bottle (Bee)": "Bottle", + "Bottle (Good Bee)": "Bottle", + "Fighter Sword": "Progressive Sword", + "Master Sword": "Progressive Sword", + "Tempered Sword": "Progressive Sword", + "Golden Sword": "Progressive Sword", + "Power Glove": "Progressive Glove", + "Titans Mitts": "Progressive Glove" + } + links = {get_id(key): get_id(value) for key, value in links.items()} + inventory = {teamnumber: {playernumber: collections.Counter() for playernumber in range(len(team))} + for teamnumber, team in enumerate(multidata["names"])} + for (team, player), locations_checked in room.multisave.get("location_checks", {}): + for location in locations_checked: + item, recipient = locations[location, player] + inventory[team][recipient][links.get(item, item)] += 1 + + from MultiServer import get_item_name_from_id + from Items import lookup_id_to_name + player_names = {} + for team, names in enumerate(multidata['names']): + for player, name in enumerate(names, 1): + player_names[(team, player)] = name + tracking_names = ["Progressive Sword", "Progressive Bow", "Progressive Bow (Alt)", "Book of Mudora", "Hammer", + "Hookshot", "Magic Mirror", "Flute", + "Pegasus Boots", "Progressive Glove", "Flippers", "Moon Pearl", "Blue Boomerang", + "Red Boomerang", "Bug Catching Net", "Cane of Byrna", "Cape", "Mushroom", "Shovel", "Lamp", + "Magic Powder", + "Cane of Somaria", "Fire Rod", "Ice Rod", "Bombos", "Ether", "Quake", + "Bottle", "Triforce"] # TODO make sure this list has what we need and sort it better + tracking_ids = [] + + for item in tracking_names: + tracking_ids.append(get_id(item)) + + return render_template("tracker.html", inventory=inventory, get_item_name_from_id=get_item_name_from_id, + lookup_id_to_name=lookup_id_to_name, player_names=player_names, + tracking_names=tracking_names, tracking_ids=tracking_ids) + else: + return "Tracker disabled for this room." + + from WebHost.customserver import run_server_process + + +@app.route('/', methods=['GET', 'POST']) +def upload_multidata(): + if request.method == 'POST': + # check if the post request has the file part + if 'file' not in request.files: + flash('No file part') + else: + file = request.files['file'] + # if user does not select file, browser also + # submit an empty part without filename + if file.filename == '': + flash('No selected file') + elif file and allowed_file(file.filename): + try: + multidata = json.loads(zlib.decompress(file.read()).decode("utf-8-sig")) + except: + flash("Could not load multidata. File may be corrupted or incompatible.") + else: + seed = Seed(multidata=multidata) + commit() # place into DB and generate ids + return redirect(url_for("view_seed", seed=seed.id)) + else: + flash("Not recognized file format. Awaiting a .multidata file.") + return render_template("upload_multidata.html") diff --git a/WebHost/customserver.py b/WebHost/customserver.py index 4d2a0e9d..58e15d07 100644 --- a/WebHost/customserver.py +++ b/WebHost/customserver.py @@ -6,6 +6,7 @@ import asyncio import socket import threading import time +import random from WebHost import LOGS_FOLDER from .models import * @@ -39,7 +40,12 @@ class WebHostContext(Context): @db_session def load(self, room_id: int): self.room_id = room_id - return self._load(Room.get(id=room_id).seed.multidata, True) + room = Room.get(id=room_id) + if room.last_port: + self.port = room.last_port + else: + self.port = get_random_port() + return self._load(room.seed.multidata, True) @db_session def init_save(self, enabled: bool = True): @@ -57,6 +63,9 @@ class WebHostContext(Context): return True +def get_random_port(): + return random.randint(49152, 65535) + def run_server_process(room_id, ponyconfig: dict): # establish DB connection for multidata and multisave db.bind(**ponyconfig) @@ -72,14 +81,24 @@ def run_server_process(room_id, ponyconfig: dict): ctx.auto_shutdown = 24 * 60 * 60 # 24 hours ctx.init_save() - ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, 0, ping_timeout=None, - ping_interval=None) + try: + ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, ctx.port, ping_timeout=None, + ping_interval=None) - await ctx.server + await ctx.server + except Exception: # likely port in use - in windows this is OSError, but I didn't check the others + ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, 0, ping_timeout=None, + ping_interval=None) + + await ctx.server for wssocket in ctx.server.ws_server.sockets: socketname = wssocket.getsockname() if wssocket.family == socket.AF_INET6: logging.info(f'Hosting game at [{get_public_ipv6()}]:{socketname[1]}') + if ctx.port != socketname[1]: # different port + with db_session: + room = Room.get(id=ctx.room_id) + room.last_port = socketname[1] elif wssocket.family == socket.AF_INET: logging.info(f'Hosting game at {get_public_ipv4()}:{socketname[1]}') ctx.auto_shutdown = 6 * 60 diff --git a/WebHost/models.py b/WebHost/models.py index 88bebbd2..094fc815 100644 --- a/WebHost/models.py +++ b/WebHost/models.py @@ -16,22 +16,15 @@ class Room(db.Entity): last_activity = Required(datetime, default=lambda: datetime.utcnow()) owner = Required(UUID) commands = Set('Command') - host_jobs = Set('HostJob') seed = Required('Seed') multisave = Optional(Json) + timeout = Required(int, default=lambda: 6) + allow_tracker = Required(bool, default=True) + last_port = Optional(int, default=lambda: 0) -class HostJob(db.Entity): - id = PrimaryKey(int, auto=True) - sockets = Set('Socket') - room = Required(Room) - scheduler_id = Required(int, unique=True) -class Socket(db.Entity): - port = PrimaryKey(int) - ipv6 = Required(bool) - host_job = Required(HostJob) class Seed(db.Entity): diff --git a/WebHost/requirements.txt b/WebHost/requirements.txt index 388659b7..cb83251b 100644 --- a/WebHost/requirements.txt +++ b/WebHost/requirements.txt @@ -1,3 +1,4 @@ flask>=1.1.2 pony>=0.7.13 -waitress>=1.4.4 \ No newline at end of file +waitress>=1.4.4 +flask-caching>=1.9.0 \ No newline at end of file diff --git a/WebHost/static/itemnames.json b/WebHost/static/itemnames.json new file mode 100644 index 00000000..01c38b34 --- /dev/null +++ b/WebHost/static/itemnames.json @@ -0,0 +1,140 @@ +{ + "11": "Bow", + "100": "Progressive Bow", + "101": "Progressive Bow (Alt)", + "29": "Book of Mudora", + "9": "Hammer", + "10": "Hookshot", + "26": "Magic Mirror", + "20": "Flute", + "75": "Pegasus Boots", + "27": "Power Glove", + "25": "Cape", + "41": "Mushroom", + "19": "Shovel", + "18": "Lamp", + "13": "Magic Powder", + "31": "Moon Pearl", + "21": "Cane of Somaria", + "7": "Fire Rod", + "30": "Flippers", + "8": "Ice Rod", + "28": "Titans Mitts", + "15": "Bombos", + "16": "Ether", + "17": "Quake", + "22": "Bottle", + "43": "Bottle (Red Potion)", + "44": "Bottle (Green Potion)", + "45": "Bottle (Blue Potion)", + "61": "Bottle (Fairy)", + "60": "Bottle (Bee)", + "72": "Bottle (Good Bee)", + "80": "Master Sword", + "2": "Tempered Sword", + "73": "Fighter Sword", + "3": "Golden Sword", + "94": "Progressive Sword", + "97": "Progressive Glove", + "88": "Silver Arrows", + "106": "Triforce", + "107": "Power Star", + "108": "Triforce Piece", + "67": "Single Arrow", + "68": "Arrows (10)", + "84": "Arrow Upgrade (+10)", + "83": "Arrow Upgrade (+5)", + "39": "Single Bomb", + "40": "Bombs (3)", + "49": "Bombs (10)", + "82": "Bomb Upgrade (+10)", + "81": "Bomb Upgrade (+5)", + "34": "Blue Mail", + "35": "Red Mail", + "96": "Progressive Armor", + "12": "Blue Boomerang", + "42": "Red Boomerang", + "4": "Blue Shield", + "5": "Red Shield", + "6": "Mirror Shield", + "95": "Progressive Shield", + "33": "Bug Catching Net", + "24": "Cane of Byrna", + "62": "Boss Heart Container", + "63": "Sanctuary Heart Container", + "23": "Piece of Heart", + "52": "Rupee (1)", + "53": "Rupees (5)", + "54": "Rupees (20)", + "65": "Rupees (50)", + "64": "Rupees (100)", + "70": "Rupees (300)", + "89": "Rupoor", + "91": "Red Clock", + "92": "Blue Clock", + "93": "Green Clock", + "98": "Single RNG", + "99": "Multi RNG", + "78": "Magic Upgrade (1/2)", + "79": "Magic Upgrade (1/4)", + "162": "Small Key (Eastern Palace)", + "157": "Big Key (Eastern Palace)", + "141": "Compass (Eastern Palace)", + "125": "Map (Eastern Palace)", + "163": "Small Key (Desert Palace)", + "156": "Big Key (Desert Palace)", + "140": "Compass (Desert Palace)", + "124": "Map (Desert Palace)", + "170": "Small Key (Tower of Hera)", + "149": "Big Key (Tower of Hera)", + "133": "Compass (Tower of Hera)", + "117": "Map (Tower of Hera)", + "160": "Small Key (Escape)", + "159": "Big Key (Escape)", + "143": "Compass (Escape)", + "127": "Map (Escape)", + "164": "Small Key (Agahnims Tower)", + "155": "Big Key (Agahnims Tower)", + "139": "Compass (Agahnims Tower)", + "123": "Map (Agahnims Tower)", + "166": "Small Key (Palace of Darkness)", + "153": "Big Key (Palace of Darkness)", + "137": "Compass (Palace of Darkness)", + "121": "Map (Palace of Darkness)", + "171": "Small Key (Thieves Town)", + "148": "Big Key (Thieves Town)", + "132": "Compass (Thieves Town)", + "116": "Map (Thieves Town)", + "168": "Small Key (Skull Woods)", + "151": "Big Key (Skull Woods)", + "135": "Compass (Skull Woods)", + "119": "Map (Skull Woods)", + "165": "Small Key (Swamp Palace)", + "154": "Big Key (Swamp Palace)", + "138": "Compass (Swamp Palace)", + "122": "Map (Swamp Palace)", + "169": "Small Key (Ice Palace)", + "150": "Big Key (Ice Palace)", + "134": "Compass (Ice Palace)", + "118": "Map (Ice Palace)", + "167": "Small Key (Misery Mire)", + "152": "Big Key (Misery Mire)", + "136": "Compass (Misery Mire)", + "120": "Map (Misery Mire)", + "172": "Small Key (Turtle Rock)", + "147": "Big Key (Turtle Rock)", + "131": "Compass (Turtle Rock)", + "115": "Map (Turtle Rock)", + "173": "Small Key (Ganons Tower)", + "146": "Big Key (Ganons Tower)", + "130": "Compass (Ganons Tower)", + "114": "Map (Ganons Tower)", + "175": "Small Key (Universal)", + "90": "Nothing", + "176": "Bee Trap", + "46": "Red Potion", + "47": "Green Potion", + "48": "Blue Potion", + "14": "Bee", + "66": "Small Heart" +} diff --git a/WebHost/templates/host_room.html b/WebHost/templates/host_room.html index 9f007798..f6355e91 100644 --- a/WebHost/templates/host_room.html +++ b/WebHost/templates/host_room.html @@ -4,6 +4,11 @@ {% endblock %} {% block body %} Room created from Seed #{{ room.seed.id }}
+ {% if room.allow_tracker %} + This room has a Multiworld Tracker enabled.
+ {% endif %} + This room will be closed after {{ room.timeout }} hours of inactivity. Should you wish to continue later, + you can simply refresh this page and the server will be started again.
{% if room.owner == session["_id"] %}
diff --git a/WebHost/templates/tracker.html b/WebHost/templates/tracker.html new file mode 100644 index 00000000..640d427c --- /dev/null +++ b/WebHost/templates/tracker.html @@ -0,0 +1,32 @@ +{% extends 'layout.html' %} +{% block head %} + Multiworld User {{ session["_id"] }} +{% endblock %} +{% block body %} + {% for team, players in inventory.items() %} + + + + + + {% for name in tracking_names %} + + {% endfor %} + + + + {% for player, items in players.items() %} + + + + {% for id in tracking_ids %} + + {% endfor %} + + {% endfor %} + +
PlayerName{{ name }}
{{ loop.index }}{{ player_names[(team, loop.index)] }} + {{ items[id] }} +
+ {% endfor %} +{% endblock %} \ No newline at end of file