From c56d0d5335eb1547cb7fd55652ad37573c7e7e1b Mon Sep 17 00:00:00 2001 From: Salzkorn Date: Fri, 11 Oct 2024 02:20:41 +0200 Subject: [PATCH 1/3] Fix cross-layer recursion error in client --- worlds/sc2/client.py | 13 +++++++++++++ worlds/sc2/mission_order/entry_rules.py | 16 +++++++--------- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/worlds/sc2/client.py b/worlds/sc2/client.py index 7fd9a77b9a09..2f3f0ded2d95 100644 --- a/worlds/sc2/client.py +++ b/worlds/sc2/client.py @@ -1424,13 +1424,26 @@ def calc_available_nodes(ctx: SC2Context) -> typing.Tuple[typing.List[int], typi received_items[network_item.item] += 1 accessible_rules: typing.Set[int] = set() + # Determine accessibility from top to bottom to avoid a recursion problem from + # missions trying to access layouts & campaigns appearing later than themselves + # Campaigns for campaign_idx, campaign in enumerate(ctx.custom_mission_order): available_layouts[campaign_idx] = [] if campaign.entry_rule.is_accessible(beaten_missions, received_items, ctx.mission_id_to_entry_rules, accessible_rules, set()): available_campaigns.append(campaign_idx) + + # Layouts + for campaign_idx, campaign in enumerate(ctx.custom_mission_order): + if campaign_idx in available_campaigns: for layout_idx, layout in enumerate(campaign.layouts): if layout.entry_rule.is_accessible(beaten_missions, received_items, ctx.mission_id_to_entry_rules, accessible_rules, set()): available_layouts[campaign_idx].append(layout_idx) + + # Missions + for campaign_idx, campaign in enumerate(ctx.custom_mission_order): + if campaign_idx in available_campaigns: + for layout_idx, layout in enumerate(campaign.layouts): + if layout_idx in available_layouts[campaign_idx]: for column in layout.missions: for mission in column: if mission.mission_id == -1: diff --git a/worlds/sc2/mission_order/entry_rules.py b/worlds/sc2/mission_order/entry_rules.py index 96d1693b38e4..1b03f337fbf8 100644 --- a/worlds/sc2/mission_order/entry_rules.py +++ b/worlds/sc2/mission_order/entry_rules.py @@ -201,15 +201,13 @@ def is_accessible( accessible_rules: Set[int], seen_rules: Set[int] ) -> bool: # Count rules are accessible if enough of their missions are beaten and accessible - # return self.amount <= sum( - # all( - # rule.is_accessible(beaten_missions, received_items, mission_id_to_entry_rules, accessible_rules, seen_rules) - # for rule in mission_id_to_entry_rules[mission_id] - # ) - # for mission_id in beaten_missions.intersection(self.mission_ids) - # ) - # Temp hotfix as the missions that have this rule themselves aren't counted in the client against this rule - return self.amount <= len(beaten_missions.intersection(self.mission_ids)) + return self.amount <= sum( + all( + rule.is_accessible(beaten_missions, received_items, mission_id_to_entry_rules, accessible_rules, seen_rules) + for rule in mission_id_to_entry_rules[mission_id] + ) + for mission_id in beaten_missions.intersection(self.mission_ids) + ) class SubRuleEntryRule(EntryRule): rule_id: int From d1923bc3331d0de0a76419cad8341b55b44a2038 Mon Sep 17 00:00:00 2001 From: EnvyDragon <138727357+EnvyDragon@users.noreply.github.com> Date: Fri, 11 Oct 2024 16:39:33 -0400 Subject: [PATCH 2/3] SC2: Fixing Warp Relocate (progressive item bug) --- worlds/sc2/item_names.py | 2 +- worlds/sc2/items.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/sc2/item_names.py b/worlds/sc2/item_names.py index 92a9287deabe..26a09f5b0a99 100644 --- a/worlds/sc2/item_names.py +++ b/worlds/sc2/item_names.py @@ -824,7 +824,7 @@ OPTIMIZED_ORDNANCE = "Optimized Ordnance (Protoss)" KHALAI_INGENUITY = "Khalai Ingenuity (Protoss)" AMPLIFIED_ASSIMILATORS = "Amplified Assimilators (Protoss)" -PROGRESSIVE_WARP_RELOCATE = "Warp Relocate (Protoss)" +PROGRESSIVE_WARP_RELOCATE = "Progressive Warp Relocate (Protoss)" # Filler items STARTING_MINERALS = "Additional Starting Minerals" diff --git a/worlds/sc2/items.py b/worlds/sc2/items.py index 80dcf231d229..e2c1503ffa73 100644 --- a/worlds/sc2/items.py +++ b/worlds/sc2/items.py @@ -1970,7 +1970,7 @@ def get_full_item_list(): item_names.AMPLIFIED_ASSIMILATORS: ItemData(812 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Solarite_Core, 12, SC2Race.PROTOSS, origin={"ext"}), item_names.PROGRESSIVE_WARP_RELOCATE: - ItemData(813 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Progressive, 1, SC2Race.PROTOSS, origin={"ext"}, quantity=2, + ItemData(813 + SC2LOTV_ITEM_ID_OFFSET, ProtossItemType.Progressive, 2, SC2Race.PROTOSS, origin={"ext"}, quantity=2, classification=ItemClassification.progression), } From 4be7d07d63df52a969e53c32d5f5a393c0bb5f6d Mon Sep 17 00:00:00 2001 From: Salzkorn Date: Fri, 11 Oct 2024 23:04:59 +0200 Subject: [PATCH 3/3] Count recursions in entry rule check in client --- worlds/sc2/client.py | 19 ++---------- worlds/sc2/mission_order/entry_rules.py | 39 ++++++++++++++++--------- 2 files changed, 28 insertions(+), 30 deletions(-) diff --git a/worlds/sc2/client.py b/worlds/sc2/client.py index 2f3f0ded2d95..378a9c6fad2e 100644 --- a/worlds/sc2/client.py +++ b/worlds/sc2/client.py @@ -1424,31 +1424,18 @@ def calc_available_nodes(ctx: SC2Context) -> typing.Tuple[typing.List[int], typi received_items[network_item.item] += 1 accessible_rules: typing.Set[int] = set() - # Determine accessibility from top to bottom to avoid a recursion problem from - # missions trying to access layouts & campaigns appearing later than themselves - # Campaigns for campaign_idx, campaign in enumerate(ctx.custom_mission_order): available_layouts[campaign_idx] = [] - if campaign.entry_rule.is_accessible(beaten_missions, received_items, ctx.mission_id_to_entry_rules, accessible_rules, set()): + if campaign.entry_rule.is_accessible(beaten_missions, received_items, ctx.mission_id_to_entry_rules, accessible_rules, []): available_campaigns.append(campaign_idx) - - # Layouts - for campaign_idx, campaign in enumerate(ctx.custom_mission_order): - if campaign_idx in available_campaigns: for layout_idx, layout in enumerate(campaign.layouts): - if layout.entry_rule.is_accessible(beaten_missions, received_items, ctx.mission_id_to_entry_rules, accessible_rules, set()): + if layout.entry_rule.is_accessible(beaten_missions, received_items, ctx.mission_id_to_entry_rules, accessible_rules, []): available_layouts[campaign_idx].append(layout_idx) - - # Missions - for campaign_idx, campaign in enumerate(ctx.custom_mission_order): - if campaign_idx in available_campaigns: - for layout_idx, layout in enumerate(campaign.layouts): - if layout_idx in available_layouts[campaign_idx]: for column in layout.missions: for mission in column: if mission.mission_id == -1: continue - if mission.entry_rule.is_accessible(beaten_missions, received_items, ctx.mission_id_to_entry_rules, accessible_rules, set()): + if mission.entry_rule.is_accessible(beaten_missions, received_items, ctx.mission_id_to_entry_rules, accessible_rules, []): available_missions.append(mission.mission_id) return available_missions, available_layouts, available_campaigns diff --git a/worlds/sc2/mission_order/entry_rules.py b/worlds/sc2/mission_order/entry_rules.py index 1b03f337fbf8..afa4f3e3e40f 100644 --- a/worlds/sc2/mission_order/entry_rules.py +++ b/worlds/sc2/mission_order/entry_rules.py @@ -70,7 +70,7 @@ def shows_single_rule(self) -> bool: def is_accessible( self, beaten_missions: Set[int], received_items: Dict[int, int], mission_id_to_entry_rules: Dict[int, MissionEntryRules], - accessible_rules: Set[int], seen_rules: Set[int] + accessible_rules: Set[int], seen_rules: List[int] ) -> bool: return False @@ -121,7 +121,7 @@ def shows_single_rule(self) -> bool: def is_accessible( self, beaten_missions: Set[int], received_items: Dict[int, int], mission_id_to_entry_rules: Dict[int, MissionEntryRules], - accessible_rules: Set[int], seen_rules: Set[int] + accessible_rules: Set[int], seen_rules: List[int] ) -> bool: # Beat rules are accessible if all their missions are beaten and accessible if not beaten_missions.issuperset(self.mission_ids): @@ -182,7 +182,9 @@ def tooltip(self, indents: int, missions: Dict[int, SC2Mission]) -> str: req = self.visual_reqs[0] req_str = missions[req].mission_name if type(req) == int else req if self.amount == 1: - return f"Beat {req_str}" + if type(req) == int: + return f"Beat {req_str}" + return f"Beat any mission from {req_str}" return f"Beat {amount} missions from {req_str}" if self.amount == 1: tooltip = f"Beat any mission from:\n{indent}- " @@ -198,7 +200,7 @@ def shows_single_rule(self) -> bool: def is_accessible( self, beaten_missions: Set[int], received_items: Dict[int, int], mission_id_to_entry_rules: Dict[int, MissionEntryRules], - accessible_rules: Set[int], seen_rules: Set[int] + accessible_rules: Set[int], seen_rules: List[int] ) -> bool: # Count rules are accessible if enough of their missions are beaten and accessible return self.amount <= sum( @@ -279,11 +281,16 @@ def parse_from_dict(data: Dict[str, Any]) -> SubRuleRuleData: **{field: value for field, value in rule_data.items()} ) sub_rules.append(rule) - return SubRuleRuleData( + rule = SubRuleRuleData( rule_id, sub_rules, amount ) + # Add an accessibility buffer for top level rules + # This is an optimization to make recalculations of large mission orders feel smoother + if rule.rule_id >= 0: + rule.buffer_accessible = False + return rule @staticmethod def empty() -> SubRuleRuleData: @@ -309,26 +316,30 @@ def shows_single_rule(self) -> bool: def is_accessible( self, beaten_missions: Set[int], received_items: Dict[int, int], mission_id_to_entry_rules: Dict[int, MissionEntryRules], - accessible_rules: Set[int], seen_rules: Set[int] + accessible_rules: Set[int], seen_rules: List[int] ) -> bool: # Early exit check for top-level entry rules if self.rule_id >= 0: - if self.rule_id in accessible_rules: + if self.buffer_accessible or self.rule_id in accessible_rules: return True # Never consider rules discovered via recursion to be accessible # (unless they succeeded, in which case they will be in accessible_rules) if self.rule_id in seen_rules: return False - seen_rules.add(self.rule_id) + seen_rules.append(self.rule_id) # Sub-rule rules are accessible if enough of their child rules are accessible - if self.amount <= sum( + success = self.amount <= sum( rule.is_accessible(beaten_missions, received_items, mission_id_to_entry_rules, accessible_rules, seen_rules) for rule in self.sub_rules - ): - if self.rule_id >= 0: + ) + if self.rule_id >= 0: + if success: accessible_rules.add(self.rule_id) - return True - return False + self.buffer_accessible = True + # Rules that failed in the middle of calculating another rule + # should be allowed to try again at a later point + seen_rules.remove(self.rule_id) + return success class ItemEntryRule(EntryRule): items_to_check: Dict[str, int] @@ -375,7 +386,7 @@ def shows_single_rule(self) -> bool: def is_accessible( self, beaten_missions: Set[int], received_items: Dict[int, int], mission_id_to_entry_rules: Dict[int, MissionEntryRules], - accessible_rules: Set[int], seen_rules: Set[int] + accessible_rules: Set[int], seen_rules: List[int] ) -> bool: return all( item in received_items and received_items[item] >= amount