From f45410c917c4e0e69f0e769e83b5b7df7cd5853c Mon Sep 17 00:00:00 2001 From: qwint Date: Tue, 15 Jul 2025 00:10:40 -0500 Subject: [PATCH 01/37] Core: Update UUID handling to be more easily sharable between libraries (#5088) moves uuid caching to appdata and uuid generation to be a random uuid instead of getnode's hardware address driven identifier and updates docs to point to the shared cache --- Utils.py | 18 ++++++++++++++---- docs/network protocol.md | 2 +- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/Utils.py b/Utils.py index 5697bb162a..d1c1dd5b5e 100644 --- a/Utils.py +++ b/Utils.py @@ -413,13 +413,23 @@ def get_adjuster_settings(game_name: str) -> Namespace: @cache_argsless def get_unique_identifier(): - uuid = persistent_load().get("client", {}).get("uuid", None) + common_path = cache_path("common.json") + if os.path.exists(common_path): + with open(common_path) as f: + common_file = json.load(f) + uuid = common_file.get("uuid", None) + else: + common_file = {} + uuid = None + if uuid: return uuid - import uuid - uuid = uuid.getnode() - persistent_store("client", "uuid", uuid) + from uuid import uuid4 + uuid = str(uuid4()) + common_file["uuid"] = uuid + with open(common_path, "w") as f: + json.dump(common_file, f, separators=(",", ":")) return uuid diff --git a/docs/network protocol.md b/docs/network protocol.md index 8c07ff10fd..27238e6b74 100644 --- a/docs/network protocol.md +++ b/docs/network protocol.md @@ -294,7 +294,7 @@ Sent by the client to initiate a connection to an Archipelago game session. | password | str | If the game session requires a password, it should be passed here. | | game | str | The name of the game the client is playing. Example: `A Link to the Past` | | name | str | The player name for this client. | -| uuid | str | Unique identifier for player client. | +| uuid | str | Unique identifier for player. Cached in the user cache \Archipelago\Cache\common.json | | version | [NetworkVersion](#NetworkVersion) | An object representing the Archipelago version this client supports. | | items_handling | int | Flags configuring which items should be sent by the server. Read below for individual flags. | | tags | list\[str\] | Denotes special features or capabilities that the sender is capable of. [Tags](#Tags) | From 9a648efa70e3bf6b3130ad4400b039238954882a Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Tue, 15 Jul 2025 11:48:28 -0400 Subject: [PATCH 02/37] Super Metroid: Only Put Relevant Options in `slot_data` (#5192) * first working single-world randomized SM rom patches * - SM now displays message when getting an item outside for someone else (fills ROM item table) This is dependant on modifications done to sm_randomizer_rom project * First working MultiWorld SM * some missing things: - player name inject in ROM and get in client - end game get from ROM in client - send self item to server - add player names table in ROM * replaced CollectionState inheritance from SMBoolManager with a composition of an array of it (required to generation more than one SM world, which is still fails but is better) * - reenabled balancing * post rebase fixes * updated SmClient.py * + added VariaRandomizer LICENSE * + added sm_randomizer_rom project (which builds sm.ips) * Moved VariaRandomizer and sm_randomizer_rom projects inside worlds/sm and done some cleaning * properly revert change made to CollectionState and more cleaning * Fixed multiworld support patch not working with VariaRandomizer's * missing file commit * Fixed syntax error in unused code to satisfy Linter * Revert "Fixed multiworld support patch not working with VariaRandomizer's" This reverts commit fb3ca18528bb331995e3d3051648c8f84d04c08b. * many fixes and improovement - fixed seeded generation - fixed broken logic when more than one SM world - added missing rules for inter-area transitions - added basic patch presence for logic - added DoorManager init call to reflect present patches for logic - moved CollectionState addition out of BaseClasses into SM world - added condition to apply progitempool presorting only if SM world is present - set Bosses item id to None to prevent them going into multidata - now use get_game_players * first working (most of the time) progression generation for SM using VariaRandomizer's rules, items, locations and accessPoint (as regions) * first working single-world randomized SM rom patches * - SM now displays message when getting an item outside for someone else (fills ROM item table) This is dependant on modifications done to sm_randomizer_rom project * First working MultiWorld SM * some missing things: - player name inject in ROM and get in client - end game get from ROM in client - send self item to server - add player names table in ROM * replaced CollectionState inheritance from SMBoolManager with a composition of an array of it (required to generation more than one SM world, which is still fails but is better) * - reenabled balancing * post rebase fixes * updated SmClient.py * + added VariaRandomizer LICENSE * + added sm_randomizer_rom project (which builds sm.ips) * Moved VariaRandomizer and sm_randomizer_rom projects inside worlds/sm and done some cleaning * properly revert change made to CollectionState and more cleaning * Fixed multiworld support patch not working with VariaRandomizer's * missing file commit * Fixed syntax error in unused code to satisfy Linter * Revert "Fixed multiworld support patch not working with VariaRandomizer's" This reverts commit fb3ca18528bb331995e3d3051648c8f84d04c08b. * many fixes and improovement - fixed seeded generation - fixed broken logic when more than one SM world - added missing rules for inter-area transitions - added basic patch presence for logic - added DoorManager init call to reflect present patches for logic - moved CollectionState addition out of BaseClasses into SM world - added condition to apply progitempool presorting only if SM world is present - set Bosses item id to None to prevent them going into multidata - now use get_game_players * Fixed multiworld support patch not working with VariaRandomizer's Added stage_fill_hook to set morph first in progitempool Added back VariaRandomizer's standard patches * + added missing files from variaRandomizer project * + added missing variaRandomizer files (custom sprites) + started integrating VariaRandomizer options (WIP) * Some fixes for player and server name display - fixed player name of 16 characters reading too far in SM client - fixed 12 bytes SM player name limit (now 16) - fixed server name not being displayed in SM when using server cheat ( now displays RECEIVED FROM ARCHIPELAGO) - request: temporarly changed default seed names displayed in SM main menu to OWTCH * Fixed Goal completion not triggering in smClient * integrated VariaRandomizer's options into AP (WIP) - startAP is working - door rando is working - skillset is working * - fixed itemsounds.ips crash by always including nofanfare.ips into multiworld.ips (itemsounds is now always applied and "itemsounds" preset must always be "off") * skillset are now instanced per player instead of being a singleton class * RomPatches are now instanced per player instead of being a singleton class * DoorManager is now instanced per player instead of being a singleton class * - fixed the last bugs that prevented generation of >1 SM world * fixed crash when no skillset preset is specified in randoPreset (default to "casual") * maxDifficulty support and itemsounds removal - added support for maxDifficulty - removed itemsounds patch as its always applied from multiworld patch for now * Fixed bad merge * Post merge adaptation * fixed player name length fix that got lost with the merge * fixed generation with other game type than SM * added default randoPreset json for SM in playerSettings.yaml * fixed broken SM client following merge * beautified json skillset presets * Fixed ArchipelagoSmClient not building * Fixed conflict between mutliworld patch and beam_doors_plms patch - doorsColorsRando now working * SM generation now outputs APBP - Fixed paths for patches and presets when frozen * added missing file and fixed multithreading issue * temporarily set data_version = 0 * more work - added support for AP starting items - fixed client crash with gamemode being None - patch.py "compatible_version" is now 3 * commited missing asm files fixed start item reserve breaking game (was using bad write offset when patching) * Nothing item are now handled game-side. the game will now skip displaying a message box for received Nothing item (but the client will still receive it). fixed crash in SMClient when loosing connection to SNI * fixed No Energy Item missing its ID fixed Plando * merge post fixes * fixed start item Grapple, XRay and Reserve HUD, as well as graphic beams (except ice palette color) * fixed freeze in blue brinstar caused by Varia's custom PLM not being filled with proper Multiworld PLM address (altLocsAddresses) * fixed start item x-ray HUD display * Fixed start items being sent by the server (is all handled in ROM) Start items are now not removed from itempool anymore Nothing Item is now local_items so no player will ever pickup Nothing. Doing so reduces contribution of this world to the Multiworld the more Nothing there is though. Fixed crash (and possibly passing but broken) at generation where the static list of IPSPatches used by all SM worlds was being modified * fixed settings that could be applied to any SM players * fixed auth to server only using player name (now does as ALTTP to authenticate) * - fixed End Credits broken text * added non SM item name display * added all supported SM options in playerSettings.yaml * fixed locations needing a list of parent regions (now generate a region for each location with one-way exits to each (previously) parent region did some cleaning (mainly reverts on unnecessary core classes * minor setting fixes and tweaks - merged Area and lightArea settings - made missileQty, superQty and powerBombQty use value from 10 to 90 and divide value by float(10) when generating - fixed inverted layoutPatch setting * added option start_inventory_removes_from_pool fixed option names formatting fixed lint errors small code and repo cleanup * Hopefully fixed ROR2 that could not send any items * - fixed missing required change to ROR2 * fixed 0 hp when respawning without having ever saved (start items were not updating the save checksum) * fixed typo with doors_colors_rando * fixed checksum * added custom sprites for off-world items (progression or not) the original AP sprite was made with PierRoulette's SM Item Sprite Utility by ijwu * - added missing change following upstream merge - changed patch filename extension from apbp to apm3 so patch can be used with the new client * added morph placement options: early means local and sphere 1 * fixed failing unit tests * - fixed broken custom_preset options * - big cleanup to remove unnecessary or unsupported features * - more cleanup * - moved sm_randomizer_rom and all always applied patches into an external project that outputs basepatch.ips - small cleanup * - added comment to refer to project for generating basepatch.ips (https://github.com/lordlou/SMBasepatch) * fixed g4_skip patch that can be not applied if hud is enabled * - fixed off world sprite that can have broken graphics (restricted to use only first 2 palette) * - updated basepatch to reflect g4_skip removal - moved more asm files to SMBasepatch project * - tourian grey doors at baby metroid are now always flashing (allowing to go back if needed) * fixed wrong path if using built as exe * - cleaned exposed maxDifficulty options - removed always enabled Knows * Merged LttPClient and SMClient into SNIClient * added varia_custom Preset Option that fetch a preset (read from a new varia_custom_preset Option) from varia's web service * small doc precision * - added death_link support - fixed broken Goal Completion - post merge fix * - removed now useless presets * - fixed bad internal mapping with maxDiff - increases maxDiff if only Bosses is preventing beating the game * - added support for lowercase custom preset sections (knows, settings and controller) - fixed controller settings not applying to ROM * - fixed death loop when dying with Door rando, bomb or speed booster as starting items - varia's backup save should now be usable (automatically enabled when doing door rando) * -added docstring for generated yaml * fixed bad merge * fixed broken infinity max difficulty * commented debug prints * adjusted credits to mark progression speed and difficulty as Non Available * added support for more than 255 players (will print Archipelago for higher player number) * fixed missing cleanup * added support for 65535 different player names in ROM * fixed generations failing when only bosses are unreachable * - replaced setting maxDiff to infinity with a bool only affecting boss logics if only bosses are left to finish * fixed failling generations when using 'fun' settings Accessibility checks are forced to 'items' if restricted locations are used by VARIA following usage of 'fun' settings * fixed debug logger * removed unsupported "suits_restriction" option * fixed generations failing when only bosses are unreachable (using a less intrusive approach for AP) * - fixed deathlink emptying reserves - added death_link_survive option that lets player survive when receiving a deathlink if the have non-empty reserves * - merged death_link and death_link_survive options * fixed death_link * added a fallback default starting location instead of failing generation if an invalid one was chosen * added Nothing and NoEnergy as hint blacklist added missing NoEnergy as local items and removed it from progression * reduced slot_data to only what should be needed by PopTracker (for https://github.com/ArchipelagoMW/Archipelago/pull/5039) --- worlds/sm/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index bc8dcd6114..3272f40c9b 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -852,7 +852,7 @@ class SMWorld(World): def fill_slot_data(self): slot_data = {} if not self.multiworld.is_race: - slot_data = self.options.as_dict(*self.options_dataclass.type_hints) + slot_data = self.options.as_dict("start_location", "max_difficulty", "area_randomization", "doors_colors_rando", "boss_randomization") slot_data["Preset"] = { "Knows": {}, "Settings": {"hardRooms": Settings.SettingsDict[self.player].hardRooms, "bossesDifficulty": Settings.SettingsDict[self.player].bossesDifficulty, From c8ca3e643d7b9f502a31edbadf9ad4ab2d8cec69 Mon Sep 17 00:00:00 2001 From: qwint Date: Tue, 15 Jul 2025 13:19:50 -0500 Subject: [PATCH 03/37] Core: Adds Visual Formatting to Option Group Headers in Template Yamls (#5092) Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- data/options.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/data/options.yaml b/data/options.yaml index 3fbe25a921..f2621124c8 100644 --- a/data/options.yaml +++ b/data/options.yaml @@ -46,7 +46,9 @@ requires: {{ yaml_dump(game) }}: {%- for group_name, group_options in option_groups.items() %} - # {{ group_name }} + ##{% for _ in group_name %}#{% endfor %}## + # {{ group_name }} # + ##{% for _ in group_name %}#{% endfor %}## {%- for option_key, option in group_options.items() %} {{ option_key }}: From c879307b8e1cffa75afb3b1b14ae7f6e3b593f5c Mon Sep 17 00:00:00 2001 From: qwint Date: Tue, 15 Jul 2025 13:30:13 -0500 Subject: [PATCH 04/37] CC: Add Assert to Catch Old Datapackage Lookup API (#5131) --- CommonClient.py | 1 + 1 file changed, 1 insertion(+) diff --git a/CommonClient.py b/CommonClient.py index 3a5f51aeee..35ed541fad 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -201,6 +201,7 @@ class CommonContext: # noinspection PyTypeChecker def __getitem__(self, key: str) -> typing.Mapping[int, str]: + assert isinstance(key, str), f"ctx.{self.lookup_type}_names used with an id, use the lookup_in_ helpers instead" return self._game_store[key] def __len__(self) -> int: From f967444ac2d7cb35dec6cb71da2e7189e42bb41e Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Tue, 15 Jul 2025 20:32:22 +0200 Subject: [PATCH 05/37] Core: Assert that all the items in the multiworld itempool are actually unplaced at the start of distribute_items_restrictive (#5109) * Assert at the beginning of distribute items restrictive that no items in the itempool already have locations associated with them * actual message * placement * oops * Update Fill.py --- Fill.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Fill.py b/Fill.py index abdad44070..d4df9fdd47 100644 --- a/Fill.py +++ b/Fill.py @@ -450,6 +450,12 @@ def distribute_early_items(multiworld: MultiWorld, def distribute_items_restrictive(multiworld: MultiWorld, panic_method: typing.Literal["swap", "raise", "start_inventory"] = "swap") -> None: + assert all(item.location is None for item in multiworld.itempool), ( + "At the start of distribute_items_restrictive, " + "there are items in the multiworld itempool that are already placed on locations:\n" + f"{[(item.location, item) for item in multiworld.itempool if item.location is not None]}" + ) + fill_locations = sorted(multiworld.get_unfilled_locations()) multiworld.random.shuffle(fill_locations) # get items to distribute From c1ae637fa7d6d152a4a52fadb8055b3ec59f4606 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Tue, 15 Jul 2025 20:32:53 +0200 Subject: [PATCH 06/37] Core: Crash on full accessibility if there are unreachable locations (Yes, you read that right) #3787 --- BaseClasses.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/BaseClasses.py b/BaseClasses.py index dbcd65ab55..ef0fa10e21 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -706,6 +706,12 @@ class MultiWorld(): sphere.append(locations.pop(n)) if not sphere: + if __debug__: + from Fill import FillError + raise FillError( + f"Could not access required locations for accessibility check. Missing: {locations}", + multiworld=self, + ) # ran out of places and did not finish yet, quit logging.warning(f"Could not access required locations for accessibility check." f" Missing: {locations}") From 507a9a53ef363e77b4dfdecb6612e8b335e6199d Mon Sep 17 00:00:00 2001 From: Mysteryem Date: Tue, 15 Jul 2025 19:33:11 +0100 Subject: [PATCH 07/37] Core: Cleanup: Replace direct calling of dunder methods on objects (#4584) Calling the dunder method has to: 1. Look up the dunder method for that object/class 2. Bind a new method instance to the object instance 3. Call the method with its arguments 4. Run the appropriate operation on the object Whereas running the appropriate operation on the object from the start skips straight to step 4. Region.Register.__getitem__ is called a lot without #4583. In that case, generation of 10 template Blasphemous yamls with `--skip_output --seed 1` and progression balancing disabled went from 19.0s to 18.8s (1.3% reduction in generation duration). From profiling with `timeit` ```py def __getitem__(self, index: int) -> Location: return self._list[index] ``` appears to be about twice as fast as the old code: ```py def __getitem__(self, index: int) -> Location: return self._list.__getitem__(index) ``` Besides this, there is not expected to be any noticeable difference in performance, and there is not expected to be any difference in semantics with these changes. Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- BaseClasses.py | 12 ++++++------ CommonClient.py | 2 +- Options.py | 12 ++++++------ 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index ef0fa10e21..c4fff017b7 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1156,13 +1156,13 @@ class Region: self.region_manager = region_manager def __getitem__(self, index: int) -> Location: - return self._list.__getitem__(index) + return self._list[index] def __setitem__(self, index: int, value: Location) -> None: raise NotImplementedError() def __len__(self) -> int: - return self._list.__len__() + return len(self._list) def __iter__(self): return iter(self._list) @@ -1176,8 +1176,8 @@ class Region: class LocationRegister(Register): def __delitem__(self, index: int) -> None: - location: Location = self._list.__getitem__(index) - self._list.__delitem__(index) + location: Location = self._list[index] + del self._list[index] del(self.region_manager.location_cache[location.player][location.name]) def insert(self, index: int, value: Location) -> None: @@ -1188,8 +1188,8 @@ class Region: class EntranceRegister(Register): def __delitem__(self, index: int) -> None: - entrance: Entrance = self._list.__getitem__(index) - self._list.__delitem__(index) + entrance: Entrance = self._list[index] + del self._list[index] del(self.region_manager.entrance_cache[entrance.player][entrance.name]) def insert(self, index: int, value: Entrance) -> None: diff --git a/CommonClient.py b/CommonClient.py index 35ed541fad..454150acbf 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -211,7 +211,7 @@ class CommonContext: return iter(self._game_store) def __repr__(self) -> str: - return self._game_store.__repr__() + return repr(self._game_store) def lookup_in_game(self, code: int, game_name: typing.Optional[str] = None) -> str: """Returns the name for an item/location id in the context of a specific game or own game if `game` is diff --git a/Options.py b/Options.py index 26e145926e..b910d21665 100644 --- a/Options.py +++ b/Options.py @@ -865,13 +865,13 @@ class OptionDict(Option[typing.Dict[str, typing.Any]], VerifyKeys, typing.Mappin return ", ".join(f"{key}: {v}" for key, v in value.items()) def __getitem__(self, item: str) -> typing.Any: - return self.value.__getitem__(item) + return self.value[item] def __iter__(self) -> typing.Iterator[str]: - return self.value.__iter__() + return iter(self.value) def __len__(self) -> int: - return self.value.__len__() + return len(self.value) # __getitem__ fallback fails for Counters, so we define this explicitly def __contains__(self, item) -> bool: @@ -1067,10 +1067,10 @@ class PlandoTexts(Option[typing.List[PlandoText]], VerifyKeys): yield from self.value def __getitem__(self, index: typing.SupportsIndex) -> PlandoText: - return self.value.__getitem__(index) + return self.value[index] def __len__(self) -> int: - return self.value.__len__() + return len(self.value) class ConnectionsMeta(AssembleOptions): @@ -1217,7 +1217,7 @@ class PlandoConnections(Option[typing.List[PlandoConnection]], metaclass=Connect connection.exit) for connection in value]) def __getitem__(self, index: typing.SupportsIndex) -> PlandoConnection: - return self.value.__getitem__(index) + return self.value[index] def __iter__(self) -> typing.Iterator[PlandoConnection]: yield from self.value From f9f386fa1948f0cb7348df2690d61792caceaa18 Mon Sep 17 00:00:00 2001 From: Mysteryem Date: Tue, 15 Jul 2025 19:33:24 +0100 Subject: [PATCH 08/37] Core: Cache previous swap states to use as the base state to sweep from (#3859) The previous swap_state can often be used as the base state to create the next swap_state. This previous swap_state will already have collected all items in item_pool and is likely to have checked many locations, meaning that creating the next swap_state from it instead of from base_state is faster. From generating with extra code to raise an exception if more than 2 previous swap states were used, and using A Hat in Time and Pokemon Red/Blue yamls that often result in lots of swapping in progression fill, I could not get a single seed go through more than 2 previous swap states. A few worlds' pre-fills do often use more than 2 previous swap states, notably LADX which sometimes goes through over 20. Given a 20 player Pokemon Red/Blue multiworld that usually generates in around 16 or 17 seconds, but on a specific seed that results in 56 swaps, generation went from about 260 seconds before this patch to about 104 seconds after this patch (generated with a meta.yaml to disable progression balancing and `python -O Generate.py --skip_output`). Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- Fill.py | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/Fill.py b/Fill.py index d4df9fdd47..94904f4f39 100644 --- a/Fill.py +++ b/Fill.py @@ -116,6 +116,13 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati else: # we filled all reachable spots. if swap: + # Keep a cache of previous safe swap states that might be usable to sweep from to produce the next + # swap state, instead of sweeping from `base_state` each time. + previous_safe_swap_state_cache: typing.Deque[CollectionState] = deque() + # Almost never are more than 2 states needed. The rare cases that do are usually highly restrictive + # single_player_placement=True pre-fills which can go through more than 10 states in some seeds. + max_swap_base_state_cache_length = 3 + # try swapping this item with previously placed items in a safe way then in an unsafe way swap_attempts = ((i, location, unsafe) for unsafe in (False, True) @@ -130,9 +137,30 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati location.item = None placed_item.location = None - swap_state = sweep_from_pool(base_state, [placed_item, *item_pool] if unsafe else item_pool, - multiworld.get_filled_locations(item.player) - if single_player_placement else None) + + for previous_safe_swap_state in previous_safe_swap_state_cache: + # If a state has already checked the location of the swap, then it cannot be used. + if location not in previous_safe_swap_state.advancements: + # Previous swap states will have collected all items in `item_pool`, so the new + # `swap_state` can skip having to collect them again. + # Previous swap states will also have already checked many locations, making the sweep + # faster. + swap_state = sweep_from_pool(previous_safe_swap_state, (placed_item,) if unsafe else (), + multiworld.get_filled_locations(item.player) + if single_player_placement else None) + break + else: + # No previous swap_state was usable as a base state to sweep from, so create a new one. + swap_state = sweep_from_pool(base_state, [placed_item, *item_pool] if unsafe else item_pool, + multiworld.get_filled_locations(item.player) + if single_player_placement else None) + # Unsafe states should not be added to the cache because they have collected `placed_item`. + if not unsafe: + if len(previous_safe_swap_state_cache) >= max_swap_base_state_cache_length: + # Remove the oldest cached state. + previous_safe_swap_state_cache.pop() + # Add the new state to the start of the cache. + previous_safe_swap_state_cache.appendleft(swap_state) # unsafe means swap_state assumes we can somehow collect placed_item before item_to_place # by continuing to swap, which is not guaranteed. This is unsafe because there is no mechanic # to clean that up later, so there is a chance generation fails. From 2aada8f683e57e70878680d1a98b2c05a6f30a16 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Tue, 15 Jul 2025 20:35:27 +0200 Subject: [PATCH 09/37] Core: Add new ItemClassification "deprioritized" which will not be placed on priority locations (if possible) (#4610) * Add new deprioritized item flag * 4 retries * indent * . * style * I think this is nicer * Nicer * remove two lines again that I added unnecessarily * I think this test makes a bit more sense like this * Idk how to word this lol * Add progression_deprioritized_skip_balancing bc why not ig * More text * Update Fill.py * Update Fill.py * I am the big stupid * Actually collect the other half of progression items into state when filling without them * More clarity on the descriptions (hopefully) * visually separate technical description and use cases * Actually make the call do what the comments say it does --- BaseClasses.py | 34 +++++++++++++++++++++++------- Fill.py | 44 ++++++++++++++++++++++++++++++++------- test/general/test_fill.py | 22 ++++++++++++++++++++ 3 files changed, 86 insertions(+), 14 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index c4fff017b7..6deb878097 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1436,27 +1436,43 @@ class Location: class ItemClassification(IntFlag): - filler = 0b0000 + filler = 0b00000 """ aka trash, as in filler items like ammo, currency etc """ - progression = 0b0001 + progression = 0b00001 """ Item that is logically relevant. Protects this item from being placed on excluded or unreachable locations. """ - useful = 0b0010 + useful = 0b00010 """ Item that is especially useful. Protects this item from being placed on excluded or unreachable locations. When combined with another flag like "progression", it means "an especially useful progression item". """ - trap = 0b0100 + trap = 0b00100 """ Item that is detrimental in some way. """ - skip_balancing = 0b1000 + skip_balancing = 0b01000 """ should technically never occur on its own Item that is logically relevant, but progression balancing should not touch. - Typically currency or other counted items. """ + + Possible reasons for why an item should not be pulled ahead by progression balancing: + 1. This item is quite insignificant, so pulling it earlier doesn't help (currency/etc.) + 2. It is important for the player experience that this item is evenly distributed in the seed (e.g. goal items) """ - progression_skip_balancing = 0b1001 # only progression gets balanced + deprioritized = 0b10000 + """ Should technically never occur on its own. + Will not be considered for priority locations, + unless Priority Locations Fill runs out of regular progression items before filling all priority locations. + + Should be used for items that would feel bad for the player to find on a priority location. + Usually, these are items that are plentiful or insignificant. """ + + progression_deprioritized_skip_balancing = 0b11001 + """ Since a common case of both skip_balancing and deprioritized is "insignificant progression", + these items often want both flags. """ + + progression_skip_balancing = 0b01001 # only progression gets balanced + progression_deprioritized = 0b10001 # only progression can be placed during priority fill def as_flag(self) -> int: """As Network API flag int.""" @@ -1504,6 +1520,10 @@ class Item: def trap(self) -> bool: return ItemClassification.trap in self.classification + @property + def deprioritized(self) -> bool: + return ItemClassification.deprioritized in self.classification + @property def filler(self) -> bool: return not (self.advancement or self.useful or self.trap) diff --git a/Fill.py b/Fill.py index 94904f4f39..29a9a530a4 100644 --- a/Fill.py +++ b/Fill.py @@ -526,18 +526,48 @@ def distribute_items_restrictive(multiworld: MultiWorld, single_player = multiworld.players == 1 and not multiworld.groups if prioritylocations: + regular_progression = [] + deprioritized_progression = [] + for item in progitempool: + if item.deprioritized: + deprioritized_progression.append(item) + else: + regular_progression.append(item) + # "priority fill" - maximum_exploration_state = sweep_from_pool(multiworld.state) - fill_restrictive(multiworld, maximum_exploration_state, prioritylocations, progitempool, + # try without deprioritized items in the mix at all. This means they need to be collected into state first. + priority_fill_state = sweep_from_pool(multiworld.state, deprioritized_progression) + fill_restrictive(multiworld, priority_fill_state, prioritylocations, regular_progression, single_player_placement=single_player, swap=False, on_place=mark_for_locking, name="Priority", one_item_per_player=True, allow_partial=True) - if prioritylocations: + if prioritylocations and regular_progression: # retry with one_item_per_player off because some priority fills can fail to fill with that optimization - maximum_exploration_state = sweep_from_pool(multiworld.state) - fill_restrictive(multiworld, maximum_exploration_state, prioritylocations, progitempool, - single_player_placement=single_player, swap=False, on_place=mark_for_locking, - name="Priority Retry", one_item_per_player=False) + # deprioritized items are still not in the mix, so they need to be collected into state first. + priority_retry_state = sweep_from_pool(multiworld.state, deprioritized_progression) + fill_restrictive(multiworld, priority_retry_state, prioritylocations, regular_progression, + single_player_placement=single_player, swap=False, on_place=mark_for_locking, + name="Priority Retry", one_item_per_player=False, allow_partial=True) + + if prioritylocations and deprioritized_progression: + # There are no more regular progression items that can be placed on any priority locations. + # We'd still prefer to place deprioritized progression items on priority locations over filler items. + # Since we're leaving out the remaining regular progression now, we need to collect it into state first. + priority_retry_2_state = sweep_from_pool(multiworld.state, regular_progression) + fill_restrictive(multiworld, priority_retry_2_state, prioritylocations, deprioritized_progression, + single_player_placement=single_player, swap=False, on_place=mark_for_locking, + name="Priority Retry 2", one_item_per_player=True, allow_partial=True) + + if prioritylocations and deprioritized_progression: + # retry with deprioritized items AND without one_item_per_player optimisation + # Since we're leaving out the remaining regular progression now, we need to collect it into state first. + priority_retry_3_state = sweep_from_pool(multiworld.state, regular_progression) + fill_restrictive(multiworld, priority_retry_3_state, prioritylocations, deprioritized_progression, + single_player_placement=single_player, swap=False, on_place=mark_for_locking, + name="Priority Retry 3", one_item_per_player=False) + + # restore original order of progitempool + progitempool[:] = [item for item in progitempool if not item.location] accessibility_corrections(multiworld, multiworld.state, prioritylocations, progitempool) defaultlocations = prioritylocations + defaultlocations diff --git a/test/general/test_fill.py b/test/general/test_fill.py index c8bcec9581..bdc38d7913 100644 --- a/test/general/test_fill.py +++ b/test/general/test_fill.py @@ -603,6 +603,28 @@ class TestDistributeItemsRestrictive(unittest.TestCase): self.assertTrue(player3.locations[2].item.advancement) self.assertTrue(player3.locations[3].item.advancement) + def test_deprioritized_does_not_land_on_priority(self): + multiworld = generate_test_multiworld(1) + player1 = generate_player_data(multiworld, 1, 2, prog_item_count=2) + + player1.prog_items[0].classification |= ItemClassification.deprioritized + player1.locations[0].progress_type = LocationProgressType.PRIORITY + + distribute_items_restrictive(multiworld) + + self.assertFalse(player1.locations[0].item.deprioritized) + + def test_deprioritized_still_goes_on_priority_ahead_of_filler(self): + multiworld = generate_test_multiworld(1) + player1 = generate_player_data(multiworld, 1, 2, prog_item_count=1, basic_item_count=1) + + player1.prog_items[0].classification |= ItemClassification.deprioritized + player1.locations[0].progress_type = LocationProgressType.PRIORITY + + distribute_items_restrictive(multiworld) + + self.assertTrue(player1.locations[0].item.advancement) + def test_can_remove_locations_in_fill_hook(self): """Test that distribute_items_restrictive calls the fill hook and allows for item and location removal""" multiworld = generate_test_multiworld() From e1b26bc76f7b69e2ec532d3ac4d9e60ba05824bc Mon Sep 17 00:00:00 2001 From: Eindall Date: Tue, 15 Jul 2025 21:02:17 +0200 Subject: [PATCH 10/37] Stardew Valley: Add French Guide (#4697) Co-authored-by: tmarquis --- worlds/stardew_valley/__init__.py | 28 ++++++--- worlds/stardew_valley/docs/setup_fr.md | 87 ++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 9 deletions(-) create mode 100644 worlds/stardew_valley/docs/setup_fr.md diff --git a/worlds/stardew_valley/__init__.py b/worlds/stardew_valley/__init__.py index ea0ce9e123..ec96a9949e 100644 --- a/worlds/stardew_valley/__init__.py +++ b/worlds/stardew_valley/__init__.py @@ -50,15 +50,25 @@ class StardewWebWorld(WebWorld): options_presets = sv_options_presets option_groups = sv_option_groups - tutorials = [ - Tutorial( - "Multiworld Setup Guide", - "A guide to playing Stardew Valley with Archipelago.", - "English", - "setup_en.md", - "setup/en", - ["KaitoKid", "Jouramie", "Witchybun (Mod Support)", "Exempt-Medic (Proofreading)"] - )] + setup_en = Tutorial( + "Multiworld Setup Guide", + "A guide to playing Stardew Valley with Archipelago.", + "English", + "setup_en.md", + "setup/en", + ["KaitoKid", "Jouramie", "Witchybun (Mod Support)", "Exempt-Medic (Proofreading)"] + ) + + setup_fr = Tutorial( + "Guide de configuration MultiWorld", + "Un guide pour configurer Stardew Valley sur Archipelago", + "Français", + "setup_fr.md", + "setup/fr", + ["Eindall"] + ) + + tutorials = [setup_en, setup_fr] class StardewValleyWorld(World): diff --git a/worlds/stardew_valley/docs/setup_fr.md b/worlds/stardew_valley/docs/setup_fr.md new file mode 100644 index 0000000000..d7866c0b16 --- /dev/null +++ b/worlds/stardew_valley/docs/setup_fr.md @@ -0,0 +1,87 @@ +# Guide de configuration du Randomizer Stardew Valley + +## Logiciels nécessaires + +- Stardew Valley 1.6 sur PC (Recommandé: [Steam](https://store.steampowered.com/app/413150/Stardew_Valley/)) +- SMAPI ([Mod loader pour Stardew Valley](https://www.nexusmods.com/stardewvalley/mods/2400?tab=files)) +- [StardewArchipelago Version 6.x.x](https://github.com/agilbert1412/StardewArchipelago/releases) + - Il est important d'utiliser une release en 6.x.x pour jouer sur des seeds générées ici. Les versions ultérieures peuvent uniquement être utilisées pour des release ultérieures du générateur de mondes, qui ne sont pas encore hébergées sur archipelago.gg + +## Logiciels optionnels + +- Launcher Archipelago à partir de la [page des versions d'Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases) + - (Uniquement pour le client textuel) +- Autres [mods supportés](https://github.com/agilbert1412/StardewArchipelago/blob/6.x.x/Documentation/Supported%20Mods.md) que vous pouvez ajouter au yaml pour les inclure dans la randomization d'Archipelago + + - Il n'est **pas** recommandé de modder Stardew Valley avec des mods non supportés, même s'il est possible de le faire. + Les interactions entre mods peuvent être imprévisibles, et aucune aide ne sera fournie pour les bugs qui y sont liés. + - Plus vous avez de mods non supportés, et plus ils sont gros, plus vous avez de chances de casser des choses. + +## Configuration du fichier YAML + +### Qu'est qu'un fichier YAML et pourquoi en ai-je besoin ? + +Voir le guide pour paramètrer un fichier YAML dans le guide de configuration d'Archipelago (en anglais): [Guide de configuration d'un MultiWorld basique](/tutorial/Archipelago/setup/en) + +### Où puis-je récupèrer un fichier YAML + +Vous pouvez personnaliser vos options en visitant la [Page d'options de joueur pour Stardew Valley](/games/Stardew%20Valley/player-options) + +## Rejoindre une partie en MultiWorld + +### Installation du mod + +- Installer [SMAPI](https://www.nexusmods.com/stardewvalley/mods/2400?tab=files) en suivant les instructions sur la page du mod. +- Télécharger et extraire le mod [StardewArchipelago](https://github.com/agilbert1412/StardewArchipelago/releases) dans le dossier "Mods" de Stardew Valley. +- *Optionnel*: Si vous voulez lancer le jeu depuis Steam, ajouter l'option de lancement suivante à Stardew Valley : `"[PATH TO STARDEW VALLEY]\Stardew Valley\StardewModdingAPI.exe" %command%` +- Sinon, exécutez juste "StardewModdingAPI.exe" dans le dossier d'installation de Stardew Valley. +- Stardew Valley devrait se lancer avec une console qui liste les informations des mods installés, et intéragit avec certains d'entre eux. + +### Se connecter au MultiServer + +Lancer Stardew Valley avec SMAPI. Une fois que vous avez atteint l'écran titre du jeu, créez une nouvelle ferme. + +Dans la fenêtre de création de personnage, vous verrez 3 nouveaux champs, qui permettent de relier votre personnage à un MultiWorld Archipelago. + +![image](https://i.imgur.com/b8KZy2F.png) + +Vous pouvez personnaliser votre personnage comme vous le souhaitez. + +Le champ "Server" nécessite l'adresse **et** le port, et le "Slotname" est le nom que vous avez spécifié dans votre YAML. + +`archipelago.gg:12345` + +`StardewPlayer` + +Le mot de passe est optionnel. + +Votre jeu se connectera automatiquement à Archipelago, et se reconnectera automatiquement également quand vous chargerez votre sauvegarde, plus tard. + +Vous n'aurez plus besoin d'entrer ces informations à nouveau pour ce personnage, à moins que votre session ne change d'ip ou de port. +Si l'ip ou le port de la session **change**, vous pouvez suivre ces instructions pour modifier les informations de connexion liées à votre sauvegarde : + +- Lancer Stardew Valley moddé +- Dans le **menu principal** du jeu, entrer la commande suivante **dans la console de SMAPI** : +- `connect_override ip:port slot password` +- Par exemple : `connect_override archipelago.gg:54321 StardewPlayer` +- Chargez votre partie. Les nouvelles informations de connexion seront utilisées à la place de celles enregistrées initialement. +- Jouez une journée, dormez et sauvegarder la partie. Les nouvelles informations de connexion iront écraser les précédentes, et deviendront permanentes. + +### Intéragir avec le MultiWorld depuis le jeu + +Quand vous vous connectez, vous devriez voir un message dans le chat vous informant de l'existence de la commande `!!help`. Cette commande liste les autres commandes exclusives à Stardew Valley que vous pouvez utiliser. + +De plus, vous pouvez utiliser le chat en jeu pour parler aux autres joueurs du MultiWorld, pour peu qu'ils aient un jeu qui supporte le chat. + +Enfin, vous pouvez également utiliser les commandes Archipelago (`!help` pour les lister) depuis le chat du jeu, permettant de demander des indices (via la commande `!hint`) sur certains objets. + +Il est important de préciser que le chat de Stardew Valley est assez limité. Par exemple, il ne permet pas de remonter l'historique de conversation. La console SMAPI qui tourne à côté aura quant à elle l'historique complet et sera plus pratique pour consulter des messages moins récents. +Pour une meilleure expérience avec le chat, vous pouvez aussi utiliser le client textuel d'Archipelago, bien qu'il ne permettra pas de lancer les commandes exclusives à Stardew Valley. + +### Jouer avec des mods supportés + +Voir la [documentation des mods supportés](https://github.com/agilbert1412/StardewArchipelago/blob/6.x.x/Documentation/Supported%20Mods.md) (en Anglais). + +### Multijoueur + +Vous ne pouvez pas jouer à Stardew Valley en mode multijoueur pour le moment. Il n'y a aucun plan d'action pour ajouter cette fonctionalité à court terme. \ No newline at end of file From f18f9e2dce6cbc59265d48b221c82a8c2d842bfc Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 15 Jul 2025 21:04:06 +0200 Subject: [PATCH 11/37] Core: increment version (#5194) --- Utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Utils.py b/Utils.py index d1c1dd5b5e..9c1171096e 100644 --- a/Utils.py +++ b/Utils.py @@ -47,7 +47,7 @@ class Version(typing.NamedTuple): return ".".join(str(item) for item in self) -__version__ = "0.6.2" +__version__ = "0.6.3" version_tuple = tuplize_version(__version__) is_linux = sys.platform.startswith("linux") From fed60ca61a5a6874744b131af9f578e5b3a33c02 Mon Sep 17 00:00:00 2001 From: qwint Date: Tue, 15 Jul 2025 14:09:56 -0500 Subject: [PATCH 12/37] Hollow Knight: Explicitly Exclude Palace Items as Filler (#5119) --- worlds/hk/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index 4a0da109fa..31770637aa 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -218,6 +218,11 @@ class HKWorld(World): wp = self.options.WhitePalace if wp <= WhitePalace.option_nopathofpain: exclusions.update(path_of_pain_locations) + exclusions.update(( + "Soul_Totem-Path_of_Pain", + "Lore_Tablet-Path_of_Pain_Entrance", + "Journal_Entry-Seal_of_Binding", + )) if wp <= WhitePalace.option_kingfragment: exclusions.update(white_palace_checks) if wp == WhitePalace.option_exclude: @@ -226,6 +231,9 @@ class HKWorld(World): # If charms are randomized, this will be junk-filled -- so transitions and events are not progression exclusions.update(white_palace_transitions) exclusions.update(white_palace_events) + exclusions.update(item_name_groups["PalaceJournal"]) + exclusions.update(item_name_groups["PalaceLore"]) + exclusions.update(item_name_groups["PalaceTotem"]) return exclusions def create_regions(self): From 6360609980bf2c02e4c18fdecd91ae258b7dbf30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dana=C3=ABl=20V=2E?= <104455676+ReverM@users.noreply.github.com> Date: Tue, 15 Jul 2025 15:43:20 -0400 Subject: [PATCH 13/37] Witness: Add French and German Setup Documentation (#2527) Co-authored-by: Lolo Co-authored-by: Scipio Wright Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- worlds/witness/__init__.py | 22 +++++++++++++-- worlds/witness/docs/setup_de.md | 46 ++++++++++++++++++++++++++++++++ worlds/witness/docs/setup_fr.md | 47 +++++++++++++++++++++++++++++++++ 3 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 worlds/witness/docs/setup_de.md create mode 100644 worlds/witness/docs/setup_fr.md diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py index b2e91c7cf0..0f96ee94e8 100644 --- a/worlds/witness/__init__.py +++ b/worlds/witness/__init__.py @@ -27,14 +27,32 @@ from .rules import set_rules class WitnessWebWorld(WebWorld): theme = "jungle" - tutorials = [Tutorial( + setup_en = Tutorial( "Multiworld Setup Guide", "A guide to playing The Witness with Archipelago.", "English", "setup_en.md", "setup/en", ["NewSoupVi", "Jarno"] - )] + ) + setup_de = Tutorial( + setup_en.tutorial_name, + setup_en.description, + "German", + "setup_de.md", + "setup/de", + ["NewSoupVi"] + ) + setup_fr = Tutorial( + setup_en.tutorial_name, + setup_en.description, + "Français", + "setup_fr.md", + "setup/fr", + ["Rever"] + ) + + tutorials = [setup_en, setup_de, setup_fr] options_presets = witness_option_presets option_groups = witness_option_groups diff --git a/worlds/witness/docs/setup_de.md b/worlds/witness/docs/setup_de.md new file mode 100644 index 0000000000..82865bb134 --- /dev/null +++ b/worlds/witness/docs/setup_de.md @@ -0,0 +1,46 @@ +# The Witness Randomizer Setup + +## Benötigte Software + +- [The Witness für ein 64-bit-Windows-Betriebssystem (z.B. Steam-Version)](https://store.steampowered.com/app/210970/The_Witness/) +- [The Witness Archipelago Randomizer](https://github.com/NewSoupVi/The-Witness-Randomizer-for-Archipelago/releases/latest) + +## Optionale Software + +- [ArchipelagoTextClient](https://github.com/ArchipelagoMW/Archipelago/releases) +- [The Witness Auto-Tracker mit Kartenansicht](https://github.com/NewSoupVi/witness_archipelago_tracker/releases), benutzbar mit [PopTracker](https://github.com/black-sliver/PopTracker/releases) + +## Verbindung mit einem Multiworld-Spiel + +1. Öffne The Witness. +2. Erstelle einen neuen Speicherstand. +3. Öffne [The Witness Archipelago Randomizer](https://github.com/NewSoupVi/The-Witness-Randomizer-for-Archipelago/releases/latest). +4. Gib die Archipelago-Adresse, deinen Namen und evtl. das Passwort ein. +5. Drücke "Connect". +6. Viel Spaß! + +Wenn du ein vorheriges Spiel fortsetzen willst, ist das auch möüglich: + +1. Öffne The Witness. +2. Lade den Speicherstand für das Multiworld-Spiel, das du weiterspielen willst - Wenn das nicht sowieso schon der ist, den das Spiel automatisch geladen hat. +3. Öffne [The Witness Archipelago Randomizer](https://github.com/NewSoupVi/The-Witness-Randomizer-for-Archipelago/releases/latest). +4. Drücke "Load Credentials", um Adresse, Namen und Passwort automatisch zu laden (oder tippe diese manuell ein). +5. Drücke "Connect". + +## Archipelago Text Client + +Es ist empfehlenswert, den "Archipelago Text Client", der eine Textansicht für gesendete und erhaltene Items liefert, beim Spielen nebenbei sichtbar zu haben. +
Diese Nachrichten werden zwar auch im Spiel angezeigt, jedoch nur für ein paar Sekunden. Es ist leicht, eine dieser Nachrichten zu übersehen. + +

Alternativ gibt es den visuellen Auto-Tracker mit Kartenansicht, der im nächsten Kapitel beschrieben wird. + +## Auto-Tracking + +The Witness hat einen voll funktionsfähigen Tracker mit Kartenansicht und Autotracking. + +1. Installiere [PopTracker](https://github.com/black-sliver/PopTracker/releases) und lade den [The Witness Auto-Tracker mit Kartenansicht](https://github.com/NewSoupVi/witness_archipelago_tracker/releases) herunter. +2. Öffne PopTracker, und lade das "The Witness"-Packet. +3. Klicke auf das "AP"-Symbol am oberen Fensterrand. +4. Gib die Archipelago-Adresse, deinen Namen und evtl. das Passwort ein. + +Der Rest sollte vollautomatisch ohne weitere Eingabe funktionieren. Der Tracker wird deine momentanen Items anzeigen und lösbare Rätsel grün auf der Karte anzeigen. Sobald du eine Rätselsequenz abschließt, wird sie grau markiert. \ No newline at end of file diff --git a/worlds/witness/docs/setup_fr.md b/worlds/witness/docs/setup_fr.md new file mode 100644 index 0000000000..db88911b92 --- /dev/null +++ b/worlds/witness/docs/setup_fr.md @@ -0,0 +1,47 @@ +# Guide d'installation du Witness randomizer + +## Logiciels Requis + +- [The Witness pour Windows 64-bit (par exemple, la version Steam)](https://store.steampowered.com/app/210970/The_Witness/) +- [The Witness Archipelago Randomizer](https://github.com/NewSoupVi/The-Witness-Randomizer-for-Archipelago/releases/latest) + +## Logiciels Facultatifs + +- [ArchipelagoTextClient](https://github.com/ArchipelagoMW/Archipelago/releases) +- [The Witness Map- et Auto-Tracker](https://github.com/NewSoupVi/witness_archipelago_tracker/releases), pour usage avec [PopTracker](https://github.com/black-sliver/PopTracker/releases) + +## Rejoindre un jeu multimonde + +1. Lancez The Witness +2. Commencez une nouvelle partie +3. Lancez [The Witness Archipelago Randomizer](https://github.com/NewSoupVi/The-Witness-Randomizer-for-Archipelago/releases/latest) +4. Inscrivez l'adresse Archipelago, votre nom de joueur et le mot de passe du jeu multimonde +5. Cliquez sur "Connect" +6. Jouez! + +Pour continuer un jeu multimonde précedemment commencé: + +1. Lancez The Witness +2. Chargez la sauvegarde sur laquelle vous avez dernièrement joué ce monde, si ce n'est pas celle qui a été chargée automatiquement +3. Lancez [The Witness Archipelago Randomizer](https://github.com/NewSoupVi/The-Witness-Randomizer-for-Archipelago/releases/latest) +4. Cliquez sur "Load Credentials" (ou tapez les manuellement) +5. Cliquez sur "Connect" + +## Archipelago Text Client + +Il est recommandé d'utiliser le "Archipelago Text Client" en parallèle afin de suivre quels items vous envoyez et recevez. +
The Witness affiche également ces informations en jeu, mais seulement pour une courte période et donc il est facile de manquer ces messages. + +

Bien sûr, vous pouvez également utiliser l'auto-tracker! + +## Auto-Tracking + +The Witness a un tracker fonctionnel qui supporte l'auto-tracking. + +1. Téléchargez [The Witness Map- and Auto-Tracker](https://github.com/NewSoupVi/witness_archipelago_tracker/releases) et [PopTracker](https://github.com/black-sliver/PopTracker/releases). +2. Ouvrez Poptracker, puis chargez le pack Witness. +3. Cliquez sur l'icone "AP" qui se situe au dessus de la carte. +4. Inscrivez l'adresse Archipelago, votre nom de joueur et le mot de passe du jeu multimonde. + +Le reste devrait être pris en charge par Poptracker - les items que vous recevrez et les puzzles que vous résolverez seront automatiquement indiqués. De plus, Poptracker est en mesure de détecter +vos paramètres de jeu - les puzzles accessibles seront alors masqués ou affichés en fonction de vos paramètres de randomization et de logique. Veuillez noter que le tracker peut être obsolète. \ No newline at end of file From 749c2435ed4270d2f8e81dd427cda878d0544c1b Mon Sep 17 00:00:00 2001 From: GreenMarco <71452195+GreenMarco@users.noreply.github.com> Date: Tue, 15 Jul 2025 13:43:54 -0600 Subject: [PATCH 14/37] Hollow Knight: Add Spanish Language Docs (#5156) Co-authored-by: qwint --- worlds/hk/__init__.py | 12 +++++- worlds/hk/docs/es_Hollow Knight.md | 25 ++++++++++++ worlds/hk/docs/setup_es.md | 64 ++++++++++++++++++++++++++++++ 3 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 worlds/hk/docs/es_Hollow Knight.md create mode 100644 worlds/hk/docs/setup_es.md diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index 31770637aa..317d29334b 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -154,7 +154,17 @@ class HKWeb(WebWorld): ["JoaoVictor-FA"] ) - tutorials = [setup_en, setup_pt_br] + setup_es = Tutorial( + setup_en.tutorial_name, + setup_en.description, + "Español", + "setup_es.md", + "setup/es", + ["GreenMarco", "Panto UwUr"] + ) + + tutorials = [setup_en, setup_pt_br, setup_es] + game_info_languages = ["en", "es"] bug_report_page = "https://github.com/Ijwu/Archipelago.HollowKnight/issues/new?assignees=&labels=bug%2C+needs+investigation&template=bug_report.md&title=" diff --git a/worlds/hk/docs/es_Hollow Knight.md b/worlds/hk/docs/es_Hollow Knight.md new file mode 100644 index 0000000000..1a086086ad --- /dev/null +++ b/worlds/hk/docs/es_Hollow Knight.md @@ -0,0 +1,25 @@ +# Hollow Knight + +## ¿Dónde está la página de opciones? + +La [página de opciones de jugador para este juego](../player-options) contiene todas las opciones que necesitas para +configurar y exportar un archivo de configuración. + +## ¿Qué se randomiza en este juego? + +El randomizer cambia la ubicación de los objetos. Los objetos que se intercambian se eligen dentro de tu YAML. +Los costes de las tiendas son aleatorios. Los objetos que podrían ser aleatorios, pero no lo son, permanecerán sin +modificar en sus ubicaciones habituales. En particular, cuando los ítems con el PadreLarva y la Vidente están +parcialmente randomizados, los ítems randomizados se obtendrán de un cofre en la habitación, mientras que los ítems no +randomizados serán dados por el NPC de forma normal. + +## ¿Qué objetos de Hollow Knight pueden aparecer en los mundos de otros jugadores? + +Esto depende enteramente de tus opciones YAML. Algunos ejemplos son: amuletos, larvas, capullos de saviavida, geo, etc. + +## ¿Qué aspecto tienen los objetos de otro mundo en Hollow Knight? + +Cuando el jugador de Hollow Knight recoja un objeto de un lugar y sea un objeto para otro juego, aparecerá en la +pantalla de objetos recientes de ese jugador como un objeto enviado a otro jugador. Si el objeto es para otro jugador +de Hollow Knight entonces el sprite será el del sprite original del objeto. Si el objeto pertenece a un jugador que no +está jugando a Hollow Knight, el sprite será el logo del Archipiélago. \ No newline at end of file diff --git a/worlds/hk/docs/setup_es.md b/worlds/hk/docs/setup_es.md new file mode 100644 index 0000000000..13628c4019 --- /dev/null +++ b/worlds/hk/docs/setup_es.md @@ -0,0 +1,64 @@ +# Hollow Knight Archipelago + +## Software requerido +* Descarga y descomprime Lumafly Mod manager desde el [sitio web de Lumafly](https://themulhima.github.io/Lumafly/) +* Tener una copia legal de Hollow Knight. + * Las versiones de Steam, GOG y Xbox Game Pass son compatibles + * Las versiones de Windows, Mac y Linux (Incluyendo Steam Deck) son compatibles + +## Instalación del mod de Archipelago con Lumafly +1. Ejecuta Lumafly y asegurate de localizar la carpeta de instalación de Hollow Knight +2. Instala el mod de Archipiélago haciendo click en cualquiera de los siguientes: + * Haz clic en uno de los enlaces de abajo para permitir Lumafly para instalar los mods. Lumafly pedirá + confirmación. + * [Archipiélago y dependencias solamente](https://themulhima.github.io/Lumafly/commands/download/?mods=Archipelago) + * [Archipelago con rando essentials](https://themulhima.github.io/Lumafly/commands/download/?mods=Archipelago/Archipelago%20Map%20Mod/RecentItemsDisplay/DebugMod/RandoStats/Additional%20Timelines/CompassAlwaysOn/AdditionalMaps/) + (incluye Archipelago Map Mod, RecentItemsDisplay, DebugMod, RandoStats, AdditionalTimelines, CompassAlwaysOn, + y AdditionalMaps). + * Haz clic en el botón "Instalar" situado junto a la entrada del mod "Archipiélago". Si lo deseas, instala también + "Archipelago Map Mod" para utilizarlo como rastreador en el juego. + Si lo requieres (Y recomiendo hacerlo) busca e instala Archipelago Map Mod para usar un tracker in-game +3. Ejecuta el juego desde el apartado de inicio haciendo click en el botón Launch with Mods + +## Que hago si Lumafly no encontro la ruta de instalación de mi juego? +1. Busca el directorio manualmente + * En Xbox Game pass: + 1. Entra a la Xbox App y dirigete sobre el icono de Hollow Knight que esta a la izquierda. + 2. Haz click en los 3 puntitos y elige el apartado Administrar + 3. Dirigete al apartado Archivos Locales y haz click en Buscar + 4. Abre en Hollow Knight, luego Content y copia la ruta de archivos que esta en la barra de navegación. + * En Steam: + 1. Si instalaste Hollow Knight en algún otro disco que no sea el predeterminado, ya sabrás donde se encuentra + el juego, ve a esa carpeta, abrela y copia la ruta de archivos que se encuentra en la barra de navegación. + * En Windows, la ruta predeterminada suele ser:`C:\Program Files (x86)\Steam\steamapps\common\Hollow Knight` + * En linux/Steam Deck suele ser: ~/.local/share/Steam/steamapps/common/Hollow Knight + * En Mac suele ser: ~/Library/Application Support/Steam/steamapps/common/Hollow Knight/hollow_knight.app +2. Ejecuta Lumafly como administrador y, cuando te pregunte por la ruta de instalación, pega la ruta que copeaste + anteriormente. + +## Configuración de tu fichero YAML +### ¿Qué es un YAML y por qué necesito uno? +Un archivo YAML es la forma en la que proporcionas tus opciones de jugador a Archipelago. +Mira la [guía básica de configuración multiworld](/tutorial/Archipelago/setup/en) aquí en la web de Archipelago para +aprender más, (solo se encuentra en Inglés). + +### ¿Dónde consigo un YAML? +Puedes usar la [página de opciones de juego para Hollow Knight](/games/Hollow%20Knight/player-options) aquí en la web +de Archipelago para generar un YAML usando una interfaz gráfica. + +## Unete a una partida de Archipelago en Hollow Knight +1. Inicia el juego con los mods necesarios indicados anteriormente. +2. Crea una **nueva partida.** +3. Elige el modo **Archipelago** en la selección de modos de partida. +4. Introduce la configuración correcta para tu servidor de Archipelago. +5. Pulsa **Iniciar** para iniciar la partida. El juego se quedará con la pantalla en negro unos segundos mientras + coloca todos los objetos. +6. El juego debera comenzar y ya estaras dentro del servidor. + * Si estas esperando a que termine un contador/timer, procura presionar el boton Start cuando el contador/timer + termine. + * Otra manera es pausar el juego y esperar a que el contador/timer termine cuando ingreses a la partida. + +## Consejos y otros comandos +Mientras juegas en un multiworld, puedes interactuar con el servidor usando varios comandos listados en la +[guía de comandos](/tutorial/Archipelago/commands/en). Puedes usar el Cliente de Texto Archipelago para hacer esto, +que está incluido en la última versión del [software de Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases/latest). \ No newline at end of file From 9e748332dcd1f66a697306c13db70c391373bccd Mon Sep 17 00:00:00 2001 From: SunCat Date: Tue, 15 Jul 2025 23:01:53 +0300 Subject: [PATCH 15/37] Various Games: Improve Custom Death Link Option Description (#4171) Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Co-authored-by: Scipio Wright Co-authored-by: LiquidCat64 <74896918+LiquidCat64@users.noreply.github.com> --- worlds/blasphemous/Options.py | 6 +----- worlds/bomb_rush_cyberfunk/Options.py | 6 +----- worlds/cv64/options.py | 17 ++++++++--------- worlds/cv64/rom.py | 6 +++--- worlds/hylics2/Options.py | 9 ++------- worlds/kh1/Options.py | 4 ++-- worlds/noita/options.py | 6 ++---- 7 files changed, 19 insertions(+), 35 deletions(-) diff --git a/worlds/blasphemous/Options.py b/worlds/blasphemous/Options.py index 2cb2d8a1d3..74eb1139ce 100644 --- a/worlds/blasphemous/Options.py +++ b/worlds/blasphemous/Options.py @@ -207,11 +207,7 @@ class EnemyScaling(DefaultOnToggle): class BlasphemousDeathLink(DeathLink): - """ - When you die, everyone dies. The reverse is also true. - - Note that Guilt Fragments will not appear when killed by Death Link. - """ + __doc__ = DeathLink.__doc__ + "\n\n Note that Guilt Fragments will not appear when killed by death link." @dataclass diff --git a/worlds/bomb_rush_cyberfunk/Options.py b/worlds/bomb_rush_cyberfunk/Options.py index 80831d0645..fd327d48ec 100644 --- a/worlds/bomb_rush_cyberfunk/Options.py +++ b/worlds/bomb_rush_cyberfunk/Options.py @@ -175,11 +175,7 @@ class DamageMultiplier(Range): class BRCDeathLink(DeathLink): - """ - When you die, everyone dies. The reverse is also true. - - This can be changed later in the options menu inside the Archipelago phone app. - """ + __doc__ = DeathLink.__doc__ + "\n\n This can be changed later in the options menu inside the Archipelago phone app." @dataclass diff --git a/worlds/cv64/options.py b/worlds/cv64/options.py index da1e1aba94..62d7ec3369 100644 --- a/worlds/cv64/options.py +++ b/worlds/cv64/options.py @@ -1,6 +1,6 @@ from dataclasses import dataclass from Options import (OptionGroup, Choice, DefaultOnToggle, ItemsAccessibility, PerGameCommonOptions, Range, Toggle, - StartInventoryPool) + StartInventoryPool, DeathLink) class CharacterStages(Choice): @@ -507,12 +507,11 @@ class WindowColorA(Range): default = 8 -class DeathLink(Choice): - """ - When you die, everyone dies. Of course the reverse is true too. - Explosive: Makes received DeathLinks kill you via the Magical Nitro explosion instead of the normal death animation. - """ - display_name = "DeathLink" +class CV64DeathLink(Choice): + __doc__ = (DeathLink.__doc__ + "\n\n Explosive: Makes received death links kill you via the Magical Nitro " + + "explosion instead of the normal death animation.") + + display_name = "Death Link" option_off = 0 alias_no = 0 alias_true = 1 @@ -575,7 +574,7 @@ class CV64Options(PerGameCommonOptions): map_lighting: MapLighting fall_guard: FallGuard cinematic_experience: CinematicExperience - death_link: DeathLink + death_link: CV64DeathLink cv64_option_groups = [ @@ -584,7 +583,7 @@ cv64_option_groups = [ RenonFightCondition, VincentFightCondition, BadEndingCondition, IncreaseItemLimit, NerfHealingItems, LoadingZoneHeals, InvisibleItems, DropPreviousSubWeapon, PermanentPowerUps, IceTrapPercentage, IceTrapAppearance, DisableTimeRestrictions, SkipGondolas, SkipWaterwayBlocks, Countdown, BigToss, PantherDash, - IncreaseShimmySpeed, FallGuard, DeathLink + IncreaseShimmySpeed, FallGuard, CV64DeathLink ]), OptionGroup("cosmetics", [ WindowColorR, WindowColorG, WindowColorB, WindowColorA, BackgroundMusic, MapLighting, CinematicExperience diff --git a/worlds/cv64/rom.py b/worlds/cv64/rom.py index 830bed2779..a40d3ab300 100644 --- a/worlds/cv64/rom.py +++ b/worlds/cv64/rom.py @@ -16,7 +16,7 @@ from .text import cv64_string_to_bytearray, cv64_text_truncate, cv64_text_wrap from .aesthetics import renon_item_dialogue, get_item_text_color from .locations import get_location_info from .options import CharacterStages, VincentFightCondition, RenonFightCondition, PostBehemothBoss, RoomOfClocksBoss, \ - BadEndingCondition, DeathLink, DraculasCondition, InvisibleItems, Countdown, PantherDash + BadEndingCondition, CV64DeathLink, DraculasCondition, InvisibleItems, Countdown, PantherDash from settings import get_settings if TYPE_CHECKING: @@ -356,7 +356,7 @@ class CV64PatchExtensions(APPatchExtension): rom_data.write_int32s(0xBFE190, patches.subweapon_surface_checker) # Make received DeathLinks blow you to smithereens instead of kill you normally. - if options["death_link"] == DeathLink.option_explosive: + if options["death_link"] == CV64DeathLink.option_explosive: rom_data.write_int32s(0xBFC0D0, patches.deathlink_nitro_edition) rom_data.write_int32(0x27A70, 0x10000008) # B [forward 0x08] rom_data.write_int32(0x27AA0, 0x0C0FFA78) # JAL 0x803FE9E0 @@ -365,7 +365,7 @@ class CV64PatchExtensions(APPatchExtension): rom_data.write_int32(0x32DBC, 0x00000000) # Set the DeathLink ROM flag if it's on at all. - if options["death_link"] != DeathLink.option_off: + if options["death_link"] != CV64DeathLink.option_off: rom_data.write_byte(0xBFBFDE, 0x01) # DeathLink counter decrementer code diff --git a/worlds/hylics2/Options.py b/worlds/hylics2/Options.py index db9c316a7b..51072edcbd 100644 --- a/worlds/hylics2/Options.py +++ b/worlds/hylics2/Options.py @@ -57,13 +57,8 @@ class ExtraLogic(DefaultOnToggle): class Hylics2DeathLink(DeathLink): - """ - When you die, everyone dies. The reverse is also true. - - Note that this also includes death by using the PERISH gesture. - - Can be toggled via in-game console command "/deathlink". - """ + __doc__ = (DeathLink.__doc__ + "\n\n Note that this also includes death by using the PERISH gesture." + + "\n\n Can be toggled via in-game console command \"/deathlink\".") @dataclass diff --git a/worlds/kh1/Options.py b/worlds/kh1/Options.py index 63732f61b2..7a79d5c1ea 100644 --- a/worlds/kh1/Options.py +++ b/worlds/kh1/Options.py @@ -287,13 +287,13 @@ class BadStartingWeapons(Toggle): class DonaldDeathLink(Toggle): """ - If Donald is KO'ed, so is Sora. If Death Link is toggled on in your client, this will send a death to everyone. + If Donald is KO'ed, so is Sora. If Death Link is toggled on in your client, this will send a death to everyone who enabled death link. """ display_name = "Donald Death Link" class GoofyDeathLink(Toggle): """ - If Goofy is KO'ed, so is Sora. If Death Link is toggled on in your client, this will send a death to everyone. + If Goofy is KO'ed, so is Sora. If Death Link is toggled on in your client, this will send a death to everyone who enabled death link. """ display_name = "Goofy Death Link" diff --git a/worlds/noita/options.py b/worlds/noita/options.py index 8a973a0d72..6798cc8ccd 100644 --- a/worlds/noita/options.py +++ b/worlds/noita/options.py @@ -121,10 +121,8 @@ class ShopPrice(Choice): class NoitaDeathLink(DeathLink): - """ - When you die, everyone dies. Of course, the reverse is true too. - You can disable this in the in-game mod options. - """ + __doc__ = (DeathLink.__doc__ + "\n\n You can disable this or set it to give yourself a trap effect when " + + "another player dies in the in-game mod options.") @dataclass From deed9de3e768e9da16b23f3f3e758d6b4c05ad09 Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Tue, 15 Jul 2025 15:40:58 -0500 Subject: [PATCH 16/37] Core: Don't Cache the `get_all_state` Result (#4795) Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- BaseClasses.py | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 6deb878097..ba07868655 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -5,6 +5,7 @@ import functools import logging import random import secrets +import warnings from argparse import Namespace from collections import Counter, deque from collections.abc import Collection, MutableSequence @@ -438,12 +439,27 @@ class MultiWorld(): def get_location(self, location_name: str, player: int) -> Location: return self.regions.location_cache[player][location_name] - def get_all_state(self, use_cache: bool, allow_partial_entrances: bool = False, + def get_all_state(self, use_cache: bool | None = None, allow_partial_entrances: bool = False, collect_pre_fill_items: bool = True, perform_sweep: bool = True) -> CollectionState: - cached = getattr(self, "_all_state", None) - if use_cache and cached: - return cached.copy() + """ + Creates a new CollectionState, and collects all precollected items, all items in the multiworld itempool, those + specified in each worlds' `get_pre_fill_items()`, and then sweeps the multiworld collecting any other items + it is able to reach, building as complete of a completed game state as possible. + :param use_cache: Deprecated and unused. + :param allow_partial_entrances: Whether the CollectionState should allow for disconnected entrances while + sweeping, such as before entrance randomization is complete. + :param collect_pre_fill_items: Whether the items in each worlds' `get_pre_fill_items()` should be added to this + state. + :param perform_sweep: Whether this state should perform a sweep for reachable locations, collecting any placed + items it can. + + :return: The completed CollectionState. + """ + if __debug__ and use_cache is not None: + # TODO swap to Utils.deprecate when we want this to crash on source and warn on frozen + warnings.warn("multiworld.get_all_state no longer caches all_state and this argument will be removed.", + DeprecationWarning) ret = CollectionState(self, allow_partial_entrances) for item in self.itempool: @@ -456,8 +472,6 @@ class MultiWorld(): if perform_sweep: ret.sweep_for_advancements() - if use_cache: - self._all_state = ret return ret def get_items(self) -> List[Item]: From 1790a389c7e85630a0ba9197f04e56d67205a793 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Tue, 15 Jul 2025 17:04:27 -0400 Subject: [PATCH 17/37] TUNIC: Update Tests Per #4982 (#5191) --- worlds/tunic/test/__init__.py | 6 ------ worlds/tunic/test/bases.py | 5 +++++ worlds/tunic/test/test_access.py | 2 +- worlds/tunic/test/test_combat.py | 6 ++---- 4 files changed, 8 insertions(+), 11 deletions(-) create mode 100644 worlds/tunic/test/bases.py diff --git a/worlds/tunic/test/__init__.py b/worlds/tunic/test/__init__.py index d0b68955c5..e69de29bb2 100644 --- a/worlds/tunic/test/__init__.py +++ b/worlds/tunic/test/__init__.py @@ -1,6 +0,0 @@ -from test.bases import WorldTestBase - - -class TunicTestBase(WorldTestBase): - game = "TUNIC" - player = 1 diff --git a/worlds/tunic/test/bases.py b/worlds/tunic/test/bases.py new file mode 100644 index 0000000000..0e51bcd013 --- /dev/null +++ b/worlds/tunic/test/bases.py @@ -0,0 +1,5 @@ +from test.bases import WorldTestBase + + +class TunicTestBase(WorldTestBase): + game = "TUNIC" diff --git a/worlds/tunic/test/test_access.py b/worlds/tunic/test/test_access.py index 6a26180cf0..1896db5d13 100644 --- a/worlds/tunic/test/test_access.py +++ b/worlds/tunic/test/test_access.py @@ -1,5 +1,5 @@ -from . import TunicTestBase from .. import options +from .bases import TunicTestBase class TestAccess(TunicTestBase): diff --git a/worlds/tunic/test/test_combat.py b/worlds/tunic/test/test_combat.py index c0e76ef92b..70324247df 100644 --- a/worlds/tunic/test/test_combat.py +++ b/worlds/tunic/test/test_combat.py @@ -1,17 +1,15 @@ from BaseClasses import ItemClassification from collections import Counter -from . import TunicTestBase -from .. import options +from .. import options, TunicWorld +from .bases import TunicTestBase from ..combat_logic import (check_combat_reqs, area_data, get_money_count, calc_effective_hp, get_potion_level, get_hp_level, get_def_level, get_sp_level, has_combat_reqs) from ..items import item_table -from .. import TunicWorld class TestCombat(TunicTestBase): options = {options.CombatLogic.internal_name: options.CombatLogic.option_on} - player = 1 world: TunicWorld combat_items = [] # these are items that are progression that do not contribute to combat logic From b90dcfb04135eb3661ab107ef1d741e52e4abdb3 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Wed, 16 Jul 2025 10:31:12 +0200 Subject: [PATCH 18/37] =?UTF-8?q?The=20Witness:=20Add=20Glass=20Factory=20?= =?UTF-8?q?Entry=20Panel=20as=20a=20location=20in=20all=20options=C2=A0#46?= =?UTF-8?q?95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- worlds/witness/data/static_locations.py | 1 + 1 file changed, 1 insertion(+) diff --git a/worlds/witness/data/static_locations.py b/worlds/witness/data/static_locations.py index a5cfc3b49f..029dcd5dcb 100644 --- a/worlds/witness/data/static_locations.py +++ b/worlds/witness/data/static_locations.py @@ -18,6 +18,7 @@ GENERAL_LOCATIONS = { "Outside Tutorial Outpost Entry Panel", "Outside Tutorial Outpost Exit Panel", + "Glass Factory Entry Panel", "Glass Factory Discard", "Glass Factory Back Wall 5", "Glass Factory Front 3", From 477028a025b2803c443ef0a4629170beb4deb58d Mon Sep 17 00:00:00 2001 From: Jacob Lewis Date: Wed, 16 Jul 2025 10:11:07 -0500 Subject: [PATCH 19/37] Dics: Add Webhost API Documententation (#4887) * capitialization changes * ditto * Revert "ditto" This reverts commit 17cf596735888e91850954c7306ce0b80d7e453d. * Revert "capitialization changes" This reverts commit 6fb86c6568da2c08b5f8e691d4fc810e3ab09a44. * full revert and full commit * Update docs/webhost api.md Co-authored-by: qwint * Update docs/webhost api.md Co-authored-by: Aaron Wagener * Update docs/webhost api.md Co-authored-by: Aaron Wagener * Update webhost api.md * Removed in-devolopment API * Apply standard capitilization and grammar flow Co-authored-by: Scipio Wright * declarative language * Apply suggestions from code review Co-authored-by: qwint * datapackage_checksum clarification, and /datapackage clairfication * /dp/checksum clarification * Detailed responces and /generation breakdown * Update webhost api.md * Made output anonomous * Update docs/webhost api.md Co-authored-by: qwint * Swapped IDs to UUID, and added language around UUID vs SUUID * Apply suggestions from code review formatting and grammar Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Condensed paragraphs and waterfalled headders --------- Co-authored-by: qwint Co-authored-by: Aaron Wagener Co-authored-by: Scipio Wright Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- docs/webhost api.md | 351 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 351 insertions(+) create mode 100644 docs/webhost api.md diff --git a/docs/webhost api.md b/docs/webhost api.md new file mode 100644 index 0000000000..dba57e554c --- /dev/null +++ b/docs/webhost api.md @@ -0,0 +1,351 @@ +# API Guide + +Archipelago has a rudimentary API that can be queried by endpoints. The API is a work-in-progress and should be improved over time. + +The following API requests are formatted as: `https:///api/` + +The returned data will be formated in a combination of JSON lists or dicts, with their keys or values being notated in `blocks` (if applicable) + +Current endpoints: +- Datapackage API + - [`/datapackage`](#datapackage) + - [`/datapackage/`](#datapackagestringchecksum) + - [`/datapackage_checksum`](#datapackagechecksum) +- Generation API + - [`/generate`](#generate) + - [`/status/`](#status) +- Room API + - [`/room_status/`](#roomstatus) +- User API + - [`/get_rooms`](#getrooms) + - [`/get_seeds`](#getseeds) + + +## UUID vs SUUID +Currently, the server reports back the item's `UUID` (Universally Unique Identifier). The item's `UUID` needs to be converted to a `base64 UUID` (nicknamed a `ShortUUID` and refered to as `SUUID` in the remainder of this document) that are URL safe in order to be queried via API endpoints. +- [PR 4944](https://github.com/ArchipelagoMW/Archipelago/pull/4944) is in progress to convert API returns into SUUIDs + +## Datapackage Endpoints +These endpoints are used by applications to acquire a room's datapackage, and validate that they have the correct datapackage for use. Datapackages normally include, item IDs, location IDs, and name groupings, for a given room, and are essential for mapping IDs received from Archipelago to their correct items or locations. + +### `/datapackage` + +Fetches the current datapackage from the WebHost. +You'll receive a dict named `games` that contains a named dict of every game and its data currently supported by Archipelago. +Each game will have: +- A checksum `checksum` +- A dict of item groups `item_name_groups` +- Item name to AP ID dict `item_name_to_id` +- A dict of location groups `location_name_groups` +- Location name to AP ID dict `location_name_to_id` + +Example: +``` +{ + "games": { + ... + "Clique": { + "checksum": "0271f7a80b44ba72187f92815c2bc8669cb464c7", + "item_name_groups": { + "Everything": [ + "A Cool Filler Item (No Satisfaction Guaranteed)", + "Button Activation", + "Feeling of Satisfaction" + ] + }, + "item_name_to_id": { + "A Cool Filler Item (No Satisfaction Guaranteed)": 69696967, + "Button Activation": 69696968, + "Feeling of Satisfaction": 69696969 + }, + "location_name_groups": { + "Everywhere": [ + "The Big Red Button", + "The Item on the Desk" + ] + }, + "location_name_to_id": { + "The Big Red Button": 69696969, + "The Item on the Desk": 69696968 + } + }, + ... + } +} +``` + +### `/datapackage/` + +Fetches a single datapackage by checksum. +Returns a dict of the game's data with: +- A checksum `checksum` +- A dict of item groups `item_name_groups` +- Item name to AP ID dict `item_name_to_id` +- A dict of location groups `location_name_groups` +- Location name to AP ID dict `location_name_to_id` + +Its format will be identical to the whole-datapackage endpoint (`/datapackage`), except you'll only be returned the single game's data in a dict. + +### `/datapackage_checksum` + +Fetches the checksums of the current static datapackages on the WebHost. +You'll receive a dict with `game:checksum` key-value pairs for all the current officially supported games. +Example: +``` +{ +... +"Donkey Kong Country 3":"f90acedcd958213f483a6a4c238e2a3faf92165e", +"Factorio":"a699194a9589db3ebc0d821915864b422c782f44", +... +} +``` + + +## Generation Endpoint +These endpoints are used internally for the WebHost to generate games and validate their generation. They are also used by external applications to generate games automatically. + +### `/generate` + +Submits a game to the WebHost for generation. +**This endpoint only accepts a POST HTTP request.** + +There are two ways to submit data for generation: With a file and with JSON. + +#### With a file: +Have your ZIP of yaml(s) or a single yaml, and submit a POST request to the `/generate` endpoint. +If the options are valid, you'll be returned a successful generation response. (see [Generation Response](#generation-response)) + +Example using the python requests library: +``` +file = {'file': open('Games.zip', 'rb')} +req = requests.post("https://archipelago.gg/api/generate", files=file) +``` + +#### With JSON: +Compile your weights/yaml data into a dict. Then insert that into a dict with the key `"weights"`. +Finally, submit a POST request to the `/generate` endpoint. +If the weighted options are valid, you'll be returned a successful generation response (see [Generation Response](#generation-response)) + +Example using the python requests library: +``` +data = {"Test":{"game": "Factorio","name": "Test","Factorio": {}},} +weights={"weights": data} +req = requests.post("https://archipelago.gg/api/generate", json=weights) +``` + +#### Generation Response: +##### Successful Generation: +Upon successful generation, you'll be sent a JSON dict response detailing the generation: +- The UUID of the generation `detail` +- The SUUID of the generation `encoded` +- The response text `text` +- The page that will resolve to the seed/room generation page once generation has completed `url` +- The API status page of the generation `wait_api_url` (see [Status Endpoint](#status)) + +Example: +``` +{ + "detail": "19878f16-5a58-4b76-aab7-d6bf38be9463", + "encoded": "GYePFlpYS3aqt9a_OL6UYw", + "text": "Generation of seed 19878f16-5a58-4b76-aab7-d6bf38be9463 started successfully.", + "url": "http://archipelago.gg/wait/GYePFlpYS3aqt9a_OL6UYw", + "wait_api_url": "http://archipelago.gg/api/status/GYePFlpYS3aqt9a_OL6UYw" +} +``` + +##### Failed Generation: + +Upon failed generation, you'll be returned a single key-value pair. The key will always be `text` +The value will give you a hint as to what may have gone wrong. +- Options without tags, and a 400 status code +- Options in a string, and a 400 status code +- Invalid file/weight string, `No options found. Expected file attachment or json weights.` with a 400 status code +- Too many slots for the server to process, `Max size of multiworld exceeded` with a 409 status code + +If the generation detects a issue in generation, you'll be sent a dict with two key-value pairs (`text` and `detail`) and a 400 status code. The values will be: +- Summary of issue in `text` +- Detailed issue in `detail` + +In the event of an unhandled server exception, you'll be provided a dict with a single key `text`: +- Exception, `Uncought Exception: ` with a 500 status code + +### `/status/` + +Retrieves the status of the seed's generation. +This endpoint will return a dict with a single key-vlaue pair. The key will always be `text` +The value will tell you the status of the generation: +- Generation was completed: `Generation done` with a 201 status code +- Generation request was not found: `Generation not found` with a 404 status code +- Generation of the seed failed: `Generation failed` with a 500 status code +- Generation is in progress still: `Generation running` with a 202 status code + +## Room Endpoints +Endpoints to fetch information of the active WebHost room with the supplied room_ID. + +### `/room_status/` + +Will provide a dict of room data with the following keys: +- Tracker UUID (`tracker`) +- A list of players (`players`) + - Each item containing a list with the Slot name and Game +- Last known hosted port (`last_port`) +- Last activity timestamp (`last_activity`) +- The room timeout counter (`timeout`) +- A list of downloads for files required for gameplay (`downloads`) + - Each item is a dict containings the download URL and slot (`slot`, `download`) + +Example: +``` +{ + "downloads": [ + { + "download": "/slot_file/kK5fmxd8TfisU5Yp_eg/1", + "slot": 1 + }, + { + "download": "/slot_file/kK5fmxd8TfisU5Yp_eg/2", + "slot": 2 + }, + { + "download": "/slot_file/kK5fmxd8TfisU5Yp_eg/3", + "slot": 3 + }, + { + "download": "/slot_file/kK5fmxd8TfisU5Yp_eg/4", + "slot": 4 + }, + { + "download": "/slot_file/kK5fmxd8TfisU5Yp_eg/5", + "slot": 5 + } + ], + "last_activity": "Fri, 18 Apr 2025 20:35:45 GMT", + "last_port": 52122, + "players": [ + [ + "Slot_Name_1", + "Ocarina of Time" + ], + [ + "Slot_Name_2", + "Ocarina of Time" + ], + [ + "Slot_Name_3", + "Ocarina of Time" + ], + [ + "Slot_Name_4", + "Ocarina of Time" + ], + [ + "Slot_Name_5", + "Ocarina of Time" + ] + ], + "timeout": 7200, + "tracker": "cf6989c0-4703-45d7-a317-2e5158431171" +} +``` + +## User Endpoints +User endpoints can get room and seed details from the current session tokens (cookies) + +### `/get_rooms` + +Retreives a list of all rooms currently owned by the session token. +Each list item will contain a dict with the room's details: +- Room UUID (`room_id`) +- Seed UUID (`seed_id`) +- Creation timestamp (`creation_time`) +- Last activity timestamp (`last_activity`) +- Last known AP port (`last_port`) +- Room timeout counter in seconds (`timeout`) +- Room tracker UUID (`tracker`) + +Example: +``` +[ + { + "creation_time": "Fri, 18 Apr 2025 19:46:53 GMT", + "last_activity": "Fri, 18 Apr 2025 21:16:02 GMT", + "last_port": 52122, + "room_id": "90ae5f9b-177c-4df8-ac53-9629fc3bff7a", + "seed_id": "efbd62c2-aaeb-4dda-88c3-f461c029cef6", + "timeout": 7200, + "tracker": "cf6989c0-4703-45d7-a317-2e5158431171" + }, + { + "creation_time": "Fri, 18 Apr 2025 20:36:42 GMT", + "last_activity": "Fri, 18 Apr 2025 20:36:46 GMT", + "last_port": 56884, + "room_id": "14465c05-d08e-4d28-96bd-916f994609d8", + "seed_id": "a528e34c-3b4f-42a9-9f8f-00a4fd40bacb", + "timeout": 7200, + "tracker": "4e624bd8-32b6-42e4-9178-aa407f72751c" + } +] +``` + +### `/get_seeds` + +Retreives a list of all seeds currently owned by the session token. +Each item in the list will contain a dict with the seed's details: +- Seed UUID (`seed_id`) +- Creation timestamp (`creation_time`) +- A list of player slots (`players`) + - Each item in the list will contain a list of the slot name and game + +Example: +``` +[ + { + "creation_time": "Fri, 18 Apr 2025 19:46:52 GMT", + "players": [ + [ + "Slot_Name_1", + "Ocarina of Time" + ], + [ + "Slot_Name_2", + "Ocarina of Time" + ], + [ + "Slot_Name_3", + "Ocarina of Time" + ], + [ + "Slot_Name_4", + "Ocarina of Time" + ], + [ + "Slot_Name_5", + "Ocarina of Time" + ] + ], + "seed_id": "efbd62c2-aaeb-4dda-88c3-f461c029cef6" + }, + { + "creation_time": "Fri, 18 Apr 2025 20:36:39 GMT", + "players": [ + [ + "Slot_Name_1", + "Clique" + ], + [ + "Slot_Name_2", + "Clique" + ], + [ + "Slot_Name_3", + "Clique" + ], + [ + "Slot_Name_4", + "Archipelago" + ] + ], + "seed_id": "a528e34c-3b4f-42a9-9f8f-00a4fd40bacb" + } +] +``` From e9e0861eb751996c2e65808ec62ffd9c5b55ec33 Mon Sep 17 00:00:00 2001 From: qwint Date: Wed, 16 Jul 2025 10:34:28 -0500 Subject: [PATCH 20/37] WebHostLib: Properly Format IDs in API Responses (#4944) * update the id formatter to use staticmethods to not fake the unused self arg, and then use the formatter for the user session endpoints * missed an id (ty treble) * clean up duplicate code * Update WebHostLib/__init__.py Co-authored-by: Aaron Wagener * keep the BaseConverter format * lol, change all the instances * revert this --------- Co-authored-by: Aaron Wagener --- WebHostLib/__init__.py | 14 +++++++++++--- WebHostLib/api/room.py | 3 ++- WebHostLib/api/user.py | 9 +++++---- test/hosting/webhost.py | 10 ++++++---- 4 files changed, 24 insertions(+), 12 deletions(-) diff --git a/WebHostLib/__init__.py b/WebHostLib/__init__.py index 934cc2498d..e928b8f3b1 100644 --- a/WebHostLib/__init__.py +++ b/WebHostLib/__init__.py @@ -61,18 +61,26 @@ cache = Cache() Compress(app) +def to_python(value): + return uuid.UUID(bytes=base64.urlsafe_b64decode(value + '==')) + + +def to_url(value): + return base64.urlsafe_b64encode(value.bytes).rstrip(b'=').decode('ascii') + + class B64UUIDConverter(BaseConverter): def to_python(self, value): - return uuid.UUID(bytes=base64.urlsafe_b64decode(value + '==')) + return to_python(value) def to_url(self, value): - return base64.urlsafe_b64encode(value.bytes).rstrip(b'=').decode('ascii') + return to_url(value) # short UUID app.url_map.converters["suuid"] = B64UUIDConverter -app.jinja_env.filters['suuid'] = lambda value: base64.urlsafe_b64encode(value.bytes).rstrip(b'=').decode('ascii') +app.jinja_env.filters["suuid"] = to_url app.jinja_env.filters["title_sorted"] = title_sorted diff --git a/WebHostLib/api/room.py b/WebHostLib/api/room.py index 9337975695..78623bbe3e 100644 --- a/WebHostLib/api/room.py +++ b/WebHostLib/api/room.py @@ -3,6 +3,7 @@ from uuid import UUID from flask import abort, url_for +from WebHostLib import to_url import worlds.Files from . import api_endpoints, get_players from ..models import Room @@ -33,7 +34,7 @@ def room_info(room_id: UUID) -> Dict[str, Any]: downloads.append(slot_download) return { - "tracker": room.tracker, + "tracker": to_url(room.tracker), "players": get_players(room.seed), "last_port": room.last_port, "last_activity": room.last_activity, diff --git a/WebHostLib/api/user.py b/WebHostLib/api/user.py index 2524cc40a6..59c8e57283 100644 --- a/WebHostLib/api/user.py +++ b/WebHostLib/api/user.py @@ -1,6 +1,7 @@ from flask import session, jsonify from pony.orm import select +from WebHostLib import to_url from WebHostLib.models import Room, Seed from . import api_endpoints, get_players @@ -10,13 +11,13 @@ def get_rooms(): response = [] for room in select(room for room in Room if room.owner == session["_id"]): response.append({ - "room_id": room.id, - "seed_id": room.seed.id, + "room_id": to_url(room.id), + "seed_id": to_url(room.seed.id), "creation_time": room.creation_time, "last_activity": room.last_activity, "last_port": room.last_port, "timeout": room.timeout, - "tracker": room.tracker, + "tracker": to_url(room.tracker), }) return jsonify(response) @@ -26,7 +27,7 @@ def get_seeds(): response = [] for seed in select(seed for seed in Seed if seed.owner == session["_id"]): response.append({ - "seed_id": seed.id, + "seed_id": to_url(seed.id), "creation_time": seed.creation_time, "players": get_players(seed), }) diff --git a/test/hosting/webhost.py b/test/hosting/webhost.py index 4db605e8c1..8888c3fb87 100644 --- a/test/hosting/webhost.py +++ b/test/hosting/webhost.py @@ -2,6 +2,8 @@ import re from pathlib import Path from typing import TYPE_CHECKING, Optional, cast +from WebHostLib import to_python + if TYPE_CHECKING: from flask import Flask from werkzeug.test import Client as FlaskClient @@ -103,7 +105,7 @@ def stop_room(app_client: "FlaskClient", poll_interval = 2 print(f"Stopping room {room_id}") - room_uuid = app.url_map.converters["suuid"].to_python(None, room_id) # type: ignore[arg-type] + room_uuid = to_python(room_id) if timeout is not None: sleep(.1) # should not be required, but other things might use threading @@ -156,7 +158,7 @@ def set_room_timeout(room_id: str, timeout: float) -> None: from WebHostLib.models import Room from WebHostLib import app - room_uuid = app.url_map.converters["suuid"].to_python(None, room_id) # type: ignore[arg-type] + room_uuid = to_python(room_id) with db_session: room: Room = Room.get(id=room_uuid) room.timeout = timeout @@ -168,7 +170,7 @@ def get_multidata_for_room(webhost_client: "FlaskClient", room_id: str) -> bytes from WebHostLib.models import Room from WebHostLib import app - room_uuid = app.url_map.converters["suuid"].to_python(None, room_id) # type: ignore[arg-type] + room_uuid = to_python(room_id) with db_session: room: Room = Room.get(id=room_uuid) return cast(bytes, room.seed.multidata) @@ -180,7 +182,7 @@ def set_multidata_for_room(webhost_client: "FlaskClient", room_id: str, data: by from WebHostLib.models import Room from WebHostLib import app - room_uuid = app.url_map.converters["suuid"].to_python(None, room_id) # type: ignore[arg-type] + room_uuid = to_python(room_id) with db_session: room: Room = Room.get(id=room_uuid) room.seed.multidata = data From 4a43a6ae138b9dc64afa7919c82d62eee05a18f2 Mon Sep 17 00:00:00 2001 From: qwint Date: Wed, 16 Jul 2025 10:51:34 -0500 Subject: [PATCH 21/37] Docs: Clean up SUUID Post #4944 (#5196) --- docs/webhost api.md | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/docs/webhost api.md b/docs/webhost api.md index dba57e554c..c8936205ec 100644 --- a/docs/webhost api.md +++ b/docs/webhost api.md @@ -21,10 +21,6 @@ Current endpoints: - [`/get_seeds`](#getseeds) -## UUID vs SUUID -Currently, the server reports back the item's `UUID` (Universally Unique Identifier). The item's `UUID` needs to be converted to a `base64 UUID` (nicknamed a `ShortUUID` and refered to as `SUUID` in the remainder of this document) that are URL safe in order to be queried via API endpoints. -- [PR 4944](https://github.com/ArchipelagoMW/Archipelago/pull/4944) is in progress to convert API returns into SUUIDs - ## Datapackage Endpoints These endpoints are used by applications to acquire a room's datapackage, and validate that they have the correct datapackage for use. Datapackages normally include, item IDs, location IDs, and name groupings, for a given room, and are essential for mapping IDs received from Archipelago to their correct items or locations. @@ -185,7 +181,7 @@ Endpoints to fetch information of the active WebHost room with the supplied room ### `/room_status/` Will provide a dict of room data with the following keys: -- Tracker UUID (`tracker`) +- Tracker SUUID (`tracker`) - A list of players (`players`) - Each item containing a list with the Slot name and Game - Last known hosted port (`last_port`) @@ -255,13 +251,13 @@ User endpoints can get room and seed details from the current session tokens (co Retreives a list of all rooms currently owned by the session token. Each list item will contain a dict with the room's details: -- Room UUID (`room_id`) -- Seed UUID (`seed_id`) +- Room SUUID (`room_id`) +- Seed SUUID (`seed_id`) - Creation timestamp (`creation_time`) - Last activity timestamp (`last_activity`) - Last known AP port (`last_port`) - Room timeout counter in seconds (`timeout`) -- Room tracker UUID (`tracker`) +- Room tracker SUUID (`tracker`) Example: ``` @@ -291,7 +287,7 @@ Example: Retreives a list of all seeds currently owned by the session token. Each item in the list will contain a dict with the seed's details: -- Seed UUID (`seed_id`) +- Seed SUUID (`seed_id`) - Creation timestamp (`creation_time`) - A list of player slots (`players`) - Each item in the list will contain a list of the slot name and game From 604ab79af9f5432ed957775a15677b3dcc6d5d37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Bolduc?= <16137441+Jouramie@users.noreply.github.com> Date: Wed, 16 Jul 2025 11:57:06 -0400 Subject: [PATCH 22/37] Stardew Valley: Add walnutsanity prefix to locations (#4934) --- worlds/stardew_valley/data/locations.csv | 188 ++++++++--------- worlds/stardew_valley/rules.py | 52 ++--- .../stardew_valley/test/TestWalnutsanity.py | 197 ++++++++++-------- 3 files changed, 231 insertions(+), 206 deletions(-) diff --git a/worlds/stardew_valley/data/locations.csv b/worlds/stardew_valley/data/locations.csv index 2829a12522..14554a3bcd 100644 --- a/worlds/stardew_valley/data/locations.csv +++ b/worlds/stardew_valley/data/locations.csv @@ -2316,100 +2316,100 @@ id,region,name,tags,mod_name 4069,Museum,Read Note From Gunther,"BOOKSANITY,BOOKSANITY_LOST", 4070,Museum,Read Goblins by M. Jasper,"BOOKSANITY,BOOKSANITY_LOST", 4071,Museum,Read Secret Statues Acrostics,"BOOKSANITY,BOOKSANITY_LOST", -4101,Clint's Blacksmith,Open Golden Coconut,"WALNUTSANITY,WALNUTSANITY_PUZZLE", -4102,Island West,Fishing Walnut 1,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", -4103,Island West,Fishing Walnut 2,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", -4104,Island North,Fishing Walnut 3,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", -4105,Island North,Fishing Walnut 4,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", -4106,Island Southeast,Fishing Walnut 5,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", -4107,Island East,Jungle Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", -4108,Island East,Banana Altar,"WALNUTSANITY,WALNUTSANITY_PUZZLE", -4109,Leo's Hut,Leo's Tree,"WALNUTSANITY,WALNUTSANITY_PUZZLE", -4110,Island Shrine,Gem Birds Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", -4111,Island Shrine,Gem Birds Shrine,"WALNUTSANITY,WALNUTSANITY_PUZZLE", -4112,Island West,Harvesting Walnut 1,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", -4113,Island West,Harvesting Walnut 2,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", -4114,Island West,Harvesting Walnut 3,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", -4115,Island West,Harvesting Walnut 4,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", -4116,Island West,Harvesting Walnut 5,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", -4117,Gourmand Frog Cave,Gourmand Frog Melon,"WALNUTSANITY,WALNUTSANITY_PUZZLE", -4118,Gourmand Frog Cave,Gourmand Frog Wheat,"WALNUTSANITY,WALNUTSANITY_PUZZLE", -4119,Gourmand Frog Cave,Gourmand Frog Garlic,"WALNUTSANITY,WALNUTSANITY_PUZZLE", -4120,Island West,Journal Scrap #6,"WALNUTSANITY,WALNUTSANITY_DIG", -4121,Island West,Mussel Node Walnut 1,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", -4122,Island West,Mussel Node Walnut 2,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", -4123,Island West,Mussel Node Walnut 3,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", -4124,Island West,Mussel Node Walnut 4,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", -4125,Island West,Mussel Node Walnut 5,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", -4126,Shipwreck,Shipwreck Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", -4127,Island West,Whack A Mole,"WALNUTSANITY,WALNUTSANITY_PUZZLE", -4128,Island West,Starfish Triangle,"WALNUTSANITY,WALNUTSANITY_DIG", -4129,Island West,Starfish Diamond,"WALNUTSANITY,WALNUTSANITY_DIG", -4130,Island West,X in the sand,"WALNUTSANITY,WALNUTSANITY_DIG", -4131,Island West,Diamond Of Indents,"WALNUTSANITY,WALNUTSANITY_DIG", -4132,Island West,Bush Behind Coconut Tree,"WALNUTSANITY,WALNUTSANITY_BUSH", -4133,Island West,Journal Scrap #4,"WALNUTSANITY,WALNUTSANITY_DIG", -4134,Island West,Walnut Room Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", -4135,Island West,Coast Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", -4136,Island West,Tiger Slime Walnut,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", -4137,Island West,Bush Behind Mahogany Tree,"WALNUTSANITY,WALNUTSANITY_BUSH", -4138,Island West,Circle Of Grass,"WALNUTSANITY,WALNUTSANITY_DIG", -4139,Island West,Below Colored Crystals Cave Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", -4140,Colored Crystals Cave,Colored Crystals,"WALNUTSANITY,WALNUTSANITY_PUZZLE", -4141,Island West,Cliff Edge Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", -4142,Island West,Diamond Of Pebbles,"WALNUTSANITY,WALNUTSANITY_DIG", -4143,Island West,Farm Parrot Express Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", -4144,Island West,Farmhouse Cliff Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", -4145,Island North,Big Circle Of Stones,"WALNUTSANITY,WALNUTSANITY_DIG", -4146,Island North,Grove Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", -4147,Island North,Diamond Of Grass,"WALNUTSANITY,WALNUTSANITY_DIG", -4148,Island North,Small Circle Of Stones,"WALNUTSANITY,WALNUTSANITY_DIG", -4149,Island North,Patch Of Sand,"WALNUTSANITY,WALNUTSANITY_DIG", -4150,Dig Site,Crooked Circle Of Stones,"WALNUTSANITY,WALNUTSANITY_DIG", -4151,Dig Site,Above Dig Site Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", -4152,Dig Site,Above Field Office Bush 1,"WALNUTSANITY,WALNUTSANITY_BUSH", -4153,Dig Site,Above Field Office Bush 2,"WALNUTSANITY,WALNUTSANITY_BUSH", -4154,Field Office,Complete Large Animal Collection,"WALNUTSANITY,WALNUTSANITY_PUZZLE", -4155,Field Office,Complete Snake Collection,"WALNUTSANITY,WALNUTSANITY_PUZZLE", -4156,Field Office,Complete Mummified Frog Collection,"WALNUTSANITY,WALNUTSANITY_PUZZLE", -4157,Field Office,Complete Mummified Bat Collection,"WALNUTSANITY,WALNUTSANITY_PUZZLE", -4158,Field Office,Purple Flowers Island Survey,"WALNUTSANITY,WALNUTSANITY_PUZZLE", -4159,Field Office,Purple Starfish Island Survey,"WALNUTSANITY,WALNUTSANITY_PUZZLE", -4160,Island North,Bush Behind Volcano Tree,"WALNUTSANITY,WALNUTSANITY_BUSH", -4161,Island North,Arc Of Stones,"WALNUTSANITY,WALNUTSANITY_DIG", -4162,Island North,Protruding Tree Walnut,"WALNUTSANITY,WALNUTSANITY_PUZZLE", -4163,Island North,Journal Scrap #10,"WALNUTSANITY,WALNUTSANITY_DIG", -4164,Island North,Northmost Point Circle Of Stones,"WALNUTSANITY,WALNUTSANITY_DIG", -4165,Island North,Hidden Passage Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", -4166,Volcano Secret Beach,Secret Beach Bush 1,"WALNUTSANITY,WALNUTSANITY_BUSH", -4167,Volcano Secret Beach,Secret Beach Bush 2,"WALNUTSANITY,WALNUTSANITY_BUSH", -4168,Volcano - Floor 5,Volcano Rocks Walnut 1,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", -4169,Volcano - Floor 5,Volcano Rocks Walnut 2,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", -4170,Volcano - Floor 10,Volcano Rocks Walnut 3,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", -4171,Volcano - Floor 10,Volcano Rocks Walnut 4,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", -4172,Volcano - Floor 10,Volcano Rocks Walnut 5,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", -4173,Volcano - Floor 5,Volcano Monsters Walnut 1,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", -4174,Volcano - Floor 5,Volcano Monsters Walnut 2,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", -4175,Volcano - Floor 10,Volcano Monsters Walnut 3,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", -4176,Volcano - Floor 10,Volcano Monsters Walnut 4,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", -4177,Volcano - Floor 10,Volcano Monsters Walnut 5,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", -4178,Volcano - Floor 5,Volcano Crates Walnut 1,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", -4179,Volcano - Floor 5,Volcano Crates Walnut 2,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", -4180,Volcano - Floor 10,Volcano Crates Walnut 3,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", -4181,Volcano - Floor 10,Volcano Crates Walnut 4,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", -4182,Volcano - Floor 10,Volcano Crates Walnut 5,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", -4183,Volcano - Floor 5,Volcano Common Chest Walnut,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", -4184,Volcano - Floor 10,Volcano Rare Chest Walnut,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", -4185,Volcano - Floor 10,Forge Entrance Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", -4186,Volcano - Floor 10,Forge Exit Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", -4187,Island North,Cliff Over Island South Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", -4188,Island Southeast,Starfish Tide Pool,"WALNUTSANITY,WALNUTSANITY_PUZZLE", -4189,Island Southeast,Diamond Of Yellow Starfish,"WALNUTSANITY,WALNUTSANITY_DIG", -4190,Island Southeast,Mermaid Song,"WALNUTSANITY,WALNUTSANITY_PUZZLE", -4191,Pirate Cove,Pirate Darts 1,"WALNUTSANITY,WALNUTSANITY_PUZZLE", -4192,Pirate Cove,Pirate Darts 2,"WALNUTSANITY,WALNUTSANITY_PUZZLE", -4193,Pirate Cove,Pirate Darts 3,"WALNUTSANITY,WALNUTSANITY_PUZZLE", -4194,Pirate Cove,Pirate Cove Patch Of Sand,"WALNUTSANITY,WALNUTSANITY_DIG", +4101,Clint's Blacksmith,Walnutsanity: Open Golden Coconut,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4102,Island West,Walnutsanity: Fishing Walnut 1,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4103,Island West,Walnutsanity: Fishing Walnut 2,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4104,Island North,Walnutsanity: Fishing Walnut 3,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4105,Island North,Walnutsanity: Fishing Walnut 4,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4106,Island Southeast,Walnutsanity: Fishing Walnut 5,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4107,Island East,Walnutsanity: Jungle Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", +4108,Island East,Walnutsanity: Banana Altar,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4109,Leo's Hut,Walnutsanity: Leo's Tree,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4110,Island Shrine,Walnutsanity: Gem Birds Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", +4111,Island Shrine,Walnutsanity: Gem Birds Shrine,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4112,Island West,Walnutsanity: Harvesting Walnut 1,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4113,Island West,Walnutsanity: Harvesting Walnut 2,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4114,Island West,Walnutsanity: Harvesting Walnut 3,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4115,Island West,Walnutsanity: Harvesting Walnut 4,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4116,Island West,Walnutsanity: Harvesting Walnut 5,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4117,Gourmand Frog Cave,Walnutsanity: Gourmand Frog Melon,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4118,Gourmand Frog Cave,Walnutsanity: Gourmand Frog Wheat,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4119,Gourmand Frog Cave,Walnutsanity: Gourmand Frog Garlic,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4120,Island West,Walnutsanity: Journal Scrap #6,"WALNUTSANITY,WALNUTSANITY_DIG", +4121,Island West,Walnutsanity: Mussel Node Walnut 1,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4122,Island West,Walnutsanity: Mussel Node Walnut 2,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4123,Island West,Walnutsanity: Mussel Node Walnut 3,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4124,Island West,Walnutsanity: Mussel Node Walnut 4,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4125,Island West,Walnutsanity: Mussel Node Walnut 5,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4126,Shipwreck,Walnutsanity: Shipwreck Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", +4127,Island West,Walnutsanity: Whack A Mole,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4128,Island West,Walnutsanity: Starfish Triangle,"WALNUTSANITY,WALNUTSANITY_DIG", +4129,Island West,Walnutsanity: Starfish Diamond,"WALNUTSANITY,WALNUTSANITY_DIG", +4130,Island West,Walnutsanity: X in the sand,"WALNUTSANITY,WALNUTSANITY_DIG", +4131,Island West,Walnutsanity: Diamond Of Indents,"WALNUTSANITY,WALNUTSANITY_DIG", +4132,Island West,Walnutsanity: Bush Behind Coconut Tree,"WALNUTSANITY,WALNUTSANITY_BUSH", +4133,Island West,Walnutsanity: Journal Scrap #4,"WALNUTSANITY,WALNUTSANITY_DIG", +4134,Island West,Walnutsanity: Walnut Room Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", +4135,Island West,Walnutsanity: Coast Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", +4136,Island West,Walnutsanity: Tiger Slime Walnut,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4137,Island West,Walnutsanity: Bush Behind Mahogany Tree,"WALNUTSANITY,WALNUTSANITY_BUSH", +4138,Island West,Walnutsanity: Circle Of Grass,"WALNUTSANITY,WALNUTSANITY_DIG", +4139,Island West,Walnutsanity: Below Colored Crystals Cave Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", +4140,Colored Crystals Cave,Walnutsanity: Colored Crystals,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4141,Island West,Walnutsanity: Cliff Edge Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", +4142,Island West,Walnutsanity: Diamond Of Pebbles,"WALNUTSANITY,WALNUTSANITY_DIG", +4143,Island West,Walnutsanity: Farm Parrot Express Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", +4144,Island West,Walnutsanity: Farmhouse Cliff Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", +4145,Island North,Walnutsanity: Big Circle Of Stones,"WALNUTSANITY,WALNUTSANITY_DIG", +4146,Island North,Walnutsanity: Grove Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", +4147,Island North,Walnutsanity: Diamond Of Grass,"WALNUTSANITY,WALNUTSANITY_DIG", +4148,Island North,Walnutsanity: Small Circle Of Stones,"WALNUTSANITY,WALNUTSANITY_DIG", +4149,Island North,Walnutsanity: Patch Of Sand,"WALNUTSANITY,WALNUTSANITY_DIG", +4150,Dig Site,Walnutsanity: Crooked Circle Of Stones,"WALNUTSANITY,WALNUTSANITY_DIG", +4151,Dig Site,Walnutsanity: Above Dig Site Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", +4152,Dig Site,Walnutsanity: Above Field Office Bush 1,"WALNUTSANITY,WALNUTSANITY_BUSH", +4153,Dig Site,Walnutsanity: Above Field Office Bush 2,"WALNUTSANITY,WALNUTSANITY_BUSH", +4154,Field Office,Walnutsanity: Complete Large Animal Collection,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4155,Field Office,Walnutsanity: Complete Snake Collection,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4156,Field Office,Walnutsanity: Complete Mummified Frog Collection,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4157,Field Office,Walnutsanity: Complete Mummified Bat Collection,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4158,Field Office,Walnutsanity: Purple Flowers Island Survey,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4159,Field Office,Walnutsanity: Purple Starfish Island Survey,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4160,Island North,Walnutsanity: Bush Behind Volcano Tree,"WALNUTSANITY,WALNUTSANITY_BUSH", +4161,Island North,Walnutsanity: Arc Of Stones,"WALNUTSANITY,WALNUTSANITY_DIG", +4162,Island North,Walnutsanity: Protruding Tree Walnut,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4163,Island North,Walnutsanity: Journal Scrap #10,"WALNUTSANITY,WALNUTSANITY_DIG", +4164,Island North,Walnutsanity: Northmost Point Circle Of Stones,"WALNUTSANITY,WALNUTSANITY_DIG", +4165,Island North,Walnutsanity: Hidden Passage Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", +4166,Volcano Secret Beach,Walnutsanity: Secret Beach Bush 1,"WALNUTSANITY,WALNUTSANITY_BUSH", +4167,Volcano Secret Beach,Walnutsanity: Secret Beach Bush 2,"WALNUTSANITY,WALNUTSANITY_BUSH", +4168,Volcano - Floor 5,Walnutsanity: Volcano Rocks Walnut 1,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4169,Volcano - Floor 5,Walnutsanity: Volcano Rocks Walnut 2,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4170,Volcano - Floor 10,Walnutsanity: Volcano Rocks Walnut 3,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4171,Volcano - Floor 10,Walnutsanity: Volcano Rocks Walnut 4,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4172,Volcano - Floor 10,Walnutsanity: Volcano Rocks Walnut 5,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4173,Volcano - Floor 5,Walnutsanity: Volcano Monsters Walnut 1,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4174,Volcano - Floor 5,Walnutsanity: Volcano Monsters Walnut 2,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4175,Volcano - Floor 10,Walnutsanity: Volcano Monsters Walnut 3,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4176,Volcano - Floor 10,Walnutsanity: Volcano Monsters Walnut 4,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4177,Volcano - Floor 10,Walnutsanity: Volcano Monsters Walnut 5,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4178,Volcano - Floor 5,Walnutsanity: Volcano Crates Walnut 1,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4179,Volcano - Floor 5,Walnutsanity: Volcano Crates Walnut 2,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4180,Volcano - Floor 10,Walnutsanity: Volcano Crates Walnut 3,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4181,Volcano - Floor 10,Walnutsanity: Volcano Crates Walnut 4,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4182,Volcano - Floor 10,Walnutsanity: Volcano Crates Walnut 5,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4183,Volcano - Floor 5,Walnutsanity: Volcano Common Chest Walnut,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4184,Volcano - Floor 10,Walnutsanity: Volcano Rare Chest Walnut,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4185,Volcano - Floor 10,Walnutsanity: Forge Entrance Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", +4186,Volcano - Floor 10,Walnutsanity: Forge Exit Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", +4187,Island North,Walnutsanity: Cliff Over Island South Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", +4188,Island Southeast,Walnutsanity: Starfish Tide Pool,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4189,Island Southeast,Walnutsanity: Diamond Of Yellow Starfish,"WALNUTSANITY,WALNUTSANITY_DIG", +4190,Island Southeast,Walnutsanity: Mermaid Song,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4191,Pirate Cove,Walnutsanity: Pirate Darts 1,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4192,Pirate Cove,Walnutsanity: Pirate Darts 2,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4193,Pirate Cove,Walnutsanity: Pirate Darts 3,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4194,Pirate Cove,Walnutsanity: Pirate Cove Patch Of Sand,"WALNUTSANITY,WALNUTSANITY_DIG", 5001,Stardew Valley,Level 1 Luck,"LUCK_LEVEL,SKILL_LEVEL",Luck Skill 5002,Stardew Valley,Level 2 Luck,"LUCK_LEVEL,SKILL_LEVEL",Luck Skill 5003,Stardew Valley,Level 3 Luck,"LUCK_LEVEL,SKILL_LEVEL",Luck Skill diff --git a/worlds/stardew_valley/rules.py b/worlds/stardew_valley/rules.py index 350da064a1..2b7eec9960 100644 --- a/worlds/stardew_valley/rules.py +++ b/worlds/stardew_valley/rules.py @@ -443,27 +443,27 @@ def set_walnut_puzzle_rules(logic: StardewLogic, multiworld, player, world_optio if WalnutsanityOptionName.puzzles not in world_options.walnutsanity: return - set_rule(multiworld.get_location("Open Golden Coconut", player), logic.has(Geode.golden_coconut)) - set_rule(multiworld.get_location("Banana Altar", player), logic.has(Fruit.banana)) - set_rule(multiworld.get_location("Leo's Tree", player), logic.tool.has_tool(Tool.axe)) - set_rule(multiworld.get_location("Gem Birds Shrine", player), logic.has(Mineral.amethyst) & logic.has(Mineral.aquamarine) & + set_rule(multiworld.get_location("Walnutsanity: Open Golden Coconut", player), logic.has(Geode.golden_coconut)) + set_rule(multiworld.get_location("Walnutsanity: Banana Altar", player), logic.has(Fruit.banana)) + set_rule(multiworld.get_location("Walnutsanity: Leo's Tree", player), logic.tool.has_tool(Tool.axe)) + set_rule(multiworld.get_location("Walnutsanity: Gem Birds Shrine", player), logic.has(Mineral.amethyst) & logic.has(Mineral.aquamarine) & logic.has(Mineral.emerald) & logic.has(Mineral.ruby) & logic.has(Mineral.topaz) & logic.region.can_reach_all((Region.island_north, Region.island_west, Region.island_east, Region.island_south))) - set_rule(multiworld.get_location("Gourmand Frog Melon", player), logic.has(Fruit.melon) & logic.region.can_reach(Region.island_west)) - set_rule(multiworld.get_location("Gourmand Frog Wheat", player), logic.has(Vegetable.wheat) & - logic.region.can_reach(Region.island_west) & logic.region.can_reach_location("Gourmand Frog Melon")) - set_rule(multiworld.get_location("Gourmand Frog Garlic", player), logic.has(Vegetable.garlic) & - logic.region.can_reach(Region.island_west) & logic.region.can_reach_location("Gourmand Frog Wheat")) - set_rule(multiworld.get_location("Whack A Mole", player), logic.tool.has_tool(Tool.watering_can, ToolMaterial.iridium)) - set_rule(multiworld.get_location("Complete Large Animal Collection", player), logic.walnut.can_complete_large_animal_collection()) - set_rule(multiworld.get_location("Complete Snake Collection", player), logic.walnut.can_complete_snake_collection()) - set_rule(multiworld.get_location("Complete Mummified Frog Collection", player), logic.walnut.can_complete_frog_collection()) - set_rule(multiworld.get_location("Complete Mummified Bat Collection", player), logic.walnut.can_complete_bat_collection()) - set_rule(multiworld.get_location("Purple Flowers Island Survey", player), logic.walnut.can_start_field_office) - set_rule(multiworld.get_location("Purple Starfish Island Survey", player), logic.walnut.can_start_field_office) - set_rule(multiworld.get_location("Protruding Tree Walnut", player), logic.combat.has_slingshot) - set_rule(multiworld.get_location("Starfish Tide Pool", player), logic.tool.has_fishing_rod(1)) - set_rule(multiworld.get_location("Mermaid Song", player), logic.has(Furniture.flute_block)) + set_rule(multiworld.get_location("Walnutsanity: Gourmand Frog Melon", player), logic.has(Fruit.melon) & logic.region.can_reach(Region.island_west)) + set_rule(multiworld.get_location("Walnutsanity: Gourmand Frog Wheat", player), logic.has(Vegetable.wheat) & + logic.region.can_reach(Region.island_west) & logic.region.can_reach_location("Walnutsanity: Gourmand Frog Melon")) + set_rule(multiworld.get_location("Walnutsanity: Gourmand Frog Garlic", player), logic.has(Vegetable.garlic) & + logic.region.can_reach(Region.island_west) & logic.region.can_reach_location("Walnutsanity: Gourmand Frog Wheat")) + set_rule(multiworld.get_location("Walnutsanity: Whack A Mole", player), logic.tool.has_tool(Tool.watering_can, ToolMaterial.iridium)) + set_rule(multiworld.get_location("Walnutsanity: Complete Large Animal Collection", player), logic.walnut.can_complete_large_animal_collection()) + set_rule(multiworld.get_location("Walnutsanity: Complete Snake Collection", player), logic.walnut.can_complete_snake_collection()) + set_rule(multiworld.get_location("Walnutsanity: Complete Mummified Frog Collection", player), logic.walnut.can_complete_frog_collection()) + set_rule(multiworld.get_location("Walnutsanity: Complete Mummified Bat Collection", player), logic.walnut.can_complete_bat_collection()) + set_rule(multiworld.get_location("Walnutsanity: Purple Flowers Island Survey", player), logic.walnut.can_start_field_office) + set_rule(multiworld.get_location("Walnutsanity: Purple Starfish Island Survey", player), logic.walnut.can_start_field_office) + set_rule(multiworld.get_location("Walnutsanity: Protruding Tree Walnut", player), logic.combat.has_slingshot) + set_rule(multiworld.get_location("Walnutsanity: Starfish Tide Pool", player), logic.tool.has_fishing_rod(1)) + set_rule(multiworld.get_location("Walnutsanity: Mermaid Song", player), logic.has(Furniture.flute_block)) def set_walnut_bushes_rules(logic, multiworld, player, world_options): @@ -490,13 +490,13 @@ def set_walnut_repeatable_rules(logic, multiworld, player, world_options): if WalnutsanityOptionName.repeatables not in world_options.walnutsanity: return for i in range(1, 6): - set_rule(multiworld.get_location(f"Fishing Walnut {i}", player), logic.tool.has_fishing_rod(1)) - set_rule(multiworld.get_location(f"Harvesting Walnut {i}", player), logic.skill.can_get_farming_xp) - set_rule(multiworld.get_location(f"Mussel Node Walnut {i}", player), logic.tool.has_tool(Tool.pickaxe)) - set_rule(multiworld.get_location(f"Volcano Rocks Walnut {i}", player), logic.tool.has_tool(Tool.pickaxe)) - set_rule(multiworld.get_location(f"Volcano Monsters Walnut {i}", player), logic.combat.has_galaxy_weapon) - set_rule(multiworld.get_location(f"Volcano Crates Walnut {i}", player), logic.combat.has_any_weapon) - set_rule(multiworld.get_location(f"Tiger Slime Walnut", player), logic.monster.can_kill(Monster.tiger_slime)) + set_rule(multiworld.get_location(f"Walnutsanity: Fishing Walnut {i}", player), logic.tool.has_fishing_rod(1)) + set_rule(multiworld.get_location(f"Walnutsanity: Harvesting Walnut {i}", player), logic.skill.can_get_farming_xp) + set_rule(multiworld.get_location(f"Walnutsanity: Mussel Node Walnut {i}", player), logic.tool.has_tool(Tool.pickaxe)) + set_rule(multiworld.get_location(f"Walnutsanity: Volcano Rocks Walnut {i}", player), logic.tool.has_tool(Tool.pickaxe)) + set_rule(multiworld.get_location(f"Walnutsanity: Volcano Monsters Walnut {i}", player), logic.combat.has_galaxy_weapon) + set_rule(multiworld.get_location(f"Walnutsanity: Volcano Crates Walnut {i}", player), logic.combat.has_any_weapon) + set_rule(multiworld.get_location(f"Walnutsanity: Tiger Slime Walnut", player), logic.monster.can_kill(Monster.tiger_slime)) def set_cropsanity_rules(logic: StardewLogic, multiworld, player, world_content: StardewContent): diff --git a/worlds/stardew_valley/test/TestWalnutsanity.py b/worlds/stardew_valley/test/TestWalnutsanity.py index e3411edd02..418eaa87c7 100644 --- a/worlds/stardew_valley/test/TestWalnutsanity.py +++ b/worlds/stardew_valley/test/TestWalnutsanity.py @@ -1,26 +1,46 @@ +import unittest + from .bases import SVTestBase from ..options import ExcludeGingerIsland, Walnutsanity, ToolProgression, SkillProgression from ..strings.ap_names.ap_option_names import WalnutsanityOptionName -class TestWalnutsanityNone(SVTestBase): +class SVWalnutsanityTestBase(SVTestBase): + expected_walnut_locations: set[str] = set() + unexpected_walnut_locations: set[str] = set() + + @classmethod + def setUpClass(cls) -> None: + if cls is SVWalnutsanityTestBase: + raise unittest.SkipTest("Base tests disabled") + + super().setUpClass() + + def test_walnut_locations(self): + location_names = {location.name for location in self.multiworld.get_locations()} + for location in self.expected_walnut_locations: + self.assertIn(location, location_names, f"{location} should be in the location names") + for location in self.unexpected_walnut_locations: + self.assertNotIn(location, location_names, f"{location} should not be in the location names") + + +class TestWalnutsanityNone(SVWalnutsanityTestBase): options = { ExcludeGingerIsland: ExcludeGingerIsland.option_false, Walnutsanity: Walnutsanity.preset_none, SkillProgression: ToolProgression.option_progressive, ToolProgression: ToolProgression.option_progressive, } - - def test_no_walnut_locations(self): - location_names = {location.name for location in self.multiworld.get_locations()} - self.assertNotIn("Open Golden Coconut", location_names) - self.assertNotIn("Fishing Walnut 4", location_names) - self.assertNotIn("Journal Scrap #6", location_names) - self.assertNotIn("Starfish Triangle", location_names) - self.assertNotIn("Bush Behind Coconut Tree", location_names) - self.assertNotIn("Purple Starfish Island Survey", location_names) - self.assertNotIn("Volcano Monsters Walnut 3", location_names) - self.assertNotIn("Cliff Over Island South Bush", location_names) + unexpected_walnut_locations = { + "Walnutsanity: Open Golden Coconut", + "Walnutsanity: Fishing Walnut 4", + "Walnutsanity: Journal Scrap #6", + "Walnutsanity: Starfish Triangle", + "Walnutsanity: Bush Behind Coconut Tree", + "Walnutsanity: Purple Starfish Island Survey", + "Walnutsanity: Volcano Monsters Walnut 3", + "Walnutsanity: Cliff Over Island South Bush", + } def test_logic_received_walnuts(self): # You need to receive 0, and collect 40 @@ -48,28 +68,30 @@ class TestWalnutsanityNone(SVTestBase): self.assertTrue(self.multiworld.state.can_reach_location("Parrot Express", self.player)) -class TestWalnutsanityPuzzles(SVTestBase): +class TestWalnutsanityPuzzles(SVWalnutsanityTestBase): options = { ExcludeGingerIsland: ExcludeGingerIsland.option_false, Walnutsanity: frozenset({WalnutsanityOptionName.puzzles}), SkillProgression: ToolProgression.option_progressive, ToolProgression: ToolProgression.option_progressive, } - - def test_only_puzzle_walnut_locations(self): - location_names = {location.name for location in self.multiworld.get_locations()} - self.assertIn("Open Golden Coconut", location_names) - self.assertNotIn("Fishing Walnut 4", location_names) - self.assertNotIn("Journal Scrap #6", location_names) - self.assertNotIn("Starfish Triangle", location_names) - self.assertNotIn("Bush Behind Coconut Tree", location_names) - self.assertIn("Purple Starfish Island Survey", location_names) - self.assertNotIn("Volcano Monsters Walnut 3", location_names) - self.assertNotIn("Cliff Over Island South Bush", location_names) + expected_walnut_locations = { + "Walnutsanity: Open Golden Coconut", + "Walnutsanity: Purple Starfish Island Survey", + } + unexpected_walnut_locations = { + "Walnutsanity: Fishing Walnut 4", + "Walnutsanity: Journal Scrap #6", + "Walnutsanity: Starfish Triangle", + "Walnutsanity: Bush Behind Coconut Tree", + "Walnutsanity: Volcano Monsters Walnut 3", + "Walnutsanity: Cliff Over Island South Bush", + } def test_field_office_locations_require_professor_snail(self): - location_names = ["Complete Large Animal Collection", "Complete Snake Collection", "Complete Mummified Frog Collection", - "Complete Mummified Bat Collection", "Purple Flowers Island Survey", "Purple Starfish Island Survey", ] + location_names = ["Walnutsanity: Complete Large Animal Collection", "Walnutsanity: Complete Snake Collection", + "Walnutsanity: Complete Mummified Frog Collection", "Walnutsanity: Complete Mummified Bat Collection", + "Walnutsanity: Purple Flowers Island Survey", "Walnutsanity: Purple Starfish Island Survey", ] self.collect("Island Obelisk") self.collect("Island North Turtle") self.collect("Island West Turtle") @@ -90,40 +112,42 @@ class TestWalnutsanityPuzzles(SVTestBase): self.assert_can_reach_location(location) -class TestWalnutsanityBushes(SVTestBase): +class TestWalnutsanityBushes(SVWalnutsanityTestBase): options = { ExcludeGingerIsland: ExcludeGingerIsland.option_false, Walnutsanity: frozenset({WalnutsanityOptionName.bushes}), } - - def test_only_bush_walnut_locations(self): - location_names = {location.name for location in self.multiworld.get_locations()} - self.assertNotIn("Open Golden Coconut", location_names) - self.assertNotIn("Fishing Walnut 4", location_names) - self.assertNotIn("Journal Scrap #6", location_names) - self.assertNotIn("Starfish Triangle", location_names) - self.assertIn("Bush Behind Coconut Tree", location_names) - self.assertNotIn("Purple Starfish Island Survey", location_names) - self.assertNotIn("Volcano Monsters Walnut 3", location_names) - self.assertIn("Cliff Over Island South Bush", location_names) + expected_walnut_locations = { + "Walnutsanity: Bush Behind Coconut Tree", + "Walnutsanity: Cliff Over Island South Bush", + } + unexpected_walnut_locations = { + "Walnutsanity: Open Golden Coconut", + "Walnutsanity: Fishing Walnut 4", + "Walnutsanity: Journal Scrap #6", + "Walnutsanity: Starfish Triangle", + "Walnutsanity: Purple Starfish Island Survey", + "Walnutsanity: Volcano Monsters Walnut 3", + } -class TestWalnutsanityPuzzlesAndBushes(SVTestBase): +class TestWalnutsanityPuzzlesAndBushes(SVWalnutsanityTestBase): options = { ExcludeGingerIsland: ExcludeGingerIsland.option_false, Walnutsanity: frozenset({WalnutsanityOptionName.puzzles, WalnutsanityOptionName.bushes}), } - - def test_only_bush_walnut_locations(self): - location_names = {location.name for location in self.multiworld.get_locations()} - self.assertIn("Open Golden Coconut", location_names) - self.assertNotIn("Fishing Walnut 4", location_names) - self.assertNotIn("Journal Scrap #6", location_names) - self.assertNotIn("Starfish Triangle", location_names) - self.assertIn("Bush Behind Coconut Tree", location_names) - self.assertIn("Purple Starfish Island Survey", location_names) - self.assertNotIn("Volcano Monsters Walnut 3", location_names) - self.assertIn("Cliff Over Island South Bush", location_names) + expected_walnut_locations = { + "Walnutsanity: Open Golden Coconut", + "Walnutsanity: Bush Behind Coconut Tree", + "Walnutsanity: Purple Starfish Island Survey", + "Walnutsanity: Cliff Over Island South Bush", + } + unexpected_walnut_locations = { + "Walnutsanity: Fishing Walnut 4", + "Walnutsanity: Journal Scrap #6", + "Walnutsanity: Starfish Triangle", + "Walnutsanity: Volcano Monsters Walnut 3", + } def test_logic_received_walnuts(self): # You need to receive 25, and collect 15 @@ -136,58 +160,59 @@ class TestWalnutsanityPuzzlesAndBushes(SVTestBase): self.assertTrue(self.multiworld.state.can_reach_location("Parrot Express", self.player)) -class TestWalnutsanityDigSpots(SVTestBase): +class TestWalnutsanityDigSpots(SVWalnutsanityTestBase): options = { ExcludeGingerIsland: ExcludeGingerIsland.option_false, Walnutsanity: frozenset({WalnutsanityOptionName.dig_spots}), } - - def test_only_dig_spots_walnut_locations(self): - location_names = {location.name for location in self.multiworld.get_locations()} - self.assertNotIn("Open Golden Coconut", location_names) - self.assertNotIn("Fishing Walnut 4", location_names) - self.assertIn("Journal Scrap #6", location_names) - self.assertIn("Starfish Triangle", location_names) - self.assertNotIn("Bush Behind Coconut Tree", location_names) - self.assertNotIn("Purple Starfish Island Survey", location_names) - self.assertNotIn("Volcano Monsters Walnut 3", location_names) - self.assertNotIn("Cliff Over Island South Bush", location_names) + expected_walnut_locations = { + "Walnutsanity: Journal Scrap #6", + "Walnutsanity: Starfish Triangle", + } + unexpected_walnut_locations = { + "Walnutsanity: Open Golden Coconut", + "Walnutsanity: Fishing Walnut 4", + "Walnutsanity: Bush Behind Coconut Tree", + "Walnutsanity: Purple Starfish Island Survey", + "Walnutsanity: Volcano Monsters Walnut 3", + "Walnutsanity: Cliff Over Island South Bush", + } -class TestWalnutsanityRepeatables(SVTestBase): +class TestWalnutsanityRepeatables(SVWalnutsanityTestBase): options = { ExcludeGingerIsland: ExcludeGingerIsland.option_false, Walnutsanity: frozenset({WalnutsanityOptionName.repeatables}), } - - def test_only_repeatable_walnut_locations(self): - location_names = {location.name for location in self.multiworld.get_locations()} - self.assertNotIn("Open Golden Coconut", location_names) - self.assertIn("Fishing Walnut 4", location_names) - self.assertNotIn("Journal Scrap #6", location_names) - self.assertNotIn("Starfish Triangle", location_names) - self.assertNotIn("Bush Behind Coconut Tree", location_names) - self.assertNotIn("Purple Starfish Island Survey", location_names) - self.assertIn("Volcano Monsters Walnut 3", location_names) - self.assertNotIn("Cliff Over Island South Bush", location_names) + expected_walnut_locations = { + "Walnutsanity: Fishing Walnut 4", + "Walnutsanity: Volcano Monsters Walnut 3", + } + unexpected_walnut_locations = { + "Walnutsanity: Open Golden Coconut", + "Walnutsanity: Journal Scrap #6", + "Walnutsanity: Starfish Triangle", + "Walnutsanity: Bush Behind Coconut Tree", + "Walnutsanity: Purple Starfish Island Survey", + "Walnutsanity: Cliff Over Island South Bush", + } -class TestWalnutsanityAll(SVTestBase): +class TestWalnutsanityAll(SVWalnutsanityTestBase): options = { ExcludeGingerIsland: ExcludeGingerIsland.option_false, Walnutsanity: Walnutsanity.preset_all, } - - def test_all_walnut_locations(self): - location_names = {location.name for location in self.multiworld.get_locations()} - self.assertIn("Open Golden Coconut", location_names) - self.assertIn("Fishing Walnut 4", location_names) - self.assertIn("Journal Scrap #6", location_names) - self.assertIn("Starfish Triangle", location_names) - self.assertIn("Bush Behind Coconut Tree", location_names) - self.assertIn("Purple Starfish Island Survey", location_names) - self.assertIn("Volcano Monsters Walnut 3", location_names) - self.assertIn("Cliff Over Island South Bush", location_names) + expected_walnut_locations = { + "Walnutsanity: Open Golden Coconut", + "Walnutsanity: Fishing Walnut 4", + "Walnutsanity: Journal Scrap #6", + "Walnutsanity: Starfish Triangle", + "Walnutsanity: Bush Behind Coconut Tree", + "Walnutsanity: Purple Starfish Island Survey", + "Walnutsanity: Volcano Monsters Walnut 3", + "Walnutsanity: Cliff Over Island South Bush", + } def test_logic_received_walnuts(self): # You need to receive 40, and collect 4 From 608a38f873eaa9d7cc4ca454774c771bb816d660 Mon Sep 17 00:00:00 2001 From: CookieCat <81494827+CookieCat45@users.noreply.github.com> Date: Wed, 16 Jul 2025 12:19:35 -0400 Subject: [PATCH 23/37] AHIT: Fix Test Fail for assert_not_all_options (#5197) --- worlds/ahit/__init__.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/worlds/ahit/__init__.py b/worlds/ahit/__init__.py index 1bcc840ae6..d258f8050d 100644 --- a/worlds/ahit/__init__.py +++ b/worlds/ahit/__init__.py @@ -260,11 +260,7 @@ class HatInTimeWorld(World): f"{item_name} ({self.multiworld.get_player_name(loc.item.player)})") slot_data["ShopItemNames"] = shop_item_names - - for name, value in self.options.as_dict(*self.options_dataclass.type_hints).items(): - if name in slot_data_options: - slot_data[name] = value - + slot_data.update(self.options.as_dict(*slot_data_options)) return slot_data def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]): From 1923d6b1bcaf35d0ffb26ede6eae1d17b8c09a3d Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Wed, 16 Jul 2025 11:57:11 -0500 Subject: [PATCH 24/37] Options: Assert Not All Option in `Options.as_dict` (#5039) * Options: forbid worlds just dumping every single option they don't need * make the equal proper --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- Options.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Options.py b/Options.py index b910d21665..e87280ca14 100644 --- a/Options.py +++ b/Options.py @@ -1315,6 +1315,7 @@ class CommonOptions(metaclass=OptionsMetaProperty): will be returned as a sorted list. """ assert option_names, "options.as_dict() was used without any option names." + assert len(option_names) < len(self.__class__.type_hints), "Specify only options you need." option_results = {} for option_name in option_names: if option_name not in type(self).type_hints: From e38d04c655258c7c7c70c485cf9a03697d21dbee Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Wed, 16 Jul 2025 23:49:01 -0400 Subject: [PATCH 25/37] Lingo: Fix Painting Gen Failures on Panels Mode Door Shuffle (#5199) Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- worlds/lingo/player_logic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/lingo/player_logic.py b/worlds/lingo/player_logic.py index 9363dfedb6..b76f12a916 100644 --- a/worlds/lingo/player_logic.py +++ b/worlds/lingo/player_logic.py @@ -394,7 +394,7 @@ class LingoPlayerLogic: or painting.room in required_painting_rooms: return False - if world.options.shuffle_doors == ShuffleDoors.option_none: + if world.options.shuffle_doors != ShuffleDoors.option_doors: if painting.req_blocked_when_no_doors: return False From ffab3a43fc17f4d06d241179ae9e2e26fdea47df Mon Sep 17 00:00:00 2001 From: Adrian Priestley <47989725+a-priestley@users.noreply.github.com> Date: Thu, 17 Jul 2025 05:30:57 -0230 Subject: [PATCH 26/37] Docker: Add initial configuration for project (#4419) * feat(docker): Add initial Docker configuration for project - Add .dockerignore file to ignore unnecessary files - Create Dockerfile with basic build and deployment configuration * feat(docker): Updated Docker configuration for improved security and build efficiency - Removed sensitive files from .dockerignore - Moved WORKDIR to /app in Dockerfile - Added gunicorn==23.0.0 dependency in RUN command - Created new docker-compose.yml file for service definition * feat(deployment): Implement containerized deployment configuration - Add additional environment variables for Python optimization - Update Dockerfile with new dependencies: eventlet, gevent, tornado - Create docker-compose.yml and configure services for web and nginx - Implement example configurations for web host settings and gunicorn - Establish nginx configuration for reverse proxy - Remove outdated docker-compose.yml from root directory * feat(deploy): Introduce Docker Compose configuration for multi-world deployment - Separate web service into two containers, one for main process and one for gunicorn - Update container configurations for improved security and maintainability - Remove unused volumes and network configurations * docs: Add new documentation for deploying Archipelago using containers - Document standalone image build and run process - Include example Docker Compose file for container orchestration - Provide information on services defined in the `docker-compose.yaml` file - Mention optional Enemizer feature and Git requirements * fixup! feat(docker): Updated Docker configuration for improved security and build efficiency - Removed sensitive files from .dockerignore - Moved WORKDIR to /app in Dockerfile - Added gunicorn==23.0.0 dependency in RUN command - Created new docker-compose.yml file for service definition * feat(deploy): Updated gunicorn configuration example - Adjusted worker and thread counts - Switched worker class from sync to gthread - Changed log level to info - Added example code snippet for customizing worker count * fix(deploy): Adjust concurrency settings for self-launch configuration - Reduce the number of world generators from 8 to 3 - Decrease the number of hosters from 5 to 4 * docs(deploy using containers): Improve readability, fix broken links - Update links to other documentation pages - Improve formatting for better readability - Remove unnecessary sections and files - Add note about building the image requiring a local copy of ArchipelagoMW source code * Update deploy/example_config.yaml Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * Update deploy/example_selflaunch.yaml Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * Update Dockerfile Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * Update deploy/example_selflaunch.yaml Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * fixup! Update Dockerfile * fix(Dockerfile): Update package installations to use latest versions - Remove specific version pins for git and libc6-dev - Ensure compatibility with newer package updates * feat(ci): Add GitHub Actions workflow for building and publishing Docker images - Create a new workflow for Docker image build and publish - Configure triggers for push and pull_request on main branch - Set up QEMU and Docker Buildx for multi-platform builds - Implement Docker login for GitHub Container Registry - Include Docker image metadata extraction and tagging * feat(healthcheck): Update Dockerfile and docker-compose for health checks - Add health check for the Webhost service in Dockerfile - Modify docker-compose to include a placeholder health check for multiworld service - Standardize comments and remove unnecessary lines * Revert "feat(ci): Add GitHub Actions workflow for building and publishing Docker images" This reverts commit 32a51b272627d99ca9796cbfda2e821bfdd95c70. * feat(docker): Enhance Dockerfile with Cython build stage - Add Cython builder stage for compiling speedups - Update package installation and organization for efficiency - Improve caching by copying requirements before installing - Add documentation for rootless Podman * fixup! feat(docker): Enhance Dockerfile with Cython build stage - Add Cython builder stage for compiling speedups - Update package installation and organization for efficiency - Improve caching by copying requirements before installing - Add documentation for rootless Podman --------- Co-authored-by: Adrian Priestley Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> Co-authored-by: Adrian Priestley --- .dockerignore | 210 ++++++++++++++++++++++++++++++++ Dockerfile | 97 +++++++++++++++ deploy/docker-compose.yml | 61 ++++++++++ deploy/example_config.yaml | 10 ++ deploy/example_gunicorn.conf.py | 19 +++ deploy/example_nginx.conf | 64 ++++++++++ deploy/example_selflaunch.yaml | 13 ++ docs/deploy using containers.md | 91 ++++++++++++++ 8 files changed, 565 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 deploy/docker-compose.yml create mode 100644 deploy/example_config.yaml create mode 100644 deploy/example_gunicorn.conf.py create mode 100644 deploy/example_nginx.conf create mode 100644 deploy/example_selflaunch.yaml create mode 100644 docs/deploy using containers.md diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..982e411032 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,210 @@ +.git +.github +.run +docs +test +typings +*Client.py + +.idea +.vscode + +*_Spoiler.txt +*.bmbp +*.apbp +*.apl2ac +*.apm3 +*.apmc +*.apz5 +*.aptloz +*.apemerald +*.pyc +*.pyd +*.sfc +*.z64 +*.n64 +*.nes +*.smc +*.sms +*.gb +*.gbc +*.gba +*.wixobj +*.lck +*.db3 +*multidata +*multisave +*.archipelago +*.apsave +*.BIN +*.puml + +setups +build +bundle/components.wxs +dist +/prof/ +README.html +.vs/ +EnemizerCLI/ +/Players/ +/SNI/ +/sni-*/ +/appimagetool* +/host.yaml +/options.yaml +/config.yaml +/logs/ +_persistent_storage.yaml +mystery_result_*.yaml +*-errors.txt +success.txt +output/ +Output Logs/ +/factorio/ +/Minecraft Forge Server/ +/WebHostLib/static/generated +/freeze_requirements.txt +/Archipelago.zip +/setup.ini +/installdelete.iss +/data/user.kv +/datapackage +/custom_worlds + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so +*.dll + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt +installer.log + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# vim editor +*.swp + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv* +env/ +venv/ +/venv*/ +ENV/ +env.bak/ +venv.bak/ +*.code-workspace +shell.nix + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# Cython intermediates +_speedups.c +_speedups.cpp +_speedups.html + +# minecraft server stuff +jdk*/ +minecraft*/ +minecraft_versions.json +!worlds/minecraft/ + +# pyenv +.python-version + +#undertale stuff +/Undertale/ + +# OS General Files +.DS_Store +.AppleDouble +.LSOverride +Thumbs.db +[Dd]esktop.ini diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..0ed61c0301 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,97 @@ +# hadolint global ignore=SC1090,SC1091 + +# Source +FROM scratch AS release +WORKDIR /release +ADD https://github.com/Ijwu/Enemizer/releases/latest/download/ubuntu.16.04-x64.zip Enemizer.zip + +# Enemizer +FROM alpine:3.21 AS enemizer +ARG TARGETARCH +WORKDIR /release +COPY --from=release /release/Enemizer.zip . + +# No release for arm architecture. Skip. +RUN if [ "$TARGETARCH" = "amd64" ]; then \ + apk add unzip=6.0-r15 --no-cache && \ + unzip -u Enemizer.zip -d EnemizerCLI && \ + chmod -R 777 EnemizerCLI; \ + else touch EnemizerCLI; fi + +# Cython builder stage +FROM python:3.12 AS cython-builder + +WORKDIR /build + +# Copy and install requirements first (better caching) +COPY requirements.txt WebHostLib/requirements.txt + +RUN pip install --no-cache-dir -r \ + WebHostLib/requirements.txt \ + setuptools + +COPY _speedups.pyx . +COPY intset.h . + +RUN cythonize -b -i _speedups.pyx + +# Archipelago +FROM python:3.12-slim AS archipelago +ARG TARGETARCH +ENV VIRTUAL_ENV=/opt/venv +ENV PYTHONUNBUFFERED=1 +WORKDIR /app + +# Install requirements +# hadolint ignore=DL3008 +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + git \ + gcc=4:12.2.0-3 \ + libc6-dev \ + libtk8.6=8.6.13-2 \ + g++=4:12.2.0-3 \ + curl && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +# Create and activate venv +RUN python -m venv $VIRTUAL_ENV; \ + . $VIRTUAL_ENV/bin/activate + +# Copy and install requirements first (better caching) +COPY WebHostLib/requirements.txt WebHostLib/requirements.txt + +RUN pip install --no-cache-dir -r \ + WebHostLib/requirements.txt \ + gunicorn==23.0.0 + +COPY . . + +COPY --from=cython-builder /build/*.so ./ + +# Run ModuleUpdate +RUN python ModuleUpdate.py -y + +# Purge unneeded packages +RUN apt-get purge -y \ + git \ + gcc \ + libc6-dev \ + g++ && \ + apt-get autoremove -y + +# Copy necessary components +COPY --from=enemizer /release/EnemizerCLI /tmp/EnemizerCLI + +# No release for arm architecture. Skip. +RUN if [ "$TARGETARCH" = "amd64" ]; then \ + cp /tmp/EnemizerCLI EnemizerCLI; \ + fi; \ + rm -rf /tmp/EnemizerCLI + +# Define health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ + CMD curl -f http://localhost:${PORT:-80} || exit 1 + +ENTRYPOINT [ "python", "WebHost.py" ] diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml new file mode 100644 index 0000000000..1472667442 --- /dev/null +++ b/deploy/docker-compose.yml @@ -0,0 +1,61 @@ +services: + multiworld: + # Build only once. Web service uses the same image build + build: + context: .. + # Name image for use in web service + image: archipelago-base + # Use locally-built image + pull_policy: never + # Launch main process without website hosting (config override) + entrypoint: python WebHost.py --config_override selflaunch.yaml + volumes: + # Mount application volume + - app_volume:/app + + # Mount configs + - ./example_config.yaml:/app/config.yaml + - ./example_selflaunch.yaml:/app/selflaunch.yaml + + # Expose on host network for access to dynamically mapped ports + network_mode: host + + # No Healthcheck in place yet for multiworld + healthcheck: + test: ["NONE"] + web: + # Use image build by multiworld service + image: archipelago-base + # Use locally-built image + pull_policy: never + # Launch gunicorn targeting WebHost application + entrypoint: gunicorn -c gunicorn.conf.py + volumes: + # Mount application volume + - app_volume:/app + + # Mount configs + - ./example_config.yaml:/app/config.yaml + - ./example_gunicorn.conf.py:/app/gunicorn.conf.py + environment: + # Bind gunicorn on 8000 + - PORT=8000 + + nginx: + image: nginx:stable-alpine + volumes: + # Mount application volume + - app_volume:/app + + # Mount config + - ./example_nginx.conf:/etc/nginx/nginx.conf + ports: + # Nginx listening internally on port 80 -- mapped to 8080 on host + - 8080:80 + depends_on: + - web + +volumes: + # Share application directory amongst multiworld and web services + # (for access to log files and the like), and nginx (for static files) + app_volume: diff --git a/deploy/example_config.yaml b/deploy/example_config.yaml new file mode 100644 index 0000000000..d74f7f238f --- /dev/null +++ b/deploy/example_config.yaml @@ -0,0 +1,10 @@ +# Refer to ../docs/webhost configuration sample.yaml + +# We'll be hosting VIA gunicorn +SELFHOST: false +# We'll start a separate process for rooms and generators +SELFLAUNCH: false + +# Host Address. This is the address encoded into the patch that will be used for client auto-connect. +# Set as your local IP (192.168.x.x) to serve over LAN. +HOST_ADDRESS: localhost diff --git a/deploy/example_gunicorn.conf.py b/deploy/example_gunicorn.conf.py new file mode 100644 index 0000000000..49f153df67 --- /dev/null +++ b/deploy/example_gunicorn.conf.py @@ -0,0 +1,19 @@ +workers = 2 +threads = 2 +wsgi_app = "WebHost:get_app()" +accesslog = "-" +access_log_format = ( + '%({x-forwarded-for}i)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"' +) +worker_class = "gthread" # "sync" | "gthread" +forwarded_allow_ips = "*" +loglevel = "info" + +""" +You can programatically set values. +For example, set number of workers to half of the cpu count: + +import multiprocessing + +workers = multiprocessing.cpu_count() / 2 +""" diff --git a/deploy/example_nginx.conf b/deploy/example_nginx.conf new file mode 100644 index 0000000000..b0c0e8e5a0 --- /dev/null +++ b/deploy/example_nginx.conf @@ -0,0 +1,64 @@ +worker_processes 1; + +user nobody nogroup; +# 'user nobody nobody;' for systems with 'nobody' as a group instead +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; # increase if you have lots of clients + accept_mutex off; # set to 'on' if nginx worker_processes > 1 + # 'use epoll;' to enable for Linux 2.6+ + # 'use kqueue;' to enable for FreeBSD, OSX + use epoll; +} + +http { + include mime.types; + # fallback in case we can't determine a type + default_type application/octet-stream; + access_log /var/log/nginx/access.log combined; + sendfile on; + + upstream app_server { + # fail_timeout=0 means we always retry an upstream even if it failed + # to return a good HTTP response + + # for UNIX domain socket setups + # server unix:/tmp/gunicorn.sock fail_timeout=0; + + # for a TCP configuration + server web:8000 fail_timeout=0; + } + + server { + # use 'listen 80 deferred;' for Linux + # use 'listen 80 accept_filter=httpready;' for FreeBSD + listen 80 deferred; + client_max_body_size 4G; + + # set the correct host(s) for your site + # server_name example.com www.example.com; + + keepalive_timeout 5; + + # path for static files + root /app/WebHostLib; + + location / { + # checks for static file, if not found proxy to app + try_files $uri @proxy_to_app; + } + + location @proxy_to_app { + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Host $http_host; + # we don't want nginx trying to do something clever with + # redirects, we set the Host: header above already. + proxy_redirect off; + + proxy_pass http://app_server; + } + } +} diff --git a/deploy/example_selflaunch.yaml b/deploy/example_selflaunch.yaml new file mode 100644 index 0000000000..41149dc18a --- /dev/null +++ b/deploy/example_selflaunch.yaml @@ -0,0 +1,13 @@ +# Refer to ../docs/webhost configuration sample.yaml + +# We'll be hosting VIA gunicorn +SELFHOST: false +# Start room and generator processes +SELFLAUNCH: true +JOB_THRESHOLD: 0 + +# Maximum concurrent world gens +GENERATORS: 3 + +# Rooms will be spread across multiple processes +HOSTERS: 4 diff --git a/docs/deploy using containers.md b/docs/deploy using containers.md new file mode 100644 index 0000000000..bb77900174 --- /dev/null +++ b/docs/deploy using containers.md @@ -0,0 +1,91 @@ +# Deploy Using Containers + +If you just want to play and there is a compiled version available on the [Archipelago releases page](https://github.com/ArchipelagoMW/Archipelago/releases), use that version. +To build the full Archipelago software stack, refer to [Running From Source](running%20from%20source.md). +Follow these steps to build and deploy a containerized instance of the web host software, optionally integrating [Gunicorn](https://gunicorn.org/) WSGI HTTP Server running behind the [nginx](https://nginx.org/) reverse proxy. + + +## Building the Container Image + +What you'll need: + * A container runtime engine such as: + * [Docker](https://www.docker.com/) + * [Podman](https://podman.io/) + * For running with rootless podman, you need to ensure all ports used are usable rootless, by default ports less than 1024 are root only. See [the official tutorial](https://github.com/containers/podman/blob/main/docs/tutorials/rootless_tutorial.md) for details. + +Starting from the root repository directory, the standalone Archipelago image can be built and run with the command: +`docker build -t archipelago .` +Or: +`podman build -t archipelago .` + +It is recommended to tag the image using `-t` to more easily identify the image and run it. + + +## Running the Container + +Running the container can be performed using: +`docker run --network host archipelago` +Or: +`podman run --network host archipelago` + +The Archipelago web host requires access to multiple ports in order to host game servers simultaneously. To simplify configuration for this purpose, specify `--network host`. + +Given the default configuration, the website will be accessible at the hostname/IP address (localhost if run locally) of the machine being deployed to, at port 80. It can be configured by creating a YAML file and mapping a volume to the container when running initially: +`docker run archipelago --network host -v /path/to/config.yaml:/app/config.yaml` +See `docs/webhost configuration sample.yaml` for example. + + +## Using Docker Compose + +An example [docker compose](../deploy/docker-compose.yml) file can be found in [deploy](../deploy), along with example configuration files used by the services it orchestrates. Using these files as-is will spin up two separate archipelago containers with special modifications to their runtime arguments, in addition to deploying an `nginx` reverse proxy container. + +To deploy in this manner, from the ["deploy"](../deploy) directory, run: +`docker compose up -d` + +### Services + +The `docker-compose.yaml` file defines three services: + * multiworld: + * Executes the main `WebHost` process, using the [example config](../deploy/example_config.yaml), and overriding with a secondary [selflaunch example config](../deploy/example_selflaunch.yaml). This is because we do not want to launch the website through this service. + * web: + * Executes `gunicorn` using its [example config](../deploy/example_gunicorn.conf.py), which will bind it to the `WebHost` application, in effect launching it. + * We mount the main [config](../deploy/example_config.yaml) without an override to specify that we are launching the website through this service. + * No ports are exposed through to the host. + * nginx: + * Serves as a reverse proxy with `web` as its upstream. + * Directs all HTTP traffic from port 80 to the upstream service. + * Exposed to the host on port 8080. This is where we can reach the website. + +### Configuration + +As these are examples, they can be copied and modified. For instance setting the value of `HOST_ADDRESS` in [example config](../deploy/example_config.yaml) to host machines local IP address, will expose the service to its local area network. + +The configuration files may be modified to handle for machine-specific optimizations, such as: + * Web pages responding too slowly + * Edit [the gunicorn config](../deploy/example_gunicorn.conf.py) to increase thread and/or worker count. + * Game generation stalls + * Increase the generator count in [selflaunch config](../deploy/example_selflaunch.yaml) + * Gameplay lags + * Increase the hoster count in [selflaunch config](../deploy/example_selflaunch.yaml) + +Changes made to `docker-compose.yaml` can be applied by running `docker compose up -d`, while those made to other files are applied by running `docker compose restart`. + + +## Windows + +It is possible to carry out these deployment steps on Windows under [Windows Subsystem for Linux](https://learn.microsoft.com/en-us/windows/wsl/install). + + +## Optional: A Link to the Past Enemizer + +Only required to generate seeds that include A Link to the Past with certain options enabled. You will receive an +error if it is required. +Enemizer can be enabled on `x86_64` platform architecture, and is included in the image build process. Enemizer requires a version 1.0 Japanese "Zelda no Densetsu" `.sfc` rom file to be placed in the application directory: +`docker run archipelago -v "/path/to/zelda.sfc:/app/Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"`. +Enemizer is not currently available for `aarch64`. + + +## Optional: Git + +Building the image requires a local copy of the ArchipelagoMW source code. +Refer to [Running From Source](running%20from%20source.md#optional-git). From 4ae36ac727e690f1de0eba233b6683f2e1ad738c Mon Sep 17 00:00:00 2001 From: NoiseCrush <168460988+NoiseCrush@users.noreply.github.com> Date: Thu, 17 Jul 2025 07:46:31 -0400 Subject: [PATCH 27/37] Super Metroid: Improve Option Descriptions and Add Option Groups (#5100) --- worlds/sm/Options.py | 132 ++++++++++++++++++++++++++++++++++-------- worlds/sm/__init__.py | 3 +- 2 files changed, 110 insertions(+), 25 deletions(-) diff --git a/worlds/sm/Options.py b/worlds/sm/Options.py index 3dad16ad3a..7bce352994 100644 --- a/worlds/sm/Options.py +++ b/worlds/sm/Options.py @@ -1,5 +1,5 @@ import typing -from Options import Choice, PerGameCommonOptions, Range, OptionDict, OptionList, OptionSet, Option, Toggle, DefaultOnToggle +from Options import Choice, PerGameCommonOptions, Range, OptionDict, OptionList, OptionSet, OptionGroup, Toggle, DefaultOnToggle from .variaRandomizer.utils.objectives import _goals from dataclasses import dataclass @@ -8,8 +8,15 @@ class StartItemsRemovesFromPool(Toggle): display_name = "StartItems Removes From Item Pool" class Preset(Choice): - """Choose one of the presets or specify "varia_custom" to use varia_custom_preset option or specify "custom" to use - custom_preset option.""" + """Determines the general difficulty of the item placements by adjusting the list of tricks that logic allows. + - Newbie: New to randomizers, but completed Super Metroid 100% and knows basic techniques (Wall Jump, Shinespark, Mid-air Morph) + - Casual: Occasional rando player. No hell runs or suitless Maridia, some easy to learn tricks in logic. + - Regular: Plays rando regularly. Knows many tricks that open up the game. + - Veteran: Experienced rando player. Harder everything, some tougher tricks in logic. + - Expert: Knows almost all tricks: full suitless Maridia, Lower Norfair hell runs, etc. + - Master: Everything on hardest, all tricks known. + In-depth details on each preset can be found on the VARIA website: https://varia.run/presets + You may also specify "varia_custom" to use varia_custom_preset option, or specify "custom" to use custom_preset option.""" display_name = "Preset" option_newbie = 0 option_casual = 1 @@ -46,7 +53,8 @@ class StartLocation(Choice): default = 1 class DeathLink(Choice): - """When DeathLink is enabled and someone dies, you will die. With survive reserve tanks can save you.""" + """When DeathLink is enabled and someone else with DeathLink dies, you will die. + If "Enable Survive" is selected, reserve tanks can save you.""" display_name = "Death Link" option_disable = 0 option_enable = 1 @@ -56,11 +64,13 @@ class DeathLink(Choice): default = 0 class RemoteItems(Toggle): - """Indicates you get items sent from your own world. This allows coop play of a world.""" - display_name = "Remote Items" + """Items from your own world are sent via the Archipelago server. This allows co-op play of a world and means that + you will not lose items on death or save file loss.""" + display_name = "Remote Items" class MaxDifficulty(Choice): - """Depending on the perceived difficulties of the techniques, bosses, hell runs etc. from the preset, it will + """Maximum difficulty of tricks that are allowed from the seed's Preset. + Depending on the perceived difficulties of the techniques, bosses, hell runs etc. from the preset, it will prevent the Randomizer from placing an item in a location too difficult to reach with the current items.""" display_name = "Maximum Difficulty" option_easy = 0 @@ -73,7 +83,7 @@ class MaxDifficulty(Choice): default = 4 class MorphPlacement(Choice): - """Influences where the Morphing Ball with be placed.""" + """Influences where the Morphing Ball will be placed.""" display_name = "Morph Placement" option_early = 0 option_normal = 1 @@ -85,21 +95,21 @@ class StrictMinors(Toggle): display_name = "Strict Minors" class MissileQty(Range): - """The higher the number the higher the probability of choosing missles when placing a minor.""" + """The higher the number, the higher the probability of choosing Missiles when placing a minor.""" display_name = "Missile Quantity" range_start = 10 range_end = 90 default = 30 class SuperQty(Range): - """The higher the number the higher the probability of choosing super missles when placing a minor.""" + """The higher the number, the higher the probability of choosing Super Missiles when placing a minor.""" display_name = "Super Quantity" range_start = 10 range_end = 90 default = 20 class PowerBombQty(Range): - """The higher the number the higher the probability of choosing power bombs when placing a minor.""" + """The higher the number, the higher the probability of choosing Power Bombs when placing a minor.""" display_name = "Power Bomb Quantity" range_start = 10 range_end = 90 @@ -123,7 +133,13 @@ class EnergyQty(Choice): default = 3 class AreaRandomization(Choice): - """Randomize areas together using bidirectional access portals.""" + """Randomize areas together using bidirectional access portals. + - Off: No change. All rooms are connected the same as in the original game. + - Full: All doors connecting areas will be randomized. "Areas" are roughly determined, but generally are regions + with different tilesets or music. For example, red Brinstar and green/pink Brinstar are different areas, Crocomire + and upper Norfair are different areas, etc. + - Light: Keep the same number of transitions between areas as in vanilla. So Crocomire area will always be connected + to upper Norfair, there'll always be two transitions between Crateria/blue Brinstar and green/pink Brinstar, etc.""" display_name = "Area Randomization" option_off = 0 option_light = 1 @@ -136,13 +152,13 @@ class AreaLayout(Toggle): display_name = "Area Layout" class DoorsColorsRando(Toggle): - """Randomize the color of Red/Green/Yellow doors. Add four new type of doors which require Ice/Wave/Spazer/Plasma - beams to open them.""" + """Randomize the color of Red/Green/Yellow doors. Add four new types of doors which require Ice/Wave/Spazer/Plasma + Beams to open them.""" display_name = "Doors Colors Rando" class AllowGreyDoors(Toggle): """When randomizing the color of Red/Green/Yellow doors, some doors can be randomized to Grey. Grey doors will never - open, you will have to go around them.""" + open; you will have to go around them.""" display_name = "Allow Grey Doors" class BossRandomization(Toggle): @@ -169,7 +185,10 @@ class LayoutPatches(DefaultOnToggle): display_name = "Layout Patches" class VariaTweaks(Toggle): - """Include minor tweaks for the game to behave 'as it should' in a randomizer context""" + """Include minor tweaks for the game to behave 'as it should' in a randomizer context: + - Bomb Torizo always activates after picking up its item and does not require Bomb to activate + - Wrecked Ship item on the Energy Tank Chozo statue is present before defeating Phantoon + - Lower Norfair Chozo statue that lowers the acid toward Gold Torizo does not require Space Jump to activate""" display_name = "Varia Tweaks" class NerfedCharge(Toggle): @@ -179,7 +198,12 @@ class NerfedCharge(Toggle): display_name = "Nerfed Charge" class GravityBehaviour(Choice): - """Modify the heat damage and enemy damage reduction qualities of the Gravity and Varia Suits.""" + """Modify the heat damage and enemy damage reduction qualities of the Gravity and Varia Suits. + - Vanilla: Gravity provides full protection against all environmental damage (heat, spikes, etc.) + - Balanced: Removes Gravity environmental protection. Doubles Varia environmental protection. Enemy damage protection + is vanilla (50% Varia, 75% Gravity). + - Progressive: Gravity provides 50% heat reduction, Varia provides full heat reduction. Each suit adds 50% enemy + and environmental reduction, stacking to 75% reduction if you have both.""" display_name = "Gravity Behaviour" option_Vanilla = 0 option_Balanced = 1 @@ -233,7 +257,7 @@ class RandomMusic(Toggle): class CustomPreset(OptionDict): """ - see https://randommetroidsolver.pythonanywhere.com/presets for detailed info on each preset settings + see https://varia.run/presets for detailed info on each preset settings knows: each skill (know) has a pair [can use, perceived difficulty using one of 1, 5, 10, 25, 50 or 100 each one matching a max_difficulty] settings: hard rooms, hellruns and bosses settings @@ -246,7 +270,7 @@ class CustomPreset(OptionDict): } class VariaCustomPreset(OptionList): - """use an entry from the preset list on https://randommetroidsolver.pythonanywhere.com/presets""" + """use an entry from the preset list on https://varia.run/presets""" display_name = "Varia Custom Preset" default = {} @@ -259,7 +283,7 @@ class EscapeRando(Toggle): During the escape sequence: - All doors are opened - Maridia tube is opened - - The Hyper Beam can destroy Bomb , Power Bomb and Super Missile blocks and open blue/green gates from both sides + - The Hyper Beam can destroy Bomb, Power Bomb and Super Missile blocks and open blue/green gates from both sides - All mini bosses are defeated - All minor enemies are removed to allow you to move faster and remove lag @@ -281,9 +305,9 @@ class RemoveEscapeEnemies(Toggle): class Tourian(Choice): """ Choose endgame Tourian behaviour: - Vanilla: regular vanilla Tourian - Fast: speed up Tourian to skip Metroids, Zebetites, and all cutscenes (including Mother Brain 3 fight). Golden Four statues are replaced by an invincible Gadora until all objectives are completed. - Disabled: skip Tourian entirely, ie. escape sequence is triggered as soon as all objectives are completed. + - Vanilla: regular vanilla Tourian + - Fast: speed up Tourian to skip Metroids, Zebetites, and all cutscenes (including Mother Brain 3 fight). Golden Four statues are replaced by an invincible Gadora until all objectives are completed. + - Disabled: skip Tourian entirely; the escape sequence is triggered as soon as all objectives are completed. """ display_name = "Endgame behavior with Tourian" option_Vanilla = 0 @@ -373,10 +397,71 @@ class RelaxedRoundRobinCF(Toggle): """ display_name = "Relaxed round robin Crystal Flash" +sm_option_groups = [ + OptionGroup("Logic", [ + Preset, + MaxDifficulty, + StartLocation, + VariaCustomPreset, + CustomPreset, + ]), + OptionGroup("Objectives and Endgame", [ + Objective, + CustomObjective, + CustomObjectiveCount, + CustomObjectiveList, + Tourian, + EscapeRando, + RemoveEscapeEnemies, + Animals, + ]), + OptionGroup("Areas and Layout", [ + AreaRandomization, + AreaLayout, + DoorsColorsRando, + AllowGreyDoors, + BossRandomization, + LayoutPatches, + ]), + OptionGroup("Item Pool", [ + MorphPlacement, + StrictMinors, + MissileQty, + SuperQty, + PowerBombQty, + MinorQty, + EnergyQty, + FunCombat, + FunMovement, + FunSuits, + ]), + OptionGroup("Misc Tweaks", [ + VariaTweaks, + GravityBehaviour, + NerfedCharge, + SpinJumpRestart, + SpeedKeep, + InfiniteSpaceJump, + RelaxedRoundRobinCF, + ]), + OptionGroup("Quality of Life", [ + ElevatorsSpeed, + DoorsSpeed, + RefillBeforeSave, + ]), + OptionGroup("Cosmetic", [ + Hud, + HideItems, + NoMusic, + RandomMusic, + ]), +] + @dataclass class SMOptions(PerGameCommonOptions): start_inventory_removes_from_pool: StartItemsRemovesFromPool preset: Preset + max_difficulty: MaxDifficulty start_location: StartLocation remote_items: RemoteItems death_link: DeathLink @@ -384,7 +469,6 @@ class SMOptions(PerGameCommonOptions): #scav_num_locs: "10" #scav_randomized: "off" #scav_escape: "off" - max_difficulty: MaxDifficulty #progression_speed": "medium" #progression_difficulty": "normal" morph_placement: MorphPlacement diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index 3272f40c9b..cdb58b72fb 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -15,7 +15,7 @@ from worlds.generic.Rules import add_rule, set_rule logger = logging.getLogger("Super Metroid") -from .Options import SMOptions +from .Options import SMOptions, sm_option_groups from .Client import SMSNIClient from .Rom import SM_ROM_MAX_PLAYERID, SM_ROM_PLAYERDATA_COUNT, SMProcedurePatch, get_sm_symbols import Utils @@ -78,6 +78,7 @@ class SMWeb(WebWorld): "multiworld/en", ["Farrak Kilhn"] )] + option_groups = sm_option_groups class ByteEdit(TypedDict): From fb9026d12da47ebbf24a487daf9ffaee93a361de Mon Sep 17 00:00:00 2001 From: David Carroll Date: Thu, 17 Jul 2025 06:48:55 -0500 Subject: [PATCH 28/37] SMZ3: Add Yaml Options to Slot Data (#5111) --- worlds/smz3/__init__.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/worlds/smz3/__init__.py b/worlds/smz3/__init__.py index dca105b162..a98ae11df3 100644 --- a/worlds/smz3/__init__.py +++ b/worlds/smz3/__init__.py @@ -500,7 +500,14 @@ class SMZ3World(World): multidata["connect_names"][new_name] = payload def fill_slot_data(self): - slot_data = {} + slot_data = { + "goal": self.options.goal.value, + "open_tower": self.options.open_tower.value, + "ganon_vulnerable": self.options.ganon_vulnerable.value, + "open_tourian": self.options.open_tourian.value, + "sm_logic": self.options.sm_logic.value, + "key_shuffle": self.options.key_shuffle.value, + } return slot_data def collect(self, state: CollectionState, item: Item) -> bool: From da0bb80fb4763a24cbf6c5cf00f01bf0dcaa3b1e Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Fri, 18 Jul 2025 07:28:05 -0400 Subject: [PATCH 29/37] Raft: Fix filler_item_types TypeError introduced in #4782 (#5203) --- worlds/raft/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/raft/__init__.py b/worlds/raft/__init__.py index 74ab9291b2..374d952d46 100644 --- a/worlds/raft/__init__.py +++ b/worlds/raft/__init__.py @@ -93,7 +93,7 @@ class RaftWorld(World): dupeItemPool = list(dupeItemPool) # Finally, add items as necessary for item in dupeItemPool: - self.extraItemNamePool.append(self.replace_item_name_as_necessary(item)) + self.extraItemNamePool.append(self.replace_item_name_as_necessary(item["name"])) assert self.extraItemNamePool, f"Don't know what extra items to create for {self.player_name}." From a535ca31a8ae99e9c2bda8f09346ef062afe27ab Mon Sep 17 00:00:00 2001 From: Adrian Priestley <47989725+a-priestley@users.noreply.github.com> Date: Sat, 19 Jul 2025 11:18:30 -0230 Subject: [PATCH 30/37] Dockerfile/Core: Prevent module update during container runtime (#5205) * fix(env): Prevent module update during requirements processing - Add environment variable SKIP_REQUIREMENTS_UPDATE check - Ensure update is skipped if SKIP_REQUIREMENTS_UPDATE is set to true * squash! fix(env): Prevent module update during requirements processing - Add environment variable SKIP_REQUIREMENTS_UPDATE check - Ensure update is skipped if SKIP_REQUIREMENTS_UPDATE is set to true --- Dockerfile | 1 + ModuleUpdate.py | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 0ed61c0301..c6d22a4fb8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -94,4 +94,5 @@ RUN if [ "$TARGETARCH" = "amd64" ]; then \ HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ CMD curl -f http://localhost:${PORT:-80} || exit 1 +ENV SKIP_REQUIREMENTS_UPDATE=true ENTRYPOINT [ "python", "WebHost.py" ] diff --git a/ModuleUpdate.py b/ModuleUpdate.py index 04cf25ea55..e6ac570e58 100644 --- a/ModuleUpdate.py +++ b/ModuleUpdate.py @@ -16,7 +16,11 @@ elif sys.version_info < (3, 10, 1): raise RuntimeError(f"Incompatible Python Version found: {sys.version_info}. 3.10.1+ is supported.") # don't run update if environment is frozen/compiled or if not the parent process (skip in subprocess) -_skip_update = bool(getattr(sys, "frozen", False) or multiprocessing.parent_process()) +_skip_update = bool( + getattr(sys, "frozen", False) or + multiprocessing.parent_process() or + os.environ.get("SKIP_REQUIREMENTS_UPDATE", "").lower() in ("1", "true", "yes") +) update_ran = _skip_update From d313a742663e68a57548826250dcec1c580f241d Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Mon, 21 Jul 2025 14:53:34 -0400 Subject: [PATCH 31/37] ALttP: Fix `pre_fill` State Sweeping Too Early (#5215) --- worlds/alttp/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index 7f8d6ddf68..773fd7050c 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -505,10 +505,11 @@ class ALTTPWorld(World): def pre_fill(self): from Fill import fill_restrictive, FillError attempts = 5 - all_state = self.multiworld.get_all_state(use_cache=False) + all_state = self.multiworld.get_all_state(perform_sweep=False) crystals = [self.create_item(name) for name in ['Red Pendant', 'Blue Pendant', 'Green Pendant', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 7', 'Crystal 5', 'Crystal 6']] for crystal in crystals: all_state.remove(crystal) + all_state.sweep_for_advancements() crystal_locations = [self.get_location('Turtle Rock - Prize'), self.get_location('Eastern Palace - Prize'), self.get_location('Desert Palace - Prize'), From 76760e1bf3ba2d2d961657dbd03c2acc41a80f98 Mon Sep 17 00:00:00 2001 From: Mysteryem Date: Wed, 23 Jul 2025 03:01:47 +0100 Subject: [PATCH 32/37] OoT: Fix remove not invalidating cached reachability (#5222) Collecting an item into a CollectionState without sweeping, finding all reachable locations, removing that item from the state, and then finding all reachable locations again could result in more locations being reachable than before the item was initially collected into the CollectionState. This issue was present because OoT was not invalidating its reachable region caches for the different ages when items were removed from the CollectionState. To fix the issue, this PR has updated `OOTWorld.remove()` to invalid its caches, like how `CollectionState.remove()` invalidates the core Archipelago caches. --- worlds/oot/__init__.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index ed025f4971..d9465f1761 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -1324,10 +1324,20 @@ class OOTWorld(World): state.prog_items[self.player][alt_item_name] -= count if state.prog_items[self.player][alt_item_name] < 1: del (state.prog_items[self.player][alt_item_name]) + # invalidate caches, nothing can be trusted anymore now + state.child_reachable_regions[self.player] = set() + state.child_blocked_connections[self.player] = set() + state.adult_reachable_regions[self.player] = set() + state.adult_blocked_connections[self.player] = set() state._oot_stale[self.player] = True return True changed = super().remove(state, item) if changed: + # invalidate caches, nothing can be trusted anymore now + state.child_reachable_regions[self.player] = set() + state.child_blocked_connections[self.player] = set() + state.adult_reachable_regions[self.player] = set() + state.adult_blocked_connections[self.player] = set() state._oot_stale[self.player] = True return changed From 6b44f217a35489f520698d25dbb8cabeebb3e348 Mon Sep 17 00:00:00 2001 From: Flore Date: Wed, 23 Jul 2025 05:39:07 +0200 Subject: [PATCH 33/37] DS3: Edit the setup docs to be more clear (#4618) * UPDATE: Dark Souls 3 setup docs to be more clear * UPDATE: DS3 Setup docs to make offline mode more explicit * UPDATE: Dark Souls 3 setup docs to be more clear * UPDATE: DS3 Setup docs to make offline mode more explicit * EDIT: DS3 setup docs to be up to date --- worlds/dark_souls_3/docs/setup_en.md | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/worlds/dark_souls_3/docs/setup_en.md b/worlds/dark_souls_3/docs/setup_en.md index 4b11c8a498..7edf0d54e1 100644 --- a/worlds/dark_souls_3/docs/setup_en.md +++ b/worlds/dark_souls_3/docs/setup_en.md @@ -39,16 +39,14 @@ randomized item and (optionally) enemy locations. You only need to do this once To run _Dark Souls III_ in Archipelago mode: -1. Start Steam. **Do not run in offline mode.** Running Steam in offline mode will make certain - scripted invaders fail to spawn. Instead, change the game itself to offline mode on the menu - screen. +1. Start Steam. **Do not run Steam in offline mode.** Running Steam in offline mode will make certain + scripted invaders fail to spawn. -2. Run `launchmod_darksouls3.bat`. This will start _Dark Souls III_ as well as a command prompt that +2. To prevent you from getting penalized, **make sure to set _Dark Souls III_ to offline mode in the game options.** + +3. Run `launchmod_darksouls3.bat`. This will start _Dark Souls III_ as well as a command prompt that you can use to interact with the Archipelago server. -3. Type `/connect {SERVER_IP}:{SERVER_PORT} {SLOT_NAME}` into the command prompt, with the - appropriate values filled in. For example: `/connect archipelago.gg:24242 PlayerName`. - 4. Start playing as normal. An "Archipelago connected" message will appear onscreen once you have control of your character and the connection is established. From 0e4314ad1e840481d12252ae41358b14217aa3f6 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Wed, 23 Jul 2025 13:04:07 +0200 Subject: [PATCH 34/37] MultiServer: CreateHints command (Allows clients to hint own items in other worlds) (#4317) * CreateHint command * Docs * oops * forgot an arg * Update MultiServer.py * Add documentation on what happens when the hint already exists but with a different status (nothing) * Early exit if no locations provided * Add a clarifying comment to the code as well * change wording a bit --- MultiServer.py | 42 ++++++++++++++++++++++++++++++++++++++++ docs/network protocol.md | 16 +++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/MultiServer.py b/MultiServer.py index f12f327c3f..108795d84f 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -1950,6 +1950,48 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict): if locs and create_as_hint: ctx.save() await ctx.send_msgs(client, [{'cmd': 'LocationInfo', 'locations': locs}]) + + elif cmd == 'CreateHints': + location_player = args.get("player", client.slot) + locations = args["locations"] + status = args.get("status", HintStatus.HINT_UNSPECIFIED) + + if not locations: + await ctx.send_msgs(client, [{"cmd": "InvalidPacket", "type": "arguments", + "text": "CreateHints: No locations specified.", "original_cmd": cmd}]) + + hints = [] + + for location in locations: + if location_player != client.slot and location not in ctx.locations[location_player]: + error_text = ( + "CreateHints: One or more of the locations do not exist for the specified off-world player. " + "Please refrain from hinting other slot's locations that you don't know contain your items." + ) + await ctx.send_msgs(client, [{"cmd": "InvalidPacket", "type": "arguments", + "text": error_text, "original_cmd": cmd}]) + return + + target_item, item_player, flags = ctx.locations[location_player][location] + + if client.slot not in ctx.slot_set(item_player): + if status != HintStatus.HINT_UNSPECIFIED: + error_text = 'CreateHints: Must use "unspecified"/None status for items from other players.' + await ctx.send_msgs(client, [{"cmd": "InvalidPacket", "type": "arguments", + "text": error_text, "original_cmd": cmd}]) + return + + if client.slot != location_player: + error_text = "CreateHints: Can only create hints for own items or own locations." + await ctx.send_msgs(client, [{"cmd": "InvalidPacket", "type": "arguments", + "text": error_text, "original_cmd": cmd}]) + return + + hints += collect_hint_location_id(ctx, client.team, location_player, location, status) + + # As of writing this code, only_new=True does not update status for existing hints + ctx.notify_hints(client.team, hints, only_new=True) + ctx.save() elif cmd == 'UpdateHint': location = args["location"] diff --git a/docs/network protocol.md b/docs/network protocol.md index 27238e6b74..4b66b7b1d3 100644 --- a/docs/network protocol.md +++ b/docs/network protocol.md @@ -276,6 +276,7 @@ These packets are sent purely from client to server. They are not accepted by cl * [Sync](#Sync) * [LocationChecks](#LocationChecks) * [LocationScouts](#LocationScouts) +* [CreateHints](#CreateHints) * [UpdateHint](#UpdateHint) * [StatusUpdate](#StatusUpdate) * [Say](#Say) @@ -347,6 +348,21 @@ This is useful in cases where an item appears in the game world, such as 'ledge | locations | list\[int\] | The ids of the locations seen by the client. May contain any number of locations, even ones sent before; duplicates do not cause issues with the Archipelago server. | | create_as_hint | int | If non-zero, the scouted locations get created and broadcasted as a player-visible hint.
If 2 only new hints are broadcast, however this does not remove them from the LocationInfo reply. | +### CreateHints + +Sent to the server to create hints for a specified list of locations. +Hints that already exist will be silently skipped and their status will not be updated. + +When creating hints for another slot's locations, the packet will fail if any of those locations don't contain items for the requesting slot. +When creating hints for your own slot's locations, non-existing locations will silently be skipped. + +#### Arguments +| Name | Type | Notes | +| ---- | ---- | ----- | +| locations | list\[int\] | The ids of the locations to create hints for. | +| player | int | The ID of the player whose locations are being hinted for. Defaults to the requesting slot. | +| status | [HintStatus](#HintStatus) | If included, sets the status of the hint to this status. Defaults to `HINT_UNSPECIFIED`. Cannot set `HINT_FOUND`. | + ### UpdateHint Sent to the server to update the status of a Hint. The client must be the 'receiving_player' of the Hint, or the update fails. From 8541c87c9785cce98820002bec4ca4b8ad1c00b6 Mon Sep 17 00:00:00 2001 From: MarioManTAW Date: Wed, 23 Jul 2025 16:27:50 -0500 Subject: [PATCH 35/37] Paint: Implement New Game (#4955) * Paint: Implement New Game * Add docstring * Remove unnecessary self.multiworld references * Implement start_inventory_from_pool * Convert logic to use LogicMixin * Add location_exists_with_options function to deduplicate code * Simplify starting tool creation * Add Paint to supported games list * Increment version to 0.4.1 * Update docs to include color selection features * Fix world attribute definitions * Fix linting errors * De-duplicate lists of traps * Move LogicMixin to __init__.py * 0.5.0 features - adjustable canvas size increment, updated similarity metric * Fix OptionError formatting * Create OptionError when generating single-player game with error-prone settings * Increment version to 0.5.1 * Update CODEOWNERS * Update documentation for 0.5.2 client changes * Simplify region creation * Add comments describing logic * Remove unnecessary f-strings * Remove unused import * Refactor rules to location class * Remove unnecessary self.multiworld references * Update logic to correctly match client-side item caps --------- Co-authored-by: Fabian Dill --- README.md | 1 + docs/CODEOWNERS | 3 + worlds/paint/__init__.py | 128 ++++++++++++++++++++++++++++++++++ worlds/paint/docs/en_Paint.md | 35 ++++++++++ worlds/paint/docs/guide_en.md | 8 +++ worlds/paint/items.py | 48 +++++++++++++ worlds/paint/locations.py | 24 +++++++ worlds/paint/options.py | 107 ++++++++++++++++++++++++++++ worlds/paint/rules.py | 40 +++++++++++ 9 files changed, 394 insertions(+) create mode 100644 worlds/paint/__init__.py create mode 100644 worlds/paint/docs/en_Paint.md create mode 100644 worlds/paint/docs/guide_en.md create mode 100644 worlds/paint/items.py create mode 100644 worlds/paint/locations.py create mode 100644 worlds/paint/options.py create mode 100644 worlds/paint/rules.py diff --git a/README.md b/README.md index 29b6206a00..44c44d72b4 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,7 @@ Currently, the following games are supported: * Jak and Daxter: The Precursor Legacy * Super Mario Land 2: 6 Golden Coins * shapez +* Paint 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/docs/CODEOWNERS b/docs/CODEOWNERS index 3104200a6c..85b31683aa 100644 --- a/docs/CODEOWNERS +++ b/docs/CODEOWNERS @@ -136,6 +136,9 @@ # Overcooked! 2 /worlds/overcooked2/ @toasterparty +# Paint +/worlds/paint/ @MarioManTAW + # Pokemon Emerald /worlds/pokemon_emerald/ @Zunawe diff --git a/worlds/paint/__init__.py b/worlds/paint/__init__.py new file mode 100644 index 0000000000..8e501ff3dc --- /dev/null +++ b/worlds/paint/__init__.py @@ -0,0 +1,128 @@ +from typing import Dict, Any + +from BaseClasses import CollectionState, Item, MultiWorld, Tutorial, Region +from Options import OptionError +from worlds.AutoWorld import LogicMixin, World, WebWorld +from .items import item_table, PaintItem, item_data_table, traps, deathlink_traps +from .locations import location_table, PaintLocation, location_data_table +from .options import PaintOptions + + +class PaintWebWorld(WebWorld): + theme = "partyTime" + + setup_en = Tutorial( + tutorial_name="Start Guide", + description="A guide to playing Paint in Archipelago.", + language="English", + file_name="guide_en.md", + link="guide/en", + authors=["MarioManTAW"] + ) + + tutorials = [setup_en] + + +class PaintWorld(World): + """ + The classic Microsoft app, reimagined as an Archipelago game! Find your tools, expand your canvas, and paint the + greatest image the world has ever seen. + """ + game = "Paint" + options_dataclass = PaintOptions + options: PaintOptions + web = PaintWebWorld() + location_name_to_id = location_table + item_name_to_id = item_table + origin_region_name = "Canvas" + + def generate_early(self) -> None: + if self.options.canvas_size_increment < 50 and self.options.logic_percent <= 55: + if self.multiworld.players == 1: + raise OptionError("Logic Percent must be greater than 55 when generating a single-player world with " + "Canvas Size Increment below 50.") + + def get_filler_item_name(self) -> str: + if self.random.randint(0, 99) >= self.options.trap_count: + return "Additional Palette Color" + elif self.options.death_link: + return self.random.choice(deathlink_traps) + else: + return self.random.choice(traps) + + def create_item(self, name: str) -> PaintItem: + item = PaintItem(name, item_data_table[name].type, item_data_table[name].code, self.player) + return item + + def create_items(self) -> None: + starting_tools = ["Brush", "Pencil", "Eraser/Color Eraser", "Airbrush", "Line", "Rectangle", "Ellipse", + "Rounded Rectangle"] + self.push_precollected(self.create_item("Magnifier")) + self.push_precollected(self.create_item(starting_tools.pop(self.options.starting_tool))) + items_to_create = ["Free-Form Select", "Select", "Fill With Color", "Pick Color", "Text", "Curve", "Polygon"] + items_to_create += starting_tools + items_to_create += ["Progressive Canvas Width"] * (400 // self.options.canvas_size_increment) + items_to_create += ["Progressive Canvas Height"] * (300 // self.options.canvas_size_increment) + depth_items = ["Progressive Color Depth (Red)", "Progressive Color Depth (Green)", + "Progressive Color Depth (Blue)"] + for item in depth_items: + self.push_precollected(self.create_item(item)) + items_to_create += depth_items * 6 + pre_filled = len(items_to_create) + to_fill = len(self.get_region("Canvas").locations) + if pre_filled > to_fill: + raise OptionError(f"{self.player_name}'s Paint world has too few locations for its required items. " + "Consider adding more locations by raising logic percent or adding fractional checks. " + "Alternatively, increasing the canvas size increment will require fewer items.") + while len(items_to_create) < (to_fill - pre_filled) * (self.options.trap_count / 100) + pre_filled: + if self.options.death_link: + items_to_create += [self.random.choice(deathlink_traps)] + else: + items_to_create += [self.random.choice(traps)] + while len(items_to_create) < to_fill: + items_to_create += ["Additional Palette Color"] + self.multiworld.itempool += [self.create_item(item) for item in items_to_create] + + def create_regions(self) -> None: + canvas = Region("Canvas", self.player, self.multiworld) + canvas.locations += [PaintLocation(self.player, loc_name, loc_data.address, canvas) + for loc_name, loc_data in location_data_table.items() + if location_exists_with_options(self, loc_data.address)] + + self.multiworld.regions += [canvas] + + def set_rules(self) -> None: + from .rules import set_completion_rules + set_completion_rules(self, self.player) + + def fill_slot_data(self) -> Dict[str, Any]: + return dict(self.options.as_dict("logic_percent", "goal_percent", "goal_image", "death_link", + "canvas_size_increment"), version="0.5.2") + + def collect(self, state: CollectionState, item: Item) -> bool: + change = super().collect(state, item) + if change: + state.paint_percent_stale[self.player] = True + return change + + def remove(self, state: CollectionState, item: Item) -> bool: + change = super().remove(state, item) + if change: + state.paint_percent_stale[self.player] = True + return change + + +def location_exists_with_options(world: PaintWorld, location: int): + l = location % 198600 + return l <= world.options.logic_percent * 4 and (l % 4 == 0 or + (l > world.options.half_percent_checks * 4 and l % 2 == 0) or + l > world.options.quarter_percent_checks * 4) + + +class PaintState(LogicMixin): + paint_percent_available: dict[int, float] # per player + paint_percent_stale: dict[int, bool] + + def init_mixin(self, multiworld: MultiWorld) -> None: + self.paint_percent_available = {player: 0 for player in multiworld.get_game_players("Paint")} + self.paint_percent_stale = {player: True for player in multiworld.get_game_players("Paint")} diff --git a/worlds/paint/docs/en_Paint.md b/worlds/paint/docs/en_Paint.md new file mode 100644 index 0000000000..845c726848 --- /dev/null +++ b/worlds/paint/docs/en_Paint.md @@ -0,0 +1,35 @@ +# Paint + +## Where is the options page? + +You can read through all the options and generate a YAML [here](../player-options). + +## What does randomization do to this game? + +Most tools are locked from the start, leaving only the Magnifier and one drawing tool, specified in the game options. +Canvas size is locked and will only expand when the Progressive Canvas Width and Progressive Canvas Height items are +obtained. Additionally, color selection is limited, starting with only a few possible colors but gaining more options +when Progressive Color Depth items are obtained in each of the red, green, and blue components. + +Location checks are sent out based on similarity to a target image, measured as a percentage. Every percentage point up +to a maximum set in the game options will send a new check, and the game will be considered done when a certain target +percentage (also set in the game options) is reached. + +## What other changes are made to the game? + +This project is based on [JS Paint](https://jspaint.app), an open-source remake of Microsoft Paint. Most features will +work similarly to this version but some features have also been removed. Most notably, pasting functionality has been +completely removed to prevent cheating. + +With the addition of a second canvas to display the target image, there are some additional features that may not be +intuitive. There are two special functions in the Extras menu to help visualize how to improve your score. Similarity +Mode (shortcut Ctrl+Shift+M) shows the similarity of each portion of the image in grayscale, with white representing +perfect similarity and black representing no similarity. Conversely, Difference Mode (shortcut Ctrl+M) visualizes the +differences between what has been drawn and the target image in full color, showing the direction both hue and +lightness need to shift to match the target. Additionally, once unlocked, the Pick Color tool can be used on both the +main and target canvases. + +Custom colors have been streamlined for Archipelago play. The only starting palette options are black and white, but +additional palette slots can be unlocked as Archipelago items. Double-clicking on any palette slot will allow you to +edit the color in that slot directly and shift-clicking a palette slot will allow you to override the slot with your +currently selected color. diff --git a/worlds/paint/docs/guide_en.md b/worlds/paint/docs/guide_en.md new file mode 100644 index 0000000000..8571ad3d4d --- /dev/null +++ b/worlds/paint/docs/guide_en.md @@ -0,0 +1,8 @@ +# Paint Randomizer Start Guide + +After rolling your seed, go to the [Archipelago Paint](https://mariomantaw.github.io/jspaint/) site and enter the +server details, your slot name, and a room password if one is required. Then click "Connect". If desired, you may then +load a custom target image with File->Open Goal Image. If playing asynchronously, note that progress is saved using the +hash that will appear at the end of the URL so it is recommended to leave the tab open or save the URL with the hash to +avoid losing progress. + diff --git a/worlds/paint/items.py b/worlds/paint/items.py new file mode 100644 index 0000000000..c2ea2001b6 --- /dev/null +++ b/worlds/paint/items.py @@ -0,0 +1,48 @@ +from typing import NamedTuple, Dict + +from BaseClasses import Item, ItemClassification + + +class PaintItem(Item): + game = "Paint" + + +class PaintItemData(NamedTuple): + code: int + type: ItemClassification + + +item_data_table: Dict[str, PaintItemData] = { + "Progressive Canvas Width": PaintItemData(198501, ItemClassification.progression), + "Progressive Canvas Height": PaintItemData(198502, ItemClassification.progression), + "Progressive Color Depth (Red)": PaintItemData(198503, ItemClassification.progression), + "Progressive Color Depth (Green)": PaintItemData(198504, ItemClassification.progression), + "Progressive Color Depth (Blue)": PaintItemData(198505, ItemClassification.progression), + "Free-Form Select": PaintItemData(198506, ItemClassification.useful), + "Select": PaintItemData(198507, ItemClassification.useful), + "Eraser/Color Eraser": PaintItemData(198508, ItemClassification.useful), + "Fill With Color": PaintItemData(198509, ItemClassification.useful), + "Pick Color": PaintItemData(198510, ItemClassification.progression), + "Magnifier": PaintItemData(198511, ItemClassification.useful), + "Pencil": PaintItemData(198512, ItemClassification.useful), + "Brush": PaintItemData(198513, ItemClassification.useful), + "Airbrush": PaintItemData(198514, ItemClassification.useful), + "Text": PaintItemData(198515, ItemClassification.useful), + "Line": PaintItemData(198516, ItemClassification.useful), + "Curve": PaintItemData(198517, ItemClassification.useful), + "Rectangle": PaintItemData(198518, ItemClassification.useful), + "Polygon": PaintItemData(198519, ItemClassification.useful), + "Ellipse": PaintItemData(198520, ItemClassification.useful), + "Rounded Rectangle": PaintItemData(198521, ItemClassification.useful), + # "Change Background Color": PaintItemData(198522, ItemClassification.useful), + "Additional Palette Color": PaintItemData(198523, ItemClassification.filler), + "Undo Trap": PaintItemData(198524, ItemClassification.trap), + "Clear Image Trap": PaintItemData(198525, ItemClassification.trap), + "Invert Colors Trap": PaintItemData(198526, ItemClassification.trap), + "Flip Horizontal Trap": PaintItemData(198527, ItemClassification.trap), + "Flip Vertical Trap": PaintItemData(198528, ItemClassification.trap), +} + +item_table = {name: data.code for name, data in item_data_table.items()} +traps = ["Undo Trap", "Clear Image Trap", "Invert Colors Trap", "Flip Horizontal Trap", "Flip Vertical Trap"] +deathlink_traps = ["Invert Colors Trap", "Flip Horizontal Trap", "Flip Vertical Trap"] diff --git a/worlds/paint/locations.py b/worlds/paint/locations.py new file mode 100644 index 0000000000..ce227991ef --- /dev/null +++ b/worlds/paint/locations.py @@ -0,0 +1,24 @@ +from typing import NamedTuple, Dict + +from BaseClasses import CollectionState, Location + + +class PaintLocation(Location): + game = "Paint" + def access_rule(self, state: CollectionState): + from .rules import paint_percent_available + return paint_percent_available(state, state.multiworld.worlds[self.player], self.player) >=\ + (self.address % 198600) / 4 + + +class PaintLocationData(NamedTuple): + region: str + address: int + + +location_data_table: Dict[str, PaintLocationData] = { + # f"Similarity: {i}%": PaintLocationData("Canvas", 198500 + i) for i in range(1, 96) + f"Similarity: {i/4}%": PaintLocationData("Canvas", 198600 + i) for i in range(1, 381) +} + +location_table = {name: data.address for name, data in location_data_table.items()} diff --git a/worlds/paint/options.py b/worlds/paint/options.py new file mode 100644 index 0000000000..95dee7d8fd --- /dev/null +++ b/worlds/paint/options.py @@ -0,0 +1,107 @@ +from dataclasses import dataclass + +from Options import Range, PerGameCommonOptions, StartInventoryPool, Toggle, Choice, Visibility + + +class LogicPercent(Range): + """Sets the maximum percent similarity required for a check to be in logic. + Higher values are more difficult and items/locations will not be generated beyond this number.""" + display_name = "Logic Percent" + range_start = 50 + range_end = 95 + default = 80 + + +class GoalPercent(Range): + """Sets the percent similarity required to achieve your goal. + If this number is higher than the value for logic percent, + reaching goal will be in logic upon obtaining all progression items.""" + display_name = "Goal Percent" + range_start = 50 + range_end = 95 + default = 80 + + +class HalfPercentChecks(Range): + """Sets the lowest percent at which locations will be created for each 0.5% of similarity. + Below this number, there will be a check every 1%. + Above this number, there will be a check every 0.5%.""" + display_name = "Half Percent Checks" + range_start = 0 + range_end = 95 + default = 50 + + +class QuarterPercentChecks(Range): + """Sets the lowest percent at which locations will be created for each 0.25% of similarity. + This number will override Half Percent Checks if it is lower.""" + display_name = "Quarter Percent Checks" + range_start = 0 + range_end = 95 + default = 70 + + +class CanvasSizeIncrement(Choice): + """Sets the number of pixels the canvas will expand for each width/height item received. + Ensure an adequate number of locations are generated if setting this below 50.""" + display_name = "Canvas Size Increment" + # option_10 = 10 + # option_20 = 20 + option_25 = 25 + option_50 = 50 + option_100 = 100 + default = 100 + + +class GoalImage(Range): + """Sets the numbered image you will be required to match. + See https://github.com/MarioManTAW/jspaint/tree/master/images/archipelago + for a list of possible images or choose random. + This can also be overwritten client-side by using File->Open.""" + display_name = "Goal Image" + range_start = 1 + range_end = 1 + default = 1 + visibility = Visibility.none + + +class StartingTool(Choice): + """Sets which tool (other than Magnifier) you will be able to use from the start.""" + option_brush = 0 + option_pencil = 1 + option_eraser = 2 + option_airbrush = 3 + option_line = 4 + option_rectangle = 5 + option_ellipse = 6 + option_rounded_rectangle = 7 + default = 0 + + +class TrapCount(Range): + """Sets the percentage of filler items to be replaced by random traps.""" + display_name = "Trap Fill Percent" + range_start = 0 + range_end = 100 + default = 0 + + +class DeathLink(Toggle): + """If on, using the Undo or Clear Image functions will send a death to all other players with death link on. + Receiving a death will clear the image and reset the history. + This option also prevents Undo and Clear Image traps from being generated in the item pool.""" + display_name = "Death Link" + + +@dataclass +class PaintOptions(PerGameCommonOptions): + logic_percent: LogicPercent + goal_percent: GoalPercent + half_percent_checks: HalfPercentChecks + quarter_percent_checks: QuarterPercentChecks + canvas_size_increment: CanvasSizeIncrement + goal_image: GoalImage + starting_tool: StartingTool + trap_count: TrapCount + death_link: DeathLink + start_inventory_from_pool: StartInventoryPool diff --git a/worlds/paint/rules.py b/worlds/paint/rules.py new file mode 100644 index 0000000000..1c7844c129 --- /dev/null +++ b/worlds/paint/rules.py @@ -0,0 +1,40 @@ +from math import sqrt + +from BaseClasses import CollectionState +from . import PaintWorld + + +def paint_percent_available(state: CollectionState, world: PaintWorld, player: int) -> bool: + if state.paint_percent_stale[player]: + state.paint_percent_available[player] = calculate_paint_percent_available(state, world, player) + state.paint_percent_stale[player] = False + return state.paint_percent_available[player] + + +def calculate_paint_percent_available(state: CollectionState, world: PaintWorld, player: int) -> float: + p = state.has("Pick Color", player) + r = min(state.count("Progressive Color Depth (Red)", player), 7) + g = min(state.count("Progressive Color Depth (Green)", player), 7) + b = min(state.count("Progressive Color Depth (Blue)", player), 7) + if not p: + r = min(r, 2) + g = min(g, 2) + b = min(b, 2) + w = state.count("Progressive Canvas Width", player) + h = state.count("Progressive Canvas Height", player) + # This code looks a little messy but it's a mathematical formula derived from the similarity calculations in the + # client. The first line calculates the maximum score achievable for a single pixel with the current items in the + # worst possible case. This per-pixel score is then multiplied by the number of pixels currently available (the + # starting canvas is 400x300) over the total number of pixels with everything unlocked (800x600) to get the + # total score achievable assuming the worst possible target image. Finally, this is multiplied by the logic percent + # option which restricts the logic so as to not require pixel perfection. + return ((1 - ((sqrt(((2 ** (7 - r) - 1) ** 2 + (2 ** (7 - g) - 1) ** 2 + (2 ** (7 - b) - 1) ** 2) * 12)) / 765)) * + min(400 + w * world.options.canvas_size_increment, 800) * + min(300 + h * world.options.canvas_size_increment, 600) * + world.options.logic_percent / 480000) + + +def set_completion_rules(world: PaintWorld, player: int) -> None: + world.multiworld.completion_condition[player] = \ + lambda state: (paint_percent_available(state, world, player) >= + min(world.options.logic_percent, world.options.goal_percent)) From 81b8f3fc0ef1b562b190875d2d68b3bb971e5c98 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 24 Jul 2025 00:01:27 +0200 Subject: [PATCH 36/37] Factorio: fix rename of mod file leading to incompatibility with base game (#5219) --- WebHostLib/templates/macros.html | 3 +++ 1 file changed, 3 insertions(+) diff --git a/WebHostLib/templates/macros.html b/WebHostLib/templates/macros.html index be664274e6..9a16bce1d3 100644 --- a/WebHostLib/templates/macros.html +++ b/WebHostLib/templates/macros.html @@ -32,6 +32,9 @@ {% elif patch.game == "Super Mario 64" and room.seed.slots|length == 1 %} Download APSM64EX File... + {% elif patch.game == "Factorio" %} + + Download Factorio Mod... {% elif patch.game | is_applayercontainer(patch.data, patch.player_id) %} Download Patch File... From 4ac1d91c16378fa9fed86ece314c3f55788468d4 Mon Sep 17 00:00:00 2001 From: Adrian Priestley <47989725+a-priestley@users.noreply.github.com> Date: Thu, 24 Jul 2025 04:05:13 -0230 Subject: [PATCH 37/37] chore(ci): exclude deployment and Docker files from unit test workflow triggers (#5214) * chore(ci): exclude deployment and Docker files from unit test workflow triggers - Modify unittests workflow to ignore changes in deploy directory and Docker-related files --- .github/workflows/unittests.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/unittests.yml b/.github/workflows/unittests.yml index 88b5d12987..2d83c649e8 100644 --- a/.github/workflows/unittests.yml +++ b/.github/workflows/unittests.yml @@ -8,18 +8,24 @@ on: paths: - '**' - '!docs/**' + - '!deploy/**' - '!setup.py' + - '!Dockerfile' - '!*.iss' - '!.gitignore' + - '!.dockerignore' - '!.github/workflows/**' - '.github/workflows/unittests.yml' pull_request: paths: - '**' - '!docs/**' + - '!deploy/**' - '!setup.py' + - '!Dockerfile' - '!*.iss' - '!.gitignore' + - '!.dockerignore' - '!.github/workflows/**' - '.github/workflows/unittests.yml'