From e5fc3271e06fd808174195286898a18cf80f8011 Mon Sep 17 00:00:00 2001 From: nicopop <6759630+nicopop@users.noreply.github.com> Date: Mon, 2 Feb 2026 15:16:22 -0500 Subject: [PATCH 1/5] remove duplicate item_count code --- src/Rules.py | 57 +++++++++++++++++++++------------------------------- 1 file changed, 23 insertions(+), 34 deletions(-) diff --git a/src/Rules.py b/src/Rules.py index ddbf007..5f0464c 100644 --- a/src/Rules.py +++ b/src/Rules.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Optional, Any from enum import IntEnum from operator import eq, ge, le @@ -178,43 +178,32 @@ def findAndRecursivelyExecuteFunctions(requires_list: str, recursionDepth: int = total = 0 if require_type == 'category': - category_items = [item for item in world.item_name_to_item.values() if "category" in item and item_name in item["category"]] - category_items += [event for event in world.event_name_to_event.values() if "category" in event and item_name in event["category"]] - category_items_counts = sum([items_counts.get(category_item["name"], 0) for category_item in category_items]) - if item_count.lower() == 'all': - item_count = category_items_counts - elif item_count.lower() == 'half': - item_count = int(category_items_counts / 2) - elif item_count.endswith('%') and len(item_count) > 1: - percent = clamp(float(item_count[:-1]) / 100, 0, 1) - item_count = math.ceil(category_items_counts * percent) - else: - try: - item_count = int(item_count) - except ValueError as e: - raise ValueError(f"Invalid item count `{item_name}` in {area}.") from e - - for category_item in category_items: - total += state.count(category_item["name"], player) - - if total >= item_count: - requires_list = requires_list.replace(item_base, "1") - elif require_type == 'item': - item_current_count = items_counts.get(item_name, 0) - if item_count.lower() == 'all': - item_count = item_current_count - elif item_count.lower() == 'half': - item_count = int(item_current_count / 2) - elif item_count.endswith('%') and len(item_count) > 1: - percent = clamp(float(item_count[:-1]) / 100, 0, 1) - item_count = math.ceil(item_current_count * percent) - else: - item_count = int(item_count) + valid_items: list[dict[str, Any]] = [item for item in world.item_name_to_item.values() if "category" in item and item_name in item["category"]] + valid_items += [event for event in world.event_name_to_event.values() if "category" in event and item_name in event["category"]] + else: + valid_items = [world.item_name_to_item[item_name]] + + item_current_count = sum([items_counts.get(valid_item["name"], 0) for valid_item in valid_items]) - total = state.count(item_name, player) + if item_count.lower() == 'all': + item_count = item_current_count + elif item_count.lower() == 'half': + item_count = int(item_current_count / 2) + elif item_count.endswith('%') and len(item_count) > 1: + percent = clamp(float(item_count[:-1]) / 100, 0, 1) + item_count = math.ceil(item_current_count * percent) + + try: + item_count = int(item_count) + except ValueError as e: + raise ValueError(f"Invalid item count `{item_name}` in {area}.") from e + + for valid_item in valid_items: + total += state.count(valid_item["name"], player) if total >= item_count: requires_list = requires_list.replace(item_base, "1") + break if total <= item_count: requires_list = requires_list.replace(item_base, "0") From f2388ba6b8dd6e7c4d3f6f6ca036fe53885c19fb Mon Sep 17 00:00:00 2001 From: nicopop <6759630+nicopop@users.noreply.github.com> Date: Mon, 2 Feb 2026 15:26:00 -0500 Subject: [PATCH 2/5] Misc small changes --- src/Rules.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Rules.py b/src/Rules.py index 5f0464c..9d7b401 100644 --- a/src/Rules.py +++ b/src/Rules.py @@ -158,17 +158,20 @@ def findAndRecursivelyExecuteFunctions(requires_list: str, recursionDepth: int = # parse user written statement into list of each item for item in re.findall(r'\|[^|]+\|', requires_list): - require_type = 'item' + if item not in requires_list: + # previous instance of this item was already processed + continue + require_category = False if '|@' in item: - require_type = 'category' + require_category = True item_base = item item = item.lstrip('|@$').rstrip('|') item_parts = item.split(":") # type: list[str] item_name = item - item_count = "1" + item_count: str | int = "1" if len(item_parts) > 1: @@ -177,7 +180,7 @@ def findAndRecursivelyExecuteFunctions(requires_list: str, recursionDepth: int = total = 0 - if require_type == 'category': + if require_category: valid_items: list[dict[str, Any]] = [item for item in world.item_name_to_item.values() if "category" in item and item_name in item["category"]] valid_items += [event for event in world.event_name_to_event.values() if "category" in event and item_name in event["category"]] else: @@ -205,7 +208,7 @@ def findAndRecursivelyExecuteFunctions(requires_list: str, recursionDepth: int = requires_list = requires_list.replace(item_base, "1") break - if total <= item_count: + if total < item_count: requires_list = requires_list.replace(item_base, "0") requires_list = re.sub(r'\s?\bAND\b\s?', '&', requires_list, count=0, flags=re.IGNORECASE) From 65805029abfe1368b5f17c7a3338b42ff273d50a Mon Sep 17 00:00:00 2001 From: nicopop <6759630+nicopop@users.noreply.github.com> Date: Mon, 2 Feb 2026 15:49:17 -0500 Subject: [PATCH 3/5] added some misc type annotation to make mypy happy --- src/Rules.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Rules.py b/src/Rules.py index 9d7b401..fc9081d 100644 --- a/src/Rules.py +++ b/src/Rules.py @@ -45,10 +45,10 @@ def construct_logic_error(location_or_region: dict, source: LogicErrorSource) -> return KeyError(f"Invalid 'requires' for {object_type} '{object_name}': {source_text} (ERROR {source})") -def infix_to_postfix(expr, location): - prec = {"&": 2, "|": 2, "!": 3} - stack = [] - postfix = "" +def infix_to_postfix(expr: str, location: dict) -> str: + prec: dict[str, int] = {"&": 2, "|": 2, "!": 3} + stack: list[str] = [] + postfix: str = "" try: for c in expr: @@ -73,8 +73,8 @@ def infix_to_postfix(expr, location): return postfix -def evaluate_postfix(expr: str, location: str) -> bool: - stack = [] +def evaluate_postfix(expr: str, location: dict) -> bool: + stack: list[bool] = [] try: for c in expr: @@ -167,9 +167,9 @@ def findAndRecursivelyExecuteFunctions(requires_list: str, recursionDepth: int = require_category = True item_base = item - item = item.lstrip('|@$').rstrip('|') + item: str = item.lstrip('|@$').rstrip('|') - item_parts = item.split(":") # type: list[str] + item_parts: list[str] = item.rsplit(":", 1) item_name = item item_count: str | int = "1" @@ -338,7 +338,7 @@ def allRegionsAccessible(state): # Victory requirement multiworld.completion_condition[player] = lambda state: state.has("__Victory__", player) - def convert_req_function_args(state: CollectionState, func, args: list[str], areaName: str): + def convert_req_function_args(state: CollectionState, func, args: list[str| Any], areaName: str): parameters = inspect.signature(func).parameters knownParameters = [World, 'ManualWorld', MultiWorld, CollectionState] index = -1 From b4904c55eba760467a55be88226c3fd25c2b224a Mon Sep 17 00:00:00 2001 From: nicopop <6759630+nicopop@users.noreply.github.com> Date: Mon, 2 Feb 2026 17:10:24 -0500 Subject: [PATCH 4/5] fix getting event items --- src/Rules.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Rules.py b/src/Rules.py index fc9081d..0c1c74c 100644 --- a/src/Rules.py +++ b/src/Rules.py @@ -179,14 +179,14 @@ def findAndRecursivelyExecuteFunctions(requires_list: str, recursionDepth: int = item_count = item_parts[1].strip() total = 0 - + valid_items: list[str] = [] if require_category: - valid_items: list[dict[str, Any]] = [item for item in world.item_name_to_item.values() if "category" in item and item_name in item["category"]] - valid_items += [event for event in world.event_name_to_event.values() if "category" in event and item_name in event["category"]] + valid_items.extend([item["name"] for item in world.item_name_to_item.values() if "category" in item and item_name in item["category"]]) + valid_items.extend([event["name"] for event in world.event_name_to_event.values() if "category" in event and item_name in event["category"]]) else: - valid_items = [world.item_name_to_item[item_name]] + valid_items.append(item_name) - item_current_count = sum([items_counts.get(valid_item["name"], 0) for valid_item in valid_items]) + item_current_count = sum([items_counts.get(valid_item, 0) for valid_item in valid_items]) if item_count.lower() == 'all': item_count = item_current_count @@ -202,7 +202,7 @@ def findAndRecursivelyExecuteFunctions(requires_list: str, recursionDepth: int = raise ValueError(f"Invalid item count `{item_name}` in {area}.") from e for valid_item in valid_items: - total += state.count(valid_item["name"], player) + total += state.count(valid_item, player) if total >= item_count: requires_list = requires_list.replace(item_base, "1") From 252f15593bcab90477132789bc3c7574fbed2560 Mon Sep 17 00:00:00 2001 From: nicopop <6759630+nicopop@users.noreply.github.com> Date: Mon, 2 Feb 2026 18:20:01 -0500 Subject: [PATCH 5/5] let devs use : in item name that can be required --- src/DataValidation.py | 73 +++++++++++++++++++------------------------ src/Rules.py | 9 ++++-- 2 files changed, 38 insertions(+), 44 deletions(-) diff --git a/src/DataValidation.py b/src/DataValidation.py index d9c0970..8775977 100644 --- a/src/DataValidation.py +++ b/src/DataValidation.py @@ -31,35 +31,31 @@ def checkItemNamesInLocationRequires(): if item.lower() == "or" or item.lower() == "and" or item == ")" or item == "(": continue else: - # if it's a category, validate that the category exists - if '@' in item: - item = item.replace("|", "") - item_parts = item.split(":") - item_name = item + is_category = '|@' in item + item: str = item.lstrip('|@').rstrip('|') + item_parts = item.rsplit(":", 1) + item_name = item - if len(item_parts) > 1: - item_name = item_parts[0] + if len(item_parts) > 1: + item_name = item_parts[0] + item_count = item_parts[1] + if not item_count.isnumeric() and item_count not in ["all", "half"] and not item_count.endswith('%'): + logging.debug(f'Invalid item_count "{item_count}" found, reverting to initial item_name "{item}"') + item_name = item - item_name = item_name[1:] + # if it's a category, validate that the category exists + if is_category: item_category_exists = len([item for item in DataValidation.item_table if item_name in item.get('category', [])]) > 0 if not item_category_exists: raise ValidationError("Item category %s is required by location %s but is misspelled or does not exist." % (item_name, location.get("name"))) continue + else: + item_exists = len([item.get("name") for item in DataValidation.item_table_with_events if item.get("name") == item_name]) > 0 - item = item.replace("|", "") - - item_parts = item.split(":") - item_name = item - - if len(item_parts) > 1: - item_name = item_parts[0] - - item_exists = len([item.get("name") for item in DataValidation.item_table_with_events if item.get("name") == item_name]) > 0 - - if not item_exists: - raise ValidationError("Item %s is required by location %s but is misspelled or does not exist." % (item_name, location.get("name"))) + if not item_exists: + raise ValidationError("Item %s is required by location %s but is misspelled or does not exist." % (item_name, location.get("name"))) else: # item access is in dict form for item in location["requires"]: @@ -107,35 +103,30 @@ def checkItemNamesInRegionRequires(): if item.lower() == "or" or item.lower() == "and" or item == ")" or item == "(": continue else: - # if it's a category, validate that the category exists - if '@' in item: - item = item.replace("|", "") - item_parts = item.split(":") - item_name = item + is_category = '|@' in item + item: str = item.lstrip('|@').rstrip('|') + item_parts = item.rsplit(":", 1) + item_name = item - if len(item_parts) > 1: - item_name = item_parts[0] + if len(item_parts) > 1: + item_name = item_parts[0] + item_count = item_parts[1] + if not item_count.isnumeric() and item_count not in ["all", "half"] and not item_count.endswith('%'): + logging.debug(f'Invalid item_count "{item_count}" found, reverting to initial item_name "{item}"') + item_name = item - item_name = item_name[1:] + # if it's a category, validate that the category exists + if is_category: item_category_exists = len([item for item in DataValidation.item_table if item_name in item.get('category', [])]) > 0 if not item_category_exists: raise ValidationError("Item category %s is required by region %s but is misspelled or does not exist." % (item_name, region_name)) - continue - - item = item.replace("|", "") - - item_parts = item.split(":") - item_name = item + else: + item_exists = len([item.get("name") for item in DataValidation.item_table_with_events if item.get("name") == item_name]) > 0 - if len(item_parts) > 1: - item_name = item_parts[0] - - item_exists = len([item.get("name") for item in DataValidation.item_table_with_events if item.get("name") == item_name]) > 0 - - if not item_exists: - raise ValidationError("Item %s is required by region %s but is misspelled or does not exist." % (item_name, region_name)) + if not item_exists: + raise ValidationError("Item %s is required by region %s but is misspelled or does not exist." % (item_name, region_name)) else: # item access is in dict form for item in region["requires"]: diff --git a/src/Rules.py b/src/Rules.py index 0c1c74c..bb844f5 100644 --- a/src/Rules.py +++ b/src/Rules.py @@ -162,9 +162,7 @@ def findAndRecursivelyExecuteFunctions(requires_list: str, recursionDepth: int = # previous instance of this item was already processed continue - require_category = False - if '|@' in item: - require_category = True + require_category = '|@' in item item_base = item item: str = item.lstrip('|@$').rstrip('|') @@ -178,6 +176,11 @@ def findAndRecursivelyExecuteFunctions(requires_list: str, recursionDepth: int = item_name = item_parts[0].strip() item_count = item_parts[1].strip() + # If invalid count assume its actually part of the item name + if not item_count.isnumeric() and item_count not in ["all", "half"] and not item_count.endswith('%'): + item_name = item + item_count = "1" + total = 0 valid_items: list[str] = [] if require_category: