From 6409f4d7609e3c0ad712ce6515b1ddd58e8331f7 Mon Sep 17 00:00:00 2001 From: Richard Snider Date: Wed, 29 Jul 2026 01:11:43 -0600 Subject: [PATCH] do not create regions that will not be used in the rando this commit does a lot of things: * region connections now support metadata * region data no longer contains a list of regions * regions are created on-demand when investigating region connections in logic * Pools.__should_include has been turned into a public member for ease of implementation * several conditions that rely on region accessibility now check for the existence of those regions to avoid crashes -- this should be treated as a temporary fix --- worlds/crosscode/codegen/ast.py | 13 ++ worlds/crosscode/codegen/merge.py | 11 ++ worlds/crosscode/codegen/parse.py | 22 +-- worlds/crosscode/regions.py | 134 +++--------------- .../crosscode/templates/regions.template.py | 1 - worlds/crosscode/types/condition.py | 10 +- worlds/crosscode/types/pools.py | 18 +-- worlds/crosscode/types/regions.py | 3 +- worlds/crosscode/world.py | 13 +- 9 files changed, 81 insertions(+), 144 deletions(-) diff --git a/worlds/crosscode/codegen/ast.py b/worlds/crosscode/codegen/ast.py index 4250f264fe70..bd88dc12a24d 100644 --- a/worlds/crosscode/codegen/ast.py +++ b/worlds/crosscode/codegen/ast.py @@ -290,6 +290,19 @@ def create_expression_region_connection(conn: RegionConnection): ] ) + if conn.metadata is not None: + # this will never be none, but it must claim that it can be to satisfy the typing gods + keys: list[ast.expr | None] = [ast.Constant(k) for k in conn.metadata.keys()] + values: list[ast.expr] = [ast.Constant(k) for k in conn.metadata.values()] + + ast_region.keywords.append(ast.keyword( + arg="metadata", + value=ast.Dict( + keys=keys, + values=values, + ) + )) + ast.fix_missing_locations(ast_region) return ast_region diff --git a/worlds/crosscode/codegen/merge.py b/worlds/crosscode/codegen/merge.py index acd07393f69b..0f89719e360c 100644 --- a/worlds/crosscode/codegen/merge.py +++ b/worlds/crosscode/codegen/merge.py @@ -46,6 +46,17 @@ def merge(original: T, addon: T, patch: bool = True) -> T: return original if isinstance(original, list): + if isinstance(addon, dict): + for idx, val in addon.items(): + if idx == "*": + for child in original: + merge(child, val, patch) + continue + if not idx.isnumeric(): + raise RuntimeError(f"Cannot merge list and dict with non-numeric keys") + merge(original[int(idx)], val, patch) + return original + if not isinstance(addon, list): raise RuntimeError(f"Cannot merge type {type(original)} with {type(addon)}") original.extend(addon) diff --git a/worlds/crosscode/codegen/parse.py b/worlds/crosscode/codegen/parse.py index 02489242bb56..9843d496da11 100644 --- a/worlds/crosscode/codegen/parse.py +++ b/worlds/crosscode/codegen/parse.py @@ -351,7 +351,12 @@ def parse_region_connection(self, raw: dict[str, typing.Any]) -> RegionConnectio if "condition" in raw: condition = self.parse_condition(raw["condition"]) - return RegionConnection(region_from, region_to, condition) + return RegionConnection( + region_from=region_from, + region_to=region_to, + cond=condition, + metadata=raw.get("metadata", None) + ) def parse_regions_data(self, raw: dict[str, typing.Any]) -> RegionsData: """ @@ -377,14 +382,10 @@ def parse_regions_data(self, raw: dict[str, typing.Any]) -> RegionsData: if not isinstance(raw_connections, list): raise JsonParserError(raw, raw_connections, "regions data", "connection must be list") - regions_seen: set[str] = set() - connections = [] for raw_conn in raw_connections: conn = self.parse_region_connection(raw_conn) - regions_seen.add(conn.region_to) - regions_seen.add(conn.region_from) connections.append(conn) @@ -398,11 +399,12 @@ def parse_regions_data(self, raw: dict[str, typing.Any]) -> RegionsData: for goal_name, goal in raw["goals"].items(): goals[goal_name] = self.parse_goal(goal) - region_list = list(regions_seen) - - region_list.sort() - - return RegionsData(start, exclude, region_list, connections, goals) + return RegionsData( + starting_region=start, + excluded_regions=exclude, + region_connections=connections, + goals=goals + ) def parse_regions_data_list(self, raw: dict[str, dict[str, typing.Any]]) -> dict[str, RegionsData]: """ diff --git a/worlds/crosscode/regions.py b/worlds/crosscode/regions.py index 3b456e1f0932..9617559b2784 100644 --- a/worlds/crosscode/regions.py +++ b/worlds/crosscode/regions.py @@ -19,43 +19,6 @@ "linear": RegionsData( starting_region = "2", excluded_regions = ['1'], - region_list = [ - '10', - '11', - '12', - '13', - '14', - '15', - '16', - '17', - '17.5', - '18', - '19', - '2', - '20', - '21', - '22', - '23', - '24', - '25', - '26', - '26.5', - '27', - '28', - '29', - '3', - '3.1', - '30', - '31', - '32', - '33', - '4', - '5', - '6', - '7', - '8', - '9', - ], region_connections = [ RegionConnection(region_from='2', region_to='3', cond=[ItemCondition(item_name='Green Leaf Shade', amount=1)]), RegionConnection(region_from='3', region_to='4', cond=[ItemCondition(item_name='Mine Pass', amount=1), ItemCondition(item_name='Guild Pass', amount=1)]), @@ -99,69 +62,6 @@ "open": RegionsData( starting_region = "open2", excluded_regions = ['open1'], - region_list = [ - 'open10', - 'open10.Grove', - 'open10.Infested', - 'open10.Left', - 'open10.Mid', - 'open10.Right', - 'open11', - 'open13.1', - 'open13.2', - 'open14.1', - 'open14.2', - 'open14.3', - 'open14.4', - 'open14.5', - 'open15.1', - 'open15.2', - 'open15.3', - 'open16', - 'open16.1', - 'open17', - 'open18', - 'open2', - 'open20', - 'open3', - 'open3.1', - 'open4.1', - 'open4.2', - 'open4.3', - 'open4.4', - 'open4.5', - 'open4.6', - 'open4.7', - 'open4.8', - 'open5', - 'open6', - 'open7.1', - 'open7.2', - 'open7.3', - 'open7.4', - 'open7.5', - 'open7.6', - 'open7.7', - 'open7.8', - 'open8', - 'open9', - 'openDLC1', - 'openDLC_Beach', - 'openDLC_DungeonB2_L', - 'openDLC_DungeonB2_L.1', - 'openDLC_DungeonB2_R', - 'openDLC_DungeonB2_R.1', - 'openDLC_DungeonB3_L', - 'openDLC_DungeonB3_R', - 'openDLC_DungeonBoss', - 'openDLC_DungeonEntry', - 'openDLC_DungeonEntry.1F', - 'openDLC_DungeonEntry.1L', - 'openDLC_DungeonEntry.1R', - 'openDLC_DungeonGF_L', - 'openDLC_DungeonGF_R', - 'openDLC_DungeonMain', - ], region_connections = [ RegionConnection(region_from='open2', region_to='open3', cond=[ItemCondition(item_name='Green Leaf Shade', amount=1)]), RegionConnection(region_from='open3', region_to='open4.1', cond=[ItemCondition(item_name='Mine Pass', amount=1)]), @@ -210,23 +110,23 @@ RegionConnection(region_from='open16', region_to='open17', cond=[ItemCondition(item_name='Old Dojo Key', amount=1)]), RegionConnection(region_from='open16', region_to='open16.1', cond=[ItemCondition(item_name='Meteor Shade', amount=1)]), RegionConnection(region_from='open16', region_to='open18', cond=[VariableCondition(name='vwPassage')]), - RegionConnection(region_from='open3', region_to='openDLC1', cond=[ItemCondition(item_name='Guild Pass', amount=1)]), - RegionConnection(region_from='open20', region_to='openDLC_Beach', cond=[ItemCondition(item_name='Azure Archipelago Pass', amount=1)]), - RegionConnection(region_from='open16.1', region_to='openDLC_DungeonEntry', cond=[ItemCondition(item_name='Ancient Shade', amount=1)]), - RegionConnection(region_from='openDLC_DungeonEntry', region_to='openDLC_DungeonEntry.1F', cond=[ItemCondition(item_name="Ku'lero Key", amount=3), ItemCondition(item_name='Wave', amount=1)]), - RegionConnection(region_from='openDLC_DungeonEntry', region_to='openDLC_DungeonEntry.1L', cond=[ItemCondition(item_name="Ku'lero Key", amount=3)]), - RegionConnection(region_from='openDLC_DungeonEntry', region_to='openDLC_DungeonEntry.1R', cond=[OrCondition(subconditions=[AndCondition(subconditions=[ItemCondition(item_name="Ku'lero Key", amount=2), ItemCondition(item_name='Wave', amount=1)]), ItemCondition(item_name="Ku'lero Key", amount=3)])]), - RegionConnection(region_from='openDLC_DungeonEntry', region_to='openDLC_DungeonGF_R', cond=[ItemCondition(item_name='Heat', amount=1), ItemCondition(item_name='Cold', amount=1), ItemCondition(item_name='Shock', amount=1)]), - RegionConnection(region_from='openDLC_DungeonEntry', region_to='openDLC_DungeonGF_L', cond=[ItemCondition(item_name='Heat', amount=1), ItemCondition(item_name='Cold', amount=1), ItemCondition(item_name='Wave', amount=1)]), - RegionConnection(region_from='openDLC_DungeonGF_R', region_to='openDLC_DungeonMain', cond=[]), - RegionConnection(region_from='openDLC_DungeonGF_L', region_to='openDLC_DungeonMain', cond=[]), - RegionConnection(region_from='openDLC_DungeonMain', region_to='openDLC_DungeonB2_R', cond=[ItemCondition(item_name='Heat', amount=1), ItemCondition(item_name='Shock', amount=1), ItemCondition(item_name='Cold', amount=1)]), - RegionConnection(region_from='openDLC_DungeonB2_R', region_to='openDLC_DungeonB2_R.1', cond=[ItemCondition(item_name='Wave', amount=1)]), - RegionConnection(region_from='openDLC_DungeonMain', region_to='openDLC_DungeonB2_L', cond=[ItemCondition(item_name='Heat', amount=1), ItemCondition(item_name='Wave', amount=1), ItemCondition(item_name='Cold', amount=1), ItemCondition(item_name='Shock', amount=1)]), - RegionConnection(region_from='openDLC_DungeonB2_L', region_to='openDLC_DungeonB2_L.1', cond=[ItemCondition(item_name="Ku'lero Key", amount=3)]), - RegionConnection(region_from='openDLC_DungeonMain', region_to='openDLC_DungeonB3_L', cond=[ItemCondition(item_name='Heat', amount=1), ItemCondition(item_name='Cold', amount=1), ItemCondition(item_name='Shock', amount=1)]), - RegionConnection(region_from='openDLC_DungeonMain', region_to='openDLC_DungeonB3_R', cond=[ItemCondition(item_name='Shock', amount=1), ItemCondition(item_name='Wave', amount=1)]), - RegionConnection(region_from='openDLC_DungeonMain', region_to='openDLC_DungeonBoss', cond=[ItemCondition(item_name='Heat', amount=1), ItemCondition(item_name='Cold', amount=1), ItemCondition(item_name='Shock', amount=1), ItemCondition(item_name='Wave', amount=1), ItemCondition(item_name="Ku'lero Master Key", amount=1)]), + RegionConnection(region_from='open3', region_to='openDLC1', cond=[ItemCondition(item_name='Guild Pass', amount=1)], metadata={'dlc': True}), + RegionConnection(region_from='open20', region_to='openDLC_Beach', cond=[ItemCondition(item_name='Azure Archipelago Pass', amount=1)], metadata={'dlc': True}), + RegionConnection(region_from='open16.1', region_to='openDLC_DungeonEntry', cond=[ItemCondition(item_name='Ancient Shade', amount=1)], metadata={'dlc': True}), + RegionConnection(region_from='openDLC_DungeonEntry', region_to='openDLC_DungeonEntry.1F', cond=[ItemCondition(item_name="Ku'lero Key", amount=3), ItemCondition(item_name='Wave', amount=1)], metadata={'dlc': True}), + RegionConnection(region_from='openDLC_DungeonEntry', region_to='openDLC_DungeonEntry.1L', cond=[ItemCondition(item_name="Ku'lero Key", amount=3)], metadata={'dlc': True}), + RegionConnection(region_from='openDLC_DungeonEntry', region_to='openDLC_DungeonEntry.1R', cond=[OrCondition(subconditions=[AndCondition(subconditions=[ItemCondition(item_name="Ku'lero Key", amount=2), ItemCondition(item_name='Wave', amount=1)]), ItemCondition(item_name="Ku'lero Key", amount=3)])], metadata={'dlc': True}), + RegionConnection(region_from='openDLC_DungeonEntry', region_to='openDLC_DungeonGF_R', cond=[ItemCondition(item_name='Heat', amount=1), ItemCondition(item_name='Cold', amount=1), ItemCondition(item_name='Shock', amount=1)], metadata={'dlc': True}), + RegionConnection(region_from='openDLC_DungeonEntry', region_to='openDLC_DungeonGF_L', cond=[ItemCondition(item_name='Heat', amount=1), ItemCondition(item_name='Cold', amount=1), ItemCondition(item_name='Wave', amount=1)], metadata={'dlc': True}), + RegionConnection(region_from='openDLC_DungeonGF_R', region_to='openDLC_DungeonMain', cond=[], metadata={'dlc': True}), + RegionConnection(region_from='openDLC_DungeonGF_L', region_to='openDLC_DungeonMain', cond=[], metadata={'dlc': True}), + RegionConnection(region_from='openDLC_DungeonMain', region_to='openDLC_DungeonB2_R', cond=[ItemCondition(item_name='Heat', amount=1), ItemCondition(item_name='Shock', amount=1), ItemCondition(item_name='Cold', amount=1)], metadata={'dlc': True}), + RegionConnection(region_from='openDLC_DungeonB2_R', region_to='openDLC_DungeonB2_R.1', cond=[ItemCondition(item_name='Wave', amount=1)], metadata={'dlc': True}), + RegionConnection(region_from='openDLC_DungeonMain', region_to='openDLC_DungeonB2_L', cond=[ItemCondition(item_name='Heat', amount=1), ItemCondition(item_name='Wave', amount=1), ItemCondition(item_name='Cold', amount=1), ItemCondition(item_name='Shock', amount=1)], metadata={'dlc': True}), + RegionConnection(region_from='openDLC_DungeonB2_L', region_to='openDLC_DungeonB2_L.1', cond=[ItemCondition(item_name="Ku'lero Key", amount=3)], metadata={'dlc': True}), + RegionConnection(region_from='openDLC_DungeonMain', region_to='openDLC_DungeonB3_L', cond=[ItemCondition(item_name='Heat', amount=1), ItemCondition(item_name='Cold', amount=1), ItemCondition(item_name='Shock', amount=1)], metadata={'dlc': True}), + RegionConnection(region_from='openDLC_DungeonMain', region_to='openDLC_DungeonB3_R', cond=[ItemCondition(item_name='Shock', amount=1), ItemCondition(item_name='Wave', amount=1)], metadata={'dlc': True}), + RegionConnection(region_from='openDLC_DungeonMain', region_to='openDLC_DungeonBoss', cond=[ItemCondition(item_name='Heat', amount=1), ItemCondition(item_name='Cold', amount=1), ItemCondition(item_name='Shock', amount=1), ItemCondition(item_name='Wave', amount=1), ItemCondition(item_name="Ku'lero Master Key", amount=1)], metadata={'dlc': True}), ], goals = { 'creator': Goal(region='open18', condition=[ItemCondition(item_name='Heat', amount=1), ItemCondition(item_name='Cold', amount=1), ItemCondition(item_name='Shock', amount=1), ItemCondition(item_name='Wave', amount=1), VariableCondition(name='vtShadeLock')]), diff --git a/worlds/crosscode/templates/regions.template.py b/worlds/crosscode/templates/regions.template.py index 993cc610d2cb..439fc686debf 100644 --- a/worlds/crosscode/templates/regions.template.py +++ b/worlds/crosscode/templates/regions.template.py @@ -14,7 +14,6 @@ "{{mode}}": RegionsData( starting_region = "{{r.starting_region}}", excluded_regions = {{r.excluded_regions}}, - region_list = {{r.region_list | emit_list("constant") | indent(8)}}, region_connections = {{r.region_connections | emit_list("region_connection") | indent(8)}}, goals = {{r.goals.items() | emit_dict("constant", "goal") | indent(8) }} ), diff --git a/worlds/crosscode/types/condition.py b/worlds/crosscode/types/condition.py index dc383b4396fa..4a3945d4342f 100644 --- a/worlds/crosscode/types/condition.py +++ b/worlds/crosscode/types/condition.py @@ -72,6 +72,12 @@ def satisfied(self, state: CollectionState, player: int, location: int | None, a mode: str = args["mode"] if self.target_mode is None or mode == self.target_mode: + # BAD BAD BAD + # This will check *every time* this condition is called whether the region exists. + # I'm only doing this because I know I'm going to optimize it later. + # If we still have to check region existence in the refactor we have to calculate it early. + if self.region_name not in state.multiworld.regions.region_cache[player]: + return False return state.can_reach_region(self.region_name, player) return True @@ -175,7 +181,9 @@ def satisfied(self, state: CollectionState, player: int, location: int | None, a collected = sum([ amount for region, amount in args["region_botanics_amounts"].items() - if state.can_reach_region(region, player) + # See the other comment beginning with "BAD BAD BAD" -- this is bad for the same reason + if region in state.multiworld.regions.region_cache[player] + and state.can_reach_region(region, player) ]) return collected / args["botanics_completion_amount"] >= self.amount diff --git a/worlds/crosscode/types/pools.py b/worlds/crosscode/types/pools.py index 4ab9a394a5de..22ed9b4232b1 100644 --- a/worlds/crosscode/types/pools.py +++ b/worlds/crosscode/types/pools.py @@ -69,17 +69,17 @@ def __init__(self, world_data: WorldData, opts: IncludeOptions): weights = {} for loc in world_data.pool_locations: - if self.__should_include(loc.metadata): + if self.should_include(loc.metadata): self.location_pool.append(loc) for ev in world_data.events_dict.values(): - if self.__should_include(ev.metadata): + if self.should_include(ev.metadata): self.event_pool.append(ev) for name, pool in world_data.item_pools_template.items(): counter = defaultdict(lambda: 0) for entry in pool: - if self.__should_include(entry.metadata): + if self.should_include(entry.metadata): counter[entry.item] += entry.quantity self.item_pools[name] = counter @@ -92,12 +92,12 @@ def __init__(self, world_data: WorldData, opts: IncludeOptions): for shop_name, locations in world_data.per_shop_locations.items(): shop_locations = [] for loc in locations.values(): - if self.__should_include(loc.metadata): + if self.should_include(loc.metadata): shop_locations.append(loc) self.per_shop_location_pool[shop_name] = shop_locations for loc in world_data.global_shop_locations.values(): - if self.__should_include(loc.metadata): + if self.should_include(loc.metadata): self.global_shop_location_pool.append(loc) for shop_pool, pool_name in ( @@ -105,14 +105,14 @@ def __init__(self, world_data: WorldData, opts: IncludeOptions): (world_data.shop_unlock_by_shop, "shop_unlock_by_shop"), (world_data.shop_unlock_by_shop_and_id, "shop_unlock_by_shop_and_id") ): - self.item_pools[pool_name] = { entry.item: 1 for entry in shop_pool.values() if self.__should_include(entry.metadata) } + self.item_pools[pool_name] = { entry.item: 1 for entry in shop_pool.values() if self.should_include(entry.metadata) } for chain_name, chain in world_data.progressive_chains.items(): item_list: list[ItemData] = [] self.progressive_chains[chain_name] = item_list items = self.locate_chain(chain) for idx, entry in enumerate(items): - if self.__should_include(entry.metadata): + if self.should_include(entry.metadata): item_list.append(entry.item) prog_item = world_data.progressive_items[chain_name].name self.item_progressive_replacements[entry.item.name].append((prog_item, idx + 1)) @@ -129,12 +129,12 @@ def locate_chain(self, chain: ProgressiveItemChain) -> list[ProgressiveChainEntr for subchain in chain.subchains: if subchain.metadata is None: return subchain.chain - if self.__should_include(subchain.metadata): + if self.should_include(subchain.metadata): return subchain.chain return [] - def __should_include(self, metadata: IncludeOptions | None) -> bool: + def should_include(self, metadata: IncludeOptions | None) -> bool: # Technically the class allows metadata to be None. # So we'll assign a local variable and use that instead. if metadata is None: diff --git a/worlds/crosscode/types/regions.py b/worlds/crosscode/types/regions.py index ec95109c4adc..bbb04169d495 100644 --- a/worlds/crosscode/types/regions.py +++ b/worlds/crosscode/types/regions.py @@ -2,12 +2,14 @@ import typing from .condition import Condition +from .metadata import IncludeOptions @dataclass class RegionConnection: region_from: str region_to: str cond: typing.Optional[list[Condition]] + metadata: typing.Optional[IncludeOptions] = None @dataclass class Goal: @@ -18,6 +20,5 @@ class Goal: class RegionsData: starting_region: str excluded_regions: typing.List[str] - region_list: typing.List[str] region_connections: typing.List[RegionConnection] goals: typing.Dict[str, Goal] diff --git a/worlds/crosscode/world.py b/worlds/crosscode/world.py index 8b099984c515..158d3ccc2f2f 100644 --- a/worlds/crosscode/world.py +++ b/worlds/crosscode/world.py @@ -483,6 +483,8 @@ def add_location(self, data: LocationData, region: Region): def create_shops(self): # don't filter the shop pool - the regions must always exist to prevent generation errors for shop_name, shop in self.world_data.shops_dict.items(): + if not self.pools.should_include(shop.metadata): + continue region = Region(shop_name, self.player, self.multiworld) self.multiworld.regions.append(region) for mode, from_region in shop.access.region.items(): @@ -502,15 +504,16 @@ def create_shops(self): self.add_location(data, self.region_dict["Menu"]) def create_regions(self): - self.multiworld.regions.extend( - Region(name, self.player, self.multiworld) - for name in self.region_pack.region_list - if name not in self.region_pack.excluded_regions - ) self.region_dict = self.multiworld.regions.region_cache[self.player] self.location_events = {} for conn in self.region_pack.region_connections: + if not self.pools.should_include(conn.metadata): + continue + if conn.region_to not in self.region_dict: + self.region_dict[conn.region_to] = Region(conn.region_to, self.player, self.multiworld) + if conn.region_from not in self.region_dict: + self.region_dict[conn.region_from] = Region(conn.region_from, self.player, self.multiworld) self.region_dict[conn.region_from].connect( self.region_dict[conn.region_to], f"{conn.region_from} => {conn.region_to}",