forked from mirror/Archipelago
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user