Webhost Update

introduce a very WIP tracker
Server will try to reuse port and also try to only use one port
This commit is contained in:
Fabian Dill 2020-06-21 15:32:31 +02:00
parent 2759f6812c
commit 6421a373e1
9 changed files with 316 additions and 49 deletions

View File

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

View File

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

View File

@ -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/<int: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/<int:seed>')
@ -143,4 +126,93 @@ def host_room(room: int):
return render_template("host_room.html", room=room)
@app.route('/tracker/<int:room>')
@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")

View File

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

View File

@ -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):

View File

@ -1,3 +1,4 @@
flask>=1.1.2
pony>=0.7.13
waitress>=1.4.4
waitress>=1.4.4
flask-caching>=1.9.0

View File

@ -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"
}

View File

@ -4,6 +4,11 @@
{% endblock %}
{% block body %}
Room created from <a href="{{ url_for("view_seed", seed=room.seed.id) }}">Seed #{{ room.seed.id }}</a><br>
{% if room.allow_tracker %}
This room has a <a href="{{ url_for("get_tracker", room=room.id) }}">Multiworld Tracker</a> enabled.<br>
{% 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.<br>
{% if room.owner == session["_id"] %}
<form method=post>
<div class="form-group">

View File

@ -0,0 +1,32 @@
{% extends 'layout.html' %}
{% block head %}
<title>Multiworld User {{ session["_id"] }}</title>
{% endblock %}
{% block body %}
{% for team, players in inventory.items() %}
<table class="table table-striped table-bordered table-hover table-sm">
<thead class="thead-dark">
<tr>
<th>Player</th>
<th>Name</th>
{% for name in tracking_names %}
<th>{{ name }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for player, items in players.items() %}
<tr>
<td>{{ loop.index }}</td>
<td>{{ player_names[(team, loop.index)] }}</td>
{% for id in tracking_ids %}
<td>
{{ items[id] }}
</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
{% endfor %}
{% endblock %}