diff --git a/schemas/Manual.items.schema.json b/schemas/Manual.items.schema.json index 0298c05f..ab054e4b 100644 --- a/schemas/Manual.items.schema.json +++ b/schemas/Manual.items.schema.json @@ -112,6 +112,15 @@ "description": "(Optional) Skips the item ID forward to the given value.\nThis can be used to provide buffer space for future items.", "type": "integer" }, + "yaml_option": { + "description": "(Optional) Array of Options that will decide if this item is enabled", + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "uniqueItems": true + }, "sort-key": { "description": "(Optional) A string to sort the items by. If not provided, items will always be sorted by name.", "type": "string" diff --git a/schemas/Manual.locations.schema.json b/schemas/Manual.locations.schema.json index 829539df..619337d5 100644 --- a/schemas/Manual.locations.schema.json +++ b/schemas/Manual.locations.schema.json @@ -100,6 +100,15 @@ "description": "(Optional) Skips the item ID forward to the given value.\nThis can be used to provide buffer space for future items.", "type": "integer" }, + "yaml_option": { + "description": "(Optional) Array of Options that will decide if this location is enabled", + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "uniqueItems": true + }, "sort-key": { "description": "(Optional) A string to sort the locations by. If not provided, locations will always be sorted by name.", "type": "string" diff --git a/src/Helpers.py b/src/Helpers.py index 3e8c51d6..41a6b0c3 100644 --- a/src/Helpers.py +++ b/src/Helpers.py @@ -5,6 +5,7 @@ import re from BaseClasses import MultiWorld, Item, ItemClassification +from Options import Option from enum import IntEnum from typing import Optional, List, Union, get_args, get_origin, Any from types import GenericAlias @@ -53,26 +54,57 @@ def clamp(value, min, max): return value def is_category_enabled(multiworld: MultiWorld, player: int, category_name: str) -> bool: - from .Data import category_table """Check if a category has been disabled by a yaml option.""" hook_result = before_is_category_enabled(multiworld, player, category_name) if hook_result is not None: return hook_result - category_data = category_table.get(category_name, {}) + category_data = multiworld.worlds[player].category_table.get(category_name, {}) return resolve_yaml_option(multiworld, player, category_data) def resolve_yaml_option(multiworld: MultiWorld, player: int, data: dict) -> bool: if "yaml_option" in data: for option_name in data["yaml_option"]: - required = True + eval_1 = lambda x, t: x.value + target = "1" + if "<=" in option_name: + option_name, target = option_name.split("<=") + eval_1 = lambda x, t: x.value <= t + elif ">=" in option_name: + option_name, target = option_name.split(">=") + eval_1 = lambda x, t: x.value >= t + elif "!=" in option_name: + option_name, target = option_name.split("!=") + eval_1 = lambda x, t: x.value != t + elif "==" in option_name: + option_name, target = option_name.split("==") + eval_1 = lambda x, t: x.value == t + elif "<" in option_name: + option_name, target = option_name.split("<") + eval_1 = lambda x, t: x.value < t + elif ">" in option_name: + option_name, target = option_name.split(">") + eval_1 = lambda x, t: x.value > t + elif "=" in option_name: + option_name, target = option_name.split("=") + eval_1 = lambda x, t: x.value == t if option_name.startswith("!"): option_name = option_name[1:] - required = False + eval_2 = lambda x, t: not eval_1(x, t) + else: + eval_2 = eval_1 option_name = format_to_valid_identifier(option_name) - if is_option_enabled(multiworld, player, option_name) != required: + option: Option | None = getattr(multiworld.worlds[player].options, option_name, None) + if option is None: + raise ValueError(f"option {option_name} is myspelt or invalid") + try: + target_eval = int(target) + except ValueError: + target_eval = option.options[target] + if not eval_2(option, target_eval): return False + return True return True def is_item_name_enabled(multiworld: MultiWorld, player: int, item_name: str) -> bool: @@ -111,13 +143,15 @@ def _is_manualobject_enabled(multiworld: MultiWorld, player: int, object: dict[s """Internal method: Check if a Manual Object has any category disabled by a yaml option. \nPlease use the proper is_'item/location'_enabled or is_'item/location'_name_enabled methods instead. """ - enabled = True for category in object.get("category", []): - if not is_category_enabled(multiworld, player, category): - enabled = False - break + resolve = is_category_enabled(multiworld, player, category) + if resolve == False: + return False - return enabled + if object.get("yaml_option") and not resolve_yaml_option(multiworld, player, object): + return False + + return True def get_items_for_player(multiworld: MultiWorld, player: int, includePrecollected: bool = False) -> List[Item]: """Return list of items of a player including placed items""" diff --git a/src/Options.py b/src/Options.py index 896763d9..41265053 100644 --- a/src/Options.py +++ b/src/Options.py @@ -203,6 +203,15 @@ def addOptionToGroup(option_name: str, group: str): for category in category_table: for option_name in category_table[category].get("yaml_option", []): + skip = False + for c in "><=": # Range and Choice options must be defined using Options.json + if c in option_name: + skip = True + option_base = option_name.split(c)[0].strip("!") + if option_base not in manual_options: + raise Exception(f"Option {option_base} was referenced in Category.json, but Range and Choice type options must be defined in Options.json first") + if skip: + continue if option_name[0] == "!": option_name = option_name[1:] option_name = format_to_valid_identifier(option_name) @@ -214,6 +223,15 @@ def addOptionToGroup(option_name: str, group: str): for starting_items in starting_items: if starting_items.get("yaml_option"): for option_name in starting_items["yaml_option"]: + skip = False + for c in "><=": # Range and Choice options must be defined using Options.json + if c in option_name: + skip = True + option_base = option_name.split(c)[0].strip("!") + if option_base not in manual_options: + raise Exception(f"Option {option_base} was referenced in starting items, but Range and Choice type options must be defined in Options.json first") + if skip: + continue if option_name[0] == "!": option_name = option_name[1:] option_name = format_to_valid_identifier(option_name) diff --git a/src/data/items.json b/src/data/items.json index 06ebf7be..1a9b881f 100644 --- a/src/data/items.json +++ b/src/data/items.json @@ -53,6 +53,7 @@ "Left Side" ], "progression": true, + "yaml_option": ["!Example_Toggle"], "sort-key": "row-1-3" }, { @@ -62,6 +63,7 @@ "Left Side" ], "progression": true, + "yaml_option": ["Example_Choice=start"], "sort-key": "row-1-4" }, { @@ -71,6 +73,7 @@ "Right Side" ], "progression": true, + "yaml_option": ["Example_Range<5"], "sort-key": "row-1-5" }, { diff --git a/src/data/locations.json b/src/data/locations.json index 0d8c939d..0437ddb2 100644 --- a/src/data/locations.json +++ b/src/data/locations.json @@ -45,17 +45,20 @@ { "name": "Beat the Game - Strider Hiryu", "category": ["Unlocked Teams", "Left Side"], - "requires": "|Strider Hiryu|" + "requires": "|Strider Hiryu|", + "yaml_option": ["!Example_Toggle"] }, { "name": "Beat the Game - Phoenix Wright", "category": ["Unlocked Teams", "Left Side"], - "requires": "|Phoenix Wright|" + "requires": "|Phoenix Wright|", + "yaml_option": ["Example_Choice=start"] }, { "name": "Beat the Game - Nova", "category": ["Unlocked Teams", "Right Side"], - "requires": "|Nova|" + "requires": "|Nova|", + "yaml_option": ["Example_Range<5"] }, { "name": "Beat the Game - Ghost Rider",