From caf068b083f4fa9f386292f025883669dfb9ef59 Mon Sep 17 00:00:00 2001 From: Salzkorn Date: Sat, 3 Feb 2024 15:58:02 +0100 Subject: [PATCH 1/3] Refactor item name groups --- worlds/sc2/ItemGroups.py | 99 ++++++++++++++++++++++++++++++++++++++++ worlds/sc2/ItemNames.py | 4 +- worlds/sc2/Items.py | 38 --------------- worlds/sc2/__init__.py | 3 +- 4 files changed, 103 insertions(+), 41 deletions(-) create mode 100644 worlds/sc2/ItemGroups.py diff --git a/worlds/sc2/ItemGroups.py b/worlds/sc2/ItemGroups.py new file mode 100644 index 000000000000..8fa287f5f271 --- /dev/null +++ b/worlds/sc2/ItemGroups.py @@ -0,0 +1,99 @@ +import typing +from . import Items, ItemNames +from .MissionTables import campaign_mission_table, SC2Campaign, SC2Mission + +""" +Item name groups, given to Archipelago and used in YAMLs and /received filtering. +For non-developers the following will be useful: +* Items with a bracket get groups named after the unbracketed part + * eg. "Advanced Healing AI (Medivac)" is accessible as "Advanced Healing AI" + * The exception to this are item names that would be ambiguous (eg. "Resource Efficiency") +* Item flaggroups get unique groups as well as combined groups for numbered flaggroups + * eg. "Unit" contains all units, "Armory" contains "Armory 1" through "Armory 6" + * The best place to look these up is at the bottom of Items.py +* Items that have a parent are grouped together + * eg. "Zergling Items" contains all items that have "Zergling" as a parent + * These groups do NOT contain the parent item + * This currently does not include items with multiple potential parents, like some LotV unit upgrades +* All items are grouped by their race ("Terran", "Protoss", "Zerg", "Any") +* Hand-crafted item groups can be found at the bottom of this file +""" + +item_name_groups: typing.Dict[str, typing.List[str]] = {} + +# Groups for use in world logic +item_name_groups["Missions"] = ["Beat " + mission.mission_name for mission in SC2Mission] +item_name_groups["WoL Missions"] = ["Beat " + mission.mission_name for mission in campaign_mission_table[SC2Campaign.WOL]] + \ + ["Beat " + mission.mission_name for mission in campaign_mission_table[SC2Campaign.PROPHECY]] + +# These item name groups should not show up in documentation +unlisted_item_name_groups = { + "Missions", "WoL Missions" +} + +# Some item names only differ in bracketed parts +# These items are ambiguous for short-hand name groups +bracketless_duplicates: typing.Set[str] +# This is a list of names in ItemNames with bracketed parts removed, for internal use +_shortened_names = [(name[:name.find(' (')] if '(' in name else name) + for name in [ItemNames.__dict__[name] for name in ItemNames.__dir__() if not name.startswith('_')]] +# Remove the first instance of every short-name from the full item list +bracketless_duplicates = set(_shortened_names) +for name in bracketless_duplicates: + _shortened_names.remove(name) +# The remaining short-names are the duplicates +bracketless_duplicates = set(_shortened_names) +del _shortened_names + +# All items get sorted into their data type +for item, data in Items.get_full_item_list().items(): + # Items get assigned to their flaggroup's type + item_name_groups.setdefault(data.type, []).append(item) + # Numbered flaggroups get sorted into an unnumbered group + # Currently supports numbers of one or two digits + if item[-2:].strip().isnumeric: + type_group = item[:-2].strip() + item_name_groups.setdefault(type_group, []).append(item) + # Items with a bracket get a short-hand name group for ease of use in YAMLs + if '(' in item: + short_name = item[:item.find(' (')] + # Ambiguous short-names are dropped + if short_name in bracketless_duplicates: + continue + item_name_groups[short_name] = [item] + # Short-name groups are unlisted + unlisted_item_name_groups.add(short_name) + # Items with a parent get assigned to their parent's group + if data.parent_item: + # The parent groups need a special name, otherwise they are ambiguous with the parent + parent_group = f"{data.parent_item} Items" + item_name_groups.setdefault(parent_group, []).append(item) + # Parent groups are unlisted + unlisted_item_name_groups.add(parent_group) + # All items get assigned to their race's group + race_group = data.race.name.capitalize() + item_name_groups.setdefault(race_group, []).append(item) + + +# Hand-made groups +item_name_groups["Aiur"] = [ + ItemNames.ZEALOT, ItemNames.DRAGOON, ItemNames.SENTRY, ItemNames.AVENGER, ItemNames.HIGH_TEMPLAR, + ItemNames.IMMORTAL, ItemNames.REAVER, + ItemNames.PHOENIX, ItemNames.SCOUT, ItemNames.ARBITER, ItemNames.CARRIER, +] +item_name_groups["Nerazim"] = [ + ItemNames.CENTURION, ItemNames.STALKER, ItemNames.DARK_TEMPLAR, ItemNames.SIGNIFIER, ItemNames.DARK_ARCHON, + ItemNames.ANNIHILATOR, + ItemNames.CORSAIR, ItemNames.ORACLE, ItemNames.VOID_RAY, +] +item_name_groups["Tal'Darim"] = [ + ItemNames.SUPPLICANT, ItemNames.SLAYER, ItemNames.HAVOC, ItemNames.BLOOD_HUNTER, ItemNames.ASCENDANT, + ItemNames.VANGUARD, ItemNames.WRATHWALKER, + ItemNames.DESTROYER, ItemNames.MOTHERSHIP, + ItemNames.WARP_PRISM_PHASE_BLASTER, +] +item_name_groups["Purifier"] = [ + ItemNames.SENTINEL, ItemNames.ADEPT, ItemNames.INSTIGATOR, ItemNames.ENERGIZER, + ItemNames.COLOSSUS, ItemNames.DISRUPTOR, + ItemNames.MIRAGE, ItemNames.TEMPEST, +] \ No newline at end of file diff --git a/worlds/sc2/ItemNames.py b/worlds/sc2/ItemNames.py index a55189a7b772..cb5d425e4224 100644 --- a/worlds/sc2/ItemNames.py +++ b/worlds/sc2/ItemNames.py @@ -640,8 +640,8 @@ ORBITAL_ASSIMILATORS = "Orbital Assimilators" WARP_HARMONIZATION = "Warp Harmonization" GUARDIAN_SHELL = "Guardian Shell" -RECONSTRUCTION_BEAM = "Reconstruction Beam" -OVERWATCH = "Overwatch" +RECONSTRUCTION_BEAM = "Reconstruction Beam (Spear of Adun Auto-Cast)" +OVERWATCH = "Overwatch (Spear of Adun Auto-Cast)" SUPERIOR_WARP_GATES = "Superior Warp Gates" ENHANCED_TARGETING = "Enhanced Targeting" OPTIMIZED_ORDNANCE = "Optimized Ordnance" diff --git a/worlds/sc2/Items.py b/worlds/sc2/Items.py index 890398e98880..e7e9fa3a9ea6 100644 --- a/worlds/sc2/Items.py +++ b/worlds/sc2/Items.py @@ -1863,44 +1863,6 @@ def get_basic_units(multiworld: MultiWorld, player: int, race: SC2Race) -> typin return basic_units[race] -item_name_group_names = { - # WoL - "Armory 1", "Armory 2", "Armory 3", "Armory 4", "Armory 5", "Armory 6" - "Laboratory", "Progressive Upgrade", - # HotS - "Ability", "Strain", "Mutation 1" -} -item_name_groups: typing.Dict[str, typing.List[str]] = {} -for item, data in get_full_item_list().items(): - item_name_groups.setdefault(data.type, []).append(item) - if data.type in item_name_group_names and '(' in item: - short_name = item[:item.find(' (')] - item_name_groups[short_name] = [item] -item_name_groups["Missions"] = ["Beat " + mission.mission_name for mission in SC2Mission] -item_name_groups["WoL Missions"] = ["Beat " + mission.mission_name for mission in campaign_mission_table[SC2Campaign.WOL]] + \ - ["Beat " + mission.mission_name for mission in campaign_mission_table[SC2Campaign.PROPHECY]] -item_name_groups["Aiur"] = [ - ItemNames.ZEALOT, ItemNames.DRAGOON, ItemNames.SENTRY, ItemNames.AVENGER, ItemNames.HIGH_TEMPLAR, - ItemNames.IMMORTAL, ItemNames.REAVER, - ItemNames.PHOENIX, ItemNames.SCOUT, ItemNames.ARBITER, ItemNames.CARRIER, -] -item_name_groups["Nerazim"] = [ - ItemNames.CENTURION, ItemNames.STALKER, ItemNames.DARK_TEMPLAR, ItemNames.SIGNIFIER, ItemNames.DARK_ARCHON, - ItemNames.ANNIHILATOR, - ItemNames.CORSAIR, ItemNames.ORACLE, ItemNames.VOID_RAY, -] -item_name_groups["Tal'Darim"] = [ - ItemNames.SUPPLICANT, ItemNames.SLAYER, ItemNames.HAVOC, ItemNames.BLOOD_HUNTER, ItemNames.ASCENDANT, - ItemNames.VANGUARD, ItemNames.WRATHWALKER, - ItemNames.DESTROYER, ItemNames.MOTHERSHIP, - ItemNames.WARP_PRISM_PHASE_BLASTER, -] -item_name_groups["Purifier"] = [ - ItemNames.SENTINEL, ItemNames.ADEPT, ItemNames.INSTIGATOR, ItemNames.ENERGIZER, - ItemNames.COLOSSUS, ItemNames.DISRUPTOR, - ItemNames.MIRAGE, ItemNames.TEMPEST, -] - # Items that can be placed before resources if not already in # General upgrades and Mercs second_pass_placeable_items: typing.Tuple[str, ...] = ( diff --git a/worlds/sc2/__init__.py b/worlds/sc2/__init__.py index cd6bcb0a5598..e943b7c47f51 100644 --- a/worlds/sc2/__init__.py +++ b/worlds/sc2/__init__.py @@ -6,10 +6,11 @@ from BaseClasses import Item, MultiWorld, Location, Tutorial, ItemClassification from worlds.AutoWorld import WebWorld, World from . import ItemNames -from .Items import StarcraftItem, filler_items, item_name_groups, get_item_table, get_full_item_list, \ +from .Items import StarcraftItem, filler_items, get_item_table, get_full_item_list, \ get_basic_units, ItemData, upgrade_included_names, progressive_if_nco, kerrigan_actives, kerrigan_passives, \ kerrigan_only_passives, progressive_if_ext, not_balanced_starting_units, spear_of_adun_calldowns, \ spear_of_adun_castable_passives, nova_equipment +from .ItemGroups import item_name_groups from .Locations import get_locations, LocationType, get_location_types, get_plando_locations from .Regions import create_regions from .Options import get_option_value, LocationInclusion, KerriganLevelItemDistribution, \ From 19409b0509b4b45943959fd6173e191939181242 Mon Sep 17 00:00:00 2001 From: Salzkorn Date: Sat, 3 Feb 2024 18:17:53 +0100 Subject: [PATCH 2/3] Rework /received filtering --- worlds/sc2/Client.py | 100 +++++++++++++++++++++++++++++-------------- 1 file changed, 68 insertions(+), 32 deletions(-) diff --git a/worlds/sc2/Client.py b/worlds/sc2/Client.py index 43a224e5d4b7..3f573282af89 100644 --- a/worlds/sc2/Client.py +++ b/worlds/sc2/Client.py @@ -23,6 +23,7 @@ from CommonClient import CommonContext, server_loop, ClientCommandProcessor, gui_enabled, get_base_parser from Utils import init_logging, is_windows, async_start from worlds.sc2 import ItemNames +from worlds.sc2.ItemGroups import item_name_groups, unlisted_item_name_groups from worlds.sc2 import Options from worlds.sc2.Options import ( MissionOrder, KerriganPrimalStatus, kerrigan_unit_available, KerriganPresence, @@ -204,26 +205,30 @@ def _cmd_game_speed(self, game_speed: str = "") -> bool: " or Default to select based on difficulty.") return False - def _cmd_received(self, filter_search: str = "aptz") -> bool: - """ - List received items. - Filter output by faction by passing in a parameter -- include 't' for terran output, 'z' for zerg, 'p' for protoss, and/or 'a' for non-categorized items. - """ - # TODO(mm): The goal is to be able to filter by race, partial item name, and item group - # This should probably wait until a refactor that replaces item groups with enums insteaad of strings - search_filter_meanings = (('a', SC2Race.ANY), ('t', SC2Race.TERRAN), ('z', SC2Race.ZERG), ('p', SC2Race.PROTOSS)) - faction_filter: typing.Set[SC2Race] = set() - filter_search = filter_search.lower() - for char, value in search_filter_meanings: - if char in filter_search: - faction_filter.add(value) - if not faction_filter: - faction_filter = set(x[1] for x in search_filter_meanings) + @mark_raw + def _cmd_received(self, filter_search: str = "") -> bool: + """List received items. + Pass in a parameter to filter the search by partial item name or exact item group.""" + # Groups must be matched case-sensitively, so we properly capitalize the search term + # eg. "Spear of Adun" over "Spear Of Adun" or "spear of adun" + # This fails a lot of item name matches, but those should be found by partial name match + formatted_filter_search = " ".join([(part.lower() if len(part) <= 3 else part.lower().capitalize()) for part in filter_search.split()]) + + def item_matches_filter(item_name: str) -> bool: + # The filter can be an exact group name or a partial item name + # Partial item name can be matched case-insensitively + if filter_search.lower() in item_name.lower(): + return True + # The search term should already be formatted as a group name + if formatted_filter_search in item_name_groups and item_name in item_name_groups[formatted_filter_search]: + return True + return False items = get_full_item_list() categorized_items: typing.Dict[SC2Race, typing.List[int]] = {} parent_to_child: typing.Dict[int, typing.List[int]] = {} items_received: typing.Dict[int, typing.List[NetworkItem]] = {} + filter_match_count = 0 for item in self.ctx.items_received: items_received.setdefault(item.item, []).append(item) items_received_set = set(items_received) @@ -232,28 +237,59 @@ def _cmd_received(self, filter_search: str = "aptz") -> bool: parent_to_child.setdefault(items[item_data.parent_item].code, []).append(item_data.code) else: categorized_items.setdefault(item_data.race, []).append(item_data.code) - for _, faction in search_filter_meanings: - if faction not in faction_filter: - continue - self.formatted_print(f" [u]{faction.name}[/u] ") + for faction in SC2Race: + has_printed_faction_title = False + def print_faction_title(): + if not has_printed_faction_title: + self.formatted_print(f" [u]{faction.name}[/u] ") + for item_id in categorized_items[faction]: - received_items_of_this_type = items_received.get(item_id, ()) - for item in received_items_of_this_type: - (ColouredMessage('* ').item(item.item, flags=item.flags) - (" from ").location(item.location, self.ctx.slot) - (" by ").player(item.player) - ).send(self.ctx) - if not received_items_of_this_type and items_received_set.intersection(parent_to_child.get(item_id, ())): - # We didn't receive this item, but we have its children - ColouredMessage("- ").coloured(self.ctx.item_names[item_id], "black")(" - not obtained").send(self.ctx) - for child_item in parent_to_child.get(item_id, ()): - received_items_of_this_type = items_received.get(child_item, ()) + item_name = self.ctx.item_names[item_id] + received_child_items = items_received_set.intersection(parent_to_child.get(item_id, [])) + matching_children = [child for child in received_child_items + if item_matches_filter(self.ctx.item_names[child])] + received_items_of_this_type = items_received.get(item_id, []) + item_is_match = item_matches_filter(item_name) + if item_is_match or len(matching_children) > 0: + # Print found item if it or its children match the filter + if item_is_match: + filter_match_count += len(received_items_of_this_type) for item in received_items_of_this_type: - (ColouredMessage(' * ').item(item.item, flags=item.flags) + print_faction_title() + has_printed_faction_title = True + (ColouredMessage('* ').item(item.item, flags=item.flags) (" from ").location(item.location, self.ctx.slot) (" by ").player(item.player) ).send(self.ctx) - self.formatted_print(f"[b]Obtained: {len(self.ctx.items_received)} items[/b]") + + if received_child_items: + # We have this item's children + if len(matching_children) == 0: + # ...but none of them match the filter + continue + + if not received_items_of_this_type: + # We didn't receive the item itself + print_faction_title() + has_printed_faction_title = True + ColouredMessage("- ").coloured(item_name, "black")(" - not obtained").send(self.ctx) + + for child_item in matching_children: + received_items_of_this_type = items_received.get(child_item, []) + for item in received_items_of_this_type: + filter_match_count += len(received_items_of_this_type) + (ColouredMessage(' * ').item(item.item, flags=item.flags) + (" from ").location(item.location, self.ctx.slot) + (" by ").player(item.player) + ).send(self.ctx) + + non_matching_children = len(received_child_items) - len(matching_children) + if non_matching_children > 0: + self.formatted_print(f" + {non_matching_children} child items that don't match the filter") + if filter_search == "": + self.formatted_print(f"[b]Obtained: {len(self.ctx.items_received)} items[/b]") + else: + self.formatted_print(f"[b]Filter \"{filter_search}\" found {filter_match_count} out of {len(self.ctx.items_received)} obtained items[/b]") return True def _cmd_option(self, option_name: str = "", option_value: str = "") -> None: From 50b6d74d41460f7505a3fee9b9cf51baf9552ba5 Mon Sep 17 00:00:00 2001 From: Salzkorn Date: Sat, 3 Feb 2024 18:40:12 +0100 Subject: [PATCH 3/3] Fix flaggroup item name groups --- worlds/sc2/ItemGroups.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/worlds/sc2/ItemGroups.py b/worlds/sc2/ItemGroups.py index 8fa287f5f271..0dc6a490ce10 100644 --- a/worlds/sc2/ItemGroups.py +++ b/worlds/sc2/ItemGroups.py @@ -51,9 +51,11 @@ item_name_groups.setdefault(data.type, []).append(item) # Numbered flaggroups get sorted into an unnumbered group # Currently supports numbers of one or two digits - if item[-2:].strip().isnumeric: - type_group = item[:-2].strip() + if data.type[-2:].strip().isnumeric: + type_group = data.type[:-2].strip() item_name_groups.setdefault(type_group, []).append(item) + # Flaggroups with numbers are unlisted + unlisted_item_name_groups.add(data.type) # Items with a bracket get a short-hand name group for ease of use in YAMLs if '(' in item: short_name = item[:item.find(' (')]