SC2: fix supreme logic hole (#5768)

* sc2: Fixing a discrepancy between slot data and logic
where story tech would not be granted for supreme if zerg was not a selected race.

* sc2: Fixed an issue where Kinetic Blast was not listed as a vanilla Kerrigan ability

* sc2: Fixing some functions that could force Kerrigan items into the pool when playing Kerriganless

* sc2: excluding zerg excludes hots for vanilla-like mission order
* Preprocessing options
* Moving general empty selection handling to option preprocessing
* Adding a unit test for empty race/campaign selection

* sc2: Properly handling non-raceswapped campaigns when excluding campaigns based on race exclusions

* sc2: Adding an explicit error message if a user excludes all missions in a way with no obvious resolution
This commit is contained in:
Phaneros
2026-01-19 11:11:31 -08:00
committed by GitHub
parent b8311a62e7
commit 9f71fe707f
5 changed files with 115 additions and 73 deletions

View File

@@ -12,8 +12,10 @@ from .item.item_tables import (
get_full_item_list,
not_balanced_starting_units, WEAPON_ARMOR_UPGRADE_MAX_LEVEL,
)
from .item import FilterItem, ItemFilterFlags, StarcraftItem, item_groups, item_names, item_tables, item_parents, \
from .item import (
FilterItem, ItemFilterFlags, StarcraftItem, item_groups, item_names, item_tables, item_parents,
ZergItemType, ProtossItemType, ItemData
)
from .locations import (
get_locations, DEFAULT_LOCATION_LIST, get_location_types, get_location_flags,
get_plando_locations, LocationType, lookup_location_id_to_type
@@ -117,7 +119,38 @@ class SC2World(World):
data = get_full_item_list()[name]
return StarcraftItem(name, data.classification, data.code, self.player)
def create_regions(self):
def generate_early(self) -> None:
# Do some options validation/recovery here
if not self.options.selected_races.value:
self.options.selected_races.value = set(options.SelectedRaces.default)
if not self.options.enabled_campaigns.value:
self.options.enabled_campaigns.value = set(options.EnabledCampaigns.default)
# Disable campaigns on vanilla-like mission orders if their race is disabled
if self.options.mission_order.value in options.static_mission_orders:
enabled_campaigns = set(self.options.enabled_campaigns.value)
if self.options.enable_race_swap.value == options.EnableRaceSwapVariants.option_disabled:
if SC2Race.TERRAN.get_title() not in self.options.selected_races.value:
enabled_campaigns.discard(SC2Campaign.WOL.campaign_name)
if SC2Race.ZERG.get_title() not in self.options.selected_races.value:
enabled_campaigns.discard(SC2Campaign.HOTS.campaign_name)
if SC2Race.PROTOSS.get_title() not in self.options.selected_races.value:
enabled_campaigns.discard(SC2Campaign.PROPHECY.campaign_name)
enabled_campaigns.discard(SC2Campaign.PROLOGUE.campaign_name)
enabled_campaigns.discard(SC2Campaign.LOTV.campaign_name)
# Epilogue and NCO don't have raceswaps currently
if SC2Race.TERRAN.get_title() not in self.options.selected_races.value:
enabled_campaigns.discard(SC2Campaign.NCO.campaign_name)
if len(self.options.selected_races.value) < 3:
enabled_campaigns.discard(SC2Campaign.EPILOGUE.campaign_name)
if not enabled_campaigns:
raise OptionError(
"Campaign and race exclusions remove all possible missions from the pool. "
"Either include more campaigns, include more races, or enable race swap."
)
self.options.enabled_campaigns.value = enabled_campaigns
def create_regions(self) -> None:
self.logic = SC2Logic(self)
self.custom_mission_order = create_mission_order(
self, get_locations(self), self.location_cache
@@ -224,7 +257,9 @@ class SC2World(World):
slot_data["custom_mission_order"] = self.custom_mission_order.get_slot_data()
slot_data["version"] = 4
if SC2Campaign.HOTS not in enabled_campaigns:
if (SC2Campaign.HOTS not in enabled_campaigns
or SC2Race.ZERG.get_title() not in self.options.selected_races.value
):
slot_data["kerrigan_presence"] = KerriganPresence.option_not_present
if self.options.mission_order_scouting != MissionOrderScouting.option_none:
@@ -574,6 +609,7 @@ def flag_mission_based_item_excludes(world: SC2World, item_list: List[FilterItem
len(kerrigan_missions) > 0
and world.options.kerrigan_presence in kerrigan_unit_available
and SC2Campaign.HOTS in get_enabled_campaigns(world) # TODO: Kerrigan available all Zerg/Everywhere
and SC2Race.ZERG.get_title() in world.options.selected_races.value
)
# TvX build missions -- check flags

View File

@@ -600,7 +600,7 @@ item_name_groups[ItemGroupNames.KERRIGAN_LOGIC_ACTIVE_ABILITIES] = kerrigan_logi
item_name for item_name in kerrigan_active_abilities if item_name != item_names.KERRIGAN_ASSIMILATION_AURA
]
item_name_groups[ItemGroupNames.KERRIGAN_TIER_1] = kerrigan_tier_1 = [
item_names.KERRIGAN_CRUSHING_GRIP, item_names.KERRIGAN_HEROIC_FORTITUDE, item_names.KERRIGAN_LEAPING_STRIKE
item_names.KERRIGAN_KINETIC_BLAST, item_names.KERRIGAN_HEROIC_FORTITUDE, item_names.KERRIGAN_LEAPING_STRIKE
]
item_name_groups[ItemGroupNames.KERRIGAN_TIER_2] = kerrigan_tier_2= [
item_names.KERRIGAN_CRUSHING_GRIP, item_names.KERRIGAN_CHAIN_REACTION, item_names.KERRIGAN_PSIONIC_SHIFT

View File

@@ -1576,7 +1576,7 @@ def get_option_value(world: Union['SC2World', None], name: str) -> int:
def get_enabled_races(world: Optional['SC2World']) -> Set[SC2Race]:
race_names = world.options.selected_races.value if world and len(world.options.selected_races.value) > 0 else SelectedRaces.valid_keys
race_names = world.options.selected_races.value if world else SelectedRaces.default
return {race for race in SC2Race if race.get_title() in race_names}
@@ -1584,16 +1584,7 @@ def get_enabled_campaigns(world: Optional['SC2World']) -> Set[SC2Campaign]:
if world is None:
return {campaign for campaign in SC2Campaign if campaign.campaign_name in EnabledCampaigns.default}
campaign_names = world.options.enabled_campaigns
campaigns = {campaign for campaign in SC2Campaign if campaign.campaign_name in campaign_names}
if (world.options.mission_order.value == MissionOrder.option_vanilla
and get_enabled_races(world) != {SC2Race.TERRAN, SC2Race.ZERG, SC2Race.PROTOSS}
and SC2Campaign.EPILOGUE in campaigns
):
campaigns.remove(SC2Campaign.EPILOGUE)
if len(campaigns) == 0:
# Everything is disabled, roll as everything enabled
return {campaign for campaign in SC2Campaign if campaign != SC2Campaign.GLOBAL}
return campaigns
return {campaign for campaign in SC2Campaign if campaign.campaign_name in campaign_names}
def get_disabled_campaigns(world: 'SC2World') -> Set[SC2Campaign]:

View File

@@ -1142,7 +1142,10 @@ class SC2Logic:
return levels >= target
def basic_kerrigan(self, state: CollectionState, story_tech_available=True) -> bool:
if story_tech_available and self.grant_story_tech == GrantStoryTech.option_grant:
if story_tech_available and (
self.grant_story_tech == GrantStoryTech.option_grant
or not self.kerrigan_unit_available
):
return True
# One active ability that can be used to defeat enemies directly
if not state.has_any(
@@ -1166,7 +1169,10 @@ class SC2Logic:
return False
def two_kerrigan_actives(self, state: CollectionState, story_tech_available=True) -> bool:
if story_tech_available and self.grant_story_tech == GrantStoryTech.option_grant:
if story_tech_available and (
self.grant_story_tech == GrantStoryTech.option_grant
or not self.kerrigan_unit_available
):
return True
return state.count_from_list(item_groups.kerrigan_logic_active_abilities, self.player) >= 2
@@ -2361,6 +2367,7 @@ class SC2Logic:
# Note(mm): This check isn't necessary as self.kerrigan_levels cover it,
# and it's not fully desirable in future when we support non-grant story tech + kerriganless.
# or not self.kerrigan_presence
or not self.kerrigan_unit_available
or state.has_any((
# Cases tested by Snarky
item_names.KERRIGAN_KINETIC_BLAST,
@@ -2399,7 +2406,8 @@ class SC2Logic:
def supreme_requirement(self, state: CollectionState) -> bool:
return (
self.grant_story_tech == GrantStoryTech.option_grant
or not self.kerrigan_unit_available or (self.grant_story_tech == GrantStoryTech.option_allow_substitutes
or not self.kerrigan_unit_available
or (self.grant_story_tech == GrantStoryTech.option_allow_substitutes
and state.has_any((
item_names.KERRIGAN_LEAPING_STRIKE,
item_names.OVERLORD_VENTRAL_SACS,
@@ -2502,15 +2510,12 @@ class SC2Logic:
self.grant_story_tech == GrantStoryTech.option_grant
or not self.kerrigan_unit_available
or (
state.has_any(
(
state.has_any((
item_names.KERRIGAN_KINETIC_BLAST,
item_names.KERRIGAN_SPAWN_BANELINGS,
item_names.KERRIGAN_LEAPING_STRIKE,
item_names.KERRIGAN_SPAWN_LEVIATHAN,
),
self.player,
)
), self.player)
and self.basic_kerrigan(state)
)
)

View File

@@ -271,9 +271,19 @@ class TestSupportedUseCases(Sc2SetupTestBase):
world_regions.remove('Menu')
for region in world_regions:
self.assertNotIn(mission_tables.lookup_name_to_mission[region].campaign,
self.assertNotIn(
mission_tables.lookup_name_to_mission[region].campaign,
([mission_tables.SC2Campaign.EPILOGUE]),
f"{region} is an epilogue mission!")
f"{region} is an epilogue mission!"
)
def test_excluding_all_factions_and_campaigns_still_generates(self) -> None:
world_options = {
'selected_races': set(),
'enabled_campaigns': set(),
}
# asserting no exception is thrown
self.generate_world(world_options)
def test_race_swap_pick_one_has_correct_length_and_includes_swaps(self) -> None:
world_options = {