Skip to content
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions schemas/Manual.items.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,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
},
"_comment": {"$ref": "#/definitions/comment"}
},
"required": ["name"]
Expand Down
9 changes: 9 additions & 0 deletions schemas/Manual.locations.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
},
"_comment": {"$ref": "#/definitions/comment"}
}
},
Expand Down
75 changes: 55 additions & 20 deletions src/Helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import os
import pkgutil
import json
from copy import deepcopy

from BaseClasses import MultiWorld, Item
from typing import Optional, List, TYPE_CHECKING, Union, get_args, get_origin
Expand Down Expand Up @@ -55,27 +56,54 @@ 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, {})
return resolve_yaml_option(multiworld, player, category_data)
category_data = multiworld.worlds[player].category_table.get(category_name, {})
Copy link
Collaborator

@FuzzyGamesOn FuzzyGamesOn Jan 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was wondering why you referenced it this way and was reminded once again that the is_*_enabled hooks don't pass in world like all the other hooks, lol

(Just an observation. This way is fine, and the hooks can be standardized later.)

resolve_option = resolve_yaml_option(multiworld, player, category_data)
return resolve_option or resolve_option is None

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
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 = getattr(multiworld.worlds[player].options, option_name, None)
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
return None

def is_item_name_enabled(multiworld: MultiWorld, player: int, item_name: str) -> bool:
"""Check if an item named 'item_name' has been disabled by a yaml option."""
Expand All @@ -85,13 +113,17 @@ def is_item_name_enabled(multiworld: MultiWorld, player: int, item_name: str) ->

return is_item_enabled(multiworld, player, item)

def is_item_enabled(multiworld: MultiWorld, player: int, item: "ManualItem") -> bool:
def is_item_enabled(multiworld: MultiWorld, player: int, item: dict) -> bool:
"""Check if an item has been disabled by a yaml option."""
hook_result = before_is_item_enabled(multiworld, player, item)
if hook_result is not None:
return hook_result

return _is_manualobject_enabled(multiworld, player, item)

try_resolve = resolve_yaml_option(multiworld, player, item)
if try_resolve is None:
return _is_manualobject_enabled(multiworld, player, item)
else:
return try_resolve
Comment on lines +124 to +129
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if I understand this part correctly if the item says its enabled via your new yaml_option then categories are not checked at all is that what you mean to do?
is that something we want?
idk but its something to think of

Copy link
Contributor

@nicopop nicopop Apr 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually there's a reason I originaly just put return _is_manualobject_enabled since the logic is the same for both loc and item maybe you could move your try_resolve call there so you dont have 2 copy of this

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if I understand this part correctly if the item says its enabled via your new yaml_option then categories are not checked at all is that what you mean to do?

Hmm. I could probably put an AND in there instead


def is_location_name_enabled(multiworld: MultiWorld, player: int, location_name: str) -> bool:
"""Check if a location named 'location_name' has been disabled by a yaml option."""
Expand All @@ -101,25 +133,28 @@ def is_location_name_enabled(multiworld: MultiWorld, player: int, location_name:

return is_location_enabled(multiworld, player, location)

def is_location_enabled(multiworld: MultiWorld, player: int, location: "ManualLocation") -> bool:
def is_location_enabled(multiworld: MultiWorld, player: int, location: dict) -> bool:
"""Check if a location has been disabled by a yaml option."""
hook_result = before_is_location_enabled(multiworld, player, location)
if hook_result is not None:
return hook_result

return _is_manualobject_enabled(multiworld, player, location)

try_resolve = resolve_yaml_option(multiworld, player, location)
if try_resolve is None:
return _is_manualobject_enabled(multiworld, player, location)
else:
return try_resolve

def _is_manualobject_enabled(multiworld: MultiWorld, player: int, object: any) -> bool:
"""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

return enabled
resolve = is_category_enabled(multiworld, player, category)
if resolve == False:
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"""
Expand Down
18 changes: 18 additions & 0 deletions src/Options.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
15 changes: 9 additions & 6 deletions src/data/items.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"Left Side"
],
"value": {"coins": 7},
"progression": true
"progression": true
},
{
"name": "Firebrand",
Expand All @@ -42,23 +42,26 @@
"Characters",
"Left Side"
],
"progression": true
"progression": true,
"yaml_option": ["!Example_Toggle"]
},
{
"name": "Phoenix Wright",
"category": [
"Characters",
"Left Side"
],
"progression": true
"progression": true,
"yaml_option": ["Example_Choice=start"]
},
{
"name": "Nova",
"category": [
"Characters",
"Right Side"
],
"progression": true
"progression": true,
"yaml_option": ["Example_Range<5"]
},
{
"name": "Ghost Rider",
Expand Down Expand Up @@ -404,6 +407,6 @@
"Right Side"
],
"progression": true
} ]

}
]
}