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
|
await ctx.shutdown_task
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
try:
|
try:
|
||||||
loop.run_until_complete(main(parse_args()))
|
asyncio.run(main(parse_args()))
|
||||||
except asyncio.exceptions.CancelledError:
|
except asyncio.exceptions.CancelledError:
|
||||||
pass
|
pass
|
||||||
|
|
10
Utils.py
10
Utils.py
|
@ -1,12 +1,18 @@
|
||||||
from __future__ import annotations
|
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__ = "2.3.3"
|
||||||
_version_tuple = tuple(int(piece, 10) for piece in __version__.split("."))
|
_version_tuple = tuplize_version(__version__)
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import typing
|
|
||||||
import functools
|
import functools
|
||||||
|
|
||||||
from yaml import load, dump
|
from yaml import load, dump
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
import typing
|
import typing
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
import threading
|
import threading
|
||||||
import json
|
|
||||||
import zlib
|
import zlib
|
||||||
|
import collections
|
||||||
|
|
||||||
from pony.orm import db_session, commit
|
|
||||||
from pony.flask import Pony
|
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 *
|
from .models import *
|
||||||
|
|
||||||
|
@ -20,9 +22,9 @@ os.makedirs(LOGS_FOLDER, exist_ok=True)
|
||||||
def allowed_file(filename):
|
def allowed_file(filename):
|
||||||
return filename.endswith('multidata')
|
return filename.endswith('multidata')
|
||||||
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
Pony(app)
|
Pony(app)
|
||||||
|
|
||||||
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
|
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
|
||||||
app.config['MAX_CONTENT_LENGTH'] = 1 * 1024 * 1024 # 1 megabyte limit
|
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
|
# 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'),
|
'filename': os.path.abspath('db.db3'),
|
||||||
'create_db': True
|
'create_db': True
|
||||||
}
|
}
|
||||||
|
app.config["CACHE_TYPE"] = "simple"
|
||||||
|
|
||||||
|
cache = Cache(app)
|
||||||
|
|
||||||
multiworlds = {}
|
multiworlds = {}
|
||||||
|
|
||||||
|
@ -66,36 +71,14 @@ class MultiworldInstance():
|
||||||
self.process.terminate()
|
self.process.terminate()
|
||||||
self.process = None
|
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>')
|
@app.route('/seed/<int:seed>')
|
||||||
def view_seed(seed: int):
|
def view_seed(seed: int):
|
||||||
seed = Seed.get(id=seed)
|
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>')
|
@app.route('/new_room/<int:seed>')
|
||||||
|
@ -143,4 +126,93 @@ def host_room(room: int):
|
||||||
return render_template("host_room.html", room=room)
|
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
|
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 socket
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
import random
|
||||||
|
|
||||||
from WebHost import LOGS_FOLDER
|
from WebHost import LOGS_FOLDER
|
||||||
from .models import *
|
from .models import *
|
||||||
|
@ -39,7 +40,12 @@ class WebHostContext(Context):
|
||||||
@db_session
|
@db_session
|
||||||
def load(self, room_id: int):
|
def load(self, room_id: int):
|
||||||
self.room_id = room_id
|
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
|
@db_session
|
||||||
def init_save(self, enabled: bool = True):
|
def init_save(self, enabled: bool = True):
|
||||||
|
@ -57,6 +63,9 @@ class WebHostContext(Context):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def get_random_port():
|
||||||
|
return random.randint(49152, 65535)
|
||||||
|
|
||||||
def run_server_process(room_id, ponyconfig: dict):
|
def run_server_process(room_id, ponyconfig: dict):
|
||||||
# establish DB connection for multidata and multisave
|
# establish DB connection for multidata and multisave
|
||||||
db.bind(**ponyconfig)
|
db.bind(**ponyconfig)
|
||||||
|
@ -72,14 +81,24 @@ def run_server_process(room_id, ponyconfig: dict):
|
||||||
ctx.auto_shutdown = 24 * 60 * 60 # 24 hours
|
ctx.auto_shutdown = 24 * 60 * 60 # 24 hours
|
||||||
ctx.init_save()
|
ctx.init_save()
|
||||||
|
|
||||||
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, 0, ping_timeout=None,
|
try:
|
||||||
ping_interval=None)
|
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:
|
for wssocket in ctx.server.ws_server.sockets:
|
||||||
socketname = wssocket.getsockname()
|
socketname = wssocket.getsockname()
|
||||||
if wssocket.family == socket.AF_INET6:
|
if wssocket.family == socket.AF_INET6:
|
||||||
logging.info(f'Hosting game at [{get_public_ipv6()}]:{socketname[1]}')
|
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:
|
elif wssocket.family == socket.AF_INET:
|
||||||
logging.info(f'Hosting game at {get_public_ipv4()}:{socketname[1]}')
|
logging.info(f'Hosting game at {get_public_ipv4()}:{socketname[1]}')
|
||||||
ctx.auto_shutdown = 6 * 60
|
ctx.auto_shutdown = 6 * 60
|
||||||
|
|
|
@ -16,22 +16,15 @@ class Room(db.Entity):
|
||||||
last_activity = Required(datetime, default=lambda: datetime.utcnow())
|
last_activity = Required(datetime, default=lambda: datetime.utcnow())
|
||||||
owner = Required(UUID)
|
owner = Required(UUID)
|
||||||
commands = Set('Command')
|
commands = Set('Command')
|
||||||
host_jobs = Set('HostJob')
|
|
||||||
seed = Required('Seed')
|
seed = Required('Seed')
|
||||||
multisave = Optional(Json)
|
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):
|
class Seed(db.Entity):
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
flask>=1.1.2
|
flask>=1.1.2
|
||||||
pony>=0.7.13
|
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 %}
|
{% endblock %}
|
||||||
{% block body %}
|
{% block body %}
|
||||||
Room created from <a href="{{ url_for("view_seed", seed=room.seed.id) }}">Seed #{{ room.seed.id }}</a><br>
|
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"] %}
|
{% if room.owner == session["_id"] %}
|
||||||
<form method=post>
|
<form method=post>
|
||||||
<div class="form-group">
|
<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