diff --git a/MultiServer.py b/MultiServer.py index e8e1cc8d..3e502f64 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -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: diff --git a/Utils.py b/Utils.py index e38b1356..8f00a910 100644 --- a/Utils.py +++ b/Utils.py @@ -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() diff --git a/WebHost.py b/WebHost.py index eb575df3..5d3c4459 100644 --- a/WebHost.py +++ b/WebHost.py @@ -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(): diff --git a/WebHostLib/__init__.py b/WebHostLib/__init__.py index a44afc74..b2d243c9 100644 --- a/WebHostLib/__init__.py +++ b/WebHostLib/__init__.py @@ -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//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//info/') -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///') -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//') -def faq(lang): - return render_template("faq.html", lang=lang) - - -@app.route('/glossary//') -def terms(lang): - return render_template("glossary.html", lang=lang) - - -@app.route('/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/') -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/') -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/', 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) diff --git a/WebHostLib/autolauncher.py b/WebHostLib/autolauncher.py index 6f978211..77445ead 100644 --- a/WebHostLib/autolauncher.py +++ b/WebHostLib/autolauncher.py @@ -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 diff --git a/WebHostLib/customserver.py b/WebHostLib/customserver.py index f78a8eb2..01f1fd25 100644 --- a/WebHostLib/customserver.py +++ b/WebHostLib/customserver.py @@ -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() diff --git a/WebHostLib/misc.py b/WebHostLib/misc.py new file mode 100644 index 00000000..f113c046 --- /dev/null +++ b/WebHostLib/misc.py @@ -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//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//info/') +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///') +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//') +def faq(lang): + return render_template("faq.html", lang=lang) + + +@app.route('/glossary//') +def terms(lang): + return render_template("glossary.html", lang=lang) + + +@app.route('/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/') +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/') +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/', 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) diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py index 5e249c19..41794789 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -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, diff --git a/worlds/meritous/__init__.py b/worlds/meritous/__init__.py index 3c29032a..3a98bfe5 100644 --- a/worlds/meritous/__init__.py +++ b/worlds/meritous/__init__.py @@ -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) diff --git a/worlds/sm64ex/__init__.py b/worlds/sm64ex/__init__.py index 401a2d68..46282fe3 100644 --- a/worlds/sm64ex/__init__.py +++ b/worlds/sm64ex/__init__.py @@ -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] diff --git a/worlds/v6/__init__.py b/worlds/v6/__init__.py index 04947716..4959ddca 100644 --- a/worlds/v6/__init__.py +++ b/worlds/v6/__init__.py @@ -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]