diff --git a/FactorioClient.py b/FactorioClient.py index abbaed4a..0e758135 100644 --- a/FactorioClient.py +++ b/FactorioClient.py @@ -7,7 +7,7 @@ from concurrent.futures import ThreadPoolExecutor import colorama import asyncio -from queue import Queue, Empty +from queue import Queue from CommonClient import CommonContext, server_loop, console_loop, ClientCommandProcessor, logger from MultiServer import mark_raw diff --git a/WebHostLib/api/generate.py b/WebHostLib/api/generate.py index cc760559..397cc032 100644 --- a/WebHostLib/api/generate.py +++ b/WebHostLib/api/generate.py @@ -1,4 +1,6 @@ +import json import pickle + from uuid import UUID from . import api_endpoints @@ -46,7 +48,7 @@ def generate_api(): gen = Generation( options=pickle.dumps({name: vars(options) for name, options in gen_options.items()}), # convert to json compatible - meta=pickle.dumps({"race": race}), state=STATE_QUEUED, + meta=json.dumps({"race": race}), state=STATE_QUEUED, owner=session["_id"]) commit() return {"text": f"Generation of seed {gen.id} started successfully.", @@ -55,7 +57,8 @@ def generate_api(): "wait_api_url": url_for("api.wait_seed_api", seed=gen.id, _external=True), "url": url_for("wait_seed", seed=gen.id, _external=True)}, 201 except Exception as e: - return {"text": "Uncaught Exception:" + str(e)}, 500 + raise + @api_endpoints.route('/status/') diff --git a/WebHostLib/autolauncher.py b/WebHostLib/autolauncher.py index c8c7910e..2f365127 100644 --- a/WebHostLib/autolauncher.py +++ b/WebHostLib/autolauncher.py @@ -1,5 +1,6 @@ from __future__ import annotations import logging +import json import multiprocessing from datetime import timedelta, datetime import concurrent.futures @@ -9,6 +10,8 @@ import time from pony.orm import db_session, select, commit +from Utils import restricted_loads + class CommonLocker(): """Uses a file lock to signal that something is already running""" @@ -78,14 +81,21 @@ def handle_generation_failure(result: BaseException): def launch_generator(pool: multiprocessing.pool.Pool, generation: Generation): - options = generation.options - logging.info(f"Generating {generation.id} for {len(options)} players") - - meta = generation.meta - pool.apply_async(gen_game, (options,), - {"race": meta["race"], "sid": generation.id, "owner": generation.owner}, - handle_generation_success, handle_generation_failure) - generation.state = STATE_STARTED + try: + meta = json.loads(generation.meta) + options = restricted_loads(generation.options) + logging.info(f"Generating {generation.id} for {len(options)} players") + pool.apply_async(gen_game, (options,), + {"race": meta["race"], + "sid": generation.id, + "owner": generation.owner}, + handle_generation_success, handle_generation_failure) + except: + generation.state = STATE_ERROR + commit() + raise + else: + generation.state = STATE_STARTED def init_db(pony_config: dict): diff --git a/WebHostLib/generate.py b/WebHostLib/generate.py index 302b8aba..d15af19c 100644 --- a/WebHostLib/generate.py +++ b/WebHostLib/generate.py @@ -1,6 +1,7 @@ import os import tempfile import random +import json from collections import Counter from flask import request, flash, redirect, url_for, session, render_template @@ -94,10 +95,6 @@ def gen_game(gen_options, race=False, owner=None, sid=None): erargs.names = ",".join(erargs.name[i] for i in range(1, playercount + 1)) del (erargs.name) - - erargs.skip_progression_balancing = {player: not balanced for player, balanced in - erargs.progression_balancing.items()} - del (erargs.progression_balancing) ERmain(erargs, seed) return upload_to_db(target.name, owner, sid, race) @@ -107,7 +104,11 @@ def gen_game(gen_options, race=False, owner=None, sid=None): gen = Generation.get(id=sid) if gen is not None: gen.state = STATE_ERROR - gen.meta = (e.__class__.__name__ + ": "+ str(e)).encode() + meta = json.loads(gen.meta) + meta["error"] = (e.__class__.__name__ + ": "+ str(e)) + gen.meta = json.dumps(meta) + + commit() raise @@ -122,7 +123,7 @@ def wait_seed(seed: UUID): if not generation: return "Generation not found." elif generation.state == STATE_ERROR: - return render_template("seedError.html", seed_error=generation.meta.decode()) + return render_template("seedError.html", seed_error=generation.meta) return render_template("waitSeed.html", seed_id=seed_id) @@ -147,10 +148,10 @@ def upload_to_db(folder, owner, sid, race:bool): with db_session: if sid: seed = Seed(multidata=multidata, spoiler=spoiler, patches=patches, owner=owner, - id=sid, meta={"tags": ["generated"]}) + id=sid, meta=json.dumps({"tags": ["generated"]})) else: seed = Seed(multidata=multidata, spoiler=spoiler, patches=patches, owner=owner, - meta={"tags": ["generated"]}) + meta=json.dumps({"tags": ["generated"]})) for patch in patches: patch.seed = seed if sid: diff --git a/WebHostLib/models.py b/WebHostLib/models.py index a8cfb085..7c910cb8 100644 --- a/WebHostLib/models.py +++ b/WebHostLib/models.py @@ -39,7 +39,7 @@ class Seed(db.Entity): creation_time = Required(datetime, default=lambda: datetime.utcnow()) patches = Set(Patch) spoiler = Optional(LongStr, lazy=True) - meta = Required(Json, lazy=True, default=lambda: {}) # additional meta information/tags + meta = Required(str, default=lambda: "{\"race\": false}") # additional meta information/tags class Command(db.Entity): @@ -51,6 +51,6 @@ class Command(db.Entity): class Generation(db.Entity): id = PrimaryKey(UUID, default=uuid4) owner = Required(UUID) - options = Required(Json, lazy=True) - meta = Required(Json, lazy=True) + options = Required(buffer, lazy=True) + meta = Required(str, default=lambda: "{\"race\": false}") state = Required(int, default=0, index=True) diff --git a/WebHostLib/templates/playerTracker.html b/WebHostLib/templates/playerTracker.html index 8a88fd20..dd8fa5b8 100644 --- a/WebHostLib/templates/playerTracker.html +++ b/WebHostLib/templates/playerTracker.html @@ -39,7 +39,7 @@ - + diff --git a/WebHostLib/templates/userContent.html b/WebHostLib/templates/userContent.html index acd4d05f..ea3bad20 100644 --- a/WebHostLib/templates/userContent.html +++ b/WebHostLib/templates/userContent.html @@ -31,10 +31,7 @@ {{ room.seed.id|suuid }} {{ room.id|suuid }} - {{ room.seed.multidata.names[0]|length }} + >={{ room.seed.patches|length }} {{ room.creation_time.strftime("%Y-%m-%d %H:%M") }} {{ room.last_activity.strftime("%Y-%m-%d %H:%M") }} @@ -59,11 +56,7 @@ {% for seed in seeds %} {{ seed.id|suuid }} - {% if seed.multidata %}{{ seed.multidata.names[0]|length }}{% else %}1{% endif %} + {% if seed.multidata %}>={{ seed.patches|length }}{% else %}1{% endif %} {{ seed.creation_time.strftime("%Y-%m-%d %H:%M") }} diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py index 5c491822..7bba3a23 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -266,6 +266,7 @@ def attribute_item(inventory, team, recipient, item): def attribute_item_solo(inventory, item): + """Adds item to inventory counter, converts everything to progressive.""" target_item = links.get(item, item) if item in levels: # non-progressive inventory[target_item] = max(inventory[target_item], levels[item]) @@ -319,11 +320,12 @@ def get_static_room_data(room: Room): player_big_key_locations = {playernumber: set() for playernumber in range(1, len(names[0]) + 1)} player_small_key_locations = {playernumber: set() for playernumber in range(1, len(names[0]) + 1)} - for _, (item_id, item_player) in locations.items(): - if item_id in ids_big_key: - player_big_key_locations[item_player].add(ids_big_key[item_id]) - if item_id in ids_small_key: - player_small_key_locations[item_player].add(ids_small_key[item_id]) + for loc_data in locations.values(): + for item_id, item_player in loc_data.values(): + if item_id in ids_big_key: + player_big_key_locations[item_player].add(ids_big_key[item_id]) + elif item_id in ids_small_key: + player_small_key_locations[item_player].add(ids_small_key[item_id]) result = locations, names, use_door_tracker, player_checks_in_area, player_location_to_area, \ player_big_key_locations, player_small_key_locations, multidata["precollected_items"] @@ -332,7 +334,7 @@ def get_static_room_data(room: Room): @app.route('/tracker///') -@cache.memoize(timeout=15) +@cache.memoize(timeout=60) # multisave is currently created at most every minute def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int): # Team and player must be positive and greater than zero if tracked_team < 0 or tracked_player < 1: @@ -362,35 +364,25 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int): multisave = {} # Add items to player inventory - for (ms_team, ms_player), locations_checked in multisave.get("location_checks", {}): + for (ms_team, ms_player), locations_checked in multisave.get("location_checks", {}).items(): # logging.info(f"{ms_team}, {ms_player}, {locations_checked}") # Skip teams and players not matching the request - + player_locations = locations[ms_player] if ms_team == tracked_team: # If the player does not have the item, do nothing for location in locations_checked: - if (location, ms_player) not in locations: - continue - - item, recipient = locations[location, ms_player] - if recipient == tracked_player: # a check done for the tracked player - attribute_item_solo(inventory, item) - if ms_player == tracked_player: # a check done by the tracked player - checks_done[location_to_area[location]] += 1 - checks_done["Total"] += 1 + if location in player_locations: + item, recipient = player_locations[location] + if recipient == tracked_player: # a check done for the tracked player + attribute_item_solo(inventory, item) + if ms_player == tracked_player: # a check done by the tracked player + checks_done[location_to_area[location]] += 1 + checks_done["Total"] += 1 # Note the presence of the triforce item - for (ms_team, ms_player), game_state in multisave.get("client_game_state", []): - # Skip teams and players not matching the request - if ms_team != tracked_team or ms_player != tracked_player: - continue - - if game_state: - inventory[106] = 1 # Triforce - - acquired_items = [] - for itm in inventory: - acquired_items.append(get_item_name_from_id(itm)) + game_state = multisave.get("client_game_state", {}).get((tracked_team, tracked_player), 0) + if game_state == 30: + inventory[106] = 1 # Triforce # Progressive items need special handling for icons and class progressive_items = { @@ -400,86 +392,42 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int): "Progressive Mail": 96, "Progressive Shield": 95, } + progressive_names = { + "Progressive Sword": [None, 'Fighter Sword', 'Master Sword', 'Tempered Sword', 'Golden Sword'], + "Progressive Glove": [None, 'Power Glove', 'Titan Mitts'], + "Progressive Bow": [None, "Bow", "Silver Bow"], + "Progressive Mail": ["Green Mail", "Blue Mail", "Red Mail"], + "Progressive Shield": [None, "Blue Shield", "Red Shield", "Mirror Shield"] + } - # Determine which icon to use for the sword - sword_url = icons["Fighter Sword"] - sword_acquired = False - sword_names = ['Fighter Sword', 'Master Sword', 'Tempered Sword', 'Golden Sword'] - if "Progressive Sword" in acquired_items: - sword_url = icons[sword_names[min(inventory[progressive_items["Progressive Sword"]], 4) - 1]] - sword_acquired = True - else: - for sword in reversed(sword_names): - if sword in acquired_items: - sword_url = icons[sword] - sword_acquired = True - break + # Determine which icon to use + display_data = {} + for item_name, item_id in progressive_items.items(): + level = min(inventory[item_id], len(progressive_names[item_name])) + display_name = progressive_names[item_name][level] + acquired = True + if not display_name: + acquired = False + display_name = progressive_names[item_name][level+1] + base_name = item_name.split(maxsplit=1)[1].lower() + display_data[base_name+"_acquired"] = acquired + display_data[base_name+"_url"] = icons[display_name] - gloves_url = icons["Power Glove"] - gloves_acquired = False - glove_names = ["Power Glove", "Titan Mitts"] - if "Progressive Glove" in acquired_items: - gloves_url = icons[glove_names[min(inventory[progressive_items["Progressive Glove"]], 2) - 1]] - gloves_acquired = True - else: - for glove in reversed(glove_names): - if glove in acquired_items: - gloves_url = icons[glove] - gloves_acquired = True - break - - bow_url = icons["Bow"] - bow_acquired = False - bow_names = ["Bow", "Silver Bow"] - if "Progressive Bow" in acquired_items: - bow_url = icons[bow_names[min(inventory[progressive_items["Progressive Bow"]], 2) - 1]] - bow_acquired = True - else: - for bow in reversed(bow_names): - if bow in acquired_items: - bow_url = icons[bow] - bow_acquired = True - break - - mail_url = icons["Green Mail"] - mail_names = ["Blue Mail", "Red Mail"] - if "Progressive Mail" in acquired_items: - mail_url = icons[mail_names[min(inventory[progressive_items["Progressive Mail"]], 2) - 1]] - else: - for mail in reversed(mail_names): - if mail in acquired_items: - mail_url = icons[mail] - break - - shield_url = icons["Blue Shield"] - shield_acquired = False - shield_names = ["Blue Shield", "Red Shield", "Mirror Shield"] - if "Progressive Shield" in acquired_items: - shield_url = icons[shield_names[min(inventory[progressive_items["Progressive Shield"]], 3) - 1]] - shield_acquired = True - else: - for shield in reversed(shield_names): - if shield in acquired_items: - shield_url = icons[shield] - shield_acquired = True - break # The single player tracker doesn't care about overworld, underworld, and total checks. Maybe it should? sp_areas = ordered_areas[2:15] return render_template("playerTracker.html", inventory=inventory, get_item_name_from_id=get_item_name_from_id, player_name=player_name, room=room, icons=icons, checks_done=checks_done, - checks_in_area=seed_checks_in_area, acquired_items=acquired_items, - sword_url=sword_url, sword_acquired=sword_acquired, gloves_url=gloves_url, - gloves_acquired=gloves_acquired, bow_url=bow_url, bow_acquired=bow_acquired, + checks_in_area=seed_checks_in_area, acquired_items={Items.lookup_id_to_name[id] for id in inventory}, small_key_ids=small_key_ids, big_key_ids=big_key_ids, sp_areas=sp_areas, key_locations=player_small_key_locations[tracked_player], big_key_locations=player_big_key_locations[tracked_player], - mail_url=mail_url, shield_url=shield_url, shield_acquired=shield_acquired) + **display_data) @app.route('/tracker/') -@cache.memoize(timeout=30) # update every 30 seconds +@cache.memoize(timeout=60) # multisave is currently created at most every minute def getTracker(tracker: UUID): room = Room.get(tracker=tracker) if not room: @@ -500,26 +448,27 @@ def getTracker(tracker: UUID): else: multisave = {} if "hints" in multisave: - for key, hintdata in multisave["hints"]: - for hint in hintdata: - hints[key[0]].add(Hint(*hint)) - for (team, player), locations_checked in multisave.get("location_checks", {}): + for (team, slot), slot_hints in multisave["hints"].items(): + hints[team] |= set(slot_hints) + + for (team, player), locations_checked in multisave.get("location_checks", {}).items(): + player_locations = locations[player] if precollected_items: precollected = precollected_items[player] for item_id in precollected: attribute_item(inventory, team, player, item_id) for location in locations_checked: - if (location, player) not in locations or location not in player_location_to_area[player]: + if location not in player_locations or location not in player_location_to_area[player]: continue - item, recipient = locations[location, player] + item, recipient = player_locations[location] attribute_item(inventory, team, recipient, item) checks_done[team][player][player_location_to_area[player][location]] += 1 checks_done[team][player]["Total"] += 1 - for (team, player), game_state in multisave.get("client_game_state", []): - if game_state: + for (team, player), game_state in multisave.get("client_game_state", {}).items(): + if game_state == 30: inventory[team][player][106] = 1 # Triforce group_big_key_locations = set() diff --git a/worlds/alttp/EntranceRandomizer.py b/worlds/alttp/EntranceRandomizer.py index 63114c61..0a62a2c3 100644 --- a/worlds/alttp/EntranceRandomizer.py +++ b/worlds/alttp/EntranceRandomizer.py @@ -362,6 +362,7 @@ def parse_arguments(argv, no_defaults=False): create a binary patch file from which the randomized rom can be recreated using MultiClient.''') parser.add_argument('--disable_glitch_boots', default=defval(False), action='store_true', help='''\ turns off starting with Pegasus Boots in glitched modes.''') + parser.add_argument('--start_hints') if multiargs.multi: for player in range(1, multiargs.multi + 1): parser.add_argument(f'--p{player}', default=defval(''), help=argparse.SUPPRESS) @@ -405,7 +406,7 @@ def parse_arguments(argv, no_defaults=False): 'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor', 'heartbeep', "progression_balancing", "triforce_pieces_available", "triforce_pieces_required", "shop_shuffle", "shop_shuffle_slots", - "required_medallions", + "required_medallions", "start_hints", "plando_items", "plando_texts", "plando_connections", "er_seeds", 'progressive', 'dungeon_counters', 'glitch_boots', 'killable_thieves', 'tile_shuffle', 'bush_shuffle', 'shuffle_prizes', 'sprite_pool', 'dark_room_logic',