mirror of
https://github.com/MarioSpore/Grinch-AP.git
synced 2025-10-21 12:11:33 -06:00

Adds The Legend of Zelda: The Wind Waker as a supported game in Archipelago. The game uses [LagoLunatic's randomizer](https://github.com/LagoLunatic/wwrando) as its base (regarding logic, options, etc.) and builds from there.
374 lines
22 KiB
Python
374 lines
22 KiB
Python
from collections.abc import Iterable
|
|
from typing import TYPE_CHECKING, NamedTuple, Optional
|
|
|
|
from BaseClasses import Item
|
|
from BaseClasses import ItemClassification as IC
|
|
from worlds.AutoWorld import World
|
|
|
|
if TYPE_CHECKING:
|
|
from .randomizers.Dungeons import Dungeon
|
|
|
|
|
|
def item_factory(items: str | Iterable[str], world: World) -> Item | list[Item]:
|
|
"""
|
|
Create items based on their names.
|
|
Depending on the input, this function can return a single item or a list of items.
|
|
|
|
:param items: The name or names of the items to create.
|
|
:param world: The game world.
|
|
:raises KeyError: If an unknown item name is provided.
|
|
:return: A single item or a list of items.
|
|
"""
|
|
ret: list[Item] = []
|
|
singleton = False
|
|
if isinstance(items, str):
|
|
items = [items]
|
|
singleton = True
|
|
for item in items:
|
|
if item in ITEM_TABLE:
|
|
ret.append(world.create_item(item))
|
|
else:
|
|
raise KeyError(f"Unknown item {item}")
|
|
|
|
return ret[0] if singleton else ret
|
|
|
|
|
|
class TWWItemData(NamedTuple):
|
|
"""
|
|
This class represents the data for an item in The Wind Waker.
|
|
|
|
:param type: The type of the item (e.g., "Item", "Dungeon Item").
|
|
:param classification: The item's classification (progression, useful, filler).
|
|
:param code: The unique code identifier for the item.
|
|
:param quantity: The number of this item available.
|
|
:param item_id: The ID used to represent the item in-game.
|
|
"""
|
|
|
|
type: str
|
|
classification: IC
|
|
code: Optional[int]
|
|
quantity: int
|
|
item_id: Optional[int]
|
|
|
|
|
|
class TWWItem(Item):
|
|
"""
|
|
This class represents an item in The Wind Waker.
|
|
|
|
:param name: The item's name.
|
|
:param player: The ID of the player who owns the item.
|
|
:param data: The data associated with this item.
|
|
:param classification: Optional classification to override the default.
|
|
"""
|
|
|
|
game: str = "The Wind Waker"
|
|
type: Optional[str]
|
|
dungeon: Optional["Dungeon"] = None
|
|
|
|
def __init__(self, name: str, player: int, data: TWWItemData, classification: Optional[IC] = None) -> None:
|
|
super().__init__(
|
|
name,
|
|
data.classification if classification is None else classification,
|
|
None if data.code is None else TWWItem.get_apid(data.code),
|
|
player,
|
|
)
|
|
|
|
self.type = data.type
|
|
self.item_id = data.item_id
|
|
|
|
@staticmethod
|
|
def get_apid(code: int) -> int:
|
|
"""
|
|
Compute the Archipelago ID for the given item code.
|
|
|
|
:param code: The unique code for the item.
|
|
:return: The computed Archipelago ID.
|
|
"""
|
|
base_id: int = 2322432
|
|
return base_id + code
|
|
|
|
@property
|
|
def dungeon_item(self) -> Optional[str]:
|
|
"""
|
|
Determine if the item is a dungeon item and, if so, returns its type.
|
|
|
|
:return: The type of dungeon item, or `None` if it is not a dungeon item.
|
|
"""
|
|
if self.type in ("Small Key", "Big Key", "Map", "Compass"):
|
|
return self.type
|
|
return None
|
|
|
|
|
|
ITEM_TABLE: dict[str, TWWItemData] = {
|
|
"Telescope": TWWItemData("Item", IC.useful, 0, 1, 0x20),
|
|
# "Boat's Sail": TWWItemData("Item", IC.progression, 1, 1, 0x78), # noqa: E131
|
|
"Wind Waker": TWWItemData("Item", IC.progression, 2, 1, 0x22),
|
|
"Grappling Hook": TWWItemData("Item", IC.progression, 3, 1, 0x25),
|
|
"Spoils Bag": TWWItemData("Item", IC.progression, 4, 1, 0x24),
|
|
"Boomerang": TWWItemData("Item", IC.progression, 5, 1, 0x2D),
|
|
"Deku Leaf": TWWItemData("Item", IC.progression, 6, 1, 0x34),
|
|
"Tingle Tuner": TWWItemData("Item", IC.progression, 7, 1, 0x21),
|
|
"Iron Boots": TWWItemData("Item", IC.progression, 8, 1, 0x29),
|
|
"Magic Armor": TWWItemData("Item", IC.progression, 9, 1, 0x2A),
|
|
"Bait Bag": TWWItemData("Item", IC.progression, 10, 1, 0x2C),
|
|
"Bombs": TWWItemData("Item", IC.progression, 11, 1, 0x31),
|
|
"Delivery Bag": TWWItemData("Item", IC.progression, 12, 1, 0x30),
|
|
"Hookshot": TWWItemData("Item", IC.progression, 13, 1, 0x2F),
|
|
"Skull Hammer": TWWItemData("Item", IC.progression, 14, 1, 0x33),
|
|
"Power Bracelets": TWWItemData("Item", IC.progression, 15, 1, 0x28),
|
|
|
|
"Hero's Charm": TWWItemData("Item", IC.useful, 16, 1, 0x43),
|
|
"Hurricane Spin": TWWItemData("Item", IC.useful, 17, 1, 0xAA),
|
|
"Dragon Tingle Statue": TWWItemData("Item", IC.progression, 18, 1, 0xA3),
|
|
"Forbidden Tingle Statue": TWWItemData("Item", IC.progression, 19, 1, 0xA4),
|
|
"Goddess Tingle Statue": TWWItemData("Item", IC.progression, 20, 1, 0xA5),
|
|
"Earth Tingle Statue": TWWItemData("Item", IC.progression, 21, 1, 0xA6),
|
|
"Wind Tingle Statue": TWWItemData("Item", IC.progression, 22, 1, 0xA7),
|
|
|
|
"Wind's Requiem": TWWItemData("Item", IC.progression, 23, 1, 0x6D),
|
|
"Ballad of Gales": TWWItemData("Item", IC.progression, 24, 1, 0x6E),
|
|
"Command Melody": TWWItemData("Item", IC.progression, 25, 1, 0x6F),
|
|
"Earth God's Lyric": TWWItemData("Item", IC.progression, 26, 1, 0x70),
|
|
"Wind God's Aria": TWWItemData("Item", IC.progression, 27, 1, 0x71),
|
|
"Song of Passing": TWWItemData("Item", IC.progression, 28, 1, 0x72),
|
|
|
|
"Triforce Shard 1": TWWItemData("Item", IC.progression, 29, 1, 0x61),
|
|
"Triforce Shard 2": TWWItemData("Item", IC.progression, 30, 1, 0x62),
|
|
"Triforce Shard 3": TWWItemData("Item", IC.progression, 31, 1, 0x63),
|
|
"Triforce Shard 4": TWWItemData("Item", IC.progression, 32, 1, 0x64),
|
|
"Triforce Shard 5": TWWItemData("Item", IC.progression, 33, 1, 0x65),
|
|
"Triforce Shard 6": TWWItemData("Item", IC.progression, 34, 1, 0x66),
|
|
"Triforce Shard 7": TWWItemData("Item", IC.progression, 35, 1, 0x67),
|
|
"Triforce Shard 8": TWWItemData("Item", IC.progression, 36, 1, 0x68),
|
|
|
|
"Skull Necklace": TWWItemData("Item", IC.filler, 37, 9, 0x45),
|
|
"Boko Baba Seed": TWWItemData("Item", IC.filler, 38, 1, 0x46),
|
|
"Golden Feather": TWWItemData("Item", IC.filler, 39, 9, 0x47),
|
|
"Knight's Crest": TWWItemData("Item", IC.filler, 40, 3, 0x48),
|
|
"Red Chu Jelly": TWWItemData("Item", IC.filler, 41, 1, 0x49),
|
|
"Green Chu Jelly": TWWItemData("Item", IC.filler, 42, 1, 0x4A),
|
|
"Joy Pendant": TWWItemData("Item", IC.filler, 43, 20, 0x1F),
|
|
"All-Purpose Bait": TWWItemData("Item", IC.filler, 44, 1, 0x82),
|
|
"Hyoi Pear": TWWItemData("Item", IC.filler, 45, 4, 0x83),
|
|
|
|
"Note to Mom": TWWItemData("Item", IC.progression, 46, 1, 0x99),
|
|
"Maggie's Letter": TWWItemData("Item", IC.progression, 47, 1, 0x9A),
|
|
"Moblin's Letter": TWWItemData("Item", IC.progression, 48, 1, 0x9B),
|
|
"Cabana Deed": TWWItemData("Item", IC.progression, 49, 1, 0x9C),
|
|
"Fill-Up Coupon": TWWItemData("Item", IC.useful, 50, 1, 0x9E),
|
|
|
|
"Nayru's Pearl": TWWItemData("Item", IC.progression, 51, 1, 0x69),
|
|
"Din's Pearl": TWWItemData("Item", IC.progression, 52, 1, 0x6A),
|
|
"Farore's Pearl": TWWItemData("Item", IC.progression, 53, 1, 0x6B),
|
|
|
|
"Progressive Sword": TWWItemData("Item", IC.progression, 54, 4, 0x38),
|
|
"Progressive Shield": TWWItemData("Item", IC.progression, 55, 2, 0x3B),
|
|
"Progressive Picto Box": TWWItemData("Item", IC.progression, 56, 2, 0x23),
|
|
"Progressive Bow": TWWItemData("Item", IC.progression, 57, 3, 0x27),
|
|
"Progressive Magic Meter": TWWItemData("Item", IC.progression, 58, 2, 0xB1),
|
|
"Quiver Capacity Upgrade": TWWItemData("Item", IC.progression, 59, 2, 0xAF),
|
|
"Bomb Bag Capacity Upgrade": TWWItemData("Item", IC.useful, 60, 2, 0xAD),
|
|
"Wallet Capacity Upgrade": TWWItemData("Item", IC.progression, 61, 2, 0xAB),
|
|
"Empty Bottle": TWWItemData("Item", IC.progression, 62, 4, 0x50),
|
|
|
|
"Triforce Chart 1": TWWItemData("Item", IC.progression_skip_balancing, 63, 1, 0xFE),
|
|
"Triforce Chart 2": TWWItemData("Item", IC.progression_skip_balancing, 64, 1, 0xFD),
|
|
"Triforce Chart 3": TWWItemData("Item", IC.progression_skip_balancing, 65, 1, 0xFC),
|
|
"Triforce Chart 4": TWWItemData("Item", IC.progression_skip_balancing, 66, 1, 0xFB),
|
|
"Triforce Chart 5": TWWItemData("Item", IC.progression_skip_balancing, 67, 1, 0xFA),
|
|
"Triforce Chart 6": TWWItemData("Item", IC.progression_skip_balancing, 68, 1, 0xF9),
|
|
"Triforce Chart 7": TWWItemData("Item", IC.progression_skip_balancing, 69, 1, 0xF8),
|
|
"Triforce Chart 8": TWWItemData("Item", IC.progression_skip_balancing, 70, 1, 0xF7),
|
|
"Treasure Chart 1": TWWItemData("Item", IC.progression_skip_balancing, 71, 1, 0xE7),
|
|
"Treasure Chart 2": TWWItemData("Item", IC.progression_skip_balancing, 72, 1, 0xEE),
|
|
"Treasure Chart 3": TWWItemData("Item", IC.progression_skip_balancing, 73, 1, 0xE0),
|
|
"Treasure Chart 4": TWWItemData("Item", IC.progression_skip_balancing, 74, 1, 0xE1),
|
|
"Treasure Chart 5": TWWItemData("Item", IC.progression_skip_balancing, 75, 1, 0xF2),
|
|
"Treasure Chart 6": TWWItemData("Item", IC.progression_skip_balancing, 76, 1, 0xEA),
|
|
"Treasure Chart 7": TWWItemData("Item", IC.progression_skip_balancing, 77, 1, 0xCC),
|
|
"Treasure Chart 8": TWWItemData("Item", IC.progression_skip_balancing, 78, 1, 0xD4),
|
|
"Treasure Chart 9": TWWItemData("Item", IC.progression_skip_balancing, 79, 1, 0xDA),
|
|
"Treasure Chart 10": TWWItemData("Item", IC.progression_skip_balancing, 80, 1, 0xDE),
|
|
"Treasure Chart 11": TWWItemData("Item", IC.progression_skip_balancing, 81, 1, 0xF6),
|
|
"Treasure Chart 12": TWWItemData("Item", IC.progression_skip_balancing, 82, 1, 0xE9),
|
|
"Treasure Chart 13": TWWItemData("Item", IC.progression_skip_balancing, 83, 1, 0xCF),
|
|
"Treasure Chart 14": TWWItemData("Item", IC.progression_skip_balancing, 84, 1, 0xDD),
|
|
"Treasure Chart 15": TWWItemData("Item", IC.progression_skip_balancing, 85, 1, 0xF5),
|
|
"Treasure Chart 16": TWWItemData("Item", IC.progression_skip_balancing, 86, 1, 0xE3),
|
|
"Treasure Chart 17": TWWItemData("Item", IC.progression_skip_balancing, 87, 1, 0xD7),
|
|
"Treasure Chart 18": TWWItemData("Item", IC.progression_skip_balancing, 88, 1, 0xE4),
|
|
"Treasure Chart 19": TWWItemData("Item", IC.progression_skip_balancing, 89, 1, 0xD1),
|
|
"Treasure Chart 20": TWWItemData("Item", IC.progression_skip_balancing, 90, 1, 0xF3),
|
|
"Treasure Chart 21": TWWItemData("Item", IC.progression_skip_balancing, 91, 1, 0xCE),
|
|
"Treasure Chart 22": TWWItemData("Item", IC.progression_skip_balancing, 92, 1, 0xD9),
|
|
"Treasure Chart 23": TWWItemData("Item", IC.progression_skip_balancing, 93, 1, 0xF1),
|
|
"Treasure Chart 24": TWWItemData("Item", IC.progression_skip_balancing, 94, 1, 0xEB),
|
|
"Treasure Chart 25": TWWItemData("Item", IC.progression_skip_balancing, 95, 1, 0xD6),
|
|
"Treasure Chart 26": TWWItemData("Item", IC.progression_skip_balancing, 96, 1, 0xD3),
|
|
"Treasure Chart 27": TWWItemData("Item", IC.progression_skip_balancing, 97, 1, 0xCD),
|
|
"Treasure Chart 28": TWWItemData("Item", IC.progression_skip_balancing, 98, 1, 0xE2),
|
|
"Treasure Chart 29": TWWItemData("Item", IC.progression_skip_balancing, 99, 1, 0xE6),
|
|
"Treasure Chart 30": TWWItemData("Item", IC.progression_skip_balancing, 100, 1, 0xF4),
|
|
"Treasure Chart 31": TWWItemData("Item", IC.progression_skip_balancing, 101, 1, 0xF0),
|
|
"Treasure Chart 32": TWWItemData("Item", IC.progression_skip_balancing, 102, 1, 0xD0),
|
|
"Treasure Chart 33": TWWItemData("Item", IC.progression_skip_balancing, 103, 1, 0xEF),
|
|
"Treasure Chart 34": TWWItemData("Item", IC.progression_skip_balancing, 104, 1, 0xE5),
|
|
"Treasure Chart 35": TWWItemData("Item", IC.progression_skip_balancing, 105, 1, 0xE8),
|
|
"Treasure Chart 36": TWWItemData("Item", IC.progression_skip_balancing, 106, 1, 0xD8),
|
|
"Treasure Chart 37": TWWItemData("Item", IC.progression_skip_balancing, 107, 1, 0xD5),
|
|
"Treasure Chart 38": TWWItemData("Item", IC.progression_skip_balancing, 108, 1, 0xED),
|
|
"Treasure Chart 39": TWWItemData("Item", IC.progression_skip_balancing, 109, 1, 0xEC),
|
|
"Treasure Chart 40": TWWItemData("Item", IC.progression_skip_balancing, 110, 1, 0xDF),
|
|
"Treasure Chart 41": TWWItemData("Item", IC.progression_skip_balancing, 111, 1, 0xD2),
|
|
|
|
"Tingle's Chart": TWWItemData("Item", IC.filler, 112, 1, 0xDC),
|
|
"Ghost Ship Chart": TWWItemData("Item", IC.progression, 113, 1, 0xDB),
|
|
"Octo Chart": TWWItemData("Item", IC.filler, 114, 1, 0xCA),
|
|
"Great Fairy Chart": TWWItemData("Item", IC.filler, 115, 1, 0xC9),
|
|
"Secret Cave Chart": TWWItemData("Item", IC.filler, 116, 1, 0xC6),
|
|
"Light Ring Chart": TWWItemData("Item", IC.filler, 117, 1, 0xC5),
|
|
"Platform Chart": TWWItemData("Item", IC.filler, 118, 1, 0xC4),
|
|
"Beedle's Chart": TWWItemData("Item", IC.filler, 119, 1, 0xC3),
|
|
"Submarine Chart": TWWItemData("Item", IC.filler, 120, 1, 0xC2),
|
|
|
|
"Green Rupee": TWWItemData("Item", IC.filler, 121, 1, 0x01),
|
|
"Blue Rupee": TWWItemData("Item", IC.filler, 122, 2, 0x02),
|
|
"Yellow Rupee": TWWItemData("Item", IC.filler, 123, 3, 0x03),
|
|
"Red Rupee": TWWItemData("Item", IC.filler, 124, 8, 0x04),
|
|
"Purple Rupee": TWWItemData("Item", IC.filler, 125, 10, 0x05),
|
|
"Orange Rupee": TWWItemData("Item", IC.useful, 126, 15, 0x06),
|
|
"Silver Rupee": TWWItemData("Item", IC.useful, 127, 20, 0x0F),
|
|
"Rainbow Rupee": TWWItemData("Item", IC.useful, 128, 1, 0xB8),
|
|
|
|
"Piece of Heart": TWWItemData("Item", IC.useful, 129, 44, 0x07),
|
|
"Heart Container": TWWItemData("Item", IC.useful, 130, 6, 0x08),
|
|
|
|
"DRC Big Key": TWWItemData("Big Key", IC.progression, 131, 1, 0x14),
|
|
"DRC Small Key": TWWItemData("Small Key", IC.progression, 132, 4, 0x13),
|
|
"FW Big Key": TWWItemData("Big Key", IC.progression, 133, 1, 0x40),
|
|
"FW Small Key": TWWItemData("Small Key", IC.progression, 134, 1, 0x1D),
|
|
"TotG Big Key": TWWItemData("Big Key", IC.progression, 135, 1, 0x5C),
|
|
"TotG Small Key": TWWItemData("Small Key", IC.progression, 136, 2, 0x5B),
|
|
"ET Big Key": TWWItemData("Big Key", IC.progression, 138, 1, 0x74),
|
|
"ET Small Key": TWWItemData("Small Key", IC.progression, 139, 3, 0x73),
|
|
"WT Big Key": TWWItemData("Big Key", IC.progression, 140, 1, 0x81),
|
|
"WT Small Key": TWWItemData("Small Key", IC.progression, 141, 2, 0x77),
|
|
"DRC Dungeon Map": TWWItemData("Map", IC.filler, 142, 1, 0x1B),
|
|
"DRC Compass": TWWItemData("Compass", IC.filler, 143, 1, 0x1C),
|
|
"FW Dungeon Map": TWWItemData("Map", IC.filler, 144, 1, 0x41),
|
|
"FW Compass": TWWItemData("Compass", IC.filler, 145, 1, 0x5A),
|
|
"TotG Dungeon Map": TWWItemData("Map", IC.filler, 146, 1, 0x5D),
|
|
"TotG Compass": TWWItemData("Compass", IC.filler, 147, 1, 0x5E),
|
|
"FF Dungeon Map": TWWItemData("Map", IC.filler, 148, 1, 0x5F),
|
|
"FF Compass": TWWItemData("Compass", IC.filler, 149, 1, 0x60),
|
|
"ET Dungeon Map": TWWItemData("Map", IC.filler, 150, 1, 0x75),
|
|
"ET Compass": TWWItemData("Compass", IC.filler, 151, 1, 0x76),
|
|
"WT Dungeon Map": TWWItemData("Map", IC.filler, 152, 1, 0x84),
|
|
"WT Compass": TWWItemData("Compass", IC.filler, 153, 1, 0x85),
|
|
|
|
"Victory": TWWItemData("Event", IC.progression, None, 1, None),
|
|
}
|
|
|
|
ISLAND_NUMBER_TO_CHART_NAME = {
|
|
1: "Treasure Chart 25",
|
|
2: "Treasure Chart 7",
|
|
3: "Treasure Chart 24",
|
|
4: "Triforce Chart 2",
|
|
5: "Treasure Chart 11",
|
|
6: "Triforce Chart 7",
|
|
7: "Treasure Chart 13",
|
|
8: "Treasure Chart 41",
|
|
9: "Treasure Chart 29",
|
|
10: "Treasure Chart 22",
|
|
11: "Treasure Chart 18",
|
|
12: "Treasure Chart 30",
|
|
13: "Treasure Chart 39",
|
|
14: "Treasure Chart 19",
|
|
15: "Treasure Chart 8",
|
|
16: "Treasure Chart 2",
|
|
17: "Treasure Chart 10",
|
|
18: "Treasure Chart 26",
|
|
19: "Treasure Chart 3",
|
|
20: "Treasure Chart 37",
|
|
21: "Treasure Chart 27",
|
|
22: "Treasure Chart 38",
|
|
23: "Triforce Chart 1",
|
|
24: "Treasure Chart 21",
|
|
25: "Treasure Chart 6",
|
|
26: "Treasure Chart 14",
|
|
27: "Treasure Chart 34",
|
|
28: "Treasure Chart 5",
|
|
29: "Treasure Chart 28",
|
|
30: "Treasure Chart 35",
|
|
31: "Triforce Chart 3",
|
|
32: "Triforce Chart 6",
|
|
33: "Treasure Chart 1",
|
|
34: "Treasure Chart 20",
|
|
35: "Treasure Chart 36",
|
|
36: "Treasure Chart 23",
|
|
37: "Treasure Chart 12",
|
|
38: "Treasure Chart 16",
|
|
39: "Treasure Chart 4",
|
|
40: "Treasure Chart 17",
|
|
41: "Treasure Chart 31",
|
|
42: "Triforce Chart 5",
|
|
43: "Treasure Chart 9",
|
|
44: "Triforce Chart 4",
|
|
45: "Treasure Chart 40",
|
|
46: "Triforce Chart 8",
|
|
47: "Treasure Chart 15",
|
|
48: "Treasure Chart 32",
|
|
49: "Treasure Chart 33",
|
|
}
|
|
|
|
|
|
LOOKUP_ID_TO_NAME: dict[int, str] = {
|
|
TWWItem.get_apid(data.code): item for item, data in ITEM_TABLE.items() if data.code is not None
|
|
}
|
|
|
|
item_name_groups = {
|
|
"Songs": {
|
|
"Wind's Requiem",
|
|
"Ballad of Gales",
|
|
"Command Melody",
|
|
"Earth God's Lyric",
|
|
"Wind God's Aria",
|
|
"Song of Passing",
|
|
},
|
|
"Mail": {
|
|
"Note to Mom",
|
|
"Maggie's Letter",
|
|
"Moblin's Letter",
|
|
},
|
|
"Special Charts": {
|
|
"Tingle's Chart",
|
|
"Ghost Ship Chart",
|
|
"Octo Chart",
|
|
"Great Fairy Chart",
|
|
"Secret Cave Chart",
|
|
"Light Ring Chart",
|
|
"Platform Chart",
|
|
"Beedle's Chart",
|
|
"Submarine Chart",
|
|
},
|
|
}
|
|
# generic groups, (Name, substring)
|
|
_simple_groups = {
|
|
("Tingle Statues", "Tingle Statue"),
|
|
("Shards", "Shard"),
|
|
("Pearls", "Pearl"),
|
|
("Triforce Charts", "Triforce Chart"),
|
|
("Treasure Charts", "Treasure Chart"),
|
|
("Small Keys", "Small Key"),
|
|
("Big Keys", "Big Key"),
|
|
("Rupees", "Rupee"),
|
|
("Dungeon Items", "Compass"),
|
|
("Dungeon Items", "Map"),
|
|
}
|
|
for basename, substring in _simple_groups:
|
|
if basename not in item_name_groups:
|
|
item_name_groups[basename] = set()
|
|
for itemname in ITEM_TABLE:
|
|
if substring in itemname:
|
|
item_name_groups[basename].add(itemname)
|