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:
parent
2759f6812c
commit
6421a373e1
|
@ -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
|
||||
|
|
10
Utils.py
10
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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
|
@ -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"
|
||||
}
|
|
@ -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">
|
||||
|
|
|
@ -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 %}
|
Loading…
Reference in New Issue