WebHost: allow customserver to skip importing worlds subsystem for hosting a Room (#877)

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
This commit is contained in:
Fabian Dill 2022-08-07 18:28:50 +02:00 committed by GitHub
parent 181cc47079
commit c1e9d0ab4f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 326 additions and 262 deletions

View File

@ -30,13 +30,8 @@ except ImportError:
OperationalError = ConnectionError
import NetUtils
from worlds.AutoWorld import AutoWorldRegister
proxy_worlds = {name: world(None, 0) for name, world in AutoWorldRegister.world_types.items()}
from worlds import network_data_package, lookup_any_item_id_to_name, lookup_any_location_id_to_name
import Utils
from Utils import get_item_name_from_id, get_location_name_from_id, \
version_tuple, restricted_loads, Version
from Utils import version_tuple, restricted_loads, Version
from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, NetworkPlayer, Permission, NetworkSlot, \
SlotType
@ -126,6 +121,11 @@ class Context:
stored_data: typing.Dict[str, object]
stored_data_notification_clients: typing.Dict[str, typing.Set[Client]]
item_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown item (ID:{code})')
location_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})')
all_item_and_group_names: typing.Dict[str, typing.Set[str]]
forced_auto_forfeits: typing.Dict[str, bool]
def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int,
hint_cost: int, item_cheat: bool, forfeit_mode: str = "disabled", collect_mode="disabled",
remaining_mode: str = "disabled", auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2,
@ -190,8 +190,43 @@ class Context:
self.stored_data = {}
self.stored_data_notification_clients = collections.defaultdict(weakref.WeakSet)
# General networking
# init empty to satisfy linter, I suppose
self.gamespackage = {}
self.item_name_groups = {}
self.all_item_and_group_names = {}
self.forced_auto_forfeits = collections.defaultdict(lambda: False)
self.non_hintable_names = {}
self._load_game_data()
self._init_game_data()
# Datapackage retrieval
def _load_game_data(self):
import worlds
self.gamespackage = worlds.network_data_package["games"]
self.item_name_groups = {world_name: world.item_name_groups for world_name, world in
worlds.AutoWorldRegister.world_types.items()}
for world_name, world in worlds.AutoWorldRegister.world_types.items():
self.forced_auto_forfeits[world_name] = world.forced_auto_forfeit
self.non_hintable_names[world_name] = world.hint_blacklist
def _init_game_data(self):
for game_name, game_package in self.gamespackage.items():
for item_name, item_id in game_package["item_name_to_id"].items():
self.item_names[item_id] = item_name
for location_name, location_id in game_package["location_name_to_id"].items():
self.location_names[location_id] = location_name
self.all_item_and_group_names[game_name] = \
set(game_package["item_name_to_id"]) | set(self.item_name_groups[game_name])
def item_names_for_game(self, game: str) -> typing.Dict[str, int]:
return self.gamespackage[game]["item_name_to_id"]
def location_names_for_game(self, game: str) -> typing.Dict[str, int]:
return self.gamespackage[game]["location_name_to_id"]
# General networking
async def send_msgs(self, endpoint: Endpoint, msgs: typing.Iterable[dict]) -> bool:
if not endpoint.socket or not endpoint.socket.open:
return False
@ -546,7 +581,7 @@ class Context:
self.notify_all(finished_msg)
if "auto" in self.forfeit_mode:
forfeit_player(self, client.team, client.slot)
elif proxy_worlds[self.games[client.slot]].forced_auto_forfeit:
elif self.forced_auto_forfeits[self.games[client.slot]]:
forfeit_player(self, client.team, client.slot)
if "auto" in self.collect_mode:
collect_player(self, client.team, client.slot)
@ -642,9 +677,10 @@ async def on_client_connected(ctx: Context, client: Client):
'permissions': get_permissions(ctx),
'hint_cost': ctx.hint_cost,
'location_check_points': ctx.location_check_points,
'datapackage_version': network_data_package["version"],
'datapackage_version': sum(game_data["version"] for game_data in ctx.gamespackage.values())
if all(game_data["version"] for game_data in ctx.gamespackage.values()) else 0,
'datapackage_versions': {game: game_data["version"] for game, game_data
in network_data_package["games"].items()},
in ctx.gamespackage.items()},
'seed_name': ctx.seed_name,
'time': time.time(),
}])
@ -822,8 +858,8 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi
send_items_to(ctx, team, target_player, new_item)
logging.info('(Team #%d) %s sent %s to %s (%s)' % (
team + 1, ctx.player_names[(team, slot)], get_item_name_from_id(item_id),
ctx.player_names[(team, target_player)], get_location_name_from_id(location)))
team + 1, ctx.player_names[(team, slot)], ctx.item_names[item_id],
ctx.player_names[(team, target_player)], ctx.location_names[location]))
info_text = json_format_send_event(new_item, target_player)
ctx.broadcast_team(team, [info_text])
@ -838,13 +874,14 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi
ctx.save()
def collect_hints(ctx: Context, team: int, slot: int, item: str) -> typing.List[NetUtils.Hint]:
def collect_hints(ctx: Context, team: int, slot: int, item_name: str) -> typing.List[NetUtils.Hint]:
hints = []
slots: typing.Set[int] = {slot}
for group_id, group in ctx.groups.items():
if slot in group:
slots.add(group_id)
seeked_item_id = proxy_worlds[ctx.games[slot]].item_name_to_id[item]
seeked_item_id = ctx.item_names_for_game(ctx.games[slot])[item_name]
for finding_player, check_data in ctx.locations.items():
for location_id, (item_id, receiving_player, item_flags) in check_data.items():
if receiving_player in slots and item_id == seeked_item_id:
@ -857,7 +894,7 @@ def collect_hints(ctx: Context, team: int, slot: int, item: str) -> typing.List[
def collect_hint_location_name(ctx: Context, team: int, slot: int, location: str) -> typing.List[NetUtils.Hint]:
seeked_location: int = proxy_worlds[ctx.games[slot]].location_name_to_id[location]
seeked_location: int = ctx.location_names_for_game(ctx.games[slot])[location]
return collect_hint_location_id(ctx, team, slot, seeked_location)
@ -874,8 +911,8 @@ def collect_hint_location_id(ctx: Context, team: int, slot: int, seeked_location
def format_hint(ctx: Context, team: int, hint: NetUtils.Hint) -> str:
text = f"[Hint]: {ctx.player_names[team, hint.receiving_player]}'s " \
f"{lookup_any_item_id_to_name[hint.item]} is " \
f"at {get_location_name_from_id(hint.location)} " \
f"{ctx.item_names[hint.item]} is " \
f"at {ctx.location_names[hint.location]} " \
f"in {ctx.player_names[team, hint.finding_player]}'s World"
if hint.entrance:
@ -1133,8 +1170,8 @@ class ClientMessageProcessor(CommonCommandProcessor):
forfeit_player(self.ctx, self.client.team, self.client.slot)
return True
elif "disabled" in self.ctx.forfeit_mode:
self.output(
"Sorry, client item releasing has been disabled on this server. You can ask the server admin for a /release")
self.output("Sorry, client item releasing has been disabled on this server. "
"You can ask the server admin for a /release")
return False
else: # is auto or goal
if self.ctx.client_game_state[self.client.team, self.client.slot] == ClientStatus.CLIENT_GOAL:
@ -1170,7 +1207,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
if self.ctx.remaining_mode == "enabled":
remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot)
if remaining_item_ids:
self.output("Remaining items: " + ", ".join(lookup_any_item_id_to_name.get(item_id, "unknown item")
self.output("Remaining items: " + ", ".join(self.ctx.item_names[item_id]
for item_id in remaining_item_ids))
else:
self.output("No remaining items found.")
@ -1183,7 +1220,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
if self.ctx.client_game_state[self.client.team, self.client.slot] == ClientStatus.CLIENT_GOAL:
remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot)
if remaining_item_ids:
self.output("Remaining items: " + ", ".join(lookup_any_item_id_to_name.get(item_id, "unknown item")
self.output("Remaining items: " + ", ".join(self.ctx.item_names[item_id]
for item_id in remaining_item_ids))
else:
self.output("No remaining items found.")
@ -1199,7 +1236,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
locations = get_missing_checks(self.ctx, self.client.team, self.client.slot)
if locations:
texts = [f'Missing: {get_location_name_from_id(location)}' for location in locations]
texts = [f'Missing: {self.ctx.location_names[location]}' for location in locations]
texts.append(f"Found {len(locations)} missing location checks")
self.ctx.notify_client_multiple(self.client, texts)
else:
@ -1212,7 +1249,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
locations = get_checked_checks(self.ctx, self.client.team, self.client.slot)
if locations:
texts = [f'Checked: {get_location_name_from_id(location)}' for location in locations]
texts = [f'Checked: {self.ctx.location_names[location]}' for location in locations]
texts.append(f"Found {len(locations)} done location checks")
self.ctx.notify_client_multiple(self.client, texts)
else:
@ -1241,11 +1278,13 @@ class ClientMessageProcessor(CommonCommandProcessor):
def _cmd_getitem(self, item_name: str) -> bool:
"""Cheat in an item, if it is enabled on this server"""
if self.ctx.item_cheat:
world = proxy_worlds[self.ctx.games[self.client.slot]]
item_name, usable, response = get_intended_text(item_name,
world.item_names)
names = self.ctx.item_names_for_game(self.ctx.games[self.client.slot])
item_name, usable, response = get_intended_text(
item_name,
names
)
if usable:
new_item = NetworkItem(world.create_item(item_name).code, -1, self.client.slot)
new_item = NetworkItem(names[item_name], -1, self.client.slot)
get_received_items(self.ctx, self.client.team, self.client.slot, False).append(new_item)
get_received_items(self.ctx, self.client.team, self.client.slot, True).append(new_item)
self.ctx.notify_all(
@ -1271,20 +1310,22 @@ class ClientMessageProcessor(CommonCommandProcessor):
f"You have {points_available} points.")
return True
else:
world = proxy_worlds[self.ctx.games[self.client.slot]]
names = world.location_names if for_location else world.all_item_and_group_names
game = self.ctx.games[self.client.slot]
names = self.ctx.location_names_for_game(game) \
if for_location else \
self.ctx.all_item_and_group_names[game]
hint_name, usable, response = get_intended_text(input_text,
names)
if usable:
if hint_name in world.hint_blacklist:
if hint_name in self.ctx.non_hintable_names[game]:
self.output(f"Sorry, \"{hint_name}\" is marked as non-hintable.")
hints = []
elif not for_location and hint_name in world.item_name_groups: # item group name
elif not for_location and hint_name in self.ctx.item_name_groups[game]: # item group name
hints = []
for item in world.item_name_groups[hint_name]:
if item in world.item_name_to_id: # ensure item has an ID
hints.extend(collect_hints(self.ctx, self.client.team, self.client.slot, item))
elif not for_location and hint_name in world.item_names: # item name
for item_name in self.ctx.item_name_groups[game][hint_name]:
if item_name in self.ctx.item_names_for_game(game): # ensure item has an ID
hints.extend(collect_hints(self.ctx, self.client.team, self.client.slot, item_name))
elif not for_location and hint_name in self.ctx.item_names_for_game(game): # item name
hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_name)
else: # location name
hints = collect_hint_location_name(self.ctx, self.client.team, self.client.slot, hint_name)
@ -1346,12 +1387,12 @@ class ClientMessageProcessor(CommonCommandProcessor):
return False
@mark_raw
def _cmd_hint(self, item: str = "") -> bool:
def _cmd_hint(self, item_name: str = "") -> bool:
"""Use !hint {item_name},
for example !hint Lamp to get a spoiler peek for that item.
If hint costs are on, this will only give you one new result,
you can rerun the command to get more in that case."""
return self.get_hints(item)
return self.get_hints(item_name)
@mark_raw
def _cmd_hint_location(self, location: str = "") -> bool:
@ -1477,23 +1518,23 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
elif cmd == "GetDataPackage":
exclusions = args.get("exclusions", [])
if "games" in args:
games = {name: game_data for name, game_data in network_data_package["games"].items()
games = {name: game_data for name, game_data in ctx.gamespackage.items()
if name in set(args.get("games", []))}
await ctx.send_msgs(client, [{"cmd": "DataPackage",
"data": {"games": games}}])
# TODO: remove exclusions behaviour around 0.5.0
elif exclusions:
exclusions = set(exclusions)
games = {name: game_data for name, game_data in network_data_package["games"].items()
games = {name: game_data for name, game_data in ctx.gamespackage.items()
if name not in exclusions}
package = network_data_package.copy()
package["games"] = games
package = {"games": games}
await ctx.send_msgs(client, [{"cmd": "DataPackage",
"data": package}])
else:
await ctx.send_msgs(client, [{"cmd": "DataPackage",
"data": network_data_package}])
"data": {"games": ctx.gamespackage}}])
elif client.auth:
if cmd == "ConnectUpdate":
@ -1549,7 +1590,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
create_as_hint: int = int(args.get("create_as_hint", 0))
hints = []
for location in args["locations"]:
if type(location) is not int or location not in lookup_any_location_id_to_name:
if type(location) is not int:
await ctx.send_msgs(client,
[{'cmd': 'InvalidPacket', "type": "arguments", "text": 'LocationScouts',
"original_cmd": cmd}])
@ -1763,18 +1804,18 @@ class ServerCommandProcessor(CommonCommandProcessor):
seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values())
if usable:
team, slot = self.ctx.player_name_lookup[seeked_player]
item = " ".join(item_name)
world = proxy_worlds[self.ctx.games[slot]]
item, usable, response = get_intended_text(item, world.item_names)
item_name = " ".join(item_name)
names = self.ctx.item_names_for_game(self.ctx.games[slot])
item_name, usable, response = get_intended_text(item_name, names)
if usable:
amount: int = int(amount)
new_items = [NetworkItem(world.item_name_to_id[item], -1, 0) for i in range(int(amount))]
new_items = [NetworkItem(names[item_name], -1, 0) for _ in range(int(amount))]
send_items_to(self.ctx, team, slot, *new_items)
send_new_items(self.ctx)
self.ctx.notify_all(
'Cheat console: sending ' + ('' if amount == 1 else f'{amount} of ') +
f'"{item}" to {self.ctx.get_aliased_name(team, slot)}')
f'"{item_name}" to {self.ctx.get_aliased_name(team, slot)}')
return True
else:
self.output(response)
@ -1787,22 +1828,22 @@ class ServerCommandProcessor(CommonCommandProcessor):
"""Sends an item to the specified player"""
return self._cmd_send_multiple(1, player_name, *item_name)
def _cmd_hint(self, player_name: str, *item: str) -> bool:
def _cmd_hint(self, player_name: str, *item_name: str) -> bool:
"""Send out a hint for a player's item to their team"""
seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values())
if usable:
team, slot = self.ctx.player_name_lookup[seeked_player]
item = " ".join(item)
world = proxy_worlds[self.ctx.games[slot]]
item, usable, response = get_intended_text(item, world.all_item_and_group_names)
item_name = " ".join(item_name)
game = self.ctx.games[slot]
item_name, usable, response = get_intended_text(item_name, self.ctx.all_item_and_group_names[game])
if usable:
if item in world.item_name_groups:
if item_name in self.ctx.item_name_groups[game]:
hints = []
for item in world.item_name_groups[item]:
if item in world.item_name_to_id: # ensure item has an ID
hints.extend(collect_hints(self.ctx, team, slot, item))
for item_name_from_group in self.ctx.item_name_groups[game][item_name]:
if item_name_from_group in self.ctx.item_names_for_game(game): # ensure item has an ID
hints.extend(collect_hints(self.ctx, team, slot, item_name_from_group))
else: # item name
hints = collect_hints(self.ctx, team, slot, item)
hints = collect_hints(self.ctx, team, slot, item_name)
if hints:
notify_hints(self.ctx, team, hints)
@ -1818,16 +1859,16 @@ class ServerCommandProcessor(CommonCommandProcessor):
self.output(response)
return False
def _cmd_hint_location(self, player_name: str, *location: str) -> bool:
def _cmd_hint_location(self, player_name: str, *location_name: str) -> bool:
"""Send out a hint for a player's location to their team"""
seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values())
if usable:
team, slot = self.ctx.player_name_lookup[seeked_player]
item = " ".join(location)
world = proxy_worlds[self.ctx.games[slot]]
item, usable, response = get_intended_text(item, world.location_names)
location_name = " ".join(location_name)
location_name, usable, response = get_intended_text(location_name,
self.ctx.location_names_for_game(self.ctx.games[slot]))
if usable:
hints = collect_hint_location_name(self.ctx, team, slot, item)
hints = collect_hint_location_name(self.ctx, team, slot, location_name)
if hints:
notify_hints(self.ctx, team, hints)
else:

View File

@ -328,16 +328,6 @@ def get_options() -> dict:
return get_options.options
def get_item_name_from_id(code: int) -> str:
from worlds import lookup_any_item_id_to_name
return lookup_any_item_id_to_name.get(code, f'Unknown item (ID:{code})')
def get_location_name_from_id(code: int) -> str:
from worlds import lookup_any_location_id_to_name
return lookup_any_location_id_to_name.get(code, f'Unknown location (ID:{code})')
def persistent_store(category: str, key: typing.Any, value: typing.Any):
path = user_path("_persistent_storage.yaml")
storage: dict = persistent_load()

View File

@ -14,7 +14,7 @@ import Utils
Utils.local_path.cached_path = os.path.dirname(__file__)
from WebHostLib import app as raw_app
from WebHostLib import register, app as raw_app
from waitress import serve
from WebHostLib.models import db
@ -22,14 +22,13 @@ from WebHostLib.autolauncher import autohost, autogen
from WebHostLib.lttpsprites import update_sprites_lttp
from WebHostLib.options import create as create_options_files
from worlds.AutoWorld import AutoWorldRegister
configpath = os.path.abspath("config.yaml")
if not os.path.exists(configpath): # fall back to config.yaml in home
configpath = os.path.abspath(Utils.user_path('config.yaml'))
def get_app():
register()
app = raw_app
if os.path.exists(configpath):
import yaml
@ -43,6 +42,7 @@ def get_app():
def create_ordered_tutorials_file() -> typing.List[typing.Dict[str, typing.Any]]:
import json
import shutil
from worlds.AutoWorld import AutoWorldRegister
worlds = {}
data = []
for game, world in AutoWorldRegister.world_types.items():

View File

@ -3,12 +3,11 @@ import uuid
import base64
import socket
import jinja2.exceptions
from pony.flask import Pony
from flask import Flask, request, redirect, url_for, render_template, Response, session, abort, send_from_directory
from flask import Flask
from flask_caching import Cache
from flask_compress import Compress
from worlds.AutoWorld import AutoWorldRegister
from werkzeug.routing import BaseConverter
from .models import *
@ -53,8 +52,6 @@ app.config["PATCH_TARGET"] = "archipelago.gg"
cache = Cache(app)
Compress(app)
from werkzeug.routing import BaseConverter
class B64UUIDConverter(BaseConverter):
@ -69,173 +66,16 @@ class B64UUIDConverter(BaseConverter):
app.url_map.converters["suuid"] = B64UUIDConverter
app.jinja_env.filters['suuid'] = lambda value: base64.urlsafe_b64encode(value.bytes).rstrip(b'=').decode('ascii')
# has automatic patch integration
import Patch
app.jinja_env.filters['supports_apdeltapatch'] = lambda game_name: game_name in Patch.AutoPatchRegister.patch_types
def register():
"""Import submodules, triggering their registering on flask routing.
Note: initializes worlds subsystem."""
# has automatic patch integration
import Patch
app.jinja_env.filters['supports_apdeltapatch'] = lambda game_name: game_name in Patch.AutoPatchRegister.patch_types
def get_world_theme(game_name: str):
if game_name in AutoWorldRegister.world_types:
return AutoWorldRegister.world_types[game_name].web.theme
return 'grass'
from WebHostLib.customserver import run_server_process
# to trigger app routing picking up on it
from . import tracker, upload, landing, check, generate, downloads, api, stats, misc
@app.before_request
def register_session():
session.permanent = True # technically 31 days after the last visit
if not session.get("_id", None):
session["_id"] = uuid4() # uniquely identify each session without needing a login
@app.errorhandler(404)
@app.errorhandler(jinja2.exceptions.TemplateNotFound)
def page_not_found(err):
return render_template('404.html'), 404
# Start Playing Page
@app.route('/start-playing')
def start_playing():
return render_template(f"startPlaying.html")
@app.route('/weighted-settings')
def weighted_settings():
return render_template(f"weighted-settings.html")
# Player settings pages
@app.route('/games/<string:game>/player-settings')
def player_settings(game):
return render_template(f"player-settings.html", game=game, theme=get_world_theme(game))
# Game Info Pages
@app.route('/games/<string:game>/info/<string:lang>')
def game_info(game, lang):
return render_template('gameInfo.html', game=game, lang=lang, theme=get_world_theme(game))
# List of supported games
@app.route('/games')
def games():
worlds = {}
for game, world in AutoWorldRegister.world_types.items():
if not world.hidden:
worlds[game] = world
return render_template("supportedGames.html", worlds=worlds)
@app.route('/tutorial/<string:game>/<string:file>/<string:lang>')
def tutorial(game, file, lang):
return render_template("tutorial.html", game=game, file=file, lang=lang, theme=get_world_theme(game))
@app.route('/tutorial/')
def tutorial_landing():
worlds = {}
for game, world in AutoWorldRegister.world_types.items():
if not world.hidden:
worlds[game] = world
return render_template("tutorialLanding.html")
@app.route('/faq/<string:lang>/')
def faq(lang):
return render_template("faq.html", lang=lang)
@app.route('/glossary/<string:lang>/')
def terms(lang):
return render_template("glossary.html", lang=lang)
@app.route('/seed/<suuid:seed>')
def view_seed(seed: UUID):
seed = Seed.get(id=seed)
if not seed:
abort(404)
return render_template("viewSeed.html", seed=seed, slot_count=count(seed.slots))
@app.route('/new_room/<suuid:seed>')
def new_room(seed: UUID):
seed = Seed.get(id=seed)
if not seed:
abort(404)
room = Room(seed=seed, owner=session["_id"], tracker=uuid4())
commit()
return redirect(url_for("host_room", room=room.id))
def _read_log(path: str):
if os.path.exists(path):
with open(path, encoding="utf-8-sig") as log:
yield from log
else:
yield f"Logfile {path} does not exist. " \
f"Likely a crash during spinup of multiworld instance or it is still spinning up."
@app.route('/log/<suuid:room>')
def display_log(room: UUID):
room = Room.get(id=room)
if room is None:
return abort(404)
if room.owner == session["_id"]:
return Response(_read_log(os.path.join("logs", str(room.id) + ".txt")), mimetype="text/plain;charset=UTF-8")
return "Access Denied", 403
@app.route('/room/<suuid:room>', methods=['GET', 'POST'])
def host_room(room: UUID):
room = Room.get(id=room)
if room is None:
return abort(404)
if request.method == "POST":
if room.owner == session["_id"]:
cmd = request.form["cmd"]
if cmd:
Command(room=room, commandtext=cmd)
commit()
with db_session:
room.last_activity = datetime.utcnow() # will trigger a spinup, if it's not already running
return render_template("hostRoom.html", room=room)
@app.route('/favicon.ico')
def favicon():
return send_from_directory(os.path.join(app.root_path, 'static/static'),
'favicon.ico', mimetype='image/vnd.microsoft.icon')
@app.route('/discord')
def discord():
return redirect("https://discord.gg/archipelago")
@app.route('/datapackage')
@cache.cached()
def get_datapackge():
"""A pretty print version of /api/datapackage"""
from worlds import network_data_package
import json
return Response(json.dumps(network_data_package, indent=4), mimetype="text/plain")
@app.route('/index')
@app.route('/sitemap')
def get_sitemap():
available_games = []
for game, world in AutoWorldRegister.world_types.items():
if not world.hidden:
available_games.append(game)
return render_template("siteMap.html", games=available_games)
from WebHostLib.customserver import run_server_process
from . import tracker, upload, landing, check, generate, downloads, api, stats # to trigger app routing picking up on it
app.register_blueprint(api.api_endpoints)
app.register_blueprint(api.api_endpoints)

View File

@ -184,7 +184,7 @@ class MultiworldInstance():
logging.info(f"Spinning up {self.room_id}")
process = multiprocessing.Process(group=None, target=run_server_process,
args=(self.room_id, self.ponyconfig),
args=(self.room_id, self.ponyconfig, get_static_server_data()),
name="MultiHost")
process.start()
# bind after start to prevent thread sync issues with guardian.
@ -238,5 +238,5 @@ def run_guardian():
from .models import Room, Generation, STATE_QUEUED, STATE_STARTED, STATE_ERROR, db, Seed
from .customserver import run_server_process
from .customserver import run_server_process, get_static_server_data
from .generate import gen_game

View File

@ -9,12 +9,13 @@ import time
import random
import pickle
import logging
import datetime
import Utils
from .models import *
from .models import db_session, Room, select, commit, Command, db
from MultiServer import Context, server, auto_shutdown, ServerCommandProcessor, ClientMessageProcessor
from Utils import get_public_ipv4, get_public_ipv6, restricted_loads
from Utils import get_public_ipv4, get_public_ipv6, restricted_loads, cache_argsless
class CustomClientMessageProcessor(ClientMessageProcessor):
@ -39,7 +40,7 @@ class CustomClientMessageProcessor(ClientMessageProcessor):
import MultiServer
MultiServer.client_message_processor = CustomClientMessageProcessor
del (MultiServer)
del MultiServer
class DBCommandProcessor(ServerCommandProcessor):
@ -48,12 +49,20 @@ class DBCommandProcessor(ServerCommandProcessor):
class WebHostContext(Context):
def __init__(self):
def __init__(self, static_server_data: dict):
# static server data is used during _load_game_data to load required data,
# without needing to import worlds system, which takes quite a bit of memory
self.static_server_data = static_server_data
super(WebHostContext, self).__init__("", 0, "", "", 1, 40, True, "enabled", "enabled", "enabled", 0, 2)
del self.static_server_data
self.main_loop = asyncio.get_running_loop()
self.video = {}
self.tags = ["AP", "WebHost"]
def _load_game_data(self):
for key, value in self.static_server_data.items():
setattr(self, key, value)
def listen_to_db_commands(self):
cmdprocessor = DBCommandProcessor(self)
@ -107,14 +116,32 @@ def get_random_port():
return random.randint(49152, 65535)
def run_server_process(room_id, ponyconfig: dict):
@cache_argsless
def get_static_server_data() -> dict:
import worlds
data = {
"forced_auto_forfeits": {},
"non_hintable_names": {},
"gamespackage": worlds.network_data_package["games"],
"item_name_groups": {world_name: world.item_name_groups for world_name, world in
worlds.AutoWorldRegister.world_types.items()},
}
for world_name, world in worlds.AutoWorldRegister.world_types.items():
data["forced_auto_forfeits"][world_name] = world.forced_auto_forfeit
data["non_hintable_names"][world_name] = world.hint_blacklist
return data
def run_server_process(room_id, ponyconfig: dict, static_server_data: dict):
# establish DB connection for multidata and multisave
db.bind(**ponyconfig)
db.generate_mapping(check_tables=False)
async def main():
Utils.init_logging(str(room_id), write_mode="a")
ctx = WebHostContext()
ctx = WebHostContext(static_server_data)
ctx.load(room_id)
ctx.init_save()

170
WebHostLib/misc.py Normal file
View File

@ -0,0 +1,170 @@
import datetime
import os
import jinja2.exceptions
from flask import request, redirect, url_for, render_template, Response, session, abort, send_from_directory
from .models import count, Seed, commit, Room, db_session, Command, UUID, uuid4
from worlds.AutoWorld import AutoWorldRegister
from . import app, cache
def get_world_theme(game_name: str):
if game_name in AutoWorldRegister.world_types:
return AutoWorldRegister.world_types[game_name].web.theme
return 'grass'
@app.before_request
def register_session():
session.permanent = True # technically 31 days after the last visit
if not session.get("_id", None):
session["_id"] = uuid4() # uniquely identify each session without needing a login
@app.errorhandler(404)
@app.errorhandler(jinja2.exceptions.TemplateNotFound)
def page_not_found(err):
return render_template('404.html'), 404
# Start Playing Page
@app.route('/start-playing')
def start_playing():
return render_template(f"startPlaying.html")
@app.route('/weighted-settings')
def weighted_settings():
return render_template(f"weighted-settings.html")
# Player settings pages
@app.route('/games/<string:game>/player-settings')
def player_settings(game):
return render_template(f"player-settings.html", game=game, theme=get_world_theme(game))
# Game Info Pages
@app.route('/games/<string:game>/info/<string:lang>')
def game_info(game, lang):
return render_template('gameInfo.html', game=game, lang=lang, theme=get_world_theme(game))
# List of supported games
@app.route('/games')
def games():
worlds = {}
for game, world in AutoWorldRegister.world_types.items():
if not world.hidden:
worlds[game] = world
return render_template("supportedGames.html", worlds=worlds)
@app.route('/tutorial/<string:game>/<string:file>/<string:lang>')
def tutorial(game, file, lang):
return render_template("tutorial.html", game=game, file=file, lang=lang, theme=get_world_theme(game))
@app.route('/tutorial/')
def tutorial_landing():
worlds = {}
for game, world in AutoWorldRegister.world_types.items():
if not world.hidden:
worlds[game] = world
return render_template("tutorialLanding.html")
@app.route('/faq/<string:lang>/')
def faq(lang):
return render_template("faq.html", lang=lang)
@app.route('/glossary/<string:lang>/')
def terms(lang):
return render_template("glossary.html", lang=lang)
@app.route('/seed/<suuid:seed>')
def view_seed(seed: UUID):
seed = Seed.get(id=seed)
if not seed:
abort(404)
return render_template("viewSeed.html", seed=seed, slot_count=count(seed.slots))
@app.route('/new_room/<suuid:seed>')
def new_room(seed: UUID):
seed = Seed.get(id=seed)
if not seed:
abort(404)
room = Room(seed=seed, owner=session["_id"], tracker=uuid4())
commit()
return redirect(url_for("host_room", room=room.id))
def _read_log(path: str):
if os.path.exists(path):
with open(path, encoding="utf-8-sig") as log:
yield from log
else:
yield f"Logfile {path} does not exist. " \
f"Likely a crash during spinup of multiworld instance or it is still spinning up."
@app.route('/log/<suuid:room>')
def display_log(room: UUID):
room = Room.get(id=room)
if room is None:
return abort(404)
if room.owner == session["_id"]:
return Response(_read_log(os.path.join("logs", str(room.id) + ".txt")), mimetype="text/plain;charset=UTF-8")
return "Access Denied", 403
@app.route('/room/<suuid:room>', methods=['GET', 'POST'])
def host_room(room: UUID):
room = Room.get(id=room)
if room is None:
return abort(404)
if request.method == "POST":
if room.owner == session["_id"]:
cmd = request.form["cmd"]
if cmd:
Command(room=room, commandtext=cmd)
commit()
with db_session:
room.last_activity = datetime.utcnow() # will trigger a spinup, if it's not already running
return render_template("hostRoom.html", room=room)
@app.route('/favicon.ico')
def favicon():
return send_from_directory(os.path.join(app.root_path, 'static/static'),
'favicon.ico', mimetype='image/vnd.microsoft.icon')
@app.route('/discord')
def discord():
return redirect("https://discord.gg/archipelago")
@app.route('/datapackage')
@cache.cached()
def get_datapackge():
"""A pretty print version of /api/datapackage"""
from worlds import network_data_package
import json
return Response(json.dumps(network_data_package, indent=4), mimetype="text/plain")
@app.route('/index')
@app.route('/sitemap')
def get_sitemap():
available_games = []
for game, world in AutoWorldRegister.world_types.items():
if not world.hidden:
available_games.append(game)
return render_template("siteMap.html", games=available_games)

View File

@ -11,7 +11,7 @@ from worlds.alttp import Items
from WebHostLib import app, cache, Room
from Utils import restricted_loads
from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name
from MultiServer import get_item_name_from_id, Context
from MultiServer import Context
from NetUtils import SlotType
alttp_icons = {
@ -1021,7 +1021,7 @@ def getTracker(tracker: UUID):
for (team, player), data in multisave.get("video", []):
video[(team, player)] = data
return render_template("tracker.html", inventory=inventory, get_item_name_from_id=get_item_name_from_id,
return render_template("tracker.html", inventory=inventory, get_item_name_from_id=lookup_any_item_id_to_name,
lookup_id_to_name=Items.lookup_id_to_name, player_names=player_names,
tracking_names=tracking_names, tracking_ids=tracking_ids, room=room, icons=alttp_icons,
multi_items=multi_items, checks_done=checks_done, ordered_areas=ordered_areas,

View File

@ -45,7 +45,6 @@ class MeritousWorld(World):
item_name_groups = item_groups
data_version = 2
forced_auto_forfeit = False
# NOTE: Remember to change this before this game goes live
required_client_version = (0, 2, 4)

View File

@ -35,9 +35,7 @@ class SM64World(World):
location_name_to_id = location_table
data_version = 6
required_client_version = (0,3,0)
forced_auto_forfeit = False
required_client_version = (0, 3, 0)
area_connections: typing.Dict[int, int]

View File

@ -35,7 +35,6 @@ class V6World(World):
location_name_to_id = location_table
data_version = 1
forced_auto_forfeit = False
area_connections: typing.Dict[int, int]
area_cost_map: typing.Dict[int,int]