Compare commits

...

3 Commits

Author SHA1 Message Date
Phaneros
d834ecec6a SC2: Fix bugs and issues around excluded/unexcluded (#5644) 2025-11-25 20:44:07 +01:00
threeandthreee
f3000a89d4 LADX: Give better feedback during patching (#5401) 2025-11-25 20:42:55 +01:00
qwint
aa2774a5d5 Tests: Move world dependencies in tests to APQuest #5668 2025-11-25 19:26:37 +01:00
16 changed files with 269 additions and 155 deletions

View File

@@ -44,19 +44,19 @@ class TestOptions(unittest.TestCase):
}], }],
[{ [{
"name": "ItemLinkGroup", "name": "ItemLinkGroup",
"item_pool": ["Hammer", "Bow"], "item_pool": ["Hammer", "Sword"],
"link_replacement": False, "link_replacement": False,
"replacement_item": None, "replacement_item": None,
}] }]
] ]
# we really need some sort of test world but generic doesn't have enough items for this # we really need some sort of test world but generic doesn't have enough items for this
world = AutoWorldRegister.world_types["A Link to the Past"] world = AutoWorldRegister.world_types["APQuest"]
plando_options = PlandoOptions.from_option_string("bosses") plando_options = PlandoOptions.from_option_string("bosses")
item_links = [ItemLinks.from_any(item_link_groups[0]), ItemLinks.from_any(item_link_groups[1])] item_links = [ItemLinks.from_any(item_link_groups[0]), ItemLinks.from_any(item_link_groups[1])]
for link in item_links: for link in item_links:
link.verify(world, "tester", plando_options) link.verify(world, "tester", plando_options)
self.assertIn("Hammer", link.value[0]["item_pool"]) self.assertIn("Hammer", link.value[0]["item_pool"])
self.assertIn("Bow", link.value[0]["item_pool"]) self.assertIn("Sword", link.value[0]["item_pool"])
# TODO test that the group created using these options has the items # TODO test that the group created using these options has the items

View File

@@ -2,8 +2,8 @@ description: Almost blank test yaml
name: Player{NUMBER} name: Player{NUMBER}
game: game:
Timespinner: 1 # what else APQuest: 1 # what else
requires: requires:
version: 0.2.6 version: 0.2.6
Timespinner: {} APQuest: {}

View File

@@ -1,11 +1,9 @@
import binascii import binascii
import importlib.util import importlib.util
import importlib.machinery import importlib.machinery
import os
import random import random
import pickle import pickle
import Utils import Utils
import settings
from collections import defaultdict from collections import defaultdict
from typing import Dict from typing import Dict
@@ -65,8 +63,27 @@ from .patches.aesthetics import rgb_to_bin, bin_to_rgb
from .. import Options from .. import Options
class VersionError(Exception):
pass
# Function to generate a final rom, this patches the rom with all required patches # Function to generate a final rom, this patches the rom with all required patches
def generateRom(base_rom: bytes, args, patch_data: Dict): def generateRom(base_rom: bytes, args, patch_data: Dict):
from .. import LinksAwakeningWorld
patcher_version = LinksAwakeningWorld.world_version
generated_version = Utils.tuplize_version(patch_data.get("generated_world_version", "2.0.0"))
if generated_version.major != patcher_version.major or generated_version.minor != patcher_version.minor:
Utils.messagebox(
"Error",
"The apworld version that this patch was generated on is incompatible with your installed world.\n\n"
f"Generated on {generated_version.as_simple_string()}\n"
f"Installed version {patcher_version.as_simple_string()}",
True
)
raise VersionError(
f"The installed world ({patcher_version.as_simple_string()}) is incompatible with the world this patch "
f"was generated on ({generated_version.as_simple_string()})"
)
random.seed(patch_data["seed"] + patch_data["player"]) random.seed(patch_data["seed"] + patch_data["player"])
multi_key = binascii.unhexlify(patch_data["multi_key"].encode()) multi_key = binascii.unhexlify(patch_data["multi_key"].encode())
item_list = pickle.loads(binascii.unhexlify(patch_data["item_list"].encode())) item_list = pickle.loads(binascii.unhexlify(patch_data["item_list"].encode()))
@@ -85,9 +102,8 @@ def generateRom(base_rom: bytes, args, patch_data: Dict):
pymod.prePatch(rom) pymod.prePatch(rom)
if options["gfxmod"]: if options["gfxmod"]:
user_settings = settings.get_settings()
try: try:
gfx_mod_file = user_settings["ladx_options"]["gfx_mod_file"] gfx_mod_file = LinksAwakeningWorld.settings.gfx_mod_file
patches.aesthetics.gfxMod(rom, gfx_mod_file) patches.aesthetics.gfxMod(rom, gfx_mod_file)
except FileNotFoundError: except FileNotFoundError:
pass # if user just doesnt provide gfxmod file, let patching continue pass # if user just doesnt provide gfxmod file, let patching continue

View File

@@ -47,6 +47,10 @@ class BadRetroArchResponse(GameboyException):
class BadRetroArchResponse(GameboyException): class BadRetroArchResponse(GameboyException):
pass pass
class VersionError(Exception):
pass
class LAClientConstants: class LAClientConstants:
@@ -518,7 +522,7 @@ class LinksAwakeningContext(CommonContext):
class LADXManager(GameManager): class LADXManager(GameManager):
logging_pairs = [ logging_pairs = [
("Client", "Archipelago"), ("Client", "Archipelago"),
("Tracker", "Tracker"), ("Tracker", "Tracker"),
] ]
base_title = f"Links Awakening DX Client {LinksAwakeningWorld.world_version.as_simple_string()} | Archipelago" base_title = f"Links Awakening DX Client {LinksAwakeningWorld.world_version.as_simple_string()} | Archipelago"
@@ -614,11 +618,20 @@ class LinksAwakeningContext(CommonContext):
def on_package(self, cmd: str, args: dict): def on_package(self, cmd: str, args: dict):
if cmd == "Connected": if cmd == "Connected":
self.game = self.slot_info[self.slot].game
self.slot_data = args.get("slot_data", {})
generated_version = Utils.tuplize_version(self.slot_data.get("world_version", "2.0.0"))
client_version = LinksAwakeningWorld.world_version
if generated_version.major != client_version.major:
self.disconnected_intentionally = True
raise VersionError(
f"The installed world ({client_version.as_simple_string()}) is incompatible with "
f"the world this game was generated on ({generated_version.as_simple_string()})" f"the world this game was generated on ({generated_version.as_simple_string()})"
) )
# This is sent to magpie over local websocket to make its own connection # This is sent to magpie over local websocket to make its own connection
self.slot_data.update({ self.slot_data.update({
"server_address": self.server_address, "server_address": self.server_address,
"slot_name": self.player_names[self.slot],
"password": self.password, "password": self.password,
"client_version": client_version.as_simple_string(), "client_version": client_version.as_simple_string(),
}) })

View File

@@ -1,4 +1,3 @@
import settings
import worlds.Files import worlds.Files
import hashlib import hashlib
import Utils import Utils
@@ -59,6 +58,7 @@ class LADXProcedurePatch(worlds.Files.APProcedurePatch):
def write_patch_data(world: "LinksAwakeningWorld", patch: LADXProcedurePatch): def write_patch_data(world: "LinksAwakeningWorld", patch: LADXProcedurePatch):
item_list = pickle.dumps([item for item in world.ladxr_logic.iteminfo_list if not isinstance(item, KeyLocation)]) item_list = pickle.dumps([item for item in world.ladxr_logic.iteminfo_list if not isinstance(item, KeyLocation)])
data_dict = { data_dict = {
"generated_world_version": world.world_version.as_simple_string(),
"out_base": world.multiworld.get_out_file_name_base(patch.player), "out_base": world.multiworld.get_out_file_name_base(patch.player),
"is_race": world.multiworld.is_race, "is_race": world.multiworld.is_race,
"seed": world.multiworld.seed, "seed": world.multiworld.seed,
@@ -125,9 +125,9 @@ def get_base_rom_bytes(file_name: str = "") -> bytes:
def get_base_rom_path(file_name: str = "") -> str: def get_base_rom_path(file_name: str = "") -> str:
options = settings.get_settings() from . import LinksAwakeningWorld
if not file_name: if not file_name:
file_name = options["ladx_options"]["rom_file"] file_name = LinksAwakeningWorld.settings.rom_file
if not os.path.exists(file_name): if not os.path.exists(file_name):
file_name = Utils.user_path(file_name) file_name = Utils.user_path(file_name)
return file_name return file_name

View File

@@ -4,8 +4,10 @@ import os
import typing import typing
import logging import logging
import re import re
import struct
import settings import settings
import Utils
from BaseClasses import CollectionState, Entrance, Item, ItemClassification, Location, Tutorial from BaseClasses import CollectionState, Entrance, Item, ItemClassification, Location, Tutorial
from Fill import fill_restrictive from Fill import fill_restrictive
from worlds.AutoWorld import WebWorld, World from worlds.AutoWorld import WebWorld, World
@@ -50,6 +52,17 @@ class LinksAwakeningSettings(settings.Group):
description = "LADX ROM File" description = "LADX ROM File"
md5s = [LADXProcedurePatch.hash] md5s = [LADXProcedurePatch.hash]
@classmethod
def validate(cls, path: str) -> None:
try:
super().validate(path)
except ValueError:
Utils.messagebox(
"Error",
"Provided rom does not match hash for English 1.0/revision-0 of Link's Awakening DX",
True)
raise
class RomStart(str): class RomStart(str):
""" """
Set this to false to never autostart a rom (such as after patching) Set this to false to never autostart a rom (such as after patching)
@@ -71,6 +84,24 @@ class LinksAwakeningSettings(settings.Group):
Only .bin or .bdiff files Only .bin or .bdiff files
The same directory will be checked for a matching text modification file The same directory will be checked for a matching text modification file
""" """
def browse(self, filetypes=None, **kwargs):
filetypes = [("Binary / Patch files", [".bin", ".bdiff"])]
return super().browse(filetypes=filetypes, **kwargs)
@classmethod
def validate(cls, path: str) -> None:
with open(path, "rb", buffering=0) as f:
header, size = struct.unpack("<II", f.read()[:8])
if path.endswith('.bin') and header == 0xDEADBEEF and size < 1024:
# detect extended spritesheets from upstream ladxr
Utils.messagebox(
"Error",
"Extended sprite sheets are not supported. Try again with a different gfxmod file, "
"or provide no file to continue without modifying graphics.",
True)
raise ValueError("Provided gfxmod file is an extended sheet, which is not supported")
rom_file: RomFile = RomFile(RomFile.copy_to) rom_file: RomFile = RomFile(RomFile.copy_to)
rom_start: typing.Union[RomStart, bool] = True rom_start: typing.Union[RomStart, bool] = True

View File

@@ -2,5 +2,5 @@
"game": "Links Awakening DX", "game": "Links Awakening DX",
"authors": [ "zig", "threeandthree" ], "authors": [ "zig", "threeandthree" ],
"minimum_ap_version": "0.6.4", "minimum_ap_version": "0.6.4",
"world_version": "2.0.0" "world_version": "2.0.1"
} }

View File

@@ -374,22 +374,32 @@ def create_and_flag_explicit_item_locks_and_excludes(world: SC2World) -> List[Fi
Handles `excluded_items`, `locked_items`, and `start_inventory` Handles `excluded_items`, `locked_items`, and `start_inventory`
Returns a list of all possible non-filler items that can be added, with an accompanying flags bitfield. Returns a list of all possible non-filler items that can be added, with an accompanying flags bitfield.
""" """
excluded_items = world.options.excluded_items excluded_items: dict[str, int] = world.options.excluded_items.value
unexcluded_items = world.options.unexcluded_items unexcluded_items: dict[str, int] = world.options.unexcluded_items.value
locked_items = world.options.locked_items locked_items: dict[str, int] = world.options.locked_items.value
start_inventory = world.options.start_inventory start_inventory: dict[str, int] = world.options.start_inventory.value
key_items = world.custom_mission_order.get_items_to_lock() key_items = world.custom_mission_order.get_items_to_lock()
def resolve_count(count: Optional[int], max_count: int) -> int: def resolve_exclude(count: int, max_count: int) -> int:
if count == 0: if count < 0:
return max_count return max_count
if count is None: return count
return 0
if max_count == 0:
return count
return min(count, max_count)
auto_excludes = {item_name: 1 for item_name in item_groups.legacy_items} def resolve_count(count: int, max_count: int, negative_value: int | None = None) -> int:
"""
Handles `count` being out of range.
* If `count > max_count`, returns `max_count`.
* If `count < 0`, returns `negative_value` (returns `max_count` if `negative_value` is unspecified)
"""
if count < 0:
if negative_value is None:
return max_count
return negative_value
if max_count and count > max_count:
return max_count
return count
auto_excludes = Counter({item_name: 1 for item_name in item_groups.legacy_items})
if world.options.exclude_overpowered_items.value == ExcludeOverpoweredItems.option_true: if world.options.exclude_overpowered_items.value == ExcludeOverpoweredItems.option_true:
for item_name in item_groups.overpowered_items: for item_name in item_groups.overpowered_items:
auto_excludes[item_name] = 1 auto_excludes[item_name] = 1
@@ -402,28 +412,29 @@ def create_and_flag_explicit_item_locks_and_excludes(world: SC2World) -> List[Fi
elif item_name in item_groups.nova_equipment: elif item_name in item_groups.nova_equipment:
continue continue
else: else:
auto_excludes[item_name] = 0 auto_excludes[item_name] = item_data.quantity
result: List[FilterItem] = [] result: List[FilterItem] = []
for item_name, item_data in item_tables.item_table.items(): for item_name, item_data in item_tables.item_table.items():
max_count = item_data.quantity max_count = item_data.quantity
auto_excluded_count = auto_excludes.get(item_name) auto_excluded_count = auto_excludes.get(item_name, 0)
excluded_count = excluded_items.get(item_name, auto_excluded_count) excluded_count = excluded_items.get(item_name, auto_excluded_count)
unexcluded_count = unexcluded_items.get(item_name) unexcluded_count = unexcluded_items.get(item_name, 0)
locked_count = locked_items.get(item_name) locked_count = locked_items.get(item_name, 0)
start_count: Optional[int] = start_inventory.get(item_name) start_count = start_inventory.get(item_name, 0)
key_count = key_items.get(item_name, 0) key_count = key_items.get(item_name, 0)
# specifying 0 in the yaml means exclude / lock all # Specifying a negative number in the yaml means exclude / lock / start all.
# start_inventory doesn't allow specifying 0 # In the case of excluded/unexcluded, resolve negatives to max_count before subtracting them,
# not specifying means don't exclude/lock/start # and after subtraction resolve negatives to just 0 (when unexcluded > excluded).
excluded_count = resolve_count(excluded_count, max_count) excluded_count = resolve_count(
unexcluded_count = resolve_count(unexcluded_count, max_count) resolve_exclude(excluded_count, max_count) - resolve_exclude(unexcluded_count, max_count),
max_count,
negative_value=0
)
locked_count = resolve_count(locked_count, max_count) locked_count = resolve_count(locked_count, max_count)
start_count = resolve_count(start_count, max_count) start_count = resolve_count(start_count, max_count)
excluded_count = max(0, excluded_count - unexcluded_count)
# Priority: start_inventory >> locked_items >> excluded_items >> unspecified # Priority: start_inventory >> locked_items >> excluded_items >> unspecified
if max_count == 0: if max_count == 0:
if excluded_count: if excluded_count:
@@ -486,8 +497,9 @@ def flag_excludes_by_faction_presence(world: SC2World, item_list: List[FilterIte
item.flags |= ItemFilterFlags.FilterExcluded item.flags |= ItemFilterFlags.FilterExcluded
continue continue
if not zerg_missions and item.data.race == SC2Race.ZERG: if not zerg_missions and item.data.race == SC2Race.ZERG:
if item.data.type != item_tables.ZergItemType.Ability \ if (item.data.type != item_tables.ZergItemType.Ability
and item.data.type != ZergItemType.Level: and item.data.type != ZergItemType.Level
):
item.flags |= ItemFilterFlags.FilterExcluded item.flags |= ItemFilterFlags.FilterExcluded
continue continue
if not protoss_missions and item.data.race == SC2Race.PROTOSS: if not protoss_missions and item.data.race == SC2Race.PROTOSS:
@@ -641,7 +653,7 @@ def flag_mission_based_item_excludes(world: SC2World, item_list: List[FilterItem
item.flags |= ItemFilterFlags.FilterExcluded item.flags |= ItemFilterFlags.FilterExcluded
# Remove Spear of Adun passives # Remove Spear of Adun passives
if item.name in item_tables.spear_of_adun_castable_passives and not soa_passive_presence: if item.name in item_groups.spear_of_adun_passives and not soa_passive_presence:
item.flags |= ItemFilterFlags.FilterExcluded item.flags |= ItemFilterFlags.FilterExcluded
# Remove matchup-specific items if you don't play that matchup # Remove matchup-specific items if you don't play that matchup

View File

@@ -40,6 +40,7 @@ from .options import (
SpearOfAdunPassivesPresentInNoBuild, EnableVoidTrade, VoidTradeAgeLimit, void_trade_age_limits_ms, VoidTradeWorkers, SpearOfAdunPassivesPresentInNoBuild, EnableVoidTrade, VoidTradeAgeLimit, void_trade_age_limits_ms, VoidTradeWorkers,
DifficultyDamageModifier, MissionOrderScouting, GenericUpgradeResearchSpeedup, MercenaryHighlanders, WarCouncilNerfs, DifficultyDamageModifier, MissionOrderScouting, GenericUpgradeResearchSpeedup, MercenaryHighlanders, WarCouncilNerfs,
is_mission_in_soa_presence, is_mission_in_soa_presence,
upgrade_included_names,
) )
from .mission_order.slot_data import CampaignSlotData, LayoutSlotData, MissionSlotData, MissionOrderObjectSlotData from .mission_order.slot_data import CampaignSlotData, LayoutSlotData, MissionSlotData, MissionOrderObjectSlotData
from .mission_order.entry_rules import SubRuleRuleData, CountMissionsRuleData, MissionEntryRules from .mission_order.entry_rules import SubRuleRuleData, CountMissionsRuleData, MissionEntryRules
@@ -71,10 +72,12 @@ from .mission_tables import (
) )
import colorama import colorama
from .options import Option, upgrade_included_names
from NetUtils import ClientStatus, NetworkItem, JSONtoTextParser, JSONMessagePart, add_json_item, add_json_location, add_json_text, JSONTypes from NetUtils import ClientStatus, NetworkItem, JSONtoTextParser, JSONMessagePart, add_json_item, add_json_location, add_json_text, JSONTypes
from MultiServer import mark_raw from MultiServer import mark_raw
if typing.TYPE_CHECKING:
from Options import Option
pool = concurrent.futures.ThreadPoolExecutor(1) pool = concurrent.futures.ThreadPoolExecutor(1)
loop = asyncio.get_event_loop_policy().new_event_loop() loop = asyncio.get_event_loop_policy().new_event_loop()
nest_asyncio.apply(loop) nest_asyncio.apply(loop)

View File

@@ -167,6 +167,7 @@ class ItemGroupNames:
LOTV_UNITS = "LotV Units" LOTV_UNITS = "LotV Units"
LOTV_ITEMS = "LotV Items" LOTV_ITEMS = "LotV Items"
LOTV_GLOBAL_UPGRADES = "LotV Global Upgrades" LOTV_GLOBAL_UPGRADES = "LotV Global Upgrades"
SOA_PASSIVES = "SOA Passive Abilities"
SOA_ITEMS = "SOA" SOA_ITEMS = "SOA"
PROTOSS_GLOBAL_UPGRADES = "Protoss Global Upgrades" PROTOSS_GLOBAL_UPGRADES = "Protoss Global Upgrades"
PROTOSS_BUILDINGS = "Protoss Buildings" PROTOSS_BUILDINGS = "Protoss Buildings"
@@ -777,11 +778,21 @@ item_name_groups[ItemGroupNames.PURIFIER_UNITS] = [
item_names.MIRAGE, item_names.DAWNBRINGER, item_names.TRIREME, item_names.TEMPEST, item_names.MIRAGE, item_names.DAWNBRINGER, item_names.TRIREME, item_names.TEMPEST,
item_names.CALADRIUS, item_names.CALADRIUS,
] ]
item_name_groups[ItemGroupNames.SOA_ITEMS] = soa_items = [ item_name_groups[ItemGroupNames.SOA_PASSIVES] = spear_of_adun_passives = [
item_names.RECONSTRUCTION_BEAM,
item_names.OVERWATCH,
item_names.GUARDIAN_SHELL,
]
spear_of_adun_actives = [
*[item_name for item_name, item_data in item_tables.item_table.items() if item_data.type == item_tables.ProtossItemType.Spear_Of_Adun], *[item_name for item_name, item_data in item_tables.item_table.items() if item_data.type == item_tables.ProtossItemType.Spear_Of_Adun],
item_names.SOA_PROGRESSIVE_PROXY_PYLON, item_names.SOA_PROGRESSIVE_PROXY_PYLON,
] ]
lotv_soa_items = [item_name for item_name in soa_items if item_name != item_names.SOA_PYLON_OVERCHARGE] item_name_groups[ItemGroupNames.SOA_ITEMS] = soa_items = spear_of_adun_actives + spear_of_adun_passives
lotv_soa_items = [
item_name
for item_name in soa_items
if item_name not in (item_names.SOA_PYLON_OVERCHARGE, item_names.OVERWATCH)
]
item_name_groups[ItemGroupNames.PROTOSS_GLOBAL_UPGRADES] = [ item_name_groups[ItemGroupNames.PROTOSS_GLOBAL_UPGRADES] = [
item_name for item_name, item_data in item_tables.item_table.items() if item_data.type == item_tables.ProtossItemType.Solarite_Core item_name for item_name, item_data in item_tables.item_table.items() if item_data.type == item_tables.ProtossItemType.Solarite_Core
] ]

View File

@@ -2293,12 +2293,6 @@ spear_of_adun_calldowns = {
item_names.SOA_SOLAR_BOMBARDMENT item_names.SOA_SOLAR_BOMBARDMENT
} }
spear_of_adun_castable_passives = {
item_names.RECONSTRUCTION_BEAM,
item_names.OVERWATCH,
item_names.GUARDIAN_SHELL,
}
nova_equipment = { nova_equipment = {
*[item_name for item_name, item_data in get_full_item_list().items() *[item_name for item_name, item_data in get_full_item_list().items()
if item_data.type == TerranItemType.Nova_Gear], if item_data.type == TerranItemType.Nova_Gear],

View File

@@ -5,14 +5,13 @@ from datetime import timedelta
from Options import ( from Options import (
Choice, Toggle, DefaultOnToggle, OptionSet, Range, Choice, Toggle, DefaultOnToggle, OptionSet, Range,
PerGameCommonOptions, Option, VerifyKeys, StartInventory, PerGameCommonOptions, VerifyKeys, StartInventory,
is_iterable_except_str, OptionGroup, Visibility, ItemDict, is_iterable_except_str, OptionGroup, Visibility, ItemDict,
Accessibility, ProgressionBalancing OptionCounter,
) )
from Utils import get_fuzzy_results from Utils import get_fuzzy_results
from BaseClasses import PlandoOptions from BaseClasses import PlandoOptions
from .item import item_names, item_tables from .item import item_names, item_tables, item_groups
from .item.item_groups import kerrigan_active_abilities, kerrigan_passives, nova_weapons, nova_gadgets
from .mission_tables import ( from .mission_tables import (
SC2Campaign, SC2Mission, lookup_name_to_mission, MissionPools, get_missions_with_any_flags_in_list, SC2Campaign, SC2Mission, lookup_name_to_mission, MissionPools, get_missions_with_any_flags_in_list,
campaign_mission_table, SC2Race, MissionFlag campaign_mission_table, SC2Race, MissionFlag
@@ -700,7 +699,7 @@ class KerriganMaxActiveAbilities(Range):
""" """
display_name = "Kerrigan Maximum Active Abilities" display_name = "Kerrigan Maximum Active Abilities"
range_start = 0 range_start = 0
range_end = len(kerrigan_active_abilities) range_end = len(item_groups.kerrigan_active_abilities)
default = range_end default = range_end
@@ -711,7 +710,7 @@ class KerriganMaxPassiveAbilities(Range):
""" """
display_name = "Kerrigan Maximum Passive Abilities" display_name = "Kerrigan Maximum Passive Abilities"
range_start = 0 range_start = 0
range_end = len(kerrigan_passives) range_end = len(item_groups.kerrigan_passives)
default = range_end default = range_end
@@ -829,7 +828,7 @@ class SpearOfAdunMaxAutocastAbilities(Range):
""" """
display_name = "Spear of Adun Maximum Passive Abilities" display_name = "Spear of Adun Maximum Passive Abilities"
range_start = 0 range_start = 0
range_end = sum(item.quantity for item_name, item in item_tables.get_full_item_list().items() if item_name in item_tables.spear_of_adun_castable_passives) range_end = sum(item_tables.item_table[item_name].quantity for item_name in item_groups.spear_of_adun_passives)
default = range_end default = range_end
@@ -883,7 +882,7 @@ class NovaMaxWeapons(Range):
""" """
display_name = "Nova Maximum Weapons" display_name = "Nova Maximum Weapons"
range_start = 0 range_start = 0
range_end = len(nova_weapons) range_end = len(item_groups.nova_weapons)
default = range_end default = range_end
@@ -897,7 +896,7 @@ class NovaMaxGadgets(Range):
""" """
display_name = "Nova Maximum Gadgets" display_name = "Nova Maximum Gadgets"
range_start = 0 range_start = 0
range_end = len(nova_gadgets) range_end = len(item_groups.nova_gadgets)
default = range_end default = range_end
@@ -932,33 +931,48 @@ class TakeOverAIAllies(Toggle):
display_name = "Take Over AI Allies" display_name = "Take Over AI Allies"
class Sc2ItemDict(Option[Dict[str, int]], VerifyKeys, Mapping[str, int]): class Sc2ItemDict(OptionCounter, VerifyKeys, Mapping[str, int]):
"""A branch of ItemDict that supports item counts of 0""" """A branch of ItemDict that supports negative item counts"""
default = {} default = {}
supports_weighting = False supports_weighting = False
verify_item_name = True verify_item_name = True
# convert_name_groups = True # convert_name_groups = True
display_name = 'Unnamed dictionary' display_name = 'Unnamed dictionary'
minimum_value: int = 0 # Note(phaneros): Limiting minimum to -1 means that if two triggers add -1 to the same item,
# the validation fails. So give trigger people space to stack a bunch of triggers.
min: int = -1000
max: int = 1000
valid_keys = set(item_tables.item_table) | set(item_groups.item_name_groups)
def __init__(self, value: Dict[str, int]): def __init__(self, value: dict[str, int]):
self.value = {key: val for key, val in value.items()} self.value = {key: val for key, val in value.items()}
@classmethod @classmethod
def from_any(cls, data: Union[List[str], Dict[str, int]]) -> 'Sc2ItemDict': def from_any(cls, data: list[str] | dict[str, int]) -> 'Sc2ItemDict':
if isinstance(data, list): if isinstance(data, list):
# This is a little default that gets us backwards compatibility with lists. raise ValueError(
# It doesn't play nice with trigger merging dicts and lists together, though, so best not to advertise it overmuch. f"{cls.display_name}: Cannot convert from list. "
data = {item: 0 for item in data} f"Use dict syntax (no dashes, 'value: number' synax)."
)
if isinstance(data, dict): if isinstance(data, dict):
for key, value in data.items(): for key, value in data.items():
if not isinstance(value, int): if not isinstance(value, int):
raise ValueError(f"Invalid type in '{cls.display_name}': element '{key}' maps to '{value}', expected an integer") raise ValueError(
if value < cls.minimum_value: f"Invalid type in '{cls.display_name}': "
raise ValueError(f"Invalid value for '{cls.display_name}': element '{key}' maps to {value}, which is less than the minimum ({cls.minimum_value})") f"element '{key}' maps to '{value}', expected an integer"
)
if value < cls.min:
raise ValueError(
f"Invalid value for '{cls.display_name}': "
f"element '{key}' maps to {value}, which is less than the minimum ({cls.min})"
)
if value > cls.max:
raise ValueError(f"Invalid value for '{cls.display_name}': "
f"element '{key}' maps to {value}, which is greater than the maximum ({cls.max})"
)
return cls(data) return cls(data)
else: else:
raise NotImplementedError(f"Cannot Convert from non-dictionary, got {type(data)}") raise NotImplementedError(f"{cls.display_name}: Cannot convert from non-dictionary, got {type(data)}")
def verify(self, world: Type['World'], player_name: str, plando_options: PlandoOptions) -> None: def verify(self, world: Type['World'], player_name: str, plando_options: PlandoOptions) -> None:
"""Overridden version of function from Options.VerifyKeys for a better error message""" """Overridden version of function from Options.VerifyKeys for a better error message"""
@@ -974,15 +988,16 @@ class Sc2ItemDict(Option[Dict[str, int]], VerifyKeys, Mapping[str, int]):
self.value = new_value self.value = new_value
for item_name in self.value: for item_name in self.value:
if item_name not in world.item_names: if item_name not in world.item_names:
from .item import item_groups
picks = get_fuzzy_results( picks = get_fuzzy_results(
item_name, item_name,
list(world.item_names) + list(item_groups.ItemGroupNames.get_all_group_names()), list(world.item_names) + list(item_groups.ItemGroupNames.get_all_group_names()),
limit=1, limit=1,
) )
raise Exception(f"Item {item_name} from option {self} " raise Exception(
f"is not a valid item name from {world.game}. " f"Item {item_name} from option {self} "
f"Did you mean '{picks[0][0]}' ({picks[0][1]}% sure)") f"is not a valid item name from {world.game}. "
f"Did you mean '{picks[0][0]}' ({picks[0][1]}% sure)"
)
def get_option_name(self, value): def get_option_name(self, value):
return ", ".join(f"{key}: {v}" for key, v in value.items()) return ", ".join(f"{key}: {v}" for key, v in value.items())
@@ -998,25 +1013,25 @@ class Sc2ItemDict(Option[Dict[str, int]], VerifyKeys, Mapping[str, int]):
class Sc2StartInventory(Sc2ItemDict): class Sc2StartInventory(Sc2ItemDict):
"""Start with these items.""" """Start with these items. Use an amount of -1 to start with all copies of an item."""
display_name = StartInventory.display_name display_name = StartInventory.display_name
class LockedItems(Sc2ItemDict): class LockedItems(Sc2ItemDict):
"""Guarantees that these items will be unlockable, in the amount specified. """Guarantees that these items will be unlockable, in the amount specified.
Specify an amount of 0 to lock all copies of an item.""" Specify an amount of -1 to lock all copies of an item."""
display_name = "Locked Items" display_name = "Locked Items"
class ExcludedItems(Sc2ItemDict): class ExcludedItems(Sc2ItemDict):
"""Guarantees that these items will not be unlockable, in the amount specified. """Guarantees that these items will not be unlockable, in the amount specified.
Specify an amount of 0 to exclude all copies of an item.""" Specify an amount of -1 to exclude all copies of an item."""
display_name = "Excluded Items" display_name = "Excluded Items"
class UnexcludedItems(Sc2ItemDict): class UnexcludedItems(Sc2ItemDict):
"""Undoes an item exclusion; useful for whitelisting or fine-tuning a category. """Undoes an item exclusion; useful for whitelisting or fine-tuning a category.
Specify an amount of 0 to unexclude all copies of an item.""" Specify an amount of -1 to unexclude all copies of an item."""
display_name = "Unexcluded Items" display_name = "Unexcluded Items"

View File

@@ -3,8 +3,7 @@ from typing import Callable, Dict, List, Set, Tuple, TYPE_CHECKING, Iterable
from BaseClasses import Location, ItemClassification from BaseClasses import Location, ItemClassification
from .item import StarcraftItem, ItemFilterFlags, item_names, item_parents, item_groups from .item import StarcraftItem, ItemFilterFlags, item_names, item_parents, item_groups
from .item.item_tables import item_table, TerranItemType, ZergItemType, spear_of_adun_calldowns, \ from .item.item_tables import item_table, TerranItemType, ZergItemType, spear_of_adun_calldowns
spear_of_adun_castable_passives
from .options import RequiredTactics from .options import RequiredTactics
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -272,7 +271,7 @@ class ValidInventory:
self.world.random.shuffle(spear_of_adun_actives) self.world.random.shuffle(spear_of_adun_actives)
cull_items_over_maximum(spear_of_adun_actives, self.world.options.spear_of_adun_max_active_abilities.value) cull_items_over_maximum(spear_of_adun_actives, self.world.options.spear_of_adun_max_active_abilities.value)
spear_of_adun_autocasts = [item for item in inventory if item.name in spear_of_adun_castable_passives] spear_of_adun_autocasts = [item for item in inventory if item.name in item_groups.spear_of_adun_passives]
self.world.random.shuffle(spear_of_adun_autocasts) self.world.random.shuffle(spear_of_adun_autocasts)
cull_items_over_maximum(spear_of_adun_autocasts, self.world.options.spear_of_adun_max_passive_abilities.value) cull_items_over_maximum(spear_of_adun_autocasts, self.world.options.spear_of_adun_max_passive_abilities.value)

View File

@@ -18,19 +18,19 @@ class TestItemFiltering(Sc2SetupTestBase):
world_options = { world_options = {
**self.ALL_CAMPAIGNS, **self.ALL_CAMPAIGNS,
'locked_items': { 'locked_items': {
item_names.MARINE: 0, item_names.MARINE: -1,
item_names.MARAUDER: 0, item_names.MARAUDER: -1,
item_names.MEDIVAC: 1, item_names.MEDIVAC: 1,
item_names.FIREBAT: 1, item_names.FIREBAT: 1,
item_names.ZEALOT: 0, item_names.ZEALOT: -1,
item_names.PROGRESSIVE_REGENERATIVE_BIO_STEEL: 2, item_names.PROGRESSIVE_REGENERATIVE_BIO_STEEL: 2,
}, },
'excluded_items': { 'excluded_items': {
item_names.MARINE: 0, item_names.MARINE: -1,
item_names.MARAUDER: 0, item_names.MARAUDER: -1,
item_names.MEDIVAC: 0, item_names.MEDIVAC: -1,
item_names.FIREBAT: 1, item_names.FIREBAT: 1,
item_names.ZERGLING: 0, item_names.ZERGLING: -1,
item_names.PROGRESSIVE_REGENERATIVE_BIO_STEEL: 2, item_names.PROGRESSIVE_REGENERATIVE_BIO_STEEL: 2,
} }
} }
@@ -50,38 +50,38 @@ class TestItemFiltering(Sc2SetupTestBase):
world_options = { world_options = {
'grant_story_tech': options.GrantStoryTech.option_grant, 'grant_story_tech': options.GrantStoryTech.option_grant,
'excluded_items': { 'excluded_items': {
item_groups.ItemGroupNames.NOVA_EQUIPMENT: 15, item_groups.ItemGroupNames.NOVA_EQUIPMENT: -1,
item_names.MARINE_PROGRESSIVE_STIMPACK: 1, item_names.MARINE_PROGRESSIVE_STIMPACK: 1,
item_names.MARAUDER_PROGRESSIVE_STIMPACK: 2, item_names.MARAUDER_PROGRESSIVE_STIMPACK: 2,
item_names.MARINE: 0, item_names.MARINE: -1,
item_names.MARAUDER: 0, item_names.MARAUDER: -1,
item_names.REAPER: 1, item_names.REAPER: 1,
item_names.DIAMONDBACK: 0, item_names.DIAMONDBACK: -1,
item_names.HELLION: 1, item_names.HELLION: 1,
# Additional excludes to increase the likelihood that unexcluded items actually appear # Additional excludes to increase the likelihood that unexcluded items actually appear
item_groups.ItemGroupNames.STARPORT_UNITS: 0, item_groups.ItemGroupNames.STARPORT_UNITS: -1,
item_names.WARHOUND: 0, item_names.WARHOUND: -1,
item_names.VULTURE: 0, item_names.VULTURE: -1,
item_names.WIDOW_MINE: 0, item_names.WIDOW_MINE: -1,
item_names.THOR: 0, item_names.THOR: -1,
item_names.GHOST: 0, item_names.GHOST: -1,
item_names.SPECTRE: 0, item_names.SPECTRE: -1,
item_groups.ItemGroupNames.MENGSK_UNITS: 0, item_groups.ItemGroupNames.MENGSK_UNITS: -1,
item_groups.ItemGroupNames.TERRAN_VETERANCY_UNITS: 0, item_groups.ItemGroupNames.TERRAN_VETERANCY_UNITS: -1,
}, },
'unexcluded_items': { 'unexcluded_items': {
item_names.NOVA_PLASMA_RIFLE: 1, # Necessary to pass logic item_names.NOVA_PLASMA_RIFLE: 1, # Necessary to pass logic
item_names.NOVA_PULSE_GRENADES: 0, # Necessary to pass logic item_names.NOVA_PULSE_GRENADES: -1, # Necessary to pass logic
item_names.NOVA_JUMP_SUIT_MODULE: 0, # Necessary to pass logic item_names.NOVA_JUMP_SUIT_MODULE: -1, # Necessary to pass logic
item_groups.ItemGroupNames.BARRACKS_UNITS: 0, item_groups.ItemGroupNames.BARRACKS_UNITS: -1,
item_names.NOVA_PROGRESSIVE_STEALTH_SUIT_MODULE: 1, item_names.NOVA_PROGRESSIVE_STEALTH_SUIT_MODULE: 1,
item_names.HELLION: 1, item_names.HELLION: 1,
item_names.MARINE_PROGRESSIVE_STIMPACK: 1, item_names.MARINE_PROGRESSIVE_STIMPACK: 1,
item_names.MARAUDER_PROGRESSIVE_STIMPACK: 0, item_names.MARAUDER_PROGRESSIVE_STIMPACK: -1,
# Additional unexcludes for logic # Additional unexcludes for logic
item_names.MEDIVAC: 0, item_names.MEDIVAC: -1,
item_names.BATTLECRUISER: 0, item_names.BATTLECRUISER: -1,
item_names.SCIENCE_VESSEL: 0, item_names.SCIENCE_VESSEL: -1,
}, },
# Terran-only # Terran-only
'enabled_campaigns': { 'enabled_campaigns': {
@@ -103,11 +103,29 @@ class TestItemFiltering(Sc2SetupTestBase):
self.assertNotIn(item_names.NOVA_BLAZEFIRE_GUNBLADE, itempool) self.assertNotIn(item_names.NOVA_BLAZEFIRE_GUNBLADE, itempool)
self.assertNotIn(item_names.NOVA_ENERGY_SUIT_MODULE, itempool) self.assertNotIn(item_names.NOVA_ENERGY_SUIT_MODULE, itempool)
def test_exclude_2_beats_unexclude_1(self) -> None:
world_options = {
options.OPTION_NAME[options.ExcludedItems]: {
item_names.MARINE: 2,
},
options.OPTION_NAME[options.UnexcludedItems]: {
item_names.MARINE: 1,
},
# Ensure enough locations that marine doesn't get culled
options.OPTION_NAME[options.SelectedRaces]: {
SC2Race.TERRAN.get_title(),
},
options.OPTION_NAME[options.VictoryCache]: 9,
}
self.generate_world(world_options)
itempool = [item.name for item in self.multiworld.itempool]
self.assertNotIn(item_names.MARINE, itempool)
def test_excluding_groups_excludes_all_items_in_group(self): def test_excluding_groups_excludes_all_items_in_group(self):
world_options = { world_options = {
'excluded_items': [ 'excluded_items': {
item_groups.ItemGroupNames.BARRACKS_UNITS.lower(), item_groups.ItemGroupNames.BARRACKS_UNITS.lower(): -1,
] },
} }
self.generate_world(world_options) self.generate_world(world_options)
itempool = [item.name for item in self.multiworld.itempool] itempool = [item.name for item in self.multiworld.itempool]
@@ -337,9 +355,9 @@ class TestItemFiltering(Sc2SetupTestBase):
# Options under test # Options under test
'vanilla_items_only': True, 'vanilla_items_only': True,
'unexcluded_items': { 'unexcluded_items': {
item_names.PROGRESSIVE_FIRE_SUPPRESSION_SYSTEM: 0, item_names.PROGRESSIVE_FIRE_SUPPRESSION_SYSTEM: -1,
item_names.WARHOUND: 1, item_names.WARHOUND: 1,
item_groups.ItemGroupNames.TERRAN_STIMPACKS: 0, item_groups.ItemGroupNames.TERRAN_STIMPACKS: -1,
}, },
# Avoid options that lock non-vanilla items for logic # Avoid options that lock non-vanilla items for logic
'required_tactics': options.RequiredTactics.option_any_units, 'required_tactics': options.RequiredTactics.option_any_units,
@@ -463,12 +481,12 @@ class TestItemFiltering(Sc2SetupTestBase):
}, },
'required_tactics': options.RequiredTactics.option_no_logic, 'required_tactics': options.RequiredTactics.option_no_logic,
'enable_morphling': options.EnableMorphling.option_true, 'enable_morphling': options.EnableMorphling.option_true,
'excluded_items': [ 'excluded_items': {
item_groups.ItemGroupNames.ZERG_UNITS.lower() item_groups.ItemGroupNames.ZERG_UNITS.lower(): -1,
], },
'unexcluded_items': [ 'unexcluded_items': {
item_groups.ItemGroupNames.ZERG_MORPHS.lower() item_groups.ItemGroupNames.ZERG_MORPHS.lower(): -1,
] },
} }
self.generate_world(world_options) self.generate_world(world_options)
itempool = [item.name for item in self.multiworld.itempool] itempool = [item.name for item in self.multiworld.itempool]
@@ -486,12 +504,12 @@ class TestItemFiltering(Sc2SetupTestBase):
}, },
'required_tactics': options.RequiredTactics.option_no_logic, 'required_tactics': options.RequiredTactics.option_no_logic,
'enable_morphling': options.EnableMorphling.option_false, 'enable_morphling': options.EnableMorphling.option_false,
'excluded_items': [ 'excluded_items': {
item_groups.ItemGroupNames.ZERG_UNITS.lower() item_groups.ItemGroupNames.ZERG_UNITS.lower(): -1,
], },
'unexcluded_items': [ 'unexcluded_items': {
item_groups.ItemGroupNames.ZERG_MORPHS.lower() item_groups.ItemGroupNames.ZERG_MORPHS.lower(): -1,
] },
} }
self.generate_world(world_options) self.generate_world(world_options)
itempool = [item.name for item in self.multiworld.itempool] itempool = [item.name for item in self.multiworld.itempool]
@@ -520,14 +538,14 @@ class TestItemFiltering(Sc2SetupTestBase):
def test_planetary_orbital_module_not_present_without_cc_spells(self) -> None: def test_planetary_orbital_module_not_present_without_cc_spells(self) -> None:
world_options = { world_options = {
"excluded_items": [ "excluded_items": {
item_names.COMMAND_CENTER_MULE, item_names.COMMAND_CENTER_MULE: -1,
item_names.COMMAND_CENTER_SCANNER_SWEEP, item_names.COMMAND_CENTER_SCANNER_SWEEP: -1,
item_names.COMMAND_CENTER_EXTRA_SUPPLIES item_names.COMMAND_CENTER_EXTRA_SUPPLIES: -1,
], },
"locked_items": [ "locked_items": {
item_names.PLANETARY_FORTRESS item_names.PLANETARY_FORTRESS: -1,
] }
} }
self.generate_world(world_options) self.generate_world(world_options)
@@ -931,10 +949,10 @@ class TestItemFiltering(Sc2SetupTestBase):
} }
}, },
'grant_story_levels': options.GrantStoryLevels.option_additive, 'grant_story_levels': options.GrantStoryLevels.option_additive,
'excluded_items': [ 'excluded_items': {
item_names.KERRIGAN_LEAPING_STRIKE, item_names.KERRIGAN_LEAPING_STRIKE: -1,
item_names.KERRIGAN_MEND, item_names.KERRIGAN_MEND: -1,
] }
} }
self.generate_world(world_options) self.generate_world(world_options)
itempool = [item.name for item in self.multiworld.itempool] itempool = [item.name for item in self.multiworld.itempool]
@@ -1208,7 +1226,7 @@ class TestItemFiltering(Sc2SetupTestBase):
'mission_order': MissionOrder.option_grid, 'mission_order': MissionOrder.option_grid,
'maximum_campaign_size': MaximumCampaignSize.range_end, 'maximum_campaign_size': MaximumCampaignSize.range_end,
'exclude_overpowered_items': ExcludeOverpoweredItems.option_true, 'exclude_overpowered_items': ExcludeOverpoweredItems.option_true,
'locked_items': [locked_item], 'locked_items': {locked_item: -1},
'enable_race_swap': options.EnableRaceSwapVariants.option_shuffle_all, 'enable_race_swap': options.EnableRaceSwapVariants.option_shuffle_all,
'selected_races': [SC2Race.TERRAN.get_title()], 'selected_races': [SC2Race.TERRAN.get_title()],
} }
@@ -1249,7 +1267,7 @@ class TestItemFiltering(Sc2SetupTestBase):
'maximum_campaign_size': MaximumCampaignSize.range_end, 'maximum_campaign_size': MaximumCampaignSize.range_end,
'exclude_overpowered_items': ExcludeOverpoweredItems.option_false, 'exclude_overpowered_items': ExcludeOverpoweredItems.option_false,
'enable_race_swap': options.EnableRaceSwapVariants.option_shuffle_all, 'enable_race_swap': options.EnableRaceSwapVariants.option_shuffle_all,
'locked_items': {item_name: 0 for item_name in unreleased_items}, 'locked_items': {item_name: -1 for item_name in unreleased_items},
} }
self.generate_world(world_options) self.generate_world(world_options)
@@ -1264,7 +1282,7 @@ class TestItemFiltering(Sc2SetupTestBase):
**self.ALL_CAMPAIGNS, **self.ALL_CAMPAIGNS,
'mission_order': MissionOrder.option_grid, 'mission_order': MissionOrder.option_grid,
'maximum_campaign_size': MaximumCampaignSize.range_end, 'maximum_campaign_size': MaximumCampaignSize.range_end,
'excluded_items': [item_name for item_name in item_groups.terran_mercenaries], 'excluded_items': {item_name: -1 for item_name in item_groups.terran_mercenaries},
'enable_race_swap': options.EnableRaceSwapVariants.option_shuffle_all, 'enable_race_swap': options.EnableRaceSwapVariants.option_shuffle_all,
'selected_races': [SC2Race.TERRAN.get_title()], 'selected_races': [SC2Race.TERRAN.get_title()],
} }
@@ -1280,7 +1298,7 @@ class TestItemFiltering(Sc2SetupTestBase):
'mission_order': MissionOrder.option_grid, 'mission_order': MissionOrder.option_grid,
'maximum_campaign_size': MaximumCampaignSize.range_end, 'maximum_campaign_size': MaximumCampaignSize.range_end,
'exclude_overpowered_items': ExcludeOverpoweredItems.option_true, 'exclude_overpowered_items': ExcludeOverpoweredItems.option_true,
'unexcluded_items': [item_names.SOA_TIME_STOP], 'unexcluded_items': {item_names.SOA_TIME_STOP: -1},
'enable_race_swap': options.EnableRaceSwapVariants.option_shuffle_all, 'enable_race_swap': options.EnableRaceSwapVariants.option_shuffle_all,
} }
@@ -1322,7 +1340,7 @@ class TestItemFiltering(Sc2SetupTestBase):
'enabled_campaigns': { 'enabled_campaigns': {
SC2Campaign.WOL.campaign_name SC2Campaign.WOL.campaign_name
}, },
'excluded_items': [item_names.MARINE, item_names.MEDIC], 'excluded_items': {item_names.MARINE: -1, item_names.MEDIC: -1},
'shuffle_no_build': False, 'shuffle_no_build': False,
'required_tactics': RequiredTactics.option_standard 'required_tactics': RequiredTactics.option_standard
} }

View File

@@ -11,7 +11,7 @@ class ItemFilterTests(Sc2SetupTestBase):
def test_excluding_all_barracks_units_excludes_infantry_upgrades(self) -> None: def test_excluding_all_barracks_units_excludes_infantry_upgrades(self) -> None:
world_options = { world_options = {
'excluded_items': { 'excluded_items': {
item_groups.ItemGroupNames.BARRACKS_UNITS: 0 item_groups.ItemGroupNames.BARRACKS_UNITS: -1,
}, },
'required_tactics': 'standard', 'required_tactics': 'standard',
'min_number_of_upgrades': 1, 'min_number_of_upgrades': 1,

View File

@@ -35,10 +35,10 @@ class TestSupportedUseCases(Sc2SetupTestBase):
SC2Campaign.NCO.campaign_name SC2Campaign.NCO.campaign_name
}, },
'excluded_items': { 'excluded_items': {
item_groups.ItemGroupNames.TERRAN_UNITS: 0, item_groups.ItemGroupNames.TERRAN_UNITS: -1,
}, },
'unexcluded_items': { 'unexcluded_items': {
item_groups.ItemGroupNames.NCO_UNITS: 0, item_groups.ItemGroupNames.NCO_UNITS: -1,
}, },
'max_number_of_upgrades': 2, 'max_number_of_upgrades': 2,
} }
@@ -81,10 +81,10 @@ class TestSupportedUseCases(Sc2SetupTestBase):
}, },
'mission_order': options.MissionOrder.option_vanilla_shuffled, 'mission_order': options.MissionOrder.option_vanilla_shuffled,
'excluded_items': { 'excluded_items': {
item_groups.ItemGroupNames.TERRAN_ITEMS: 0, item_groups.ItemGroupNames.TERRAN_ITEMS: -1,
}, },
'unexcluded_items': { 'unexcluded_items': {
item_groups.ItemGroupNames.NCO_MAX_PROGRESSIVE_ITEMS: 0, item_groups.ItemGroupNames.NCO_MAX_PROGRESSIVE_ITEMS: -1,
item_groups.ItemGroupNames.NCO_MIN_PROGRESSIVE_ITEMS: 1, item_groups.ItemGroupNames.NCO_MIN_PROGRESSIVE_ITEMS: 1,
}, },
'excluded_missions': [ 'excluded_missions': [
@@ -398,7 +398,7 @@ class TestSupportedUseCases(Sc2SetupTestBase):
self.generate_world(world_options) self.generate_world(world_options)
world_item_names = [item.name for item in self.multiworld.itempool] world_item_names = [item.name for item in self.multiworld.itempool]
spear_of_adun_actives = [item_name for item_name in world_item_names if item_name in item_tables.spear_of_adun_calldowns] spear_of_adun_actives = [item_name for item_name in world_item_names if item_name in item_groups.spear_of_adun_actives]
self.assertLessEqual(len(spear_of_adun_actives), target_number) self.assertLessEqual(len(spear_of_adun_actives), target_number)
@@ -418,7 +418,9 @@ class TestSupportedUseCases(Sc2SetupTestBase):
self.generate_world(world_options) self.generate_world(world_options)
world_item_names = [item.name for item in self.multiworld.itempool] world_item_names = [item.name for item in self.multiworld.itempool]
spear_of_adun_autocasts = [item_name for item_name in world_item_names if item_name in item_tables.spear_of_adun_castable_passives] spear_of_adun_autocasts = [
item_name for item_name in world_item_names if item_name in item_groups.spear_of_adun_passives
]
self.assertLessEqual(len(spear_of_adun_autocasts), target_number) self.assertLessEqual(len(spear_of_adun_autocasts), target_number)
@@ -471,12 +473,12 @@ class TestSupportedUseCases(Sc2SetupTestBase):
], ],
'required_tactics': options.RequiredTactics.option_any_units, 'required_tactics': options.RequiredTactics.option_any_units,
'excluded_items': { 'excluded_items': {
item_groups.ItemGroupNames.TERRAN_UNITS: 0, item_groups.ItemGroupNames.TERRAN_UNITS: -1,
item_groups.ItemGroupNames.ZERG_UNITS: 0, item_groups.ItemGroupNames.ZERG_UNITS: -1,
}, },
'unexcluded_items': { 'unexcluded_items': {
item_groups.ItemGroupNames.TERRAN_MERCENARIES: 0, item_groups.ItemGroupNames.TERRAN_MERCENARIES: -1,
item_groups.ItemGroupNames.ZERG_MERCENARIES: 0, item_groups.ItemGroupNames.ZERG_MERCENARIES: -1,
}, },
'start_inventory': { 'start_inventory': {
item_names.PROGRESSIVE_FAST_DELIVERY: 1, item_names.PROGRESSIVE_FAST_DELIVERY: 1,