diff --git a/AHITClient.py b/AHITClient.py new file mode 100644 index 000000000000..6ed7d7b49d48 --- /dev/null +++ b/AHITClient.py @@ -0,0 +1,8 @@ +from worlds.ahit.Client import launch +import Utils +import ModuleUpdate +ModuleUpdate.update() + +if __name__ == "__main__": + Utils.init_logging("AHITClient", exception_logger="Client") + launch() diff --git a/Fill.py b/Fill.py index d9919c133847..d8147b2eac80 100644 --- a/Fill.py +++ b/Fill.py @@ -35,8 +35,8 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati """ :param multiworld: Multiworld to be filled. :param base_state: State assumed before fill. - :param locations: Locations to be filled with item_pool - :param item_pool: Items to fill into the locations + :param locations: Locations to be filled with item_pool, gets mutated by removing locations that get filled. + :param item_pool: Items to fill into the locations, gets mutated by removing items that get placed. :param single_player_placement: if true, can speed up placement if everything belongs to a single player :param lock: locations are set to locked as they are filled :param swap: if true, swaps of already place items are done in the event of a dead end @@ -220,7 +220,8 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati def remaining_fill(multiworld: MultiWorld, locations: typing.List[Location], itempool: typing.List[Item], - name: str = "Remaining") -> None: + name: str = "Remaining", + move_unplaceable_to_start_inventory: bool = False) -> None: unplaced_items: typing.List[Item] = [] placements: typing.List[Location] = [] swapped_items: typing.Counter[typing.Tuple[int, str]] = Counter() @@ -284,13 +285,21 @@ def remaining_fill(multiworld: MultiWorld, if unplaced_items and locations: # There are leftover unplaceable items and locations that won't accept them - raise FillError(f"No more spots to place {len(unplaced_items)} items. Remaining locations are invalid.\n" - f"Unplaced items:\n" - f"{', '.join(str(item) for item in unplaced_items)}\n" - f"Unfilled locations:\n" - f"{', '.join(str(location) for location in locations)}\n" - f"Already placed {len(placements)}:\n" - f"{', '.join(str(place) for place in placements)}") + if move_unplaceable_to_start_inventory: + last_batch = [] + for item in unplaced_items: + logging.debug(f"Moved {item} to start_inventory to prevent fill failure.") + multiworld.push_precollected(item) + last_batch.append(multiworld.worlds[item.player].create_filler()) + remaining_fill(multiworld, locations, unplaced_items, name + " Start Inventory Retry") + else: + raise FillError(f"No more spots to place {len(unplaced_items)} items. Remaining locations are invalid.\n" + f"Unplaced items:\n" + f"{', '.join(str(item) for item in unplaced_items)}\n" + f"Unfilled locations:\n" + f"{', '.join(str(location) for location in locations)}\n" + f"Already placed {len(placements)}:\n" + f"{', '.join(str(place) for place in placements)}") itempool.extend(unplaced_items) @@ -420,7 +429,8 @@ def distribute_early_items(multiworld: MultiWorld, return fill_locations, itempool -def distribute_items_restrictive(multiworld: MultiWorld) -> None: +def distribute_items_restrictive(multiworld: MultiWorld, + panic_method: typing.Literal["swap", "raise", "start_inventory"] = "swap") -> None: fill_locations = sorted(multiworld.get_unfilled_locations()) multiworld.random.shuffle(fill_locations) # get items to distribute @@ -470,8 +480,29 @@ def mark_for_locking(location: Location): if progitempool: # "advancement/progression fill" - fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, single_player_placement=multiworld.players == 1, - name="Progression") + if panic_method == "swap": + fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, + swap=True, + on_place=mark_for_locking, name="Progression", single_player_placement=multiworld.players == 1) + elif panic_method == "raise": + fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, + swap=False, + on_place=mark_for_locking, name="Progression", single_player_placement=multiworld.players == 1) + elif panic_method == "start_inventory": + fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, + swap=False, allow_partial=True, + on_place=mark_for_locking, name="Progression", single_player_placement=multiworld.players == 1) + if progitempool: + for item in progitempool: + logging.debug(f"Moved {item} to start_inventory to prevent fill failure.") + multiworld.push_precollected(item) + filleritempool.append(multiworld.worlds[item.player].create_filler()) + logging.warning(f"{len(progitempool)} items moved to start inventory," + f" due to failure in Progression fill step.") + progitempool[:] = [] + + else: + raise ValueError(f"Generator Panic Method {panic_method} not recognized.") if progitempool: raise FillError( f"Not enough locations for progression items. " @@ -486,7 +517,9 @@ def mark_for_locking(location: Location): inaccessible_location_rules(multiworld, multiworld.state, defaultlocations) - remaining_fill(multiworld, excludedlocations, filleritempool, "Remaining Excluded") + remaining_fill(multiworld, excludedlocations, filleritempool, "Remaining Excluded", + move_unplaceable_to_start_inventory=panic_method=="start_inventory") + if excludedlocations: raise FillError( f"Not enough filler items for excluded locations. " @@ -495,7 +528,8 @@ def mark_for_locking(location: Location): restitempool = filleritempool + usefulitempool - remaining_fill(multiworld, defaultlocations, restitempool) + remaining_fill(multiworld, defaultlocations, restitempool, + move_unplaceable_to_start_inventory=panic_method=="start_inventory") unplaced = restitempool unfilled = defaultlocations diff --git a/Main.py b/Main.py index 1be91a8bb2f1..8b15a57a69e5 100644 --- a/Main.py +++ b/Main.py @@ -13,7 +13,7 @@ from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld, Region from Fill import balance_multiworld_progression, distribute_items_restrictive, distribute_planned, flood_items from Options import StartInventoryPool -from Utils import __version__, output_path, version_tuple +from Utils import __version__, output_path, version_tuple, get_settings from settings import get_settings from worlds import AutoWorld from worlds.generic.Rules import exclusion_rules, locality_rules @@ -272,7 +272,7 @@ def find_common_pool(players: Set[int], shared_pool: Set[str]) -> Tuple[ if multiworld.algorithm == 'flood': flood_items(multiworld) # different algo, biased towards early game progress items elif multiworld.algorithm == 'balanced': - distribute_items_restrictive(multiworld) + distribute_items_restrictive(multiworld, get_settings().generator.panic_method) AutoWorld.call_all(multiworld, 'post_fill') diff --git a/Options.py b/Options.py index 7f833d5aff57..39fd56765615 100644 --- a/Options.py +++ b/Options.py @@ -746,6 +746,7 @@ def from_text(cls, text: str) -> Range: class FreezeValidKeys(AssembleOptions): def __new__(mcs, name, bases, attrs): + assert not "_valid_keys" in attrs, "'_valid_keys' gets set by FreezeValidKeys, define 'valid_keys' instead." if "valid_keys" in attrs: attrs["_valid_keys"] = frozenset(attrs["valid_keys"]) return super(FreezeValidKeys, mcs).__new__(mcs, name, bases, attrs) diff --git a/README.md b/README.md index c009d54fbe57..4633c99c664d 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,9 @@ Currently, the following games are supported: * Yoshi's Island * Mario & Luigi: Superstar Saga * Bomb Rush Cyberfunk +* Aquaria * Yu-Gi-Oh! Ultimate Masters: World Championship Tournament 2006 +* A Hat in Time For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/). Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled diff --git a/WebHostLib/customserver.py b/WebHostLib/customserver.py index bc9f74bacee7..16769b7a760e 100644 --- a/WebHostLib/customserver.py +++ b/WebHostLib/customserver.py @@ -270,15 +270,19 @@ async def start_room(room_id): await ctx.shutdown_task except (KeyboardInterrupt, SystemExit): - pass - except Exception: + if ctx.saving: + ctx._save() + except Exception as e: with db_session: room = Room.get(id=room_id) room.last_port = -1 + logger.exception(e) raise + else: + if ctx.saving: + ctx._save() finally: try: - ctx._save() with (db_session): # ensure the Room does not spin up again on its own, minute of safety buffer room = Room.get(id=room_id) diff --git a/WebHostLib/options.py b/WebHostLib/options.py index f52f0f3d9f91..94f173df70cb 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -79,10 +79,6 @@ def test_ordered(obj): @cache.cached() def option_presets(game: str) -> Response: world = AutoWorldRegister.world_types[game] - presets = {} - - if world.web.options_presets: - presets = presets | world.web.options_presets class SetEncoder(json.JSONEncoder): def default(self, obj): @@ -91,7 +87,7 @@ def default(self, obj): return list(obj) return json.JSONEncoder.default(self, obj) - json_data = json.dumps(presets, cls=SetEncoder) + json_data = json.dumps(world.web.options_presets, cls=SetEncoder) response = Response(json_data) response.headers["Content-Type"] = "application/json" return response diff --git a/WebHostLib/templates/playerOptions/macros.html b/WebHostLib/templates/playerOptions/macros.html index 64964682fe5f..c4d97255d85e 100644 --- a/WebHostLib/templates/playerOptions/macros.html +++ b/WebHostLib/templates/playerOptions/macros.html @@ -114,7 +114,7 @@ {% macro ItemDict(option_name, option, world) %} {{ OptionTitle(option_name, option) }}