Skip to content

Commit

Permalink
Merge pull request Ziktofel#159 from Salzkorn/sc2-next
Browse files Browse the repository at this point in the history
Item name group refactor & /received filtering rework
  • Loading branch information
Ziktofel authored Feb 3, 2024
2 parents e1d4fe1 + 50b6d74 commit f29aa26
Show file tree
Hide file tree
Showing 5 changed files with 173 additions and 73 deletions.
100 changes: 68 additions & 32 deletions worlds/sc2/Client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand All @@ -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:
Expand Down
101 changes: 101 additions & 0 deletions worlds/sc2/ItemGroups.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
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 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(' (')]
# 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,
]
4 changes: 2 additions & 2 deletions worlds/sc2/ItemNames.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
38 changes: 0 additions & 38 deletions worlds/sc2/Items.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, ...] = (
Expand Down
3 changes: 2 additions & 1 deletion worlds/sc2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, \
Expand Down

0 comments on commit f29aa26

Please sign in to comment.