From 4623d59206e88132432b6db74945a717057b2f8a Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 7 Jul 2025 15:51:39 +0200 Subject: [PATCH 01/15] Core: ensure slot_data and er_hint_info are only base data types (#5144) --------- Co-authored-by: Doug Hoskisson --- Main.py | 4 ++++ NetUtils.py | 21 +++++++++++++++++++++ test/general/test_implemented.py | 4 ++-- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/Main.py b/Main.py index 442c2ff404..456820a461 100644 --- a/Main.py +++ b/Main.py @@ -12,6 +12,7 @@ import worlds from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld from Fill import FillError, balance_multiworld_progression, distribute_items_restrictive, flood_items, \ parse_planned_blocks, distribute_planned_blocks, resolve_early_locations_for_planned +from NetUtils import convert_to_base_types from Options import StartInventoryPool from Utils import __version__, output_path, version_tuple from settings import get_settings @@ -334,6 +335,9 @@ def main(args, seed=None, baked_server_options: dict[str, object] | None = None) } AutoWorld.call_all(multiworld, "modify_multidata", multidata) + for key in ("slot_data", "er_hint_data"): + multidata[key] = convert_to_base_types(multidata[key]) + multidata = zlib.compress(pickle.dumps(multidata), 9) with open(os.path.join(temp_dir, f'{outfilebase}.archipelago'), 'wb') as f: diff --git a/NetUtils.py b/NetUtils.py index f2ae2a63a0..cc6e917c88 100644 --- a/NetUtils.py +++ b/NetUtils.py @@ -106,6 +106,27 @@ def _scan_for_TypedTuples(obj: typing.Any) -> typing.Any: return obj +_base_types = str | int | bool | float | None | tuple["_base_types", ...] | dict["_base_types", "base_types"] + + +def convert_to_base_types(obj: typing.Any) -> _base_types: + if isinstance(obj, (tuple, list, set, frozenset)): + return tuple(convert_to_base_types(o) for o in obj) + elif isinstance(obj, dict): + return {convert_to_base_types(key): convert_to_base_types(value) for key, value in obj.items()} + elif obj is None or type(obj) in (str, int, float, bool): + return obj + # unwrap simple types to their base, such as StrEnum + elif isinstance(obj, str): + return str(obj) + elif isinstance(obj, int): + return int(obj) + elif isinstance(obj, float): + return float(obj) + else: + raise Exception(f"Cannot handle {type(obj)}") + + _encode = JSONEncoder( ensure_ascii=False, check_circular=False, diff --git a/test/general/test_implemented.py b/test/general/test_implemented.py index b74f82b738..cf0624a288 100644 --- a/test/general/test_implemented.py +++ b/test/general/test_implemented.py @@ -1,7 +1,7 @@ import unittest from Fill import distribute_items_restrictive -from NetUtils import encode +from NetUtils import convert_to_base_types from worlds.AutoWorld import AutoWorldRegister, call_all from worlds import failed_world_loads from . import setup_solo_multiworld @@ -47,7 +47,7 @@ class TestImplemented(unittest.TestCase): call_all(multiworld, "post_fill") for key, data in multiworld.worlds[1].fill_slot_data().items(): self.assertIsInstance(key, str, "keys in slot data must be a string") - self.assertIsInstance(encode(data), str, f"object {type(data).__name__} not serializable.") + convert_to_base_types(data) # only put base data types into slot data def test_no_failed_world_loads(self): if failed_world_loads: From 95e09c8e2a681ecd5666822b04fe7fed3ed9dec1 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Mon, 7 Jul 2025 16:24:35 +0200 Subject: [PATCH 02/15] Core: Take Counter back out of RestrictedUnpickler #5169 --- Utils.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/Utils.py b/Utils.py index 84d3a33dc7..6212b93288 100644 --- a/Utils.py +++ b/Utils.py @@ -441,9 +441,6 @@ class RestrictedUnpickler(pickle.Unpickler): def find_class(self, module: str, name: str) -> type: if module == "builtins" and name in safe_builtins: return getattr(builtins, name) - # used by OptionCounter - if module == "collections" and name == "Counter": - return collections.Counter # used by MultiServer -> savegame/multidata if module == "NetUtils" and name in {"NetworkItem", "ClientStatus", "Hint", "SlotType", "NetworkSlot", "HintStatus"}: From d4ebace99f17299c0fa861a1ccad42bdf4e332fe Mon Sep 17 00:00:00 2001 From: massimilianodelliubaldini <8584296+massimilianodelliubaldini@users.noreply.github.com> Date: Mon, 7 Jul 2025 13:15:37 -0400 Subject: [PATCH 03/15] =?UTF-8?q?[Jak=20and=20Daxter]=20Auto=20Detect=20In?= =?UTF-8?q?stall=20Path=20after=20Game=20Launcher=20Update=C2=A0#5152?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- worlds/jakanddaxter/client.py | 36 ++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/worlds/jakanddaxter/client.py b/worlds/jakanddaxter/client.py index 2b669d3847..90e6a42aa7 100644 --- a/worlds/jakanddaxter/client.py +++ b/worlds/jakanddaxter/client.py @@ -367,7 +367,7 @@ def find_root_directory(ctx: JakAndDaxterContext): f" Close all launchers, games, clients, and console windows, then restart Archipelago.") if not os.path.exists(settings_path): - msg = (f"{err_title}: the OpenGOAL settings file does not exist.\n" + msg = (f"{err_title}: The OpenGOAL settings file does not exist.\n" f"{alt_instructions}") ctx.on_log_error(logger, msg) return @@ -375,14 +375,44 @@ def find_root_directory(ctx: JakAndDaxterContext): with open(settings_path, "r") as f: load = json.load(f) - jak1_installed = load["games"]["Jak 1"]["isInstalled"] + # This settings file has changed format once before, and may do so again in the future. + # Guard against future incompatibilities by checking the file version first, and use that to determine + # what JSON keys to look for next. + try: + settings_version = load["version"] + logger.debug(f"OpenGOAL settings file version: {settings_version}") + except KeyError: + msg = (f"{err_title}: The OpenGOAL settings file has no version number!\n" + f"{alt_instructions}") + ctx.on_log_error(logger, msg) + return + + try: + if settings_version == "2.0": + jak1_installed = load["games"]["Jak 1"]["isInstalled"] + mod_sources = load["games"]["Jak 1"]["modsInstalledVersion"] + + elif settings_version == "3.0": + jak1_installed = load["games"]["jak1"]["isInstalled"] + mod_sources = load["games"]["jak1"]["mods"] + + else: + msg = (f"{err_title}: The OpenGOAL settings file has an unknown version number ({settings_version}).\n" + f"{alt_instructions}") + ctx.on_log_error(logger, msg) + return + except KeyError as e: + msg = (f"{err_title}: The OpenGOAL settings file does not contain key entry {e}!\n" + f"{alt_instructions}") + ctx.on_log_error(logger, msg) + return + if not jak1_installed: msg = (f"{err_title}: The OpenGOAL Launcher is missing a normal install of Jak 1!\n" f"{alt_instructions}") ctx.on_log_error(logger, msg) return - mod_sources = load["games"]["Jak 1"]["modsInstalledVersion"] if mod_sources is None: msg = (f"{err_title}: No mod sources have been configured in the OpenGOAL Launcher!\n" f"{alt_instructions}") From f4b5422f66c0f5332cb05998ad0f7731d4a436f3 Mon Sep 17 00:00:00 2001 From: Remy Jette Date: Mon, 7 Jul 2025 13:57:55 -0700 Subject: [PATCH 04/15] Factorio: Fix link to world_gen documentation (#5171) --- worlds/factorio/Options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/factorio/Options.py b/worlds/factorio/Options.py index 12fc90c1fd..0a789669d5 100644 --- a/worlds/factorio/Options.py +++ b/worlds/factorio/Options.py @@ -321,7 +321,7 @@ class InventorySpillTrapCount(TrapCount): class FactorioWorldGen(OptionDict): """World Generation settings. Overview of options at https://wiki.factorio.com/Map_generator, - with in-depth documentation at https://lua-api.factorio.com/latest/Concepts.html#MapGenSettings""" + with in-depth documentation at https://lua-api.factorio.com/latest/concepts/MapGenSettings.html""" display_name = "World Generation" # FIXME: do we want default be a rando-optimized default or in-game DS? value: dict[str, dict[str, typing.Any]] From b1ff55dd061af9158c1747a57472d157e42ded92 Mon Sep 17 00:00:00 2001 From: axe-y <58866768+axe-y@users.noreply.github.com> Date: Thu, 10 Jul 2025 08:33:52 -0400 Subject: [PATCH 05/15] DLCQ: Fix/Refactor LFoD Start Inventory (#5176) --- worlds/dlcquest/Items.py | 40 +++++++++++----------------------------- 1 file changed, 11 insertions(+), 29 deletions(-) diff --git a/worlds/dlcquest/Items.py b/worlds/dlcquest/Items.py index 550d92419b..5496885a74 100644 --- a/worlds/dlcquest/Items.py +++ b/worlds/dlcquest/Items.py @@ -30,7 +30,6 @@ class Group(enum.Enum): Deprecated = enum.auto() - @dataclass(frozen=True) class ItemData: code_without_offset: offset @@ -98,14 +97,15 @@ def create_trap_items(world, world_options: Options.DLCQuestOptions, trap_needed return traps -def create_items(world, world_options: Options.DLCQuestOptions, locations_count: int, excluded_items: list[str], random: Random): +def create_items(world, world_options: Options.DLCQuestOptions, locations_count: int, excluded_items: list[str], + random: Random): created_items = [] if world_options.campaign == Options.Campaign.option_basic or world_options.campaign == Options.Campaign.option_both: - create_items_basic(world_options, created_items, world, excluded_items) + create_items_campaign(world_options, created_items, world, excluded_items, Group.DLCQuest, 825, 250) if (world_options.campaign == Options.Campaign.option_live_freemium_or_die or world_options.campaign == Options.Campaign.option_both): - create_items_lfod(world_options, created_items, world, excluded_items) + create_items_campaign(world_options, created_items, world, excluded_items, Group.Freemium, 889, 200) trap_items = create_trap_items(world, world_options, locations_count - len(created_items), random) created_items += trap_items @@ -113,27 +113,8 @@ def create_items(world, world_options: Options.DLCQuestOptions, locations_count: return created_items -def create_items_lfod(world_options, created_items, world, excluded_items): - for item in items_by_group[Group.Freemium]: - if item.name in excluded_items: - excluded_items.remove(item) - continue - - if item.has_any_group(Group.DLC): - created_items.append(world.create_item(item)) - if item.has_any_group(Group.Item) and world_options.item_shuffle == Options.ItemShuffle.option_shuffled: - created_items.append(world.create_item(item)) - if item.has_any_group(Group.Twice): - created_items.append(world.create_item(item)) - if world_options.coinsanity == Options.CoinSanity.option_coin: - if world_options.coinbundlequantity == -1: - create_coin_piece(created_items, world, 889, 200, Group.Freemium) - return - create_coin(world_options, created_items, world, 889, 200, Group.Freemium) - - -def create_items_basic(world_options, created_items, world, excluded_items): - for item in items_by_group[Group.DLCQuest]: +def create_items_campaign(world_options: Options.DLCQuestOptions, created_items: list[DLCQuestItem], world, excluded_items: list[str], group: Group, total_coins: int, required_coins: int): + for item in items_by_group[group]: if item.name in excluded_items: excluded_items.remove(item.name) continue @@ -146,14 +127,15 @@ def create_items_basic(world_options, created_items, world, excluded_items): created_items.append(world.create_item(item)) if world_options.coinsanity == Options.CoinSanity.option_coin: if world_options.coinbundlequantity == -1: - create_coin_piece(created_items, world, 825, 250, Group.DLCQuest) + create_coin_piece(created_items, world, total_coins, required_coins, group) return - create_coin(world_options, created_items, world, 825, 250, Group.DLCQuest) + create_coin(world_options, created_items, world, total_coins, required_coins, group) def create_coin(world_options, created_items, world, total_coins, required_coins, group): coin_bundle_required = math.ceil(required_coins / world_options.coinbundlequantity) - coin_bundle_useful = math.ceil((total_coins - coin_bundle_required * world_options.coinbundlequantity) / world_options.coinbundlequantity) + coin_bundle_useful = math.ceil( + (total_coins - coin_bundle_required * world_options.coinbundlequantity) / world_options.coinbundlequantity) for item in items_by_group[group]: if item.has_any_group(Group.Coin): for i in range(coin_bundle_required): @@ -165,7 +147,7 @@ def create_coin(world_options, created_items, world, total_coins, required_coins def create_coin_piece(created_items, world, total_coins, required_coins, group): for item in items_by_group[group]: if item.has_any_group(Group.Piece): - for i in range(required_coins*10): + for i in range(required_coins * 10): created_items.append(world.create_item(item)) for i in range((total_coins - required_coins) * 10): created_items.append(world.create_item(item, ItemClassification.useful)) From edc0c89753b6e5283d7289d6e60c5e050e5d8303 Mon Sep 17 00:00:00 2001 From: Carter Hesterman Date: Thu, 10 Jul 2025 07:10:56 -0600 Subject: [PATCH 06/15] CIV 6: Remove Erroneous Boost Prereqs for Computers Boost (#5134) --- worlds/civ_6/data/boosts.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/worlds/civ_6/data/boosts.py b/worlds/civ_6/data/boosts.py index a397720815..49cedfdfd9 100644 --- a/worlds/civ_6/data/boosts.py +++ b/worlds/civ_6/data/boosts.py @@ -78,8 +78,8 @@ boosts: List[CivVIBoostData] = [ CivVIBoostData( "BOOST_TECH_IRON_WORKING", "ERA_CLASSICAL", - ["TECH_MINING"], - 1, + ["TECH_MINING", "TECH_BRONZE_WORKING"], + 2, "DEFAULT", ), CivVIBoostData( @@ -165,15 +165,9 @@ boosts: List[CivVIBoostData] = [ "BOOST_TECH_CASTLES", "ERA_MEDIEVAL", [ - "CIVIC_DIVINE_RIGHT", - "CIVIC_EXPLORATION", - "CIVIC_REFORMED_CHURCH", "CIVIC_SUFFRAGE", "CIVIC_TOTALITARIANISM", "CIVIC_CLASS_STRUGGLE", - "CIVIC_DIGITAL_DEMOCRACY", - "CIVIC_CORPORATE_LIBERTARIANISM", - "CIVIC_SYNTHETIC_TECHNOCRACY", ], 1, "DEFAULT", @@ -393,9 +387,6 @@ boosts: List[CivVIBoostData] = [ "CIVIC_SUFFRAGE", "CIVIC_TOTALITARIANISM", "CIVIC_CLASS_STRUGGLE", - "CIVIC_DIGITAL_DEMOCRACY", - "CIVIC_CORPORATE_LIBERTARIANISM", - "CIVIC_SYNTHETIC_TECHNOCRACY", ], 1, "DEFAULT", From 2974f7d11f57e97da00a568b1c03a670fd8938d0 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Fri, 11 Jul 2025 19:27:28 +0200 Subject: [PATCH 07/15] Core: Replace Clique with V6 in unit tests (#5181) * replace Clique with V6 in unit tests * no hard mode in V6 * modify regex in copy_world to allow : str * oops * I see now * work around all typing * there actually needs to be something --- .github/workflows/build.yml | 4 ++-- test/hosting/__main__.py | 8 ++++---- test/hosting/generate.py | 2 +- test/hosting/world.py | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d6b80965f0..07ae1136fc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -98,7 +98,7 @@ jobs: shell: bash run: | cd build/exe* - cp Players/Templates/Clique.yaml Players/ + cp Players/Templates/VVVVVV.yaml Players/ timeout 30 ./ArchipelagoGenerate - name: Store 7z uses: actions/upload-artifact@v4 @@ -189,7 +189,7 @@ jobs: shell: bash run: | cd build/exe* - cp Players/Templates/Clique.yaml Players/ + cp Players/Templates/VVVVVV.yaml Players/ timeout 30 ./ArchipelagoGenerate - name: Store AppImage uses: actions/upload-artifact@v4 diff --git a/test/hosting/__main__.py b/test/hosting/__main__.py index 6640c637b5..e235d7bb72 100644 --- a/test/hosting/__main__.py +++ b/test/hosting/__main__.py @@ -63,12 +63,12 @@ if __name__ == "__main__": spacer = '=' * 80 with TemporaryDirectory() as tempdir: - multis = [["Clique"], ["Temp World"], ["Clique", "Temp World"]] + multis = [["VVVVVV"], ["Temp World"], ["VVVVVV", "Temp World"]] p1_games = [] data_paths = [] rooms = [] - copy_world("Clique", "Temp World") + copy_world("VVVVVV", "Temp World") try: for n, games in enumerate(multis, 1): print(f"Generating [{n}] {', '.join(games)}") @@ -101,7 +101,7 @@ if __name__ == "__main__": with Client(host.address, game, "Player1") as client: local_data_packages = client.games_packages local_collected_items = len(client.checked_locations) - if collected_items < 2: # Clique only has 2 Locations + if collected_items < 2: # Don't collect anything on the last iteration client.collect_any() # TODO: Ctrl+C test here as well @@ -125,7 +125,7 @@ if __name__ == "__main__": with Client(host.address, game, "Player1") as client: web_data_packages = client.games_packages web_collected_items = len(client.checked_locations) - if collected_items < 2: # Clique only has 2 Locations + if collected_items < 2: # Don't collect anything on the last iteration client.collect_any() if collected_items == 1: sleep(1) # wait for the server to collect the item diff --git a/test/hosting/generate.py b/test/hosting/generate.py index d5d39dc95e..e90868eb6f 100644 --- a/test/hosting/generate.py +++ b/test/hosting/generate.py @@ -34,7 +34,7 @@ def _generate_local_inner(games: Iterable[str], f.write(json.dumps({ "name": f"Player{n}", "game": game, - game: {"hard_mode": "true"}, + game: {}, "description": f"generate_local slot {n} ('Player{n}'): {game}", })) diff --git a/test/hosting/world.py b/test/hosting/world.py index 7412641201..cd53453c10 100644 --- a/test/hosting/world.py +++ b/test/hosting/world.py @@ -30,7 +30,7 @@ def copy(src: str, dst: str) -> None: _new_worlds[dst] = str(dst_folder) with open(dst_folder / "__init__.py", "r", encoding="utf-8-sig") as f: contents = f.read() - contents = re.sub(r'game\s*=\s*[\'"]' + re.escape(src) + r'[\'"]', f'game = "{dst}"', contents) + contents = re.sub(r'game\s*(:\s*[a-zA-Z\[\]]+)?\s*=\s*[\'"]' + re.escape(src) + r'[\'"]', f'game = "{dst}"', contents) with open(dst_folder / "__init__.py", "w", encoding="utf-8") as f: f.write(contents) From 6af34b66fb166af42f73879152c1a030ff2423f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zach=20=E2=80=9CPhar=E2=80=9D=20Parks?= <11338376+ThePhar@users.noreply.github.com> Date: Fri, 11 Jul 2025 12:34:46 -0500 Subject: [PATCH 08/15] Various: Remove Rogue Legacy and Clique (#5177) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Various: Remove Rogue Legacy and Clique * Remove Clique from setup.py and revert network diagram.md change. * Try again. * Update network diagram.md --------- Co-authored-by: Zach “Phar” Parks Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- README.md | 2 - docs/CODEOWNERS | 6 - docs/network diagram/network diagram.md | 4 +- setup.py | 1 - worlds/clique/Items.py | 38 -- worlds/clique/Locations.py | 37 -- worlds/clique/Options.py | 34 -- worlds/clique/Regions.py | 11 - worlds/clique/Rules.py | 13 - worlds/clique/__init__.py | 102 ------ worlds/clique/docs/de_Clique.md | 18 - worlds/clique/docs/en_Clique.md | 16 - worlds/clique/docs/guide_de.md | 25 -- worlds/clique/docs/guide_en.md | 22 -- worlds/rogue_legacy/Items.py | 111 ------ worlds/rogue_legacy/Locations.py | 94 ----- worlds/rogue_legacy/Options.py | 387 -------------------- worlds/rogue_legacy/Presets.py | 61 --- worlds/rogue_legacy/Regions.py | 114 ------ worlds/rogue_legacy/Rules.py | 117 ------ worlds/rogue_legacy/__init__.py | 243 ------------ worlds/rogue_legacy/docs/en_Rogue Legacy.md | 34 -- worlds/rogue_legacy/docs/rogue-legacy_en.md | 35 -- worlds/rogue_legacy/test/TestUnique.py | 23 -- worlds/rogue_legacy/test/__init__.py | 5 - 25 files changed, 1 insertion(+), 1552 deletions(-) delete mode 100644 worlds/clique/Items.py delete mode 100644 worlds/clique/Locations.py delete mode 100644 worlds/clique/Options.py delete mode 100644 worlds/clique/Regions.py delete mode 100644 worlds/clique/Rules.py delete mode 100644 worlds/clique/__init__.py delete mode 100644 worlds/clique/docs/de_Clique.md delete mode 100644 worlds/clique/docs/en_Clique.md delete mode 100644 worlds/clique/docs/guide_de.md delete mode 100644 worlds/clique/docs/guide_en.md delete mode 100644 worlds/rogue_legacy/Items.py delete mode 100644 worlds/rogue_legacy/Locations.py delete mode 100644 worlds/rogue_legacy/Options.py delete mode 100644 worlds/rogue_legacy/Presets.py delete mode 100644 worlds/rogue_legacy/Regions.py delete mode 100644 worlds/rogue_legacy/Rules.py delete mode 100644 worlds/rogue_legacy/__init__.py delete mode 100644 worlds/rogue_legacy/docs/en_Rogue Legacy.md delete mode 100644 worlds/rogue_legacy/docs/rogue-legacy_en.md delete mode 100644 worlds/rogue_legacy/test/TestUnique.py delete mode 100644 worlds/rogue_legacy/test/__init__.py diff --git a/README.md b/README.md index afad4b15f0..29b6206a00 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,6 @@ Currently, the following games are supported: * Super Metroid * Secret of Evermore * Final Fantasy -* Rogue Legacy * VVVVVV * Raft * Super Mario 64 @@ -41,7 +40,6 @@ Currently, the following games are supported: * The Messenger * Kingdom Hearts 2 * The Legend of Zelda: Link's Awakening DX -* Clique * Adventure * DLC Quest * Noita diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS index 4bdd49b629..3104200a6c 100644 --- a/docs/CODEOWNERS +++ b/docs/CODEOWNERS @@ -48,9 +48,6 @@ # Civilization VI /worlds/civ6/ @hesto2 -# Clique -/worlds/clique/ @ThePhar - # Dark Souls III /worlds/dark_souls_3/ @Marechal-L @nex3 @@ -148,9 +145,6 @@ # Raft /worlds/raft/ @SunnyBat -# Rogue Legacy -/worlds/rogue_legacy/ @ThePhar - # Risk of Rain 2 /worlds/ror2/ @kindasneaki diff --git a/docs/network diagram/network diagram.md b/docs/network diagram/network diagram.md index 0e31eaa3e4..2d0a1174f5 100644 --- a/docs/network diagram/network diagram.md +++ b/docs/network diagram/network diagram.md @@ -125,10 +125,8 @@ flowchart LR NM[Mod with Archipelago.MultiClient.Net] subgraph FNA/XNA TS[Timespinner] - RL[Rogue Legacy] end NM <-- TsRandomizer --> TS - NM <-- RogueLegacyRandomizer --> RL subgraph Unity ROR[Risk of Rain 2] SN[Subnautica] @@ -177,4 +175,4 @@ flowchart LR FMOD <--> FMAPI end CC <-- Integrated --> FC -``` \ No newline at end of file +``` diff --git a/setup.py b/setup.py index 959746717a..cd1b1e8710 100644 --- a/setup.py +++ b/setup.py @@ -63,7 +63,6 @@ non_apworlds: set[str] = { "Adventure", "ArchipIDLE", "Archipelago", - "Clique", "Lufia II Ancient Cave", "Meritous", "Ocarina of Time", diff --git a/worlds/clique/Items.py b/worlds/clique/Items.py deleted file mode 100644 index 81e2540bac..0000000000 --- a/worlds/clique/Items.py +++ /dev/null @@ -1,38 +0,0 @@ -from typing import Callable, Dict, NamedTuple, Optional, TYPE_CHECKING - -from BaseClasses import Item, ItemClassification - -if TYPE_CHECKING: - from . import CliqueWorld - - -class CliqueItem(Item): - game = "Clique" - - -class CliqueItemData(NamedTuple): - code: Optional[int] = None - type: ItemClassification = ItemClassification.filler - can_create: Callable[["CliqueWorld"], bool] = lambda world: True - - -item_data_table: Dict[str, CliqueItemData] = { - "Feeling of Satisfaction": CliqueItemData( - code=69696969, - type=ItemClassification.progression, - ), - "Button Activation": CliqueItemData( - code=69696968, - type=ItemClassification.progression, - can_create=lambda world: world.options.hard_mode, - ), - "A Cool Filler Item (No Satisfaction Guaranteed)": CliqueItemData( - code=69696967, - can_create=lambda world: False # Only created from `get_filler_item_name`. - ), - "The Urge to Push": CliqueItemData( - type=ItemClassification.progression, - ), -} - -item_table = {name: data.code for name, data in item_data_table.items() if data.code is not None} diff --git a/worlds/clique/Locations.py b/worlds/clique/Locations.py deleted file mode 100644 index 900b497eb4..0000000000 --- a/worlds/clique/Locations.py +++ /dev/null @@ -1,37 +0,0 @@ -from typing import Callable, Dict, NamedTuple, Optional, TYPE_CHECKING - -from BaseClasses import Location - -if TYPE_CHECKING: - from . import CliqueWorld - - -class CliqueLocation(Location): - game = "Clique" - - -class CliqueLocationData(NamedTuple): - region: str - address: Optional[int] = None - can_create: Callable[["CliqueWorld"], bool] = lambda world: True - locked_item: Optional[str] = None - - -location_data_table: Dict[str, CliqueLocationData] = { - "The Big Red Button": CliqueLocationData( - region="The Button Realm", - address=69696969, - ), - "The Item on the Desk": CliqueLocationData( - region="The Button Realm", - address=69696968, - can_create=lambda world: world.options.hard_mode, - ), - "In the Player's Mind": CliqueLocationData( - region="The Button Realm", - locked_item="The Urge to Push", - ), -} - -location_table = {name: data.address for name, data in location_data_table.items() if data.address is not None} -locked_locations = {name: data for name, data in location_data_table.items() if data.locked_item} diff --git a/worlds/clique/Options.py b/worlds/clique/Options.py deleted file mode 100644 index d88a128990..0000000000 --- a/worlds/clique/Options.py +++ /dev/null @@ -1,34 +0,0 @@ -from dataclasses import dataclass -from Options import Choice, Toggle, PerGameCommonOptions, StartInventoryPool - - -class HardMode(Toggle): - """Only for the most masochistically inclined... Requires button activation!""" - display_name = "Hard Mode" - - -class ButtonColor(Choice): - """Customize your button! Now available in 12 unique colors.""" - display_name = "Button Color" - option_red = 0 - option_orange = 1 - option_yellow = 2 - option_green = 3 - option_cyan = 4 - option_blue = 5 - option_magenta = 6 - option_purple = 7 - option_pink = 8 - option_brown = 9 - option_white = 10 - option_black = 11 - - -@dataclass -class CliqueOptions(PerGameCommonOptions): - color: ButtonColor - hard_mode: HardMode - start_inventory_from_pool: StartInventoryPool - - # DeathLink is always on. Always. - # death_link: DeathLink diff --git a/worlds/clique/Regions.py b/worlds/clique/Regions.py deleted file mode 100644 index 04e317067f..0000000000 --- a/worlds/clique/Regions.py +++ /dev/null @@ -1,11 +0,0 @@ -from typing import Dict, List, NamedTuple - - -class CliqueRegionData(NamedTuple): - connecting_regions: List[str] = [] - - -region_data_table: Dict[str, CliqueRegionData] = { - "Menu": CliqueRegionData(["The Button Realm"]), - "The Button Realm": CliqueRegionData(), -} diff --git a/worlds/clique/Rules.py b/worlds/clique/Rules.py deleted file mode 100644 index 63ecd4e9e1..0000000000 --- a/worlds/clique/Rules.py +++ /dev/null @@ -1,13 +0,0 @@ -from typing import Callable, TYPE_CHECKING - -from BaseClasses import CollectionState - -if TYPE_CHECKING: - from . import CliqueWorld - - -def get_button_rule(world: "CliqueWorld") -> Callable[[CollectionState], bool]: - if world.options.hard_mode: - return lambda state: state.has("Button Activation", world.player) - - return lambda state: True diff --git a/worlds/clique/__init__.py b/worlds/clique/__init__.py deleted file mode 100644 index 70777c51b0..0000000000 --- a/worlds/clique/__init__.py +++ /dev/null @@ -1,102 +0,0 @@ -from typing import List, Dict, Any - -from BaseClasses import Region, Tutorial -from worlds.AutoWorld import WebWorld, World -from .Items import CliqueItem, item_data_table, item_table -from .Locations import CliqueLocation, location_data_table, location_table, locked_locations -from .Options import CliqueOptions -from .Regions import region_data_table -from .Rules import get_button_rule - - -class CliqueWebWorld(WebWorld): - theme = "partyTime" - - setup_en = Tutorial( - tutorial_name="Start Guide", - description="A guide to playing Clique.", - language="English", - file_name="guide_en.md", - link="guide/en", - authors=["Phar"] - ) - - setup_de = Tutorial( - tutorial_name="Anleitung zum Anfangen", - description="Eine Anleitung um Clique zu spielen.", - language="Deutsch", - file_name="guide_de.md", - link="guide/de", - authors=["Held_der_Zeit"] - ) - - tutorials = [setup_en, setup_de] - game_info_languages = ["en", "de"] - - -class CliqueWorld(World): - """The greatest game of all time.""" - - game = "Clique" - web = CliqueWebWorld() - options: CliqueOptions - options_dataclass = CliqueOptions - location_name_to_id = location_table - item_name_to_id = item_table - - def create_item(self, name: str) -> CliqueItem: - return CliqueItem(name, item_data_table[name].type, item_data_table[name].code, self.player) - - def create_items(self) -> None: - item_pool: List[CliqueItem] = [] - for name, item in item_data_table.items(): - if item.code and item.can_create(self): - item_pool.append(self.create_item(name)) - - self.multiworld.itempool += item_pool - - def create_regions(self) -> None: - # Create regions. - for region_name in region_data_table.keys(): - region = Region(region_name, self.player, self.multiworld) - self.multiworld.regions.append(region) - - # Create locations. - for region_name, region_data in region_data_table.items(): - region = self.get_region(region_name) - region.add_locations({ - location_name: location_data.address for location_name, location_data in location_data_table.items() - if location_data.region == region_name and location_data.can_create(self) - }, CliqueLocation) - region.add_exits(region_data_table[region_name].connecting_regions) - - # Place locked locations. - for location_name, location_data in locked_locations.items(): - # Ignore locations we never created. - if not location_data.can_create(self): - continue - - locked_item = self.create_item(location_data_table[location_name].locked_item) - self.get_location(location_name).place_locked_item(locked_item) - - # Set priority location for the Big Red Button! - self.options.priority_locations.value.add("The Big Red Button") - - def get_filler_item_name(self) -> str: - return "A Cool Filler Item (No Satisfaction Guaranteed)" - - def set_rules(self) -> None: - button_rule = get_button_rule(self) - self.get_location("The Big Red Button").access_rule = button_rule - self.get_location("In the Player's Mind").access_rule = button_rule - - # Do not allow button activations on buttons. - self.get_location("The Big Red Button").item_rule = lambda item: item.name != "Button Activation" - - # Completion condition. - self.multiworld.completion_condition[self.player] = lambda state: state.has("The Urge to Push", self.player) - - def fill_slot_data(self) -> Dict[str, Any]: - return { - "color": self.options.color.current_key - } diff --git a/worlds/clique/docs/de_Clique.md b/worlds/clique/docs/de_Clique.md deleted file mode 100644 index cde0a23cf6..0000000000 --- a/worlds/clique/docs/de_Clique.md +++ /dev/null @@ -1,18 +0,0 @@ -# Clique - -## Was ist das für ein Spiel? - -~~Clique ist ein psychologisches Überlebens-Horror Spiel, in dem der Spieler der Versuchung wiederstehen muss große~~ -~~(rote) Knöpfe zu drücken.~~ - -Clique ist ein scherzhaftes Spiel, welches für Archipelago im März 2023 entwickelt wurde, um zu zeigen, wie einfach -es sein kann eine Welt für Archipelago zu entwicklen. Das Ziel des Spiels ist es den großen (standardmäßig) roten -Knopf zu drücken. Wenn ein Spieler auf dem `hard_mode` (schwieriger Modus) spielt, muss dieser warten bis jemand -anderes in der Multiworld den Knopf aktiviert, damit er gedrückt werden kann. - -Clique kann auf den meisten modernen, HTML5-fähigen Browsern gespielt werden. - -## Wo ist die Seite für die Einstellungen? - -Die [Seite für die Spielereinstellungen dieses Spiels](../player-options) enthält alle Optionen die man benötigt um -eine YAML-Datei zu konfigurieren und zu exportieren. diff --git a/worlds/clique/docs/en_Clique.md b/worlds/clique/docs/en_Clique.md deleted file mode 100644 index e9cb164fec..0000000000 --- a/worlds/clique/docs/en_Clique.md +++ /dev/null @@ -1,16 +0,0 @@ -# Clique - -## What is this game? - -~~Clique is a psychological survival horror game where a player must survive the temptation to press red buttons.~~ - -Clique is a joke game developed for Archipelago in March 2023 to showcase how easy it can be to develop a world for -Archipelago. The objective of the game is to press the big red button. If a player is playing on `hard_mode`, they must -wait for someone else in the multiworld to "activate" their button before they can press it. - -Clique can be played on most modern HTML5-capable browsers. - -## Where is the options page? - -The [player options page for this game](../player-options) contains all the options you need to configure -and export a config file. diff --git a/worlds/clique/docs/guide_de.md b/worlds/clique/docs/guide_de.md deleted file mode 100644 index 26e08dbbdd..0000000000 --- a/worlds/clique/docs/guide_de.md +++ /dev/null @@ -1,25 +0,0 @@ -# Clique Anleitung - -Nachdem dein Seed generiert wurde, gehe auf die Website von [Clique dem Spiel](http://clique.pharware.com/) und gib -Server-Daten, deinen Slot-Namen und ein Passwort (falls vorhanden) ein. Klicke dann auf "Connect" (Verbinden). - -Wenn du auf "Einfach" spielst, kannst du unbedenklich den Knopf drücken und deine "Befriedigung" erhalten. - -Wenn du auf "Schwer" spielst, ist es sehr wahrscheinlich, dass du warten musst bevor du dein Ziel erreichen kannst. -Glücklicherweise läuft Click auf den meißten großen Browsern, die HTML5 unterstützen. Das heißt du kannst Clique auf -deinem Handy starten und produktiv sein während du wartest! - -Falls du einige Ideen brauchst was du tun kannst, während du wartest bis der Knopf aktiviert wurde, versuche -(mindestens) eins der Folgenden: - -- Dein Zimmer aufräumen. -- Die Wäsche machen. -- Etwas Essen von einem X-Belieben Fast Food Restaruant holen. -- Das tägliche Wordle machen. -- ~~Deine Seele an **Phar** verkaufen.~~ -- Deine Hausaufgaben erledigen. -- Deine Post abholen. - - -~~Solltest du auf irgendwelche Probleme in diesem Spiel stoßen, solltest du keinesfalls nicht **thephar** auf~~ -~~Discord kontaktieren. *zwinker* *zwinker*~~ diff --git a/worlds/clique/docs/guide_en.md b/worlds/clique/docs/guide_en.md deleted file mode 100644 index c3c113fe90..0000000000 --- a/worlds/clique/docs/guide_en.md +++ /dev/null @@ -1,22 +0,0 @@ -# Clique Start Guide - -After rolling your seed, go to the [Clique Game](http://clique.pharware.com/) site and enter the server details, your -slot name, and a room password if one is required. Then click "Connect". - -If you're playing on "easy mode", just click the button and receive "Satisfaction". - -If you're playing on "hard mode", you may need to wait for activation before you can complete your objective. Luckily, -Clique runs in most major browsers that support HTML5, so you can load Clique on your phone and be productive while -you wait! - -If you need some ideas for what to do while waiting for button activation, give the following a try: - -- Clean your room. -- Wash the dishes. -- Get some food from a non-descript fast food restaurant. -- Do the daily Wordle. -- ~~Sell your soul to Phar.~~ -- Do your school work. - - -~~If you run into any issues with this game, definitely do not contact **thephar** on discord. *wink* *wink*~~ diff --git a/worlds/rogue_legacy/Items.py b/worlds/rogue_legacy/Items.py deleted file mode 100644 index efa24df05a..0000000000 --- a/worlds/rogue_legacy/Items.py +++ /dev/null @@ -1,111 +0,0 @@ -from typing import Dict, NamedTuple, Optional - -from BaseClasses import Item, ItemClassification - - -class RLItem(Item): - game: str = "Rogue Legacy" - - -class RLItemData(NamedTuple): - category: str - code: Optional[int] = None - classification: ItemClassification = ItemClassification.filler - max_quantity: int = 1 - weight: int = 1 - - -def get_items_by_category(category: str) -> Dict[str, RLItemData]: - item_dict: Dict[str, RLItemData] = {} - for name, data in item_table.items(): - if data.category == category: - item_dict.setdefault(name, data) - - return item_dict - - -item_table: Dict[str, RLItemData] = { - # Vendors - "Blacksmith": RLItemData("Vendors", 90_000, ItemClassification.progression), - "Enchantress": RLItemData("Vendors", 90_001, ItemClassification.progression), - "Architect": RLItemData("Vendors", 90_002, ItemClassification.useful), - - # Classes - "Progressive Knights": RLItemData("Classes", 90_003, ItemClassification.useful, 2), - "Progressive Mages": RLItemData("Classes", 90_004, ItemClassification.useful, 2), - "Progressive Barbarians": RLItemData("Classes", 90_005, ItemClassification.useful, 2), - "Progressive Knaves": RLItemData("Classes", 90_006, ItemClassification.useful, 2), - "Progressive Shinobis": RLItemData("Classes", 90_007, ItemClassification.useful, 2), - "Progressive Miners": RLItemData("Classes", 90_008, ItemClassification.useful, 2), - "Progressive Liches": RLItemData("Classes", 90_009, ItemClassification.useful, 2), - "Progressive Spellthieves": RLItemData("Classes", 90_010, ItemClassification.useful, 2), - "Dragons": RLItemData("Classes", 90_096, ItemClassification.progression), - "Traitors": RLItemData("Classes", 90_097, ItemClassification.useful), - - # Skills - "Health Up": RLItemData("Skills", 90_013, ItemClassification.progression_skip_balancing, 15), - "Mana Up": RLItemData("Skills", 90_014, ItemClassification.progression_skip_balancing, 15), - "Attack Up": RLItemData("Skills", 90_015, ItemClassification.progression_skip_balancing, 15), - "Magic Damage Up": RLItemData("Skills", 90_016, ItemClassification.progression_skip_balancing, 15), - "Armor Up": RLItemData("Skills", 90_017, ItemClassification.useful, 15), - "Equip Up": RLItemData("Skills", 90_018, ItemClassification.useful, 5), - "Crit Chance Up": RLItemData("Skills", 90_019, ItemClassification.useful, 5), - "Crit Damage Up": RLItemData("Skills", 90_020, ItemClassification.useful, 5), - "Down Strike Up": RLItemData("Skills", 90_021), - "Gold Gain Up": RLItemData("Skills", 90_022), - "Potion Efficiency Up": RLItemData("Skills", 90_023), - "Invulnerability Time Up": RLItemData("Skills", 90_024), - "Mana Cost Down": RLItemData("Skills", 90_025), - "Death Defiance": RLItemData("Skills", 90_026, ItemClassification.useful), - "Haggling": RLItemData("Skills", 90_027, ItemClassification.useful), - "Randomize Children": RLItemData("Skills", 90_028, ItemClassification.useful), - - # Blueprints - "Progressive Blueprints": RLItemData("Blueprints", 90_055, ItemClassification.useful, 15), - "Squire Blueprints": RLItemData("Blueprints", 90_040, ItemClassification.useful), - "Silver Blueprints": RLItemData("Blueprints", 90_041, ItemClassification.useful), - "Guardian Blueprints": RLItemData("Blueprints", 90_042, ItemClassification.useful), - "Imperial Blueprints": RLItemData("Blueprints", 90_043, ItemClassification.useful), - "Royal Blueprints": RLItemData("Blueprints", 90_044, ItemClassification.useful), - "Knight Blueprints": RLItemData("Blueprints", 90_045, ItemClassification.useful), - "Ranger Blueprints": RLItemData("Blueprints", 90_046, ItemClassification.useful), - "Sky Blueprints": RLItemData("Blueprints", 90_047, ItemClassification.useful), - "Dragon Blueprints": RLItemData("Blueprints", 90_048, ItemClassification.useful), - "Slayer Blueprints": RLItemData("Blueprints", 90_049, ItemClassification.useful), - "Blood Blueprints": RLItemData("Blueprints", 90_050, ItemClassification.useful), - "Sage Blueprints": RLItemData("Blueprints", 90_051, ItemClassification.useful), - "Retribution Blueprints": RLItemData("Blueprints", 90_052, ItemClassification.useful), - "Holy Blueprints": RLItemData("Blueprints", 90_053, ItemClassification.useful), - "Dark Blueprints": RLItemData("Blueprints", 90_054, ItemClassification.useful), - - # Runes - "Vault Runes": RLItemData("Runes", 90_060, ItemClassification.progression), - "Sprint Runes": RLItemData("Runes", 90_061, ItemClassification.progression), - "Vampire Runes": RLItemData("Runes", 90_062, ItemClassification.useful), - "Sky Runes": RLItemData("Runes", 90_063, ItemClassification.progression), - "Siphon Runes": RLItemData("Runes", 90_064, ItemClassification.useful), - "Retaliation Runes": RLItemData("Runes", 90_065), - "Bounty Runes": RLItemData("Runes", 90_066), - "Haste Runes": RLItemData("Runes", 90_067), - "Curse Runes": RLItemData("Runes", 90_068), - "Grace Runes": RLItemData("Runes", 90_069), - "Balance Runes": RLItemData("Runes", 90_070, ItemClassification.useful), - - # Junk - "Triple Stat Increase": RLItemData("Filler", 90_030, weight=6), - "1000 Gold": RLItemData("Filler", 90_031, weight=3), - "3000 Gold": RLItemData("Filler", 90_032, weight=2), - "5000 Gold": RLItemData("Filler", 90_033, weight=1), -} - -event_item_table: Dict[str, RLItemData] = { - "Defeat Khidr": RLItemData("Event", classification=ItemClassification.progression), - "Defeat Alexander": RLItemData("Event", classification=ItemClassification.progression), - "Defeat Ponce de Leon": RLItemData("Event", classification=ItemClassification.progression), - "Defeat Herodotus": RLItemData("Event", classification=ItemClassification.progression), - "Defeat Neo Khidr": RLItemData("Event", classification=ItemClassification.progression), - "Defeat Alexander IV": RLItemData("Event", classification=ItemClassification.progression), - "Defeat Ponce de Freon": RLItemData("Event", classification=ItemClassification.progression), - "Defeat Astrodotus": RLItemData("Event", classification=ItemClassification.progression), - "Defeat The Fountain": RLItemData("Event", classification=ItemClassification.progression), -} diff --git a/worlds/rogue_legacy/Locations.py b/worlds/rogue_legacy/Locations.py deleted file mode 100644 index db9e1db3b0..0000000000 --- a/worlds/rogue_legacy/Locations.py +++ /dev/null @@ -1,94 +0,0 @@ -from typing import Dict, NamedTuple, Optional - -from BaseClasses import Location - - -class RLLocation(Location): - game: str = "Rogue Legacy" - - -class RLLocationData(NamedTuple): - category: str - code: Optional[int] = None - - -def get_locations_by_category(category: str) -> Dict[str, RLLocationData]: - location_dict: Dict[str, RLLocationData] = {} - for name, data in location_table.items(): - if data.category == category: - location_dict.setdefault(name, data) - - return location_dict - - -location_table: Dict[str, RLLocationData] = { - # Manor Renovation - "Manor - Ground Road": RLLocationData("Manor", 91_000), - "Manor - Main Base": RLLocationData("Manor", 91_001), - "Manor - Main Bottom Window": RLLocationData("Manor", 91_002), - "Manor - Main Top Window": RLLocationData("Manor", 91_003), - "Manor - Main Rooftop": RLLocationData("Manor", 91_004), - "Manor - Left Wing Base": RLLocationData("Manor", 91_005), - "Manor - Left Wing Window": RLLocationData("Manor", 91_006), - "Manor - Left Wing Rooftop": RLLocationData("Manor", 91_007), - "Manor - Left Big Base": RLLocationData("Manor", 91_008), - "Manor - Left Big Upper 1": RLLocationData("Manor", 91_009), - "Manor - Left Big Upper 2": RLLocationData("Manor", 91_010), - "Manor - Left Big Windows": RLLocationData("Manor", 91_011), - "Manor - Left Big Rooftop": RLLocationData("Manor", 91_012), - "Manor - Left Far Base": RLLocationData("Manor", 91_013), - "Manor - Left Far Roof": RLLocationData("Manor", 91_014), - "Manor - Left Extension": RLLocationData("Manor", 91_015), - "Manor - Left Tree 1": RLLocationData("Manor", 91_016), - "Manor - Left Tree 2": RLLocationData("Manor", 91_017), - "Manor - Right Wing Base": RLLocationData("Manor", 91_018), - "Manor - Right Wing Window": RLLocationData("Manor", 91_019), - "Manor - Right Wing Rooftop": RLLocationData("Manor", 91_020), - "Manor - Right Big Base": RLLocationData("Manor", 91_021), - "Manor - Right Big Upper": RLLocationData("Manor", 91_022), - "Manor - Right Big Rooftop": RLLocationData("Manor", 91_023), - "Manor - Right High Base": RLLocationData("Manor", 91_024), - "Manor - Right High Upper": RLLocationData("Manor", 91_025), - "Manor - Right High Tower": RLLocationData("Manor", 91_026), - "Manor - Right Extension": RLLocationData("Manor", 91_027), - "Manor - Right Tree": RLLocationData("Manor", 91_028), - "Manor - Observatory Base": RLLocationData("Manor", 91_029), - "Manor - Observatory Telescope": RLLocationData("Manor", 91_030), - - # Boss Rewards - "Castle Hamson Boss Reward": RLLocationData("Boss", 91_100), - "Forest Abkhazia Boss Reward": RLLocationData("Boss", 91_102), - "The Maya Boss Reward": RLLocationData("Boss", 91_104), - "Land of Darkness Boss Reward": RLLocationData("Boss", 91_106), - - # Special Locations - "Jukebox": RLLocationData("Special", 91_200), - "Painting": RLLocationData("Special", 91_201), - "Cheapskate Elf's Game": RLLocationData("Special", 91_202), - "Carnival": RLLocationData("Special", 91_203), - - # Diaries - **{f"Diary {i+1}": RLLocationData("Diary", 91_300 + i) for i in range(0, 25)}, - - # Chests - **{f"Castle Hamson - Chest {i+1}": RLLocationData("Chests", 91_600 + i) for i in range(0, 50)}, - **{f"Forest Abkhazia - Chest {i+1}": RLLocationData("Chests", 91_700 + i) for i in range(0, 50)}, - **{f"The Maya - Chest {i+1}": RLLocationData("Chests", 91_800 + i) for i in range(0, 50)}, - **{f"Land of Darkness - Chest {i+1}": RLLocationData("Chests", 91_900 + i) for i in range(0, 50)}, - **{f"Chest {i+1}": RLLocationData("Chests", 92_000 + i) for i in range(0, 200)}, - - # Fairy Chests - **{f"Castle Hamson - Fairy Chest {i+1}": RLLocationData("Fairies", 91_400 + i) for i in range(0, 15)}, - **{f"Forest Abkhazia - Fairy Chest {i+1}": RLLocationData("Fairies", 91_450 + i) for i in range(0, 15)}, - **{f"The Maya - Fairy Chest {i+1}": RLLocationData("Fairies", 91_500 + i) for i in range(0, 15)}, - **{f"Land of Darkness - Fairy Chest {i+1}": RLLocationData("Fairies", 91_550 + i) for i in range(0, 15)}, - **{f"Fairy Chest {i+1}": RLLocationData("Fairies", 92_200 + i) for i in range(0, 60)}, -} - -event_location_table: Dict[str, RLLocationData] = { - "Castle Hamson Boss Room": RLLocationData("Event"), - "Forest Abkhazia Boss Room": RLLocationData("Event"), - "The Maya Boss Room": RLLocationData("Event"), - "Land of Darkness Boss Room": RLLocationData("Event"), - "Fountain Room": RLLocationData("Event"), -} diff --git a/worlds/rogue_legacy/Options.py b/worlds/rogue_legacy/Options.py deleted file mode 100644 index 139ff60944..0000000000 --- a/worlds/rogue_legacy/Options.py +++ /dev/null @@ -1,387 +0,0 @@ -from Options import Choice, Range, Toggle, DeathLink, DefaultOnToggle, OptionSet, PerGameCommonOptions - -from dataclasses import dataclass - - -class StartingGender(Choice): - """ - Determines the gender of your initial 'Sir Lee' character. - """ - display_name = "Starting Gender" - option_sir = 0 - option_lady = 1 - alias_male = 0 - alias_female = 1 - default = "random" - - -class StartingClass(Choice): - """ - Determines the starting class of your initial 'Sir Lee' character. - """ - display_name = "Starting Class" - option_knight = 0 - option_mage = 1 - option_barbarian = 2 - option_knave = 3 - option_shinobi = 4 - option_miner = 5 - option_spellthief = 6 - option_lich = 7 - default = 0 - - -class NewGamePlus(Choice): - """ - Puts the castle in new game plus mode which vastly increases enemy level, but increases gold gain by 50%. Not - recommended for those inexperienced to Rogue Legacy! - """ - display_name = "New Game Plus" - option_normal = 0 - option_new_game_plus = 1 - option_new_game_plus_2 = 2 - alias_hard = 1 - alias_brutal = 2 - default = 0 - - -class LevelScaling(Range): - """ - A percentage modifier for scaling enemy level as you continue throughout the castle. 100 means enemies will have - 100% level scaling (normal). Setting this too high will result in enemies with absurdly high levels, you have been - warned. - """ - display_name = "Enemy Level Scaling Percentage" - range_start = 1 - range_end = 300 - default = 100 - - -class FairyChestsPerZone(Range): - """ - Determines the number of Fairy Chests in a given zone that contain items. After these have been checked, only stat - bonuses can be found in Fairy Chests. - """ - display_name = "Fairy Chests Per Zone" - range_start = 0 - range_end = 15 - default = 1 - - -class ChestsPerZone(Range): - """ - Determines the number of Non-Fairy Chests in a given zone that contain items. After these have been checked, only - gold or stat bonuses can be found in Chests. - """ - display_name = "Chests Per Zone" - range_start = 20 - range_end = 50 - default = 20 - - -class UniversalFairyChests(Toggle): - """ - Determines if fairy chests should be combined into one pool instead of per zone, similar to Risk of Rain 2. - """ - display_name = "Universal Fairy Chests" - - -class UniversalChests(Toggle): - """ - Determines if non-fairy chests should be combined into one pool instead of per zone, similar to Risk of Rain 2. - """ - display_name = "Universal Non-Fairy Chests" - - -class Vendors(Choice): - """ - Determines where to place the Blacksmith and Enchantress unlocks in logic (or start with them unlocked). - """ - display_name = "Vendors" - option_start_unlocked = 0 - option_early = 1 - option_normal = 2 - option_anywhere = 3 - default = 1 - - -class Architect(Choice): - """ - Determines where the Architect sits in the item pool. - """ - display_name = "Architect" - option_start_unlocked = 0 - option_early = 1 - option_anywhere = 2 - option_disabled = 3 - alias_normal = 2 - default = 2 - - -class ArchitectFee(Range): - """ - Determines how large of a percentage the architect takes from the player when utilizing his services. 100 means he - takes all your gold. 0 means his services are free. - """ - display_name = "Architect Fee Percentage" - range_start = 0 - range_end = 100 - default = 40 - - -class DisableCharon(Toggle): - """ - Prevents Charon from taking your money when you re-enter the castle. Also removes Haggling from the Item Pool. - """ - display_name = "Disable Charon" - - -class RequirePurchasing(DefaultOnToggle): - """ - Determines where you will be required to purchase equipment and runes from the Blacksmith and Enchantress before - equipping them. If you disable require purchasing, Manor Renovations are scaled to take this into account. - """ - display_name = "Require Purchasing" - - -class ProgressiveBlueprints(Toggle): - """ - Instead of shuffling blueprints randomly into the pool, blueprint unlocks are progressively unlocked. You would get - Squire first, then Knight, etc., until finally Dark. - """ - display_name = "Progressive Blueprints" - - -class GoldGainMultiplier(Choice): - """ - Adjusts the multiplier for gaining gold from all sources. - """ - display_name = "Gold Gain Multiplier" - option_normal = 0 - option_quarter = 1 - option_half = 2 - option_double = 3 - option_quadruple = 4 - default = 0 - - -class NumberOfChildren(Range): - """ - Determines the number of offspring you can choose from on the lineage screen after a death. - """ - display_name = "Number of Children" - range_start = 1 - range_end = 5 - default = 3 - - -class AdditionalLadyNames(OptionSet): - """ - Set of additional names your potential offspring can have. If Allow Default Names is disabled, this is the only list - of names your children can have. The first value will also be your initial character's name depending on Starting - Gender. - """ - display_name = "Additional Lady Names" - -class AdditionalSirNames(OptionSet): - """ - Set of additional names your potential offspring can have. If Allow Default Names is disabled, this is the only list - of names your children can have. The first value will also be your initial character's name depending on Starting - Gender. - """ - display_name = "Additional Sir Names" - - -class AllowDefaultNames(DefaultOnToggle): - """ - Determines if the default names defined in the vanilla game are allowed to be used. Warning: Your world will not - generate if the number of Additional Names defined is less than the Number of Children value. - """ - display_name = "Allow Default Names" - - -class CastleScaling(Range): - """ - Adjusts the scaling factor for how big a castle can be. Larger castles scale enemies quicker and also take longer - to generate. 100 means normal castle size. - """ - display_name = "Castle Size Scaling Percentage" - range_start = 50 - range_end = 300 - default = 100 - - -class ChallengeBossKhidr(Choice): - """ - Determines if Neo Khidr replaces Khidr in their boss room. - """ - display_name = "Khidr" - option_vanilla = 0 - option_challenge = 1 - default = 0 - - -class ChallengeBossAlexander(Choice): - """ - Determines if Alexander the IV replaces Alexander in their boss room. - """ - display_name = "Alexander" - option_vanilla = 0 - option_challenge = 1 - default = 0 - - -class ChallengeBossLeon(Choice): - """ - Determines if Ponce de Freon replaces Ponce de Leon in their boss room. - """ - display_name = "Ponce de Leon" - option_vanilla = 0 - option_challenge = 1 - default = 0 - - -class ChallengeBossHerodotus(Choice): - """ - Determines if Astrodotus replaces Herodotus in their boss room. - """ - display_name = "Herodotus" - option_vanilla = 0 - option_challenge = 1 - default = 0 - - -class HealthUpPool(Range): - """ - Determines the number of Health Ups in the item pool. - """ - display_name = "Health Up Pool" - range_start = 0 - range_end = 15 - default = 15 - - -class ManaUpPool(Range): - """ - Determines the number of Mana Ups in the item pool. - """ - display_name = "Mana Up Pool" - range_start = 0 - range_end = 15 - default = 15 - - -class AttackUpPool(Range): - """ - Determines the number of Attack Ups in the item pool. - """ - display_name = "Attack Up Pool" - range_start = 0 - range_end = 15 - default = 15 - - -class MagicDamageUpPool(Range): - """ - Determines the number of Magic Damage Ups in the item pool. - """ - display_name = "Magic Damage Up Pool" - range_start = 0 - range_end = 15 - default = 15 - - -class ArmorUpPool(Range): - """ - Determines the number of Armor Ups in the item pool. - """ - display_name = "Armor Up Pool" - range_start = 0 - range_end = 10 - default = 10 - - -class EquipUpPool(Range): - """ - Determines the number of Equip Ups in the item pool. - """ - display_name = "Equip Up Pool" - range_start = 0 - range_end = 10 - default = 10 - - -class CritChanceUpPool(Range): - """ - Determines the number of Crit Chance Ups in the item pool. - """ - display_name = "Crit Chance Up Pool" - range_start = 0 - range_end = 5 - default = 5 - - -class CritDamageUpPool(Range): - """ - Determines the number of Crit Damage Ups in the item pool. - """ - display_name = "Crit Damage Up Pool" - range_start = 0 - range_end = 5 - default = 5 - - -class FreeDiaryOnGeneration(DefaultOnToggle): - """ - Allows the player to get a free diary check every time they regenerate the castle in the starting room. - """ - display_name = "Free Diary On Generation" - - -class AvailableClasses(OptionSet): - """ - List of classes that will be in the item pool to find. The upgraded form of the class will be added with it. - The upgraded form of your starting class will be available regardless. - """ - display_name = "Available Classes" - default = frozenset( - {"Knight", "Mage", "Barbarian", "Knave", "Shinobi", "Miner", "Spellthief", "Lich", "Dragon", "Traitor"} - ) - valid_keys = {"Knight", "Mage", "Barbarian", "Knave", "Shinobi", "Miner", "Spellthief", "Lich", "Dragon", "Traitor"} - - -@dataclass -class RLOptions(PerGameCommonOptions): - starting_gender: StartingGender - starting_class: StartingClass - available_classes: AvailableClasses - new_game_plus: NewGamePlus - fairy_chests_per_zone: FairyChestsPerZone - chests_per_zone: ChestsPerZone - universal_fairy_chests: UniversalFairyChests - universal_chests: UniversalChests - vendors: Vendors - architect: Architect - architect_fee: ArchitectFee - disable_charon: DisableCharon - require_purchasing: RequirePurchasing - progressive_blueprints: ProgressiveBlueprints - gold_gain_multiplier: GoldGainMultiplier - number_of_children: NumberOfChildren - free_diary_on_generation: FreeDiaryOnGeneration - khidr: ChallengeBossKhidr - alexander: ChallengeBossAlexander - leon: ChallengeBossLeon - herodotus: ChallengeBossHerodotus - health_pool: HealthUpPool - mana_pool: ManaUpPool - attack_pool: AttackUpPool - magic_damage_pool: MagicDamageUpPool - armor_pool: ArmorUpPool - equip_pool: EquipUpPool - crit_chance_pool: CritChanceUpPool - crit_damage_pool: CritDamageUpPool - allow_default_names: AllowDefaultNames - additional_lady_names: AdditionalLadyNames - additional_sir_names: AdditionalSirNames - death_link: DeathLink diff --git a/worlds/rogue_legacy/Presets.py b/worlds/rogue_legacy/Presets.py deleted file mode 100644 index 2dfeee64d8..0000000000 --- a/worlds/rogue_legacy/Presets.py +++ /dev/null @@ -1,61 +0,0 @@ -from typing import Any, Dict - -from .Options import Architect, GoldGainMultiplier, Vendors - -rl_options_presets: Dict[str, Dict[str, Any]] = { - # Example preset using only literal values. - "Unknown Fate": { - "progression_balancing": "random", - "accessibility": "random", - "starting_gender": "random", - "starting_class": "random", - "new_game_plus": "random", - "fairy_chests_per_zone": "random", - "chests_per_zone": "random", - "universal_fairy_chests": "random", - "universal_chests": "random", - "vendors": "random", - "architect": "random", - "architect_fee": "random", - "disable_charon": "random", - "require_purchasing": "random", - "progressive_blueprints": "random", - "gold_gain_multiplier": "random", - "number_of_children": "random", - "free_diary_on_generation": "random", - "khidr": "random", - "alexander": "random", - "leon": "random", - "herodotus": "random", - "health_pool": "random", - "mana_pool": "random", - "attack_pool": "random", - "magic_damage_pool": "random", - "armor_pool": "random", - "equip_pool": "random", - "crit_chance_pool": "random", - "crit_damage_pool": "random", - "allow_default_names": True, - "death_link": "random", - }, - # A preset I actually use, using some literal values and some from the option itself. - "Limited Potential": { - "progression_balancing": "disabled", - "fairy_chests_per_zone": 2, - "starting_class": "random", - "chests_per_zone": 30, - "vendors": Vendors.option_normal, - "architect": Architect.option_disabled, - "gold_gain_multiplier": GoldGainMultiplier.option_half, - "number_of_children": 2, - "free_diary_on_generation": False, - "health_pool": 10, - "mana_pool": 10, - "attack_pool": 10, - "magic_damage_pool": 10, - "armor_pool": 5, - "equip_pool": 10, - "crit_chance_pool": 5, - "crit_damage_pool": 5, - } -} diff --git a/worlds/rogue_legacy/Regions.py b/worlds/rogue_legacy/Regions.py deleted file mode 100644 index 61b0ef73ec..0000000000 --- a/worlds/rogue_legacy/Regions.py +++ /dev/null @@ -1,114 +0,0 @@ -from typing import Dict, List, NamedTuple, Optional, TYPE_CHECKING - -from BaseClasses import MultiWorld, Region, Entrance -from .Locations import RLLocation, location_table, get_locations_by_category - -if TYPE_CHECKING: - from . import RLWorld - - -class RLRegionData(NamedTuple): - locations: Optional[List[str]] - region_exits: Optional[List[str]] - - -def create_regions(world: "RLWorld"): - regions: Dict[str, RLRegionData] = { - "Menu": RLRegionData(None, ["Castle Hamson"]), - "The Manor": RLRegionData([], []), - "Castle Hamson": RLRegionData([], ["Forest Abkhazia", "The Maya", "Land of Darkness", - "The Fountain Room", "The Manor"]), - "Forest Abkhazia": RLRegionData([], []), - "The Maya": RLRegionData([], []), - "Land of Darkness": RLRegionData([], []), - "The Fountain Room": RLRegionData([], None), - } - - # Artificially stagger diary spheres for progression. - for diary in range(0, 25): - region: str - if 0 <= diary < 6: - region = "Castle Hamson" - elif 6 <= diary < 12: - region = "Forest Abkhazia" - elif 12 <= diary < 18: - region = "The Maya" - elif 18 <= diary < 24: - region = "Land of Darkness" - else: - region = "The Fountain Room" - regions[region].locations.append(f"Diary {diary + 1}") - - # Manor & Special - for manor in get_locations_by_category("Manor").keys(): - regions["The Manor"].locations.append(manor) - for special in get_locations_by_category("Special").keys(): - regions["Castle Hamson"].locations.append(special) - - # Boss Rewards - regions["Castle Hamson"].locations.append("Castle Hamson Boss Reward") - regions["Forest Abkhazia"].locations.append("Forest Abkhazia Boss Reward") - regions["The Maya"].locations.append("The Maya Boss Reward") - regions["Land of Darkness"].locations.append("Land of Darkness Boss Reward") - - # Events - regions["Castle Hamson"].locations.append("Castle Hamson Boss Room") - regions["Forest Abkhazia"].locations.append("Forest Abkhazia Boss Room") - regions["The Maya"].locations.append("The Maya Boss Room") - regions["Land of Darkness"].locations.append("Land of Darkness Boss Room") - regions["The Fountain Room"].locations.append("Fountain Room") - - # Chests - chests = int(world.options.chests_per_zone) - for i in range(0, chests): - if world.options.universal_chests: - regions["Castle Hamson"].locations.append(f"Chest {i + 1}") - regions["Forest Abkhazia"].locations.append(f"Chest {i + 1 + chests}") - regions["The Maya"].locations.append(f"Chest {i + 1 + (chests * 2)}") - regions["Land of Darkness"].locations.append(f"Chest {i + 1 + (chests * 3)}") - else: - regions["Castle Hamson"].locations.append(f"Castle Hamson - Chest {i + 1}") - regions["Forest Abkhazia"].locations.append(f"Forest Abkhazia - Chest {i + 1}") - regions["The Maya"].locations.append(f"The Maya - Chest {i + 1}") - regions["Land of Darkness"].locations.append(f"Land of Darkness - Chest {i + 1}") - - # Fairy Chests - chests = int(world.options.fairy_chests_per_zone) - for i in range(0, chests): - if world.options.universal_fairy_chests: - regions["Castle Hamson"].locations.append(f"Fairy Chest {i + 1}") - regions["Forest Abkhazia"].locations.append(f"Fairy Chest {i + 1 + chests}") - regions["The Maya"].locations.append(f"Fairy Chest {i + 1 + (chests * 2)}") - regions["Land of Darkness"].locations.append(f"Fairy Chest {i + 1 + (chests * 3)}") - else: - regions["Castle Hamson"].locations.append(f"Castle Hamson - Fairy Chest {i + 1}") - regions["Forest Abkhazia"].locations.append(f"Forest Abkhazia - Fairy Chest {i + 1}") - regions["The Maya"].locations.append(f"The Maya - Fairy Chest {i + 1}") - regions["Land of Darkness"].locations.append(f"Land of Darkness - Fairy Chest {i + 1}") - - # Set up the regions correctly. - for name, data in regions.items(): - world.multiworld.regions.append(create_region(world.multiworld, world.player, name, data)) - - world.get_entrance("Castle Hamson").connect(world.get_region("Castle Hamson")) - world.get_entrance("The Manor").connect(world.get_region("The Manor")) - world.get_entrance("Forest Abkhazia").connect(world.get_region("Forest Abkhazia")) - world.get_entrance("The Maya").connect(world.get_region("The Maya")) - world.get_entrance("Land of Darkness").connect(world.get_region("Land of Darkness")) - world.get_entrance("The Fountain Room").connect(world.get_region("The Fountain Room")) - - -def create_region(multiworld: MultiWorld, player: int, name: str, data: RLRegionData): - region = Region(name, player, multiworld) - if data.locations: - for loc_name in data.locations: - loc_data = location_table.get(loc_name) - location = RLLocation(player, loc_name, loc_data.code if loc_data else None, region) - region.locations.append(location) - - if data.region_exits: - for exit in data.region_exits: - entrance = Entrance(player, exit, region) - region.exits.append(entrance) - - return region diff --git a/worlds/rogue_legacy/Rules.py b/worlds/rogue_legacy/Rules.py deleted file mode 100644 index 505bbdd635..0000000000 --- a/worlds/rogue_legacy/Rules.py +++ /dev/null @@ -1,117 +0,0 @@ -from BaseClasses import CollectionState -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from . import RLWorld - - -def get_upgrade_total(world: "RLWorld") -> int: - return int(world.options.health_pool) + int(world.options.mana_pool) + \ - int(world.options.attack_pool) + int(world.options.magic_damage_pool) - - -def get_upgrade_count(state: CollectionState, player: int) -> int: - return state.count("Health Up", player) + state.count("Mana Up", player) + \ - state.count("Attack Up", player) + state.count("Magic Damage Up", player) - - -def has_vendors(state: CollectionState, player: int) -> bool: - return state.has_all({"Blacksmith", "Enchantress"}, player) - - -def has_upgrade_amount(state: CollectionState, player: int, amount: int) -> bool: - return get_upgrade_count(state, player) >= amount - - -def has_upgrades_percentage(state: CollectionState, world: "RLWorld", percentage: float) -> bool: - return has_upgrade_amount(state, world.player, round(get_upgrade_total(world) * (percentage / 100))) - - -def has_movement_rune(state: CollectionState, player: int) -> bool: - return state.has("Vault Runes", player) or state.has("Sprint Runes", player) or state.has("Sky Runes", player) - - -def has_fairy_progression(state: CollectionState, player: int) -> bool: - return state.has("Dragons", player) or (state.has("Enchantress", player) and has_movement_rune(state, player)) - - -def has_defeated_castle(state: CollectionState, player: int) -> bool: - return state.has("Defeat Khidr", player) or state.has("Defeat Neo Khidr", player) - - -def has_defeated_forest(state: CollectionState, player: int) -> bool: - return state.has("Defeat Alexander", player) or state.has("Defeat Alexander IV", player) - - -def has_defeated_tower(state: CollectionState, player: int) -> bool: - return state.has("Defeat Ponce de Leon", player) or state.has("Defeat Ponce de Freon", player) - - -def has_defeated_dungeon(state: CollectionState, player: int) -> bool: - return state.has("Defeat Herodotus", player) or state.has("Defeat Astrodotus", player) - - -def set_rules(world: "RLWorld", player: int): - # If 'vendors' are 'normal', then expect it to show up in the first half(ish) of the spheres. - if world.options.vendors == "normal": - world.get_location("Forest Abkhazia Boss Reward").access_rule = \ - lambda state: has_vendors(state, player) - - # Gate each manor location so everything isn't dumped into sphere 1. - manor_rules = { - "Defeat Khidr" if world.options.khidr == "vanilla" else "Defeat Neo Khidr": [ - "Manor - Left Wing Window", - "Manor - Left Wing Rooftop", - "Manor - Right Wing Window", - "Manor - Right Wing Rooftop", - "Manor - Left Big Base", - "Manor - Right Big Base", - "Manor - Left Tree 1", - "Manor - Left Tree 2", - "Manor - Right Tree", - ], - "Defeat Alexander" if world.options.alexander == "vanilla" else "Defeat Alexander IV": [ - "Manor - Left Big Upper 1", - "Manor - Left Big Upper 2", - "Manor - Left Big Windows", - "Manor - Left Big Rooftop", - "Manor - Left Far Base", - "Manor - Left Far Roof", - "Manor - Left Extension", - "Manor - Right Big Upper", - "Manor - Right Big Rooftop", - "Manor - Right Extension", - ], - "Defeat Ponce de Leon" if world.options.leon == "vanilla" else "Defeat Ponce de Freon": [ - "Manor - Right High Base", - "Manor - Right High Upper", - "Manor - Right High Tower", - "Manor - Observatory Base", - "Manor - Observatory Telescope", - ] - } - - # Set rules for manor locations. - for event, locations in manor_rules.items(): - for location in locations: - world.get_location(location).access_rule = lambda state: state.has(event, player) - - # Set rules for fairy chests to decrease headache of expectation to find non-movement fairy chests. - for fairy_location in [location for location in world.multiworld.get_locations(player) if "Fairy" in location.name]: - fairy_location.access_rule = lambda state: has_fairy_progression(state, player) - - # Region rules. - world.get_entrance("Forest Abkhazia").access_rule = \ - lambda state: has_upgrades_percentage(state, world, 12.5) and has_defeated_castle(state, player) - - world.get_entrance("The Maya").access_rule = \ - lambda state: has_upgrades_percentage(state, world, 25) and has_defeated_forest(state, player) - - world.get_entrance("Land of Darkness").access_rule = \ - lambda state: has_upgrades_percentage(state, world, 37.5) and has_defeated_tower(state, player) - - world.get_entrance("The Fountain Room").access_rule = \ - lambda state: has_upgrades_percentage(state, world, 50) and has_defeated_dungeon(state, player) - - # Win condition. - world.multiworld.completion_condition[player] = lambda state: state.has("Defeat The Fountain", player) diff --git a/worlds/rogue_legacy/__init__.py b/worlds/rogue_legacy/__init__.py deleted file mode 100644 index 7ffdd459db..0000000000 --- a/worlds/rogue_legacy/__init__.py +++ /dev/null @@ -1,243 +0,0 @@ -from typing import List - -from BaseClasses import Tutorial -from worlds.AutoWorld import WebWorld, World -from .Items import RLItem, RLItemData, event_item_table, get_items_by_category, item_table -from .Locations import RLLocation, location_table -from .Options import RLOptions -from .Presets import rl_options_presets -from .Regions import create_regions -from .Rules import set_rules - - -class RLWeb(WebWorld): - theme = "stone" - tutorials = [Tutorial( - "Multiworld Setup Guide", - "A guide to setting up the Rogue Legacy Randomizer software on your computer. This guide covers single-player, " - "multiworld, and related software.", - "English", - "rogue-legacy_en.md", - "rogue-legacy/en", - ["Phar"] - )] - bug_report_page = "https://github.com/ThePhar/RogueLegacyRandomizer/issues/new?assignees=&labels=bug&template=" \ - "report-an-issue---.md&title=%5BIssue%5D" - options_presets = rl_options_presets - - -class RLWorld(World): - """ - Rogue Legacy is a genealogical rogue-"LITE" where anyone can be a hero. Each time you die, your child will succeed - you. Every child is unique. One child might be colorblind, another might have vertigo-- they could even be a dwarf. - But that's OK, because no one is perfect, and you don't have to be to succeed. - """ - game = "Rogue Legacy" - options_dataclass = RLOptions - options: RLOptions - topology_present = True - required_client_version = (0, 3, 5) - web = RLWeb() - - item_name_to_id = {name: data.code for name, data in item_table.items() if data.code is not None} - location_name_to_id = {name: data.code for name, data in location_table.items() if data.code is not None} - - def fill_slot_data(self) -> dict: - return self.options.as_dict(*[name for name in self.options_dataclass.type_hints.keys()]) - - def generate_early(self): - # Check validation of names. - additional_lady_names = len(self.options.additional_lady_names.value) - additional_sir_names = len(self.options.additional_sir_names.value) - if not self.options.allow_default_names: - if additional_lady_names < int(self.options.number_of_children): - raise Exception( - f"allow_default_names is off, but not enough names are defined in additional_lady_names. " - f"Expected {int(self.options.number_of_children)}, Got {additional_lady_names}") - - if additional_sir_names < int(self.options.number_of_children): - raise Exception( - f"allow_default_names is off, but not enough names are defined in additional_sir_names. " - f"Expected {int(self.options.number_of_children)}, Got {additional_sir_names}") - - def create_items(self): - item_pool: List[RLItem] = [] - total_locations = len(self.multiworld.get_unfilled_locations(self.player)) - for name, data in item_table.items(): - quantity = data.max_quantity - - # Architect - if name == "Architect": - if self.options.architect == "disabled": - continue - if self.options.architect == "start_unlocked": - self.multiworld.push_precollected(self.create_item(name)) - continue - if self.options.architect == "early": - self.multiworld.local_early_items[self.player]["Architect"] = 1 - - # Blacksmith and Enchantress - if name == "Blacksmith" or name == "Enchantress": - if self.options.vendors == "start_unlocked": - self.multiworld.push_precollected(self.create_item(name)) - continue - if self.options.vendors == "early": - self.multiworld.local_early_items[self.player]["Blacksmith"] = 1 - self.multiworld.local_early_items[self.player]["Enchantress"] = 1 - - # Haggling - if name == "Haggling" and self.options.disable_charon: - continue - - # Blueprints - if data.category == "Blueprints": - # No progressive blueprints if progressive_blueprints are disabled. - if name == "Progressive Blueprints" and not self.options.progressive_blueprints: - continue - # No distinct blueprints if progressive_blueprints are enabled. - elif name != "Progressive Blueprints" and self.options.progressive_blueprints: - continue - - # Classes - if data.category == "Classes": - if name == "Progressive Knights": - if "Knight" not in self.options.available_classes: - continue - - if self.options.starting_class == "knight": - quantity = 1 - if name == "Progressive Mages": - if "Mage" not in self.options.available_classes: - continue - - if self.options.starting_class == "mage": - quantity = 1 - if name == "Progressive Barbarians": - if "Barbarian" not in self.options.available_classes: - continue - - if self.options.starting_class == "barbarian": - quantity = 1 - if name == "Progressive Knaves": - if "Knave" not in self.options.available_classes: - continue - - if self.options.starting_class == "knave": - quantity = 1 - if name == "Progressive Miners": - if "Miner" not in self.options.available_classes: - continue - - if self.options.starting_class == "miner": - quantity = 1 - if name == "Progressive Shinobis": - if "Shinobi" not in self.options.available_classes: - continue - - if self.options.starting_class == "shinobi": - quantity = 1 - if name == "Progressive Liches": - if "Lich" not in self.options.available_classes: - continue - - if self.options.starting_class == "lich": - quantity = 1 - if name == "Progressive Spellthieves": - if "Spellthief" not in self.options.available_classes: - continue - - if self.options.starting_class == "spellthief": - quantity = 1 - if name == "Dragons": - if "Dragon" not in self.options.available_classes: - continue - if name == "Traitors": - if "Traitor" not in self.options.available_classes: - continue - - # Skills - if name == "Health Up": - quantity = self.options.health_pool.value - elif name == "Mana Up": - quantity = self.options.mana_pool.value - elif name == "Attack Up": - quantity = self.options.attack_pool.value - elif name == "Magic Damage Up": - quantity = self.options.magic_damage_pool.value - elif name == "Armor Up": - quantity = self.options.armor_pool.value - elif name == "Equip Up": - quantity = self.options.equip_pool.value - elif name == "Crit Chance Up": - quantity = self.options.crit_chance_pool.value - elif name == "Crit Damage Up": - quantity = self.options.crit_damage_pool.value - - # Ignore filler, it will be added in a later stage. - if data.category == "Filler": - continue - - item_pool += [self.create_item(name) for _ in range(0, quantity)] - - # Fill any empty locations with filler items. - while len(item_pool) < total_locations: - item_pool.append(self.create_item(self.get_filler_item_name())) - - self.multiworld.itempool += item_pool - - def get_filler_item_name(self) -> str: - fillers = get_items_by_category("Filler") - weights = [data.weight for data in fillers.values()] - return self.random.choices([filler for filler in fillers.keys()], weights, k=1)[0] - - def create_item(self, name: str) -> RLItem: - data = item_table[name] - return RLItem(name, data.classification, data.code, self.player) - - def create_event(self, name: str) -> RLItem: - data = event_item_table[name] - return RLItem(name, data.classification, data.code, self.player) - - def set_rules(self): - set_rules(self, self.player) - - def create_regions(self): - create_regions(self) - self._place_events() - - def _place_events(self): - # Fountain - self.multiworld.get_location("Fountain Room", self.player).place_locked_item( - self.create_event("Defeat The Fountain")) - - # Khidr / Neo Khidr - if self.options.khidr == "vanilla": - self.multiworld.get_location("Castle Hamson Boss Room", self.player).place_locked_item( - self.create_event("Defeat Khidr")) - else: - self.multiworld.get_location("Castle Hamson Boss Room", self.player).place_locked_item( - self.create_event("Defeat Neo Khidr")) - - # Alexander / Alexander IV - if self.options.alexander == "vanilla": - self.multiworld.get_location("Forest Abkhazia Boss Room", self.player).place_locked_item( - self.create_event("Defeat Alexander")) - else: - self.multiworld.get_location("Forest Abkhazia Boss Room", self.player).place_locked_item( - self.create_event("Defeat Alexander IV")) - - # Ponce de Leon / Ponce de Freon - if self.options.leon == "vanilla": - self.multiworld.get_location("The Maya Boss Room", self.player).place_locked_item( - self.create_event("Defeat Ponce de Leon")) - else: - self.multiworld.get_location("The Maya Boss Room", self.player).place_locked_item( - self.create_event("Defeat Ponce de Freon")) - - # Herodotus / Astrodotus - if self.options.herodotus == "vanilla": - self.multiworld.get_location("Land of Darkness Boss Room", self.player).place_locked_item( - self.create_event("Defeat Herodotus")) - else: - self.multiworld.get_location("Land of Darkness Boss Room", self.player).place_locked_item( - self.create_event("Defeat Astrodotus")) diff --git a/worlds/rogue_legacy/docs/en_Rogue Legacy.md b/worlds/rogue_legacy/docs/en_Rogue Legacy.md deleted file mode 100644 index dd203c73ac..0000000000 --- a/worlds/rogue_legacy/docs/en_Rogue Legacy.md +++ /dev/null @@ -1,34 +0,0 @@ -# Rogue Legacy (PC) - -## Where is the options page? - -The [player options page for this game](../player-options) contains most of the options you need to -configure and export a config file. Some options can only be made in YAML, but an explanation can be found in the -[template yaml here](../../../static/generated/configs/Rogue%20Legacy.yaml). - -## What does randomization do to this game? - -Rogue Legacy Randomizer takes all the classes, skills, runes, and blueprints and spreads them out into chests, the manor -upgrade screen, bosses, and some special individual locations. The goal is to become powerful enough to defeat the four -zone bosses and then defeat The Fountain. - -## What items and locations get shuffled? -All the skill upgrades, class upgrades, runes packs, and equipment packs are shuffled in the manor upgrade screen, diary -checks, chests and fairy chests, and boss rewards. Skill upgrades are also grouped in packs of 5 to make the finding of -stats less of a chore. Runes and Equipment are also grouped together. - -Some additional locations that can contain items are the Jukebox, the Portraits, and the mini-game rewards. - -## Which items can be in another player's world? - -Any of the items which can be shuffled may also be placed into another player's world. It is possible to choose to limit -certain items to your own world. -## When the player receives an item, what happens? - -When the player receives an item, your character will hold the item above their head and display it to the world. It's -good for business! - -## What do I do if I encounter a bug with the game? - -Please reach out to Phar#4444 on Discord or you can drop a bug report on the -[GitHub page for Rogue Legacy Randomizer](https://github.com/ThePhar/RogueLegacyRandomizer/issues/new?assignees=&labels=bug&template=report-an-issue---.md&title=%5BIssue%5D). diff --git a/worlds/rogue_legacy/docs/rogue-legacy_en.md b/worlds/rogue_legacy/docs/rogue-legacy_en.md deleted file mode 100644 index fc9f692017..0000000000 --- a/worlds/rogue_legacy/docs/rogue-legacy_en.md +++ /dev/null @@ -1,35 +0,0 @@ -# Rogue Legacy Randomizer Setup Guide - -## Required Software - -- Rogue Legacy Randomizer from the - [Rogue Legacy Randomizer Releases Page](https://github.com/ThePhar/RogueLegacyRandomizer/releases) - -## Recommended Installation Instructions - -Please read the README file on the -[Rogue Legacy Randomizer GitHub](https://github.com/ThePhar/RogueLegacyRandomizer/blob/master/README.md) page for -up-to-date installation instructions. - -## Configuring your YAML file - -### What is a YAML file and why do I need one? - -Your YAML file contains a set of configuration options which provide the generator with information about how it should -generate your game. Each player of a multiworld will provide their own YAML file. This setup allows each player to enjoy -an experience customized for their taste, and different players in the same multiworld can all have different options. - -### Where do I get a YAML file? - -you can customize your options by visiting the [Rogue Legacy Options Page](/games/Rogue%20Legacy/player-options). - -### Connect to the MultiServer - -Once in game, press the start button and the AP connection screen should appear. You will fill out the hostname, port, -slot name, and password (if applicable). You should only need to fill out hostname, port, and password if the server -provides an alternative one to the default values. - -### Play the game - -Once you have entered the required values, you go to Connect and then select Confirm on the "Ready to Start" screen. Now -you're off to start your legacy! diff --git a/worlds/rogue_legacy/test/TestUnique.py b/worlds/rogue_legacy/test/TestUnique.py deleted file mode 100644 index 1ae9968d55..0000000000 --- a/worlds/rogue_legacy/test/TestUnique.py +++ /dev/null @@ -1,23 +0,0 @@ -from typing import Dict - -from . import RLTestBase -from ..Items import item_table -from ..Locations import location_table - - -class UniqueTest(RLTestBase): - @staticmethod - def test_item_ids_are_all_unique(): - item_ids: Dict[int, str] = {} - for name, data in item_table.items(): - assert data.code not in item_ids.keys(), f"'{name}': {data.code}, is not unique. " \ - f"'{item_ids[data.code]}' also has this identifier." - item_ids[data.code] = name - - @staticmethod - def test_location_ids_are_all_unique(): - location_ids: Dict[int, str] = {} - for name, data in location_table.items(): - assert data.code not in location_ids.keys(), f"'{name}': {data.code}, is not unique. " \ - f"'{location_ids[data.code]}' also has this identifier." - location_ids[data.code] = name diff --git a/worlds/rogue_legacy/test/__init__.py b/worlds/rogue_legacy/test/__init__.py deleted file mode 100644 index 3346476ba6..0000000000 --- a/worlds/rogue_legacy/test/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from test.bases import WorldTestBase - - -class RLTestBase(WorldTestBase): - game = "Rogue Legacy" From 7a6fb5e35b471ef196437dd97d23fd26402d903e Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Fri, 11 Jul 2025 23:28:18 +0200 Subject: [PATCH 09/15] Revert "Core: Take Counter back out of RestrictedUnpickler" (#5184) * Revert "Core: Take Counter back out of RestrictedUnpickler #5169" This reverts commit 95e09c8e2a681ecd5666822b04fe7fed3ed9dec1. * Update Utils.py --- Utils.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Utils.py b/Utils.py index 6212b93288..5697bb162a 100644 --- a/Utils.py +++ b/Utils.py @@ -441,6 +441,10 @@ class RestrictedUnpickler(pickle.Unpickler): def find_class(self, module: str, name: str) -> type: if module == "builtins" and name in safe_builtins: return getattr(builtins, name) + # used by OptionCounter + # necessary because the actual Options class instances are pickled when transfered to WebHost generation pool + if module == "collections" and name == "Counter": + return collections.Counter # used by MultiServer -> savegame/multidata if module == "NetUtils" and name in {"NetworkItem", "ClientStatus", "Hint", "SlotType", "NetworkSlot", "HintStatus"}: From a79423534c30de502a71944a5ec1d95ff32847fe Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Fri, 11 Jul 2025 18:44:26 -0400 Subject: [PATCH 10/15] LADX: Update marin.txt (#5178) --- worlds/ladx/LADXR/patches/marin.txt | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/worlds/ladx/LADXR/patches/marin.txt b/worlds/ladx/LADXR/patches/marin.txt index a179e35fc6..782f4129ce 100644 --- a/worlds/ladx/LADXR/patches/marin.txt +++ b/worlds/ladx/LADXR/patches/marin.txt @@ -255,7 +255,6 @@ Try Bumper Stickers! Try Castlevania 64! Try Celeste 64! Try ChecksFinder! -Try Clique! Try Dark Souls III! Try DLCQuest! Try Donkey Kong Country 3! @@ -268,6 +267,7 @@ Try A Hat in Time! Try Heretic! Try Hollow Knight! Try Hylics 2! +Try Jak and Daxter: The Precursor Legacy! Try Kingdom Hearts 2! Try Kirby's Dream Land 3! Try Landstalker - The Treasures of King Nole! @@ -288,11 +288,10 @@ Try Pokemon Emerald! Try Pokemon Red and Blue! Try Raft! Try Risk of Rain 2! -Try Rogue Legacy! Try Secret of Evermore! +Try shapez! Try Shivers! Try A Short Hike! -Try Slay the Spire! Try SMZ3! Try Sonic Adventure 2 Battle! Try Starcraft 2! @@ -300,6 +299,7 @@ Try Stardew Valley! Try Subnautica! Try Sudoku! Try Super Mario 64! +Try Super Mario Land 2: 6 Golden Coins! Try Super Mario World! Try Super Metroid! Try Terraria! @@ -312,7 +312,6 @@ Try The Witness! Try Yoshi's Island! Try Yu-Gi-Oh! 2006! Try Zillion! -Try Zork Grand Inquisitor! Try Old School Runescape! Try Kingdom Hearts! Try Mega Man 2! @@ -369,7 +368,6 @@ Have they added Among Us to AP yet? Every copy of LADX is personalized, David. Looks like you're going on A Short Hike. Bring back feathers please? Functioning Brain is at...\nWait. This isn't Witness. Wrong game, sorry. -Don't forget to check your Clique!\nIf, y'know, you have one. No pressure... :3 Sorry ######, but your progression item is in another world. &newgames\n&oldgames From 909565e5d958459093014e134ea21b18767cd1fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Bolduc?= <16137441+Jouramie@users.noreply.github.com> Date: Sat, 12 Jul 2025 07:12:04 -0400 Subject: [PATCH 11/15] Stardew Valley: Remove Rarecrow Locations from Night Market when Museumsanity is Disabled (#5146) --- worlds/stardew_valley/locations.py | 3 ++ worlds/stardew_valley/logic/museum_logic.py | 8 --- .../stardew_valley/test/rules/TestMuseum.py | 50 ++++++++++++++++++- 3 files changed, 51 insertions(+), 10 deletions(-) diff --git a/worlds/stardew_valley/locations.py b/worlds/stardew_valley/locations.py index 0d621fda49..fa4d50ce79 100644 --- a/worlds/stardew_valley/locations.py +++ b/worlds/stardew_valley/locations.py @@ -279,6 +279,9 @@ def extend_festival_locations(randomized_locations: List[LocationData], options: return festival_locations = locations_by_tag[LocationTags.FESTIVAL] + if not options.museumsanity: + festival_locations = [location for location in festival_locations if location.name not in ("Rarecrow #7 (Tanuki)", "Rarecrow #8 (Tribal Mask)")] + randomized_locations.extend(festival_locations) extend_hard_festival_locations(randomized_locations, options) extend_desert_festival_chef_locations(randomized_locations, options, random) diff --git a/worlds/stardew_valley/logic/museum_logic.py b/worlds/stardew_valley/logic/museum_logic.py index 2237cd89ea..21718db27c 100644 --- a/worlds/stardew_valley/logic/museum_logic.py +++ b/worlds/stardew_valley/logic/museum_logic.py @@ -1,13 +1,5 @@ -from typing import Union - from Utils import cache_self1 -from .action_logic import ActionLogicMixin from .base_logic import BaseLogic, BaseLogicMixin -from .has_logic import HasLogicMixin -from .received_logic import ReceivedLogicMixin -from .region_logic import RegionLogicMixin -from .time_logic import TimeLogicMixin -from .tool_logic import ToolLogicMixin from .. import options from ..data.museum_data import MuseumItem, all_museum_items, all_museum_artifacts, all_museum_minerals from ..stardew_rule import StardewRule, False_ diff --git a/worlds/stardew_valley/test/rules/TestMuseum.py b/worlds/stardew_valley/test/rules/TestMuseum.py index 231bbafe22..1a22e8800c 100644 --- a/worlds/stardew_valley/test/rules/TestMuseum.py +++ b/worlds/stardew_valley/test/rules/TestMuseum.py @@ -1,12 +1,16 @@ from collections import Counter +from unittest.mock import patch from ..bases import SVTestBase -from ...options import Museumsanity +from ..options import presets +from ... import options, StardewLogic, StardewRule +from ...logic.museum_logic import MuseumLogic +from ...stardew_rule import true_, LiteralStardewRule class TestMuseumMilestones(SVTestBase): options = { - Museumsanity.internal_name: Museumsanity.option_milestones + options.Museumsanity: options.Museumsanity.option_milestones } def test_50_milestone(self): @@ -14,3 +18,45 @@ class TestMuseumMilestones(SVTestBase): milestone_rule = self.world.logic.museum.can_find_museum_items(50) self.assert_rule_false(milestone_rule, self.multiworld.state) + + +class DisabledMuseumRule(LiteralStardewRule): + value = False + + def __or__(self, other) -> StardewRule: + return other + + def __and__(self, other) -> StardewRule: + return self + + def __repr__(self): + return "Disabled Museum Rule" + + +class TestMuseumsanityDisabledExcludesMuseumDonationsFromOtherLocations(SVTestBase): + options = { + **presets.allsanity_mods_6_x_x(), + options.Museumsanity.internal_name: options.Museumsanity.option_none + } + + def test_museum_donations_are_never_required_in_any_locations(self): + with patch("worlds.stardew_valley.logic.museum_logic.MuseumLogic") as MockMuseumLogic: + museum_logic: MuseumLogic = MockMuseumLogic.return_value + museum_logic.can_donate_museum_items.return_value = DisabledMuseumRule() + museum_logic.can_donate_museum_artifacts.return_value = DisabledMuseumRule() + museum_logic.can_find_museum_artifacts.return_value = DisabledMuseumRule() + museum_logic.can_find_museum_minerals.return_value = DisabledMuseumRule() + museum_logic.can_find_museum_items.return_value = DisabledMuseumRule() + museum_logic.can_complete_museum.return_value = DisabledMuseumRule() + museum_logic.can_donate.return_value = DisabledMuseumRule() + # Allowing calls to museum rules since a lot of other logic depends on it, for minerals for instance. + museum_logic.can_find_museum_item.return_value = true_ + + regions = {region.name for region in self.multiworld.regions} + self.world.logic = StardewLogic(self.player, self.world.options, self.world.content, regions) + self.world.set_rules() + + self.collect_everything() + for location in self.get_real_locations(): + with self.subTest(location.name): + self.assert_can_reach_location(location) From 585cbf95a6d1b65facd1f0058ba49c6174b24a00 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Sat, 12 Jul 2025 07:14:34 -0400 Subject: [PATCH 12/15] TUNIC: Add UT Support for Breakables (#5182) --- worlds/tunic/ut_stuff.py | 492 +++++++++++++++++++++++++++++++-------- 1 file changed, 399 insertions(+), 93 deletions(-) diff --git a/worlds/tunic/ut_stuff.py b/worlds/tunic/ut_stuff.py index 8296452c73..82d58eaeb8 100644 --- a/worlds/tunic/ut_stuff.py +++ b/worlds/tunic/ut_stuff.py @@ -67,99 +67,6 @@ def map_page_index(data: Any) -> int: # mapping of everything after the second to last slash and the location id # lua used for the name: string.match(full_name, "[^/]*/[^/]*$") poptracker_data: dict[str, int] = { - "[Powered Secret Room] Chest/Follow the Purple Energy Road": 509342400, - "[Entryway] Chest/Mind the Slorms": 509342401, - "[Third Room] Beneath Platform Chest/Run from the tentacles!": 509342402, - "[Third Room] Tentacle Chest/Water Sucks": 509342403, - "[Entryway] Obscured Behind Waterfall/You can just go in there": 509342404, - "[Save Room] Upper Floor Chest 1/Through the Power of Prayer": 509342405, - "[Save Room] Upper Floor Chest 2/Above the Fox Shrine": 509342406, - "[Second Room] Underwater Chest/Hidden Passage": 509342407, - "[Back Corridor] Right Secret/Hidden Path": 509342408, - "[Back Corridor] Left Secret/Behind the Slorms": 509342409, - "[Second Room] Obscured Behind Waterfall/Just go in there": 509342410, - "[Side Room] Chest By Pots/Just Climb up There": 509342411, - "[Side Room] Chest By Phrends/So Many Phrends!": 509342412, - "[Second Room] Page/Ruined Atoll Map": 509342413, - "[Passage To Dark Tomb] Page Pickup/Siege Engine": 509342414, - "[1F] Guarded By Lasers/Beside 3 Miasma Seekers": 509342415, - "[1F] Near Spikes/Mind the Miasma Seeker": 509342416, - "Birdcage Room/[2F] Bird Room": 509342417, - "[2F] Entryway Upper Walkway/Overlooking Miasma": 509342418, - "[1F] Library/By the Books": 509342419, - "[2F] Library/Behind the Ladder": 509342420, - "[2F] Guarded By Lasers/Before the big reveal...": 509342421, - "Birdcage Room/[2F] Bird Room Secret": 509342422, - "[1F] Library Secret/Pray to the Wallman": 509342423, - "Spike Maze Near Exit/Watch out!": 509342424, - "2nd Laser Room/Can you roll?": 509342425, - "1st Laser Room/Use a bomb?": 509342426, - "Spike Maze Upper Walkway/Just walk right!": 509342427, - "Skulls Chest/Move the Grave": 509342428, - "Spike Maze Near Stairs/In the Corner": 509342429, - "1st Laser Room Obscured/Follow the red laser of death": 509342430, - "Guardhouse 2 - Upper Floor/In the Mound": 509342431, - "Guardhouse 2 - Bottom Floor Secret/Hidden Hallway": 509342432, - "Guardhouse 1 Obscured/Upper Floor Obscured": 509342433, - "Guardhouse 1/Upper Floor": 509342434, - "Guardhouse 1 Ledge HC/Dancing Fox Spirit Holy Cross": 509342435, - "Golden Obelisk Holy Cross/Use the Holy Cross": 509342436, - "Ice Rod Grapple Chest/Freeze the Blob and ascend With Orb": 509342437, - "Above Save Point/Chest": 509342438, - "Above Save Point Obscured/Hidden Path": 509342439, - "Guardhouse 1 Ledge/From Guardhouse 1 Chest": 509342440, - "Near Save Point/Chest": 509342441, - "Ambushed by Spiders/Beneath Spider Chest": 509342442, - "Near Telescope/Up on the Wall": 509342443, - "Ambushed by Spiders/Spider Chest": 509342444, - "Lower Dash Chest/Dash Across": 509342445, - "Lower Grapple Chest/Grapple Across": 509342446, - "Bombable Wall/Follow the Flowers": 509342447, - "Page On Teleporter/Page": 509342448, - "Forest Belltower Save Point/Near Save Point": 509342449, - "Forest Belltower - After Guard Captain/Chest": 509342450, - "East Bell/Forest Belltower - Obscured Near Bell Top Floor": 509342451, - "Forest Belltower Obscured/Obscured Beneath Bell Bottom Floor": 509342452, - "Forest Belltower Page/Page Pickup": 509342453, - "Forest Grave Path - Holy Cross Code by Grave/Single Money Chest": 509342454, - "Forest Grave Path - Above Gate/Chest": 509342455, - "Forest Grave Path - Obscured Chest/Behind the Trees": 509342456, - "Forest Grave Path - Upper Walkway/From the top of the Guardhouse": 509342457, - "The Hero's Sword/Forest Grave Path - Sword Pickup": 509342458, - "The Hero's Sword/Hero's Grave - Tooth Relic": 509342459, - "Fortress Courtyard - From East Belltower/Crack in the Wall": 509342460, - "Fortress Leaf Piles - Secret Chest/Dusty": 509342461, - "Fortress Arena/Hexagon Red": 509342462, - "Fortress Arena/Siege Engine|Vault Key Pickup": 509342463, - "Fortress East Shortcut - Chest Near Slimes/Mind the Custodians": 509342464, - "[West Wing] Candles Holy Cross/Use the Holy Cross": 509342465, - "Westmost Upper Room/[West Wing] Dark Room Chest 1": 509342466, - "Westmost Upper Room/[West Wing] Dark Room Chest 2": 509342467, - "[East Wing] Bombable Wall/Bomb the Wall": 509342468, - "[West Wing] Page Pickup/He will never visit the Far Shore": 509342469, - "Fortress Grave Path - Upper Walkway/Go Around the East Wing": 509342470, - "Vault Hero's Grave/Fortress Grave Path - Chest Right of Grave": 509342471, - "Vault Hero's Grave/Fortress Grave Path - Obscured Chest Left of Grave": 509342472, - "Vault Hero's Grave/Hero's Grave - Flowers Relic": 509342473, - "Bridge/Chest": 509342474, - "Cell Chest 1/Drop the Shortcut Rope": 509342475, - "Obscured Behind Waterfall/Muffling Bell": 509342476, - "Back Room Chest/Lose the Lure or take 2 Damage": 509342477, - "Cell Chest 2/Mind the Custodian": 509342478, - "Near Vault/Already Stolen": 509342479, - "Slorm Room/Tobias was Trapped Here Once...": 509342480, - "Escape Chest/Don't Kick Fimbleton!": 509342481, - "Grapple Above Hot Tub/Look Up": 509342482, - "Above Vault/Obscured Doorway Ledge": 509342483, - "Main Room Top Floor/Mind the Adult Frog": 509342484, - "Main Room Bottom Floor/Altar Chest": 509342485, - "Side Room Secret Passage/Upper Right Corner": 509342486, - "Side Room Chest/Oh No! Our Frogs! They're Dead!": 509342487, - "Side Room Grapple Secret/Grapple on Over": 509342488, - "Magic Orb Pickup/Frult Meeting": 509342489, - "The Librarian/Hexagon Green": 509342490, - "Library Hall/Holy Cross Chest": 509342491, - "Library Lab Chest by Shrine 2/Chest": 509342492, "Library Lab Chest by Shrine 1/Chest": 509342493, "Library Lab Chest by Shrine 3/Chest": 509342494, "Library Lab by Fuse/Behind Chalkboard": 509342495, @@ -369,6 +276,405 @@ poptracker_data: dict[str, int] = { "[North] Page Pickup/Survival Tips": 509342699, "[Southeast Lowlands] Ice Dagger Pickup/Ice Dagger Cave": 509342700, "Hero's Grave/Effigy Relic": 509342701, + "[East] Bombable Wall/Break Bombable Wall": 509350705, + "[West] Upper Area Bombable Wall/Break Bombable Wall": 509350704, + "[East Wing] Bombable Wall/Break Bombable Wall": 509350703, + "Bombable Wall/Break Bombable Wall": 509350702, + "[Northwest] Bombable Wall/Break Bombable Wall": 509350701, + "[Southwest] Bombable Wall Near Fountain/Break Bombable Wall": 509350700, + "Cube Cave/Break Bombable Wall": 509350699, + "[Central] Bombable Wall/Break Bombable Wall": 509350698, + "Purgatory Pots/Pot 33": 509350697, + "Purgatory Pots/Pot 32": 509350696, + "Purgatory Pots/Pot 31": 509350695, + "Purgatory Pots/Pot 30": 509350694, + "Purgatory Pots/Pot 29": 509350693, + "Purgatory Pots/Pot 28": 509350692, + "Purgatory Pots/Pot 27": 509350691, + "Purgatory Pots/Pot 26": 509350690, + "Purgatory Pots/Pot 25": 509350689, + "Purgatory Pots/Pot 24": 509350688, + "Purgatory Pots/Pot 23": 509350687, + "Purgatory Pots/Pot 22": 509350686, + "Purgatory Pots/Pot 21": 509350685, + "Purgatory Pots/Pot 20": 509350684, + "Purgatory Pots/Pot 19": 509350683, + "Purgatory Pots/Pot 18": 509350682, + "Purgatory Pots/Pot 17": 509350681, + "Purgatory Pots/Pot 16": 509350680, + "Purgatory Pots/Pot 15": 509350679, + "Purgatory Pots/Pot 14": 509350678, + "Purgatory Pots/Pot 13": 509350677, + "Purgatory Pots/Pot 12": 509350676, + "Purgatory Pots/Pot 11": 509350675, + "Purgatory Pots/Pot 10": 509350674, + "Purgatory Pots/Pot 9": 509350673, + "Purgatory Pots/Pot 8": 509350672, + "Purgatory Pots/Pot 7": 509350671, + "Purgatory Pots/Pot 6": 509350670, + "Purgatory Pots/Pot 5": 509350669, + "Purgatory Pots/Pot 4": 509350668, + "Purgatory Pots/Pot 3": 509350667, + "Purgatory Pots/Pot 2": 509350666, + "Purgatory Pots/Pot 1": 509350665, + "[1F] Pots by Stairs/Pot 2": 509350664, + "[1F] Pots by Stairs/Pot 1": 509350663, + "Crates/Crate 9": 509350662, + "Crates/Crate 8": 509350661, + "Crates/Crate 7": 509350660, + "Crates/Crate 6": 509350659, + "Crates/Crate 5": 509350658, + "Crates/Crate 4": 509350657, + "Crates/Crate 3": 509350656, + "Crates/Crate 2": 509350655, + "Crates/Crate 1": 509350654, + "[Lowlands] Crates/Crate 2": 509350653, + "[Lowlands] Crates/Crate 1": 509350652, + "[West] Near Isolated Chest/Crate 5": 509350651, + "[West] Near Isolated Chest/Crate 4": 509350650, + "[West] Near Isolated Chest/Crate 3": 509350649, + "[West] Near Isolated Chest/Crate 2": 509350648, + "[West] Near Isolated Chest/Crate 1": 509350647, + "[West] Crates by Shooting Range/Crate 5": 509350646, + "[West] Crates by Shooting Range/Crate 4": 509350645, + "[West] Crates by Shooting Range/Crate 3": 509350644, + "[West] Crates by Shooting Range/Crate 2": 509350643, + "[West] Crates by Shooting Range/Crate 1": 509350642, + "[West] Near Isolated Chest/Explosive Pot 2": 509350641, + "[West] Near Isolated Chest/Explosive Pot 1": 509350640, + "[West] Explosive Pot above Shooting Range/Explosive Pot": 509350639, + "[West] Explosive Pots near Bombable Wall/Explosive Pot 2": 509350638, + "[West] Explosive Pots near Bombable Wall/Explosive Pot 1": 509350637, + "[Central] Crates near Shortcut Ladder/Crate 5": 509350636, + "[Central] Crates near Shortcut Ladder/Crate 4": 509350635, + "[Central] Crates near Shortcut Ladder/Crate 3": 509350634, + "[Central] Crates near Shortcut Ladder/Crate 2": 509350633, + "[Central] Crates near Shortcut Ladder/Crate 1": 509350632, + "[Central] Explosive Pots near Shortcut Ladder/Explosive Pot 2": 509350631, + "[Central] Explosive Pots near Shortcut Ladder/Explosive Pot 1": 509350630, + "[Back Entrance] Pots/Pot 5": 509350629, + "[Back Entrance] Pots/Pot 4": 509350628, + "[Back Entrance] Pots/Pot 3": 509350627, + "[Back Entrance] Pots/Pot 2": 509350626, + "[Back Entrance] Pots/Pot 1": 509350625, + "[Central] Explosive Pots near Monastery/Explosive Pot 2": 509350624, + "[Central] Explosive Pots near Monastery/Explosive Pot 1": 509350623, + "[East] Explosive Pot beneath Scaffolding/Explosive Pot": 509350622, + "[East] Explosive Pots/Explosive Pot 3": 509350621, + "[East] Explosive Pots/Explosive Pot 2": 509350620, + "[East] Explosive Pots/Explosive Pot 1": 509350619, + "Display Cases/Display Case 3": 509350618, + "Display Cases/Display Case 2": 509350617, + "Display Cases/Display Case 1": 509350616, + "Orb Room Explosive Pots/Explosive Pot 2": 509350615, + "Orb Room Explosive Pots/Explosive Pot 1": 509350614, + "Pots after Gate/Pot 2": 509350613, + "Pots after Gate/Pot 1": 509350612, + "Slorm Room/Pot": 509350611, + "Main Room Pots/Pot 2": 509350610, + "Main Room Pots/Pot 1": 509350609, + "Side Room Pots/Pot 3": 509350608, + "Side Room Pots/Pot 2": 509350607, + "Side Room Pots/Pot 1": 509350606, + "Pots above Orb Altar/Pot 2": 509350605, + "Pots above Orb Altar/Pot 1": 509350604, + "[Upper] Pots/Pot 6": 509350603, + "[Upper] Pots/Pot 5": 509350602, + "[Upper] Pots/Pot 4": 509350601, + "[Upper] Pots/Pot 3": 509350600, + "[Upper] Pots/Pot 2": 509350599, + "[Upper] Pots/Pot 1": 509350598, + "[South] Explosive Pot near Birds/Explosive Pot": 509350597, + "[West] Broken House/Table": 509350596, + "[West] Broken House/Pot 2": 509350595, + "[West] Broken House/Pot 1": 509350594, + "Fortress Arena/Pot 2": 509350593, + "Fortress Arena/Pot 1": 509350592, + "Fortress Leaf Piles - Secret Chest/Leaf Pile 4": 509350591, + "Fortress Leaf Piles - Secret Chest/Leaf Pile 3": 509350590, + "Fortress Leaf Piles - Secret Chest/Leaf Pile 2": 509350589, + "Fortress Leaf Piles - Secret Chest/Leaf Pile 1": 509350588, + "Barrels/Back Room Barrel 7": 509350587, + "Barrels/Back Room Barrel 6": 509350586, + "Barrels/Back Room Barrel 5": 509350585, + "[Northwest] Sign by Quarry Gate/Sign": 509350400, + "[Central] Sign South of Checkpoint/Sign": 509350401, + "[Central] Sign by Ruined Passage/Sign": 509350402, + "[East] Pots near Slimes/Pot 1": 509350403, + "[East] Pots near Slimes/Pot 2": 509350404, + "[East] Pots near Slimes/Pot 3": 509350405, + "[East] Pots near Slimes/Pot 4": 509350406, + "[East] Pots near Slimes/Pot 5": 509350407, + "[East] Forest Sign/Sign": 509350408, + "[East] Fortress Sign/Sign": 509350409, + "[North] Pots/Pot 1": 509350410, + "[North] Pots/Pot 2": 509350411, + "[North] Pots/Pot 3": 509350412, + "[North] Pots/Pot 4": 509350413, + "[West] Sign Near West Garden Entrance/Sign": 509350414, + "Stick House/Pot 1": 509350415, + "Stick House/Pot 2": 509350416, + "Stick House/Pot 3": 509350417, + "Stick House/Pot 4": 509350418, + "Stick House/Pot 5": 509350419, + "Stick House/Pot 6": 509350420, + "Stick House/Pot 7": 509350421, + "Ruined Shop/Pot 1": 509350422, + "Ruined Shop/Pot 2": 509350423, + "Ruined Shop/Pot 3": 509350424, + "Ruined Shop/Pot 4": 509350425, + "Ruined Shop/Pot 5": 509350426, + "Inside Hourglass Cave/Sign": 509350427, + "Pots by Slimes/Pot 1": 509350428, + "Pots by Slimes/Pot 2": 509350429, + "Pots by Slimes/Pot 3": 509350430, + "Pots by Slimes/Pot 4": 509350431, + "Pots by Slimes/Pot 5": 509350432, + "Pots by Slimes/Pot 6": 509350433, + "[Upper] Barrels/Barrel 1": 509350434, + "[Upper] Barrels/Barrel 2": 509350435, + "[Upper] Barrels/Barrel 3": 509350436, + "Pots after Guard Captain/Pot 1": 509350437, + "Pots after Guard Captain/Pot 2": 509350438, + "Pots after Guard Captain/Pot 3": 509350439, + "Pots after Guard Captain/Pot 4": 509350440, + "Pots after Guard Captain/Pot 5": 509350441, + "Pots after Guard Captain/Pot 6": 509350442, + "Pots after Guard Captain/Pot 7": 509350443, + "Pots after Guard Captain/Pot 8": 509350444, + "Pots after Guard Captain/Pot 9": 509350445, + "Pots/Pot 1": 509350446, + "Pots/Pot 2": 509350447, + "Pots/Pot 3": 509350448, + "Pots/Pot 4": 509350449, + "Pots/Pot 5": 509350450, + "Sign by Grave Path/Sign": 509350451, + "Sign by Guardhouse 1/Sign": 509350452, + "Pots by Grave Path/Pot 1": 509350453, + "Pots by Grave Path/Pot 2": 509350454, + "Pots by Grave Path/Pot 3": 509350455, + "Pots by Envoy/Pot 1": 509350456, + "Pots by Envoy/Pot 2": 509350457, + "Pots by Envoy/Pot 3": 509350458, + "Bottom Floor Pots/Pot 1": 509350459, + "Bottom Floor Pots/Pot 2": 509350460, + "Bottom Floor Pots/Pot 3": 509350461, + "Bottom Floor Pots/Pot 4": 509350462, + "Bottom Floor Pots/Pot 5": 509350463, + "[Side Room] Pots by Chest/Pot 1": 509350464, + "[Side Room] Pots by Chest/Pot 2": 509350465, + "[Side Room] Pots by Chest/Pot 3": 509350466, + "[Third Room] Barrels by Bridge/Barrel 1": 509350467, + "[Third Room] Barrels by Bridge/Barrel 2": 509350468, + "[Third Room] Barrels by Bridge/Barrel 3": 509350469, + "[Third Room] Barrels after Back Corridor/Barrel 1": 509350470, + "[Third Room] Barrels after Back Corridor/Barrel 2": 509350471, + "[Third Room] Barrels after Back Corridor/Barrel 3": 509350472, + "[Third Room] Barrels after Back Corridor/Barrel 4": 509350473, + "[Third Room] Barrels after Back Corridor/Barrel 5": 509350474, + "[Third Room] Barrels by West Turret/Barrel 1": 509350475, + "[Third Room] Barrels by West Turret/Barrel 2": 509350476, + "[Third Room] Barrels by West Turret/Barrel 3": 509350477, + "[Third Room] Pots by East Turret/Pot 1": 509350478, + "[Third Room] Pots by East Turret/Pot 2": 509350479, + "[Third Room] Pots by East Turret/Pot 3": 509350480, + "[Third Room] Pots by East Turret/Pot 4": 509350481, + "[Third Room] Pots by East Turret/Pot 5": 509350482, + "[Third Room] Pots by East Turret/Pot 6": 509350483, + "[Third Room] Pots by East Turret/Pot 7": 509350484, + "Barrels/Barrel 1": 509350485, + "Barrels/Barrel 2": 509350486, + "Pot Hallway Pots/Pot 1": 509350487, + "Pot Hallway Pots/Pot 2": 509350488, + "Pot Hallway Pots/Pot 3": 509350489, + "Pot Hallway Pots/Pot 4": 509350490, + "Pot Hallway Pots/Pot 5": 509350491, + "Pot Hallway Pots/Pot 6": 509350492, + "Pot Hallway Pots/Pot 7": 509350493, + "Pot Hallway Pots/Pot 8": 509350494, + "Pot Hallway Pots/Pot 9": 509350495, + "Pot Hallway Pots/Pot 10": 509350496, + "Pot Hallway Pots/Pot 11": 509350497, + "Pot Hallway Pots/Pot 12": 509350498, + "Pot Hallway Pots/Pot 13": 509350499, + "Pot Hallway Pots/Pot 14": 509350500, + "2nd Laser Room Pots/Pot 1": 509350501, + "2nd Laser Room Pots/Pot 2": 509350502, + "2nd Laser Room Pots/Pot 3": 509350503, + "2nd Laser Room Pots/Pot 4": 509350504, + "2nd Laser Room Pots/Pot 5": 509350505, + "[Southeast Lowlands] Ice Dagger Pickup/Pot 1": 509350506, + "[Southeast Lowlands] Ice Dagger Pickup/Pot 2": 509350507, + "[Southeast Lowlands] Ice Dagger Pickup/Pot 3": 509350508, + "Fire Pots/Fire Pot 1": 509350509, + "Fire Pots/Fire Pot 2": 509350510, + "Fire Pots/Fire Pot 3": 509350511, + "Fire Pots/Fire Pot 4": 509350512, + "Fire Pots/Fire Pot 5": 509350513, + "Fire Pots/Fire Pot 6": 509350514, + "Fire Pots/Fire Pot 7": 509350515, + "Fire Pots/Fire Pot 8": 509350516, + "Upper Fire Pot/Fire Pot": 509350517, + "[Entry] Pots/Pot 1": 509350518, + "[Entry] Pots/Pot 2": 509350519, + "[By Grave] Pots/Pot 1": 509350520, + "[By Grave] Pots/Pot 2": 509350521, + "[By Grave] Pots/Pot 3": 509350522, + "[By Grave] Pots/Pot 4": 509350523, + "[By Grave] Pots/Pot 5": 509350524, + "[By Grave] Pots/Pot 6": 509350525, + "[Central] Fire Pots/Fire Pot 1": 509350526, + "[Central] Fire Pots/Fire Pot 2": 509350527, + "[Central] Pots by Door/Pot 1": 509350528, + "[Central] Pots by Door/Pot 2": 509350529, + "[Central] Pots by Door/Pot 3": 509350530, + "[Central] Pots by Door/Pot 4": 509350531, + "[Central] Pots by Door/Pot 5": 509350532, + "[Central] Pots by Door/Pot 6": 509350533, + "[Central] Pots by Door/Pot 7": 509350534, + "[Central] Pots by Door/Pot 8": 509350535, + "[Central] Pots by Door/Pot 9": 509350536, + "[Central] Pots by Door/Pot 10": 509350537, + "[Central] Pots by Door/Pot 11": 509350538, + "[East Wing] Pots by Broken Checkpoint/Pot 1": 509350539, + "[East Wing] Pots by Broken Checkpoint/Pot 2": 509350540, + "[East Wing] Pots by Broken Checkpoint/Pot 3": 509350541, + "[West Wing] Pots by Checkpoint/Pot 1": 509350542, + "[West Wing] Pots by Checkpoint/Pot 2": 509350543, + "[West Wing] Pots by Checkpoint/Pot 3": 509350544, + "[West Wing] Pots by Overlook/Pot 1": 509350545, + "[West Wing] Pots by Overlook/Pot 2": 509350546, + "[West Wing] Slorm Room Pots/Pot 1": 509350547, + "[West Wing] Slorm Room Pots/Pot 2": 509350548, + "[West Wing] Slorm Room Pots/Pot 3": 509350549, + "[West Wing] Chest Room Pots/Pot 1": 509350550, + "[West Wing] Chest Room Pots/Pot 2": 509350551, + "[West Wing] Pots by Stairs to Basement/Pot 1": 509350552, + "[West Wing] Pots by Stairs to Basement/Pot 2": 509350553, + "[West Wing] Pots by Stairs to Basement/Pot 3": 509350554, + "Entry Spot/Pot 1": 509350555, + "Entry Spot/Pot 2": 509350556, + "Entry Spot/Crate 1": 509350557, + "Entry Spot/Crate 2": 509350558, + "Entry Spot/Crate 3": 509350559, + "Entry Spot/Crate 4": 509350560, + "Entry Spot/Crate 5": 509350561, + "Entry Spot/Crate 6": 509350562, + "Entry Spot/Crate 7": 509350563, + "Slorm Room Crates/Crate 1": 509350564, + "Slorm Room Crates/Crate 2": 509350565, + "Crates under Rope/Crate 1": 509350566, + "Crates under Rope/Crate 2": 509350567, + "Crates under Rope/Crate 3": 509350568, + "Crates under Rope/Crate 4": 509350569, + "Crates under Rope/Crate 5": 509350570, + "Crates under Rope/Crate 6": 509350571, + "Fuse Room Fire Pots/Fire Pot 1": 509350572, + "Fuse Room Fire Pots/Fire Pot 2": 509350573, + "Fuse Room Fire Pots/Fire Pot 3": 509350574, + "Barrels/Barrel by Back Room 1": 509350575, + "Barrels/Barrel by Back Room 2": 509350576, + "Barrels/Barrel by Back Room 3": 509350577, + "Barrels/Barrel by Back Room 4": 509350578, + "Barrels/Barrel by Back Room 5": 509350579, + "Barrels/Barrel by Back Room 6": 509350580, + "Barrels/Back Room Barrel 1": 509350581, + "Barrels/Back Room Barrel 2": 509350582, + "Barrels/Back Room Barrel 3": 509350583, + "[Powered Secret Room] Chest/Follow the Purple Energy Road": 509342400, + "[Entryway] Chest/Mind the Slorms": 509342401, + "[Third Room] Beneath Platform Chest/Run from the tentacles!": 509342402, + "[Third Room] Tentacle Chest/Water Sucks": 509342403, + "[Entryway] Obscured Behind Waterfall/You can just go in there": 509342404, + "[Save Room] Upper Floor Chest 1/Through the Power of Prayer": 509342405, + "[Save Room] Upper Floor Chest 2/Above the Fox Shrine": 509342406, + "[Second Room] Underwater Chest/Hidden Passage": 509342407, + "[Back Corridor] Right Secret/Hidden Path": 509342408, + "[Back Corridor] Left Secret/Behind the Slorms": 509342409, + "[Second Room] Obscured Behind Waterfall/Just go in there": 509342410, + "[Side Room] Chest By Pots/Just Climb up There": 509342411, + "[Side Room] Chest By Phrends/So Many Phrends!": 509342412, + "[Second Room] Page/Ruined Atoll Map": 509342413, + "[Passage To Dark Tomb] Page Pickup/Siege Engine": 509342414, + "[1F] Guarded By Lasers/Beside 3 Miasma Seekers": 509342415, + "[1F] Near Spikes/Mind the Miasma Seeker": 509342416, + "Birdcage Room/[2F] Bird Room": 509342417, + "[2F] Entryway Upper Walkway/Overlooking Miasma": 509342418, + "[1F] Library/By the Books": 509342419, + "[2F] Library/Behind the Ladder": 509342420, + "[2F] Guarded By Lasers/Before the big reveal...": 509342421, + "Birdcage Room/[2F] Bird Room Secret": 509342422, + "[1F] Library Secret/Pray to the Wallman": 509342423, + "Spike Maze Near Exit/Watch out!": 509342424, + "2nd Laser Room/Can you roll?": 509342425, + "1st Laser Room/Use a bomb?": 509342426, + "Spike Maze Upper Walkway/Just walk right!": 509342427, + "Skulls Chest/Move the Grave": 509342428, + "Spike Maze Near Stairs/In the Corner": 509342429, + "1st Laser Room Obscured/Follow the red laser of death": 509342430, + "Guardhouse 2 - Upper Floor/In the Mound": 509342431, + "Guardhouse 2 - Bottom Floor Secret/Hidden Hallway": 509342432, + "Guardhouse 1 Obscured/Upper Floor Obscured": 509342433, + "Guardhouse 1/Upper Floor": 509342434, + "Guardhouse 1 Ledge HC/Dancing Fox Spirit Holy Cross": 509342435, + "Golden Obelisk Holy Cross/Use the Holy Cross": 509342436, + "Ice Rod Grapple Chest/Freeze the Blob and ascend With Orb": 509342437, + "Above Save Point/Chest": 509342438, + "Above Save Point Obscured/Hidden Path": 509342439, + "Guardhouse 1 Ledge/From Guardhouse 1 Chest": 509342440, + "Near Save Point/Chest": 509342441, + "Ambushed by Spiders/Beneath Spider Chest": 509342442, + "Near Telescope/Up on the Wall": 509342443, + "Ambushed by Spiders/Spider Chest": 509342444, + "Lower Dash Chest/Dash Across": 509342445, + "Lower Grapple Chest/Grapple Across": 509342446, + "Bombable Wall/Follow the Flowers": 509342447, + "Page On Teleporter/Page": 509342448, + "Forest Belltower Save Point/Near Save Point": 509342449, + "Forest Belltower - After Guard Captain/Chest": 509342450, + "East Bell/Forest Belltower - Obscured Near Bell Top Floor": 509342451, + "Forest Belltower Obscured/Obscured Beneath Bell Bottom Floor": 509342452, + "Forest Belltower Page/Page Pickup": 509342453, + "Forest Grave Path - Holy Cross Code by Grave/Single Money Chest": 509342454, + "Forest Grave Path - Above Gate/Chest": 509342455, + "Forest Grave Path - Obscured Chest/Behind the Trees": 509342456, + "Forest Grave Path - Upper Walkway/From the top of the Guardhouse": 509342457, + "The Hero's Sword/Forest Grave Path - Sword Pickup": 509342458, + "The Hero's Sword/Hero's Grave - Tooth Relic": 509342459, + "Fortress Courtyard - From East Belltower/Crack in the Wall": 509342460, + "Fortress Leaf Piles - Secret Chest/Dusty": 509342461, + "Fortress Arena/Hexagon Red": 509342462, + "Fortress Arena/Siege Engine|Vault Key Pickup": 509342463, + "Fortress East Shortcut - Chest Near Slimes/Mind the Custodians": 509342464, + "[West Wing] Candles Holy Cross/Use the Holy Cross": 509342465, + "Westmost Upper Room/[West Wing] Dark Room Chest 1": 509342466, + "Westmost Upper Room/[West Wing] Dark Room Chest 2": 509342467, + "[East Wing] Bombable Wall/Bomb the Wall": 509342468, + "[West Wing] Page Pickup/He will never visit the Far Shore": 509342469, + "Fortress Grave Path - Upper Walkway/Go Around the East Wing": 509342470, + "Vault Hero's Grave/Fortress Grave Path - Chest Right of Grave": 509342471, + "Vault Hero's Grave/Fortress Grave Path - Obscured Chest Left of Grave": 509342472, + "Vault Hero's Grave/Hero's Grave - Flowers Relic": 509342473, + "Bridge/Chest": 509342474, + "Cell Chest 1/Drop the Shortcut Rope": 509342475, + "Obscured Behind Waterfall/Muffling Bell": 509342476, + "Back Room Chest/Lose the Lure or take 2 Damage": 509342477, + "Cell Chest 2/Mind the Custodian": 509342478, + "Near Vault/Already Stolen": 509342479, + "Slorm Room/Tobias was Trapped Here Once...": 509342480, + "Escape Chest/Don't Kick Fimbleton!": 509342481, + "Grapple Above Hot Tub/Look Up": 509342482, + "Above Vault/Obscured Doorway Ledge": 509342483, + "Main Room Top Floor/Mind the Adult Frog": 509342484, + "Main Room Bottom Floor/Altar Chest": 509342485, + "Side Room Secret Passage/Upper Right Corner": 509342486, + "Side Room Chest/Oh No! Our Frogs! They're Dead!": 509342487, + "Side Room Grapple Secret/Grapple on Over": 509342488, + "Magic Orb Pickup/Frult Meeting": 509342489, + "The Librarian/Hexagon Green": 509342490, + "Library Hall/Holy Cross Chest": 509342491, + "Library Lab Chest by Shrine 2/Chest": 509342492, + "Barrels/Back Room Barrel 4": 509350584, } From 125d053b61733031d0ebe8559f6edd006e1c4e94 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Sat, 12 Jul 2025 07:52:02 -0400 Subject: [PATCH 13/15] TUNIC: Fix missing line for UT stuff #5185 --- worlds/tunic/ut_stuff.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/tunic/ut_stuff.py b/worlds/tunic/ut_stuff.py index 82d58eaeb8..2cf2f96a4f 100644 --- a/worlds/tunic/ut_stuff.py +++ b/worlds/tunic/ut_stuff.py @@ -681,7 +681,7 @@ poptracker_data: dict[str, int] = { # for setting up the poptracker integration tracker_world = { "map_page_maps": ["maps/maps_pop.json"], - "map_page_locations": ["locations/locations_pop_er.json"], + "map_page_locations": ["locations/locations_pop_er.json", "locations/locations_breakables.json"], "map_page_setting_key": "Slot:{player}:Current Map", "map_page_index": map_page_index, "external_pack_key": "ut_poptracker_path", From a9b35de7ee9d02d320aaae4b28259ae0ec3139ad Mon Sep 17 00:00:00 2001 From: Justus Lind Date: Sat, 12 Jul 2025 23:02:49 +1000 Subject: [PATCH 14/15] Muse Dash: Update song list to Rotaeno Update/7th Anniversary (#5066) --- worlds/musedash/MuseDashData.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/worlds/musedash/MuseDashData.py b/worlds/musedash/MuseDashData.py index f2bcf1220f..bcb4ab8e2a 100644 --- a/worlds/musedash/MuseDashData.py +++ b/worlds/musedash/MuseDashData.py @@ -641,4 +641,17 @@ SONG_DATA: Dict[str, SongData] = { "Save Yourself": SongData(2900765, "85-3", "Happy Otaku Pack Vol.20", True, 5, 7, 10), "Menace": SongData(2900766, "85-4", "Happy Otaku Pack Vol.20", True, 7, 9, 11), "Dangling": SongData(2900767, "85-5", "Happy Otaku Pack Vol.20", True, 6, 8, 10), + "Inverted World": SongData(2900768, "86-0", "Aquaria Cruising Guide", True, 4, 6, 8), + "Suito": SongData(2900769, "86-1", "Aquaria Cruising Guide", True, 6, 8, 11), + "The Promised Land": SongData(2900770, "86-2", "Aquaria Cruising Guide", True, 4, 6, 9), + "Alfheim's faith": SongData(2900771, "86-3", "Aquaria Cruising Guide", True, 6, 8, 11), + "Heaven's Cage": SongData(2900772, "86-4", "Aquaria Cruising Guide", True, 5, 7, 10), + "Broomstick adventure!": SongData(2900773, "86-5", "Aquaria Cruising Guide", True, 7, 9, 11), + "Strong Nurse Buro-chan!": SongData(2900774, "43-61", "MD Plus Project", True, 5, 7, 9), + "Cubism": SongData(2900775, "43-62", "MD Plus Project", False, 5, 7, 9), + "Cubibibibism": SongData(2900776, "43-63", "MD Plus Project", False, 6, 8, 10), + "LET'S TOAST!!": SongData(2900777, "43-64", "MD Plus Project", False, 6, 8, 10), + "#YamiKawa": SongData(2900778, "43-65", "MD Plus Project", False, 5, 7, 10), + "Rainy Step": SongData(2900779, "43-66", "MD Plus Project", False, 2, 5, 8), + "OHOSHIKATSU": SongData(2900780, "43-67", "MD Plus Project", False, 5, 7, 10), } From ec3f168a09b54d8ee41e44f8fca70a582f9f0ddf Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Mon, 14 Jul 2025 07:22:10 +0000 Subject: [PATCH 15/15] Doc: match statement in style guide (#5187) * Test: add micro benchmark for match * Doc: add 'match' to python style guide --- docs/style.md | 4 +++ test/benchmark/match.py | 66 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 test/benchmark/match.py diff --git a/docs/style.md b/docs/style.md index 81853f4172..5333155db9 100644 --- a/docs/style.md +++ b/docs/style.md @@ -29,6 +29,10 @@ * New classes, attributes, and methods in core code should have docstrings that follow [reST style](https://peps.python.org/pep-0287/). * Worlds that do not follow PEP8 should still have a consistent style across its files to make reading easier. +* [Match statements](https://docs.python.org/3/tutorial/controlflow.html#tut-match) + may be used instead of `if`-`elif` if they result in nicer code, or they actually use pattern matching. + Beware of the performance: they are not `goto`s, but `if`-`elif` under the hood, and you may have less control. When + in doubt, just don't use it. ## Markdown diff --git a/test/benchmark/match.py b/test/benchmark/match.py new file mode 100644 index 0000000000..ccb600c0ba --- /dev/null +++ b/test/benchmark/match.py @@ -0,0 +1,66 @@ +"""Micro benchmark comparing match as "switch" with if-elif and dict access""" + +from timeit import timeit + + +def make_match(count: int) -> str: + code = f"for val in range({count}):\n match val:\n" + for n in range(count): + m = n + 1 + code += f" case {n}:\n" + code += f" res = {m}\n" + return code + + +def make_elif(count: int) -> str: + code = f"for val in range({count}):\n" + for n in range(count): + m = n + 1 + code += f" {'' if n == 0 else 'el'}if val == {n}:\n" + code += f" res = {m}\n" + return code + + +def make_dict(count: int, mode: str) -> str: + if mode == "value": + code = "dct = {\n" + for n in range(count): + m = n + 1 + code += f" {n}: {m},\n" + code += "}\n" + code += f"for val in range({count}):\n res = dct[val]" + return code + elif mode == "call": + code = "" + for n in range(count): + m = n + 1 + code += f"def func{n}():\n val = {m}\n\n" + code += "dct = {\n" + for n in range(count): + code += f" {n}: func{n},\n" + code += "}\n" + code += f"for val in range({count}):\n dct[val]()" + return code + return "" + + +def timeit_best_of_5(stmt: str, setup: str = "pass") -> float: + """ + Benchmark some code, returning the best of 5 runs. + :param stmt: Code to benchmark + :param setup: Optional code to set up environment + :return: Time taken in microseconds + """ + return min(timeit(stmt, setup, number=10000, globals={}) for _ in range(5)) * 100 + + +def main() -> None: + for count in (3, 5, 8, 10, 20, 30): + print(f"value of {count:-2} with match: {timeit_best_of_5(make_match(count)) / count:.3f} us") + print(f"value of {count:-2} with elif: {timeit_best_of_5(make_elif(count)) / count:.3f} us") + print(f"value of {count:-2} with dict: {timeit_best_of_5(make_dict(count, 'value')) / count:.3f} us") + print(f"call of {count:-2} with dict: {timeit_best_of_5(make_dict(count, 'call')) / count:.3f} us") + + +if __name__ == "__main__": + main()