From d1b25bbc373f688feeba496177716a5cb0806c47 Mon Sep 17 00:00:00 2001 From: nicopop <6759630+nicopop@users.noreply.github.com> Date: Thu, 9 Apr 2026 15:13:38 -0400 Subject: [PATCH 1/8] move place_item(_category) to create_items --- src/__init__.py | 157 +++++++++++++++++++++++++-------------------- src/hooks/World.py | 8 +-- 2 files changed, 90 insertions(+), 75 deletions(-) diff --git a/src/__init__.py b/src/__init__.py index cdfb1478..956e7f0b 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -21,16 +21,16 @@ from .Helpers import is_item_enabled, get_option_value, remove_specific_item, resolve_yaml_option, format_state_prog_items_key, convert_string_to_itemclassification, ProgItemsCat from .container import APManualFile -from BaseClasses import CollectionState, ItemClassification, Item +from BaseClasses import CollectionState, ItemClassification, Item, Location from Options import PerGameCommonOptions from worlds.AutoWorld import World from .hooks.World import \ hook_get_filler_item_name, before_create_regions, after_create_regions, \ - before_create_items_all, before_create_items_starting, before_create_items_filler, after_create_items, \ + before_create_items_all, before_create_items_starting, before_create_items_filler, before_create_items_place_items, after_create_items, \ before_create_item, after_create_item, \ before_set_rules, after_set_rules, \ - before_generate_basic, after_generate_basic, \ + before_generate_basic, \ before_fill_slot_data, after_fill_slot_data, before_write_spoiler, \ before_extend_hint_information, after_extend_hint_information, \ after_collect_item, after_remove_item, before_generate_early, hook_interpret_slot_data @@ -243,6 +243,84 @@ def create_items(self): pool = before_create_items_filler(pool, self, self.multiworld, self.player) pool = self.adjust_filler_items(pool, traps) + + pool = before_create_items_place_items(pool, self, self.multiworld, self.player) + # Handle item forbidding/placement + manual_locations_with_placements: dict[str, dict[str, Any]] = {} + manual_locations_with_forbid: dict[str, dict[str, Any]] = {} + for name, l in location_name_to_location.items(): + if l.get("place_item") or l.get("place_item_category"): + manual_locations_with_placements[name] = l + elif l.get("dont_place_item") or l.get("dont_place_item_category"): + manual_locations_with_forbid[name] = l + locations_with_forbid: list[Location] = [] + locations_with_placements: list[Location] = [] + for location in self.multiworld.get_unfilled_locations(player=self.player): + if location.name in manual_locations_with_placements.keys(): + locations_with_placements.append(location) + elif location.name in manual_locations_with_forbid.keys(): + locations_with_forbid.append(location) + + # Handle specific item forbidding using forbid_items_for_player + for location in locations_with_forbid: + manual_location = manual_locations_with_forbid[location.name] + forbidden_item_names = [] + + if manual_location.get("dont_place_item"): + forbidden_item_names.extend([i["name"] for i in item_name_to_item.values() if i["name"] in manual_location["dont_place_item"]]) + + if manual_location.get("dont_place_item_category"): + forbidden_item_names.extend([i["name"] for i in item_name_to_item.values() if "category" in i and set(i["category"]).intersection(manual_location["dont_place_item_category"])]) + + if forbidden_item_names: + forbid_items_for_player(location, set(forbidden_item_names), self.player) + + # Handle specific item placements using place_locked_item + for location in locations_with_placements: + manual_location = manual_locations_with_placements[location.name] + eligible_items = [] + eligible_item_names = [] + forbidden_item_names = [] + place_messages = [] + forbid_messages = [] + + #First we get possible items names + if manual_location.get("place_item"): + eligible_item_names += manual_location["place_item"] + place_messages.append('", "'.join(manual_location["place_item"])) + + if manual_location.get("place_item_category"): + eligible_item_names += [i["name"] for i in item_name_to_item.values() if "category" in i and set(i["category"]).intersection(manual_location["place_item_category"])] + place_messages.append('", "'.join(manual_location["place_item_category"]) + " category(ies)") + + # Second we check for forbidden items names + if manual_location.get("dont_place_item"): + forbidden_item_names += manual_location["dont_place_item"] + forbid_messages.append('", "'.join(manual_location["dont_place_item"]) + ' items') + + if manual_location.get("dont_place_item_category"): + forbidden_item_names += [i["name"] for i in item_name_to_item.values() if "category" in i and set(i["category"]).intersection(manual_location["dont_place_item_category"])] + forbid_messages.append('", "'.join(manual_location["dont_place_item_category"]) + ' category(ies)') + + # If we forbid some names, check for those in the possible names and remove them + if forbidden_item_names: + eligible_item_names = [name for name in eligible_item_names if name not in forbidden_item_names] + + if eligible_item_names: + eligible_items = [item for item in pool if item.name in eligible_item_names] + + if len(eligible_items) == 0: + nl = "\n" + if forbidden_item_names: + raise Exception(f'Could not find a suitable item to place at "{manual_location["name"]}".\n No items that match "{f"{nl} or ".join(place_messages)}"\n Maybe because of forbidden "{f"{nl} or ".join(forbid_messages)}"') + raise Exception(f'Could not find a suitable item to place at "{manual_location["name"]}". \n No items that match "{f"{nl} or ".join(place_messages)}"') + + item_to_place = self.random.choice(eligible_items) + location.place_locked_item(item_to_place) + + # remove the item we're about to place from the pool so it isn't placed twice + remove_specific_item(pool, item_to_place) + pool = after_create_items(pool, self, self.multiworld, self.player) # need to put all of the items in the pool so we can have a full state for placement @@ -259,8 +337,12 @@ def create_items(self): items_iter = iter([i for i in precollected_items if i.name == item]) for _ in range(count): precollected_items.remove(next(items_iter)) + # Placed items: + placed_pool: list[Item] = [] + for location in self.multiworld.get_filled_locations(self.player): + placed_pool.append(location.item) - real_pool = pool + precollected_items + real_pool = pool + precollected_items + placed_pool self.item_counts[self.player] = self.get_item_counts(pool=real_pool) self.item_counts_progression[self.player] = self.get_item_counts(pool=real_pool, only_progression=True) @@ -336,73 +418,6 @@ def set_rules(self): def generate_basic(self): before_generate_basic(self, self.multiworld, self.player) - # Handle item forbidding - manual_locations_with_forbid = {location['name']: location for location in location_name_to_location.values() if "dont_place_item" in location or "dont_place_item_category" in location} - locations_with_forbid = [l for l in self.multiworld.get_unfilled_locations(player=self.player) if l.name in manual_locations_with_forbid.keys()] - for location in locations_with_forbid: - manual_location = manual_locations_with_forbid[location.name] - forbidden_item_names = [] - - if manual_location.get("dont_place_item"): - forbidden_item_names.extend([i["name"] for i in item_name_to_item.values() if i["name"] in manual_location["dont_place_item"]]) - - if manual_location.get("dont_place_item_category"): - forbidden_item_names.extend([i["name"] for i in item_name_to_item.values() if "category" in i and set(i["category"]).intersection(manual_location["dont_place_item_category"])]) - - if forbidden_item_names: - forbid_items_for_player(location, set(forbidden_item_names), self.player) - - # Handle specific item placements using fill_restrictive - manual_locations_with_placements = {location['name']: location for location in location_name_to_location.values() if "place_item" in location or "place_item_category" in location} - locations_with_placements = [l for l in self.multiworld.get_unfilled_locations(player=self.player) if l.name in manual_locations_with_placements.keys()] - for location in locations_with_placements: - manual_location = manual_locations_with_placements[location.name] - eligible_items = [] - eligible_item_names = [] - forbidden_item_names = [] - place_messages = [] - forbid_messages = [] - - #First we get possible items names - if manual_location.get("place_item"): - eligible_item_names += manual_location["place_item"] - place_messages.append('", "'.join(manual_location["place_item"])) - - if manual_location.get("place_item_category"): - eligible_item_names += [i["name"] for i in item_name_to_item.values() if "category" in i and set(i["category"]).intersection(manual_location["place_item_category"])] - place_messages.append('", "'.join(manual_location["place_item_category"]) + " category(ies)") - - # Second we check for forbidden items names - if manual_location.get("dont_place_item"): - forbidden_item_names += manual_location["dont_place_item"] - forbid_messages.append('", "'.join(manual_location["dont_place_item"]) + ' items') - - if manual_location.get("dont_place_item_category"): - forbidden_item_names += [i["name"] for i in item_name_to_item.values() if "category" in i and set(i["category"]).intersection(manual_location["dont_place_item_category"])] - forbid_messages.append('", "'.join(manual_location["dont_place_item_category"]) + ' category(ies)') - - # If we forbid some names, check for those in the possible names and remove them - if forbidden_item_names: - eligible_item_names = [name for name in eligible_item_names if name not in forbidden_item_names] - - if eligible_item_names: - eligible_items = [item for item in self.multiworld.itempool if item.player == self.player and item.name in eligible_item_names] - - if len(eligible_items) == 0: - nl = "\n" - if forbidden_item_names: - raise Exception(f'Could not find a suitable item to place at "{manual_location["name"]}".\n No items that match "{f"{nl} or ".join(place_messages)}"\n Maybe because of forbidden "{f"{nl} or ".join(forbid_messages)}"') - raise Exception(f'Could not find a suitable item to place at "{manual_location["name"]}". \n No items that match "{f"{nl} or ".join(place_messages)}"') - - item_to_place = self.random.choice(eligible_items) - location.place_locked_item(item_to_place) - - # remove the item we're about to place from the pool so it isn't placed twice - remove_specific_item(self.multiworld.itempool, item_to_place) - - - after_generate_basic(self, self.multiworld, self.player) - # Enable this in Meta.json to generate a diagram of your manual. Only works on 0.4.4+ if get_option_value(self.multiworld, self.player, "generate_region_diagram"): from Utils import visualize_regions diff --git a/src/hooks/World.py b/src/hooks/World.py index 930f476b..8ff14426 100644 --- a/src/hooks/World.py +++ b/src/hooks/World.py @@ -100,6 +100,10 @@ def before_create_items_filler(item_pool: list, world: World, multiworld: MultiW # location.place_locked_item(item_to_place) # remove_specific_item(item_pool, item_to_place) +# The item pool before place_item(_category) are processed, in case you want to see the raw item pool at that stage +def before_create_items_place_items(item_pool: list, world: World, multiworld: MultiWorld, player: int) -> list: + return item_pool + # The complete item pool prior to being set for generation is provided here, in case you want to make changes to it def after_create_items(item_pool: list, world: World, multiworld: MultiWorld, player: int) -> list: return item_pool @@ -140,10 +144,6 @@ def after_create_item(item: ManualItem, world: World, multiworld: MultiWorld, pl def before_generate_basic(world: World, multiworld: MultiWorld, player: int): pass -# This method is run at the very end of pre-generation, once the place_item options have been handled and before AP generation occurs -def after_generate_basic(world: World, multiworld: MultiWorld, player: int): - pass - # This method is run every time an item is added to the state, can be used to modify the value of an item. # IMPORTANT! Any changes made in this hook must be cancelled/undone in after_remove_item def after_collect_item(world: World, state: CollectionState, Changed: bool, item: Item): From abb05a0096de84fda5ed83a748b75ec1c1580fac Mon Sep 17 00:00:00 2001 From: nicopop <6759630+nicopop@users.noreply.github.com> Date: Thu, 9 Apr 2026 15:24:41 -0400 Subject: [PATCH 2/8] add tests item/locations for every properties --- src/data/categories.json | 12 ++++++++ src/data/items.json | 56 ++++++++++++++++++++++++++++++++++- src/data/locations.json | 64 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 130 insertions(+), 2 deletions(-) diff --git a/src/data/categories.json b/src/data/categories.json index bb0dbf14..c6d60ebf 100644 --- a/src/data/categories.json +++ b/src/data/categories.json @@ -5,5 +5,17 @@ }, "Example Yaml-option category": { "yaml_option": ["DLC_enabled"] + }, + "For_Tests_And_Examples": { + "hidden": true, + "yaml_option": ["Tests_content"] + }, + "place_item_category_test": { + "hidden": true, + "yaml_option": ["Tests_content"] + }, + "dont_place_item_category_test": { + "hidden": true, + "yaml_option": ["Tests_content"] } } diff --git a/src/data/items.json b/src/data/items.json index 06ebf7be..d9b823dd 100644 --- a/src/data/items.json +++ b/src/data/items.json @@ -4,7 +4,8 @@ { "name": "Item Name for OptionCount Example", "count": 5, - "progression": true + "progression": true, + "category": ["For_Tests_And_Examples"] }, { "name": "Jill", @@ -460,6 +461,59 @@ ], "progression": true, "sort-key": "row-6-8" + }, + { + "name": "Test item A", + "id": 200, + "_comment": "This and the following items are there to make sure we tests everything against Archipelago's Unit tests", + "progression": true, + "category": ["For_Tests_And_Examples", "place_item_category_test", "dont_place_item_category_test"] + }, + { + "name": "Test item B", + "progression": true, + "category": ["For_Tests_And_Examples", "place_item_category_test", "dont_place_item_category_test"] + }, + { + "name": "Test item C", + "progression": true, + "category": ["For_Tests_And_Examples", "place_item_category_test"] + }, + { + "name": "Test item D", + "progression": true, + "category": ["For_Tests_And_Examples", "place_item_category_test"] + }, + { + "name": "Test Local item", + "local": true, + "progression": true, + "category": ["For_Tests_And_Examples", "place_item_category_test"] + }, + { + "name": "Test early item A", + "early": true, + "progression": true, + "category": ["For_Tests_And_Examples", "place_item_category_test", "dont_place_item_category_test"] + }, + { + "name": "Test early item B", + "early": 1, + "progression": true, + "category": ["For_Tests_And_Examples", "place_item_category_test", "dont_place_item_category_test"] + }, + { + "name": "Test local-early item A", + "local_early": true, + "progression": true, + "category": ["For_Tests_And_Examples", "place_item_category_test", "dont_place_item_category_test"] + }, + { + "name": "Test local-early item B", + "local_early": 1, + "progression": true, + "category": ["For_Tests_And_Examples", "place_item_category_test", "dont_place_item_category_test"] } + ] } diff --git a/src/data/locations.json b/src/data/locations.json index 48aa476c..bb19c334 100644 --- a/src/data/locations.json +++ b/src/data/locations.json @@ -3,7 +3,8 @@ "data": [ { "name": "Example_Range decide the amount of items required for this location", - "requires": "{OptionCount(Item Name for OptionCount Example, Example_Range)}" + "requires": "{OptionCount(Item Name for OptionCount Example, Example_Range)}", + "category": ["For_Tests_And_Examples"] }, { "name": "Beat the Game - Ryu", @@ -282,6 +283,67 @@ "victory": true, "category": ["All Characters Complete!"], "requires": "{ItemValue(star:10)} and {ItemValue(coins:10)}" + }, + + { + "name": "Test dont_place_item alone", + "id": 200, + "requires": "", + "dont_place_item": ["Test item A"], + "category": ["For_Tests_And_Examples"] + }, + { + "name": "Test place_item alone", + "requires": "", + "place_item": ["Test item A"], + "category": ["For_Tests_And_Examples"] + }, + { + "name": "Test dont_place_item_category alone", + "requires": "", + "dont_place_item_category": ["place_item_category_test"], + "category": ["For_Tests_And_Examples"] + }, + { + "name": "Test place_item_category alone", + "requires": "", + "place_item_category": ["place_item_category_test"], + "category": ["For_Tests_And_Examples"] + }, + { + "name": "Test place_item_category + dont_place_item", + "place_item_category": ["place_item_category_test"], + "dont_place_item": ["Test item B"], + "requires": "", + "category": ["For_Tests_And_Examples"] + }, + { + "name": "Test place_item_category + dont_place_item_category", + "place_item_category": ["place_item_category_test"], + "dont_place_item_category": ["dont_place_item_category_test"], + "requires": "", + "category": ["For_Tests_And_Examples"] + }, + { + "name": "Free location for tests items 1", + "_comment": "added those so we don't have more items than locations", + "requires": "", + "category": ["For_Tests_And_Examples"] + }, + { + "name": "Free location for tests items 2", + "requires": "", + "category": ["For_Tests_And_Examples"] + }, + { + "name": "Free location for tests items 3", + "requires": "", + "category": ["For_Tests_And_Examples"] + }, + { + "name": "Free location for tests items 4", + "requires": "", + "category": ["For_Tests_And_Examples"] } ] } From 80ecc66d807391cd0e2ba146abd810e325c9f560 Mon Sep 17 00:00:00 2001 From: nicopop <6759630+nicopop@users.noreply.github.com> Date: Thu, 9 Apr 2026 15:45:14 -0400 Subject: [PATCH 3/8] Moved place_item handling to before starting_items --- src/__init__.py | 94 ++++++++++++++++++++-------------------- src/data/categories.json | 6 +-- src/data/items.json | 2 +- src/hooks/World.py | 8 ++-- 4 files changed, 54 insertions(+), 56 deletions(-) diff --git a/src/__init__.py b/src/__init__.py index 956e7f0b..efcc7fa3 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -27,7 +27,7 @@ from .hooks.World import \ hook_get_filler_item_name, before_create_regions, after_create_regions, \ - before_create_items_all, before_create_items_starting, before_create_items_filler, before_create_items_place_items, after_create_items, \ + before_create_items_all, before_create_items_place_items, before_create_items_starting, before_create_items_filler, after_create_items, \ before_create_item, after_create_item, \ before_set_rules, after_set_rules, \ before_generate_basic, \ @@ -199,53 +199,8 @@ def create_items(self): else: raise Exception(f"Item {name}'s 'local_early' has an invalid value of '{item['local_early']}'. \nA boolean or an integer was expected.") - - pool = before_create_items_starting(pool, self, self.multiworld, self.player) - - items_started: list[Item] = [] - - if starting_items: - for starting_item_block in starting_items: - if not resolve_yaml_option(self.multiworld, self.player, starting_item_block): - continue - # if there's a condition on having a previous item, check for any of them - # if not found in items started, this starting item rule shouldn't execute, and check the next one - if "if_previous_item" in starting_item_block: - matching_items = [item for item in items_started if item.name in starting_item_block["if_previous_item"]] - - if len(matching_items) == 0: - continue - - # start with the full pool of items - items = pool - - # if the setting lists specific item names, limit the items to just those - if "items" in starting_item_block: - items = [item for item in pool if item.name in starting_item_block["items"]] - - # if the setting lists specific item categories, limit the items to ones that have any of those categories - if "item_categories" in starting_item_block: - items_in_categories = [item["name"] for item in self.item_name_to_item.values() if "category" in item and len(set(starting_item_block["item_categories"]).intersection(item["category"])) > 0] - items = [item for item in pool if item.name in items_in_categories] - - self.random.shuffle(items) - - # if the setting lists a specific number of random items that should be pulled, only use a subset equal to that number - if "random" in starting_item_block: - items = items[0:starting_item_block["random"]] - - for starting_item in items: - items_started.append(starting_item) - self.multiworld.push_precollected(starting_item) - remove_specific_item(pool, starting_item) - - self.start_inventory = {i.name: items_started.count(i) for i in items_started} - - pool = before_create_items_filler(pool, self, self.multiworld, self.player) - pool = self.adjust_filler_items(pool, traps) - - pool = before_create_items_place_items(pool, self, self.multiworld, self.player) # Handle item forbidding/placement + pool = before_create_items_place_items(pool, self, self.multiworld, self.player) manual_locations_with_placements: dict[str, dict[str, Any]] = {} manual_locations_with_forbid: dict[str, dict[str, Any]] = {} for name, l in location_name_to_location.items(): @@ -321,6 +276,51 @@ def create_items(self): # remove the item we're about to place from the pool so it isn't placed twice remove_specific_item(pool, item_to_place) + # Handle game.json's starting_items + pool = before_create_items_starting(pool, self, self.multiworld, self.player) + + items_started: list[Item] = [] + + if starting_items: + for starting_item_block in starting_items: + if not resolve_yaml_option(self.multiworld, self.player, starting_item_block): + continue + # if there's a condition on having a previous item, check for any of them + # if not found in items started, this starting item rule shouldn't execute, and check the next one + if "if_previous_item" in starting_item_block: + matching_items = [item for item in items_started if item.name in starting_item_block["if_previous_item"]] + + if len(matching_items) == 0: + continue + + # start with the full pool of items + items = pool + + # if the setting lists specific item names, limit the items to just those + if "items" in starting_item_block: + items = [item for item in pool if item.name in starting_item_block["items"]] + + # if the setting lists specific item categories, limit the items to ones that have any of those categories + if "item_categories" in starting_item_block: + items_in_categories = [item["name"] for item in self.item_name_to_item.values() if "category" in item and len(set(starting_item_block["item_categories"]).intersection(item["category"])) > 0] + items = [item for item in pool if item.name in items_in_categories] + + self.random.shuffle(items) + + # if the setting lists a specific number of random items that should be pulled, only use a subset equal to that number + if "random" in starting_item_block: + items = items[0:starting_item_block["random"]] + + for starting_item in items: + items_started.append(starting_item) + self.multiworld.push_precollected(starting_item) + remove_specific_item(pool, starting_item) + + self.start_inventory = {i.name: items_started.count(i) for i in items_started} + + pool = before_create_items_filler(pool, self, self.multiworld, self.player) + pool = self.adjust_filler_items(pool, traps) + pool = after_create_items(pool, self, self.multiworld, self.player) # need to put all of the items in the pool so we can have a full state for placement diff --git a/src/data/categories.json b/src/data/categories.json index c6d60ebf..72b6b6ae 100644 --- a/src/data/categories.json +++ b/src/data/categories.json @@ -11,11 +11,9 @@ "yaml_option": ["Tests_content"] }, "place_item_category_test": { - "hidden": true, - "yaml_option": ["Tests_content"] + "hidden": true }, "dont_place_item_category_test": { - "hidden": true, - "yaml_option": ["Tests_content"] + "hidden": true } } diff --git a/src/data/items.json b/src/data/items.json index d9b823dd..24867293 100644 --- a/src/data/items.json +++ b/src/data/items.json @@ -467,7 +467,7 @@ "id": 200, "_comment": "This and the following items are there to make sure we tests everything against Archipelago's Unit tests", "progression": true, - "category": ["For_Tests_And_Examples", "place_item_category_test", "dont_place_item_category_test"] + "category": ["For_Tests_And_Examples", "dont_place_item_category_test"] }, { "name": "Test item B", diff --git a/src/hooks/World.py b/src/hooks/World.py index 8ff14426..384f7b73 100644 --- a/src/hooks/World.py +++ b/src/hooks/World.py @@ -72,6 +72,10 @@ def after_create_regions(world: World, multiworld: MultiWorld, player: int): def before_create_items_all(item_config: dict[str, int|dict], world: World, multiworld: MultiWorld, player: int) -> dict[str, int|dict]: return item_config +# The item pool before place_item(_category) are processed, in case you want to see the raw item pool at that stage +def before_create_items_place_items(item_pool: list, world: World, multiworld: MultiWorld, player: int) -> list: + return item_pool + # The item pool before starting items are processed, in case you want to see the raw item pool at that stage def before_create_items_starting(item_pool: list, world: World, multiworld: MultiWorld, player: int) -> list: return item_pool @@ -100,10 +104,6 @@ def before_create_items_filler(item_pool: list, world: World, multiworld: MultiW # location.place_locked_item(item_to_place) # remove_specific_item(item_pool, item_to_place) -# The item pool before place_item(_category) are processed, in case you want to see the raw item pool at that stage -def before_create_items_place_items(item_pool: list, world: World, multiworld: MultiWorld, player: int) -> list: - return item_pool - # The complete item pool prior to being set for generation is provided here, in case you want to make changes to it def after_create_items(item_pool: list, world: World, multiworld: MultiWorld, player: int) -> list: return item_pool From 9391e214843320f405bda3eb4d272a96f7be2edd Mon Sep 17 00:00:00 2001 From: nicopop <6759630+nicopop@users.noreply.github.com> Date: Thu, 9 Apr 2026 15:47:12 -0400 Subject: [PATCH 4/8] moved local item handling to generate_early --- src/__init__.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/__init__.py b/src/__init__.py index efcc7fa3..1f8e02b8 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -91,6 +91,11 @@ def stage_assert_generate(cls, multiworld) -> None: def generate_early(self) -> None: before_generate_early(self, self.multiworld, self.player) + for item in item_table: + if item.get("local"): + if item.get("name") not in self.options.local_items.value: + self.options.local_items.value.add(item["name"]) + if hasattr(self.multiworld, "re_gen_passthrough"): slot_data = self.multiworld.re_gen_passthrough.get(self.game, {}) if slot_data: @@ -185,10 +190,6 @@ def create_items(self): else: raise Exception(f"Item {name}'s 'early' has an invalid value of '{item['early']}'. \nA boolean or an integer was expected.") - if item.get("local"): # All local - if name not in self.options.local_items.value: - self.options.local_items.value.add(name) - if item.get("local_early"): # Some or all local and early if isinstance(item["local_early"],int) or (isinstance(item["local_early"],str) and item["local_early"].isnumeric()): self.multiworld.local_early_items[self.player][name] = int(item["local_early"]) From f6ef25e24091abcc93ceac38ebc002f4c51094b4 Mon Sep 17 00:00:00 2001 From: nicopop <6759630+nicopop@users.noreply.github.com> Date: Thu, 9 Apr 2026 16:04:33 -0400 Subject: [PATCH 5/8] Cleanup of comments --- src/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/__init__.py b/src/__init__.py index 1f8e02b8..08aa75d0 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -321,24 +321,24 @@ def create_items(self): pool = before_create_items_filler(pool, self, self.multiworld, self.player) pool = self.adjust_filler_items(pool, traps) - pool = after_create_items(pool, self, self.multiworld, self.player) # need to put all of the items in the pool so we can have a full state for placement - # then will remove specific item placements below from the overall pool self.multiworld.itempool += pool - # Filter Precollected items for those not in logic aka created by start_inventory(_from_pool) + # Preparing to count the items: precollected_items = list(self.multiworld.precollected_items[self.player]) # UT doesn't precollect the exceptions so this can be skipped if not hasattr(self.multiworld, "generation_is_fake"): + # Filter Precollected items for those not in logic aka created by start_inventory(_from_pool) precollected_exceptions = self.options.start_inventory.value + self.options.start_inventory_from_pool.value # type: ignore for item, count in precollected_exceptions.items(): items_iter = iter([i for i in precollected_items if i.name == item]) for _ in range(count): precollected_items.remove(next(items_iter)) - # Placed items: + + # Placed items detections: placed_pool: list[Item] = [] for location in self.multiworld.get_filled_locations(self.player): placed_pool.append(location.item) From a2a2f49f5ce0a1e2fb99e58c0f661caa540126c0 Mon Sep 17 00:00:00 2001 From: nicopop <6759630+nicopop@users.noreply.github.com> Date: Thu, 9 Apr 2026 17:02:25 -0400 Subject: [PATCH 6/8] remove unneeded looping in item forbidding/placement --- src/__init__.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/__init__.py b/src/__init__.py index 08aa75d0..24496e26 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -220,13 +220,14 @@ def create_items(self): # Handle specific item forbidding using forbid_items_for_player for location in locations_with_forbid: manual_location = manual_locations_with_forbid[location.name] - forbidden_item_names = [] + forbidden_item_names: set[str] = set() if manual_location.get("dont_place_item"): - forbidden_item_names.extend([i["name"] for i in item_name_to_item.values() if i["name"] in manual_location["dont_place_item"]]) + forbidden_item_names |= set(manual_location["dont_place_item"]) if manual_location.get("dont_place_item_category"): - forbidden_item_names.extend([i["name"] for i in item_name_to_item.values() if "category" in i and set(i["category"]).intersection(manual_location["dont_place_item_category"])]) + for cat in manual_location["dont_place_item_category"]: + forbidden_item_names |= self.item_name_groups.get(cat, set()) if forbidden_item_names: forbid_items_for_player(location, set(forbidden_item_names), self.player) @@ -235,32 +236,34 @@ def create_items(self): for location in locations_with_placements: manual_location = manual_locations_with_placements[location.name] eligible_items = [] - eligible_item_names = [] - forbidden_item_names = [] + eligible_item_names: set[str] = set() + forbidden_item_names: set[str] = set() place_messages = [] forbid_messages = [] #First we get possible items names if manual_location.get("place_item"): - eligible_item_names += manual_location["place_item"] + eligible_item_names |= set(manual_location["place_item"]) place_messages.append('", "'.join(manual_location["place_item"])) if manual_location.get("place_item_category"): - eligible_item_names += [i["name"] for i in item_name_to_item.values() if "category" in i and set(i["category"]).intersection(manual_location["place_item_category"])] + for cat in manual_location["place_item_category"]: + eligible_item_names |= self.item_name_groups.get(cat, set()) place_messages.append('", "'.join(manual_location["place_item_category"]) + " category(ies)") # Second we check for forbidden items names if manual_location.get("dont_place_item"): - forbidden_item_names += manual_location["dont_place_item"] + forbidden_item_names |= set(manual_location["dont_place_item"]) forbid_messages.append('", "'.join(manual_location["dont_place_item"]) + ' items') if manual_location.get("dont_place_item_category"): - forbidden_item_names += [i["name"] for i in item_name_to_item.values() if "category" in i and set(i["category"]).intersection(manual_location["dont_place_item_category"])] + for cat in manual_location["dont_place_item_category"]: + forbidden_item_names |= self.item_name_groups.get(cat, set()) forbid_messages.append('", "'.join(manual_location["dont_place_item_category"]) + ' category(ies)') # If we forbid some names, check for those in the possible names and remove them if forbidden_item_names: - eligible_item_names = [name for name in eligible_item_names if name not in forbidden_item_names] + eligible_item_names = {name for name in eligible_item_names if name not in forbidden_item_names} if eligible_item_names: eligible_items = [item for item in pool if item.name in eligible_item_names] From eacdbab3e3c208bdc01a2d404806ff872c511c64 Mon Sep 17 00:00:00 2001 From: nicopop <6759630+nicopop@users.noreply.github.com> Date: Thu, 9 Apr 2026 17:33:19 -0400 Subject: [PATCH 7/8] remove another unneeded loop --- src/__init__.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/__init__.py b/src/__init__.py index 24496e26..b9db0f5b 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -202,24 +202,18 @@ def create_items(self): # Handle item forbidding/placement pool = before_create_items_place_items(pool, self, self.multiworld, self.player) - manual_locations_with_placements: dict[str, dict[str, Any]] = {} - manual_locations_with_forbid: dict[str, dict[str, Any]] = {} - for name, l in location_name_to_location.items(): - if l.get("place_item") or l.get("place_item_category"): - manual_locations_with_placements[name] = l - elif l.get("dont_place_item") or l.get("dont_place_item_category"): - manual_locations_with_forbid[name] = l locations_with_forbid: list[Location] = [] locations_with_placements: list[Location] = [] for location in self.multiworld.get_unfilled_locations(player=self.player): - if location.name in manual_locations_with_placements.keys(): + manual_location = self.location_name_to_location.get(location.name, {}) + if manual_location.get("place_item") or manual_location.get("place_item_category"): locations_with_placements.append(location) - elif location.name in manual_locations_with_forbid.keys(): + elif manual_location.get("dont_place_item") or manual_location.get("dont_place_item_category"): locations_with_forbid.append(location) # Handle specific item forbidding using forbid_items_for_player for location in locations_with_forbid: - manual_location = manual_locations_with_forbid[location.name] + manual_location = self.location_name_to_location.get(location.name, {}) forbidden_item_names: set[str] = set() if manual_location.get("dont_place_item"): From 9b6d49547a8027320d770e4d40db407f328894f5 Mon Sep 17 00:00:00 2001 From: nicopop <6759630+nicopop@users.noreply.github.com> Date: Thu, 9 Apr 2026 17:43:41 -0400 Subject: [PATCH 8/8] accidentally didn't stage a line oops --- src/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/__init__.py b/src/__init__.py index b9db0f5b..10a74193 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -228,7 +228,7 @@ def create_items(self): # Handle specific item placements using place_locked_item for location in locations_with_placements: - manual_location = manual_locations_with_placements[location.name] + manual_location = self.location_name_to_location.get(location.name, {}) eligible_items = [] eligible_item_names: set[str] = set() forbidden_item_names: set[str] = set()