From f7583c299abbdfe5a857eff3bc325d69c88ec106 Mon Sep 17 00:00:00 2001 From: nicopop <6759630+nicopop@users.noreply.github.com> Date: Mon, 6 Jan 2025 17:31:58 -0500 Subject: [PATCH 01/15] add YamlCompare and imports --- src/Rules.py | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 65 insertions(+), 2 deletions(-) diff --git a/src/Rules.py b/src/Rules.py index 229ca3ab..ff6dbda3 100644 --- a/src/Rules.py +++ b/src/Rules.py @@ -1,11 +1,13 @@ from typing import TYPE_CHECKING, Optional from enum import IntEnum +from operator import eq, ne, ge, le, lt, gt from worlds.generic.Rules import set_rule, add_rule from .Regions import regionMap from .hooks import Rules +from Options import Choice, Toggle, Range, NamedRange from BaseClasses import MultiWorld, CollectionState -from .Helpers import clamp, is_item_enabled, get_items_with_value, is_option_enabled, get_option_value, convert_string_to_type +from .Helpers import clamp, is_item_enabled, get_items_with_value, is_option_enabled, get_option_value, convert_string_to_type, format_to_valid_identifier from worlds.AutoWorld import World import re @@ -35,7 +37,7 @@ def construct_logic_error(location_or_region: dict, source: LogicErrorSource) -> elif source == LogicErrorSource.EVALUATE_POSTFIX: source_text = "There may be missing || around item names, or an AND/OR that is missing a value on one side, or other invalid syntax for the requires." elif source == LogicErrorSource.EVALUATE_STACK_SIZE: - source_text = "There may be missing {} around requirement functions like YamlEnabled() / YamlDisabled(), or other invalid syntax for the requires." + source_text = "There may be missing {} around requirement functions like YamlEnabled() / YamlDisabled(), or other invalid syntax for the requires." else: source_text = "This requires includes invalid syntax." @@ -482,3 +484,64 @@ def YamlEnabled(world: "ManualWorld", multiworld: MultiWorld, state: CollectionS def YamlDisabled(world: "ManualWorld", multiworld: MultiWorld, state: CollectionState, player: int, param: str) -> bool: """Is a yaml option disabled?""" return not is_option_enabled(multiworld, player, param) + +def YamlCompare(world: "ManualWorld", multiworld: MultiWorld, state: CollectionState, player: int, option_name: str, comparator: str, value:str, skipCache: bool = False) -> bool: + """Is a yaml option's value compared using {comparator} to the requested value + \nFormat it like {YamlCompare(OptionName, ==, value)} + \nWhere == can be any of the following: ==, !=, >=, <=, <, > + \nExample: {YamlCompare(Example_Range, >, 5)}""" + comp_symbols = { #Maybe find a better name for this + '==' : eq, + '!=' : ne, + '>=' : ge, + '<=' : le, + '<' : lt, + '>' : gt, + } + initial_option_name = str(option_name) #For exception messages + + option_name = format_to_valid_identifier(option_name) + comparator = comparator.strip() + value = value.strip() + + if comparator not in comp_symbols.keys(): + raise KeyError(f"YamlCompare requested comparator '{comparator}' but its not in {comp_symbols.keys()}") + + option = getattr(world.options, option_name, None) + if option is None: + raise AttributeError(f"YamlCompare could not find an option called '{initial_option_name}' to compare against, its either missing on misspelt") + + if not skipCache: + cacheindex = option_name + str(list(comp_symbols.keys()).index(comparator)) + format_to_valid_identifier(value.lower()) + + if not hasattr(world, 'yaml_compare_rule_cache'): #Cache made for optimization purposes + world.yaml_compare_rule_cache = dict[str,bool]() + + if skipCache or world.yaml_compare_rule_cache.get(cacheindex, None) is None: + try: + if issubclass(type(option), Choice): + value = convert_string_to_type(value, str|int) + if isinstance(value, str): + value = option.from_text(value).value + + elif issubclass(type(option), Range): + if type(option).__base__ == NamedRange: + value = convert_string_to_type(value, str|int) + if isinstance(value, str): + value = option.from_text(value).value + + else: + value = convert_string_to_type(value, int) + + elif issubclass(type(option), Toggle): + value = int(convert_string_to_type(value, bool)) + except Exception as ex: + raise TypeError(f"YamlCompare failed to convert the requested value to what a {type(option).__base__.__name__} option supports.\ + \nCaused By:\ + \n\n{type(ex).__name__}:{ex}") + + if skipCache: + return comp_symbols[comparator](option.value, value) + world.yaml_compare_rule_cache[cacheindex] = comp_symbols[comparator](option.value, value) + + return world.yaml_compare_rule_cache[cacheindex] From 8acb6f1f6c3a371da4aa0e3b9fe9108a2a21423f Mon Sep 17 00:00:00 2001 From: nicopop <6759630+nicopop@users.noreply.github.com> Date: Mon, 6 Jan 2025 18:10:20 -0500 Subject: [PATCH 02/15] add Examples --- src/data/locations.json | 6 +++--- src/data/regions.json | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/data/locations.json b/src/data/locations.json index a512305f..1f60ab90 100644 --- a/src/data/locations.json +++ b/src/data/locations.json @@ -4,17 +4,17 @@ { "name": "Beat the Game - Ryu", "category": ["Starter Team", "Left Side"], - "requires": "|Ryu|" + "region": "ExampleRegion" }, { "name": "Beat the Game - Chun-Li", "category": ["Starter Team", "Left Side"], - "requires": "|Chun-Li|" + "requires": "|Chun-Li| or {YamlCompare(Example_Choice, ==, start)}" }, { "name": "Beat the Game - Akuma", "category": ["Starter Team", "Left Side"], - "requires": "|Akuma|" + "requires": "|Akuma| or {YamlCompare(Example_Toggle, !=, True)}" }, { diff --git a/src/data/regions.json b/src/data/regions.json index c2126abd..a7d1623d 100644 --- a/src/data/regions.json +++ b/src/data/regions.json @@ -2,6 +2,7 @@ "$schema": "https://github.com/ManualForArchipelago/Manual/raw/main/schemas/Manual.regions.schema.json", "ExampleRegion":{ "connects_to": ["Second Region"], + "requires": "|Ryu| or {YamlCompare(Example_Range, >=, example )}", "starting": true }, "Second Region": { From d6e693c9fb3eeea57c144ec7f30d92af352901ed Mon Sep 17 00:00:00 2001 From: nicopop <6759630+nicopop@users.noreply.github.com> Date: Mon, 6 Jan 2025 18:41:00 -0500 Subject: [PATCH 03/15] add doc for the function and lint it --- docs/syntax/requires.md | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/docs/syntax/requires.md b/docs/syntax/requires.md index 60e57f4a..0d4360d8 100644 --- a/docs/syntax/requires.md +++ b/docs/syntax/requires.md @@ -33,6 +33,7 @@ For example, from the example above about Link to the Past and Thieves Town in t ### Additional Examples of Boolean Logic Boss 1 Requires Ladder and Gloves, OR Sword and Shield, OR Bow and Quiver and Arrow (separate items): a simple case of various successful item sets. It's a few sets of ANDs separated by ORs. + ```json { "name": "Boss 1", @@ -41,6 +42,7 @@ Boss 1 Requires Ladder and Gloves, OR Sword and Shield, OR Bow and Quiver and Ar ``` Boss 2 simply requires one heart, a way to strike it (Sword, Spear or Club) and a way to dodge it (Double Jump, Dash or Slide): we're looking at different sets, and picking one item from which. It's many ORs inside a big set of ANDs. + ```json { "name": "Boss 2", @@ -49,6 +51,7 @@ Boss 2 simply requires one heart, a way to strike it (Sword, Spear or Club) and ``` Now, say the final boss is a big dragon with a glaring weakness to Blizzard. However, if you don't have blizzard, you will need a spear for its reach and a way to dodge it, which is one of the three mobility from before. This is an OR (the mobility), inside an AND (Spear and Mobility), inside an OR (Blizzard it or fight it legitimately). Layered logic is as such: + ```json { "name": "Final Boss", @@ -68,6 +71,7 @@ The way to do this is a short suffix added to the end of any required item name Now that we know how to require multiple of an item, we can revise our Boss 2 example from above to make the boss a little easier to handle in-logic: > Boss 2 simply requires **FIVE hearts**, a way to strike it (Sword, Spear or Club) and a way to dodge it (Double Jump, Dash or Slide): we're looking at different sets, and picking one item from which. It's many ORs inside a big set of ANDs. +> > ```json >{ > "name": "Boss 2", @@ -125,7 +129,6 @@ Checks if you've collected the specificed value of a value-based item. For Example, `{ItemValue(Coins:12)}` will check if the player has collect at least 12 coins worth of items - ### `OptOne(ItemName)` Requires an item only if that item exists. Useful if an item might have been disabled by a yaml option. @@ -136,7 +139,6 @@ Takes an entire requires string, and applies the above check to each item inside For example, `requires: "{OptAll(|DisabledItem| and |@CategoryWithModifedCount:10|)} and |other items|"` will be transformed into `"|DisabledItem:0| and |@CategoryWithModifedCount:2| and |other items|"` - ### `YamlEnabled(option_name)` and `YamlDisabled(option_name)` These allow you to check yaml options within your logic. @@ -166,3 +168,19 @@ You can even combine the two in complex ways "name": "This is probably a region", "requires": "({YamlEnabled(easy_mode)} and |Gravity|) or ({YamlDisabled(easy_mode)} and |Jump| and |Blizzard| and |Water|)" } +``` + +### `YamlCompare(option_name, comparator_symbol, value)` + +Verify that the result of the option called _option_name_'s value compared using the _comparator_symbol_ with the requested _value_ + +The comparator symbol can be any of the following: `==, !=, >=, <=, <, >` + +The folowing example would check that the player.yaml value of the range option Example_Range is bigger than 5 or that the `Item A` item is present: + +```json +{ + "name": "Example Region", + "requires": "|Item A| or {YamlCompare(Example_Range, >, 5)}" +} +``` From 7bf676be14908a472467c2a89db3654d101c9c6d Mon Sep 17 00:00:00 2001 From: nicopop <6759630+nicopop@users.noreply.github.com> Date: Mon, 6 Jan 2025 21:24:02 -0500 Subject: [PATCH 04/15] add option typed documentation --- docs/syntax/requires.md | 381 ++++++++++++++++++++-------------------- 1 file changed, 195 insertions(+), 186 deletions(-) diff --git a/docs/syntax/requires.md b/docs/syntax/requires.md index 0d4360d8..d9a573e7 100644 --- a/docs/syntax/requires.md +++ b/docs/syntax/requires.md @@ -1,186 +1,195 @@ -# Requires for Locations and Regions - -_Let's start with the bad:_ - -If you don't put requires on your locations and/or regions, every location in your world will be in sphere 0. As in, any multiworld you play in will expect you to complete your entire world as soon as the multi starts. - -So it's really important to have requires! But what are they? - -Location/region requires tell the world what the logic is for accessing that location or region. Example: If you're playing Link to the Past and you have a region for Thieves Town, one of the dungeons in the Dark World. Your requires for that region would say that it requires Moon Pearl (for navigating the Dark World in human form) and either Hammer + Power Glove or just Titan's Mitt (for getting to the Dark World at all). Until you get the bare minimum of those items, that location will not be in logic, so the multi will not expect you to do that location yet. - -Okay, so we know what requires are. Let's talk about the different ways you can write out those requirements! - -## Boolean Logic (AND/OR) - -**Boolean logic is the default way to write requires in Manual.** It's called "boolean logic" because you're writing your logic much like you'd describe it normally: with a series of AND/OR combos. - -For example, from the example above about Link to the Past and Thieves Town in the Dark World, let's assume that the first chest location in the dungeon has no additional requirements. So, we'd describe our logic for that first chest location as being "Moon Pearl and (either (Hammer and Power Glove) or Titan's Mitt)", same as the region itself. In Manual's boolean logic syntax, that would be: - -```json -{ - "name": "First chest in Thieves Town", - "requires": "|Moon Pearl| and ((|Hammer| and |Power Glove|) or |Titan's Mitt|)" -} -``` - -**You use `|pipes|` around item names and `(parentheses)` around your layers of nesting, if needed.** - -- Pipes tell Manual where to look for entire item names. -- Parentheses tell Manual exactly how you're grouping your logic, since there's a difference between "Hammer and Power Glove or Titan's Mitt" and "(Hammer and Power Glove) or Titan's Mitt". - - The former essentially evaluates to "Hammer and either Power Glove or Titan's Mitt", while the latter is very explicit about what the logic should be and evaluates correctly. - - There's no theoretical limit to how many parentheses you can use, but try to not get past the practical limit of how many sets of parentheses you can reliably keep track of. - -### Additional Examples of Boolean Logic - -Boss 1 Requires Ladder and Gloves, OR Sword and Shield, OR Bow and Quiver and Arrow (separate items): a simple case of various successful item sets. It's a few sets of ANDs separated by ORs. - -```json -{ - "name": "Boss 1", - "requires": "(|Ladder| and |Gloves|) or (|Sword| and |Shield|) or (|Bow| and |Quiver| and |Arrow|)" -} -``` - -Boss 2 simply requires one heart, a way to strike it (Sword, Spear or Club) and a way to dodge it (Double Jump, Dash or Slide): we're looking at different sets, and picking one item from which. It's many ORs inside a big set of ANDs. - -```json -{ - "name": "Boss 2", - "requires": "|Heart| and (|Sword| or |Spear| or |Club|) and (|Double Jump| or |Dash| or |Slide|)" -} -``` - -Now, say the final boss is a big dragon with a glaring weakness to Blizzard. However, if you don't have blizzard, you will need a spear for its reach and a way to dodge it, which is one of the three mobility from before. This is an OR (the mobility), inside an AND (Spear and Mobility), inside an OR (Blizzard it or fight it legitimately). Layered logic is as such: - -```json -{ - "name": "Final Boss", - "requires": "|Blizzard| or (|Spear| and (|Double Jump| or |Dash| or |Slide|))", - "victory": true -} -``` - -## Item Counts - -As demonstrated in the [Making Items: Count](making/items.md#count) docs, you can configure an item to have more than one copy of that item in the world's item pool. Sometimes, you want to use multiple copies of an item as a requirement for accessing a location or region, and Manual supports this as well. - -The way to do this is a short suffix added to the end of any required item name separated by a colon, like this: `|Coin:25|`. - -- That will tell Manual that the location/region requires 25 of that Coin item. - -Now that we know how to require multiple of an item, we can revise our Boss 2 example from above to make the boss a little easier to handle in-logic: - -> Boss 2 simply requires **FIVE hearts**, a way to strike it (Sword, Spear or Club) and a way to dodge it (Double Jump, Dash or Slide): we're looking at different sets, and picking one item from which. It's many ORs inside a big set of ANDs. -> -> ```json ->{ -> "name": "Boss 2", -> "requires": "|Heart:5| and (|Sword| or |Spear| or |Club|) and (|Double Jump| or |Dash| or |Slide|)" ->} -> ``` - -In addition to specific item counts, you can also specify a broad relative amount like "all of this item" or "half of this item", or even percentages of that item versus the total in the pool. We'll demonstrate those briefly below as well. - -- `|Coin:ALL|` will make a location/region require every `Coin` item in the world's item pool before being accessible. So, if you have 50 Coins in the pool, it will require all 50. (The "ALL" is not case sensitive, so it can be lowercase too.) -- `|Coin:HALF|` will make a location/region require half of the `Coin` items in the world's item pool before being accessible. So, if you have 50 Coins in the pool, it will require 25. (The "HALF" is not case sensitive, so it can be lowercase too.) -- `|Coin:90%|` will make a location/region require 90% of the `Coin` items in the world's item pool before being accessible. So, if you have 50 Coins in the pool, it will require 45. (Supports percentages between 0 and 100.) - -## Requiring Categories - -As demonstrated in the [Making Items: Category](making/items.md#categories) docs, you can configure an item to belong to a category, potentially with other related items. Sometimes, you want to use a category of items as a requirement for accessing a location or region, and Manual supports this as well. - -The way to do this is a short ampersand prefix added to the beginning of any required item name, like this: `|@Coins Category|` - -- That will tell Manual that the location/region requires 1 item from the "Coins Category" category. - -Additionally, you can use counts as described above for required categories, just as you would use them for required item names. Let's see the demonstrated counts from above in category form: - -- `|@Coins Category:ALL|` will make a location/region require every item in the `Coins Category` category before being accessible. So, if you have 50 items in the `Coins Category` category, it will require all 50. (The "ALL" is not case sensitive, so it can be lowercase too.) -- `|@Coins Category:HALF|` will make a location/region require half of the items in the `Coins Category` category before being accessible. So, if you have 50 items in the `Coins Category` category, it will require any 25 of them. (The "HALF" is not case sensitive, so it can be lowercase too.) -- `|@Coins Category:90%|` will make a location/region require 90% of the items in the `Coins Category` category before being accessible. So, if you have 50 items in the `Coins Category` category, it will require any 45 of them. (Supports percentages between 0 and 100.) - -## Requirement Functions - -Requirement functions are functions that you write in the Rules hook file and can use in requires in locations/regions. We do provide a couple of default ones as examples, but only a couple of generic ones for very specific cases (more on that below \*). In most cases, you'll be working with hooks to take advantage of requirement functions. - -You'd typically use requirement functions if you have requirements that are too cumbersome to type out by hand, have requirements that rely on some dynamic piece of information, or have requirements that don't fit into the templating syntax that Manual provides for requirements. - -The way to do this is using curly braces around the function name that you want to call, like this: `{myFunction()}` - -- Note the lack of pipes (`|`). Functions are processed entirely differently than items/categories used as requirements. -- Doing this will tell Manual that the function will either return a requires string to be processed, or will return true/false based on whether this requirement was met. - -Requirement functions can have no function arguments, or have any number of function arguments separated by commas. - -- Example with no function arguments: https://github.com/ManualForArchipelago/Manual/blob/main/src/hooks/Rules.py#L8-L15. -- Example with one argument, add str arguments to the end of the function for more: https://github.com/ManualForArchipelago/Manual/blob/main/src/hooks/Rules.py#L17-L24 - -Additionally, those functions can themselves return a dynamically-created requires string, which would then be processed normally in the spot where the function call was. - -- Example of a returned requires string: https://github.com/ManualForArchipelago/Manual/blob/main/src/hooks/Rules.py#L26-L29 - -## Bundled functions - -In addition to writing your own Requirement Functions, Manual comes with some helpful functions built in: - -### `ItemValue(ValueName:Count)` - -Checks if you've collected the specificed value of a value-based item. - -For Example, `{ItemValue(Coins:12)}` will check if the player has collect at least 12 coins worth of items - -### `OptOne(ItemName)` - -Requires an item only if that item exists. Useful if an item might have been disabled by a yaml option. - -### `OptAll(ItemName)` - -Takes an entire requires string, and applies the above check to each item inside it. - -For example, `requires: "{OptAll(|DisabledItem| and |@CategoryWithModifedCount:10|)} and |other items|"` will be transformed into `"|DisabledItem:0| and |@CategoryWithModifedCount:2| and |other items|"` - -### `YamlEnabled(option_name)` and `YamlDisabled(option_name)` - -These allow you to check yaml options within your logic. - -You might use this to allow glitches - -```json -{ - "name": "Item on Cliff", - "requires": "|Double Jump| or {YamlEnabled(allow_hard_glitches)}" -} -``` - -Or make key items optional - -```json -{ - "name": "Hidden Item in Pokemon", - "requires": "|Itemfinder| or {YamlDisabled(require_itemfinder)}" -} -``` - -You can even combine the two in complex ways - -```json -{ - "name": "This is probably a region", - "requires": "({YamlEnabled(easy_mode)} and |Gravity|) or ({YamlDisabled(easy_mode)} and |Jump| and |Blizzard| and |Water|)" -} -``` - -### `YamlCompare(option_name, comparator_symbol, value)` - -Verify that the result of the option called _option_name_'s value compared using the _comparator_symbol_ with the requested _value_ - -The comparator symbol can be any of the following: `==, !=, >=, <=, <, >` - -The folowing example would check that the player.yaml value of the range option Example_Range is bigger than 5 or that the `Item A` item is present: - -```json -{ - "name": "Example Region", - "requires": "|Item A| or {YamlCompare(Example_Range, >, 5)}" -} -``` +# Requires for Locations and Regions + +_Let's start with the bad:_ + +If you don't put requires on your locations and/or regions, every location in your world will be in sphere 0. As in, any multiworld you play in will expect you to complete your entire world as soon as the multi starts. + +So it's really important to have requires! But what are they? + +Location/region requires tell the world what the logic is for accessing that location or region. Example: If you're playing Link to the Past and you have a region for Thieves Town, one of the dungeons in the Dark World. Your requires for that region would say that it requires Moon Pearl (for navigating the Dark World in human form) and either Hammer + Power Glove or just Titan's Mitt (for getting to the Dark World at all). Until you get the bare minimum of those items, that location will not be in logic, so the multi will not expect you to do that location yet. + +Okay, so we know what requires are. Let's talk about the different ways you can write out those requirements! + +## Boolean Logic (AND/OR) + +**Boolean logic is the default way to write requires in Manual.** It's called "boolean logic" because you're writing your logic much like you'd describe it normally: with a series of AND/OR combos. + +For example, from the example above about Link to the Past and Thieves Town in the Dark World, let's assume that the first chest location in the dungeon has no additional requirements. So, we'd describe our logic for that first chest location as being "Moon Pearl and (either (Hammer and Power Glove) or Titan's Mitt)", same as the region itself. In Manual's boolean logic syntax, that would be: + +```json +{ + "name": "First chest in Thieves Town", + "requires": "|Moon Pearl| and ((|Hammer| and |Power Glove|) or |Titan's Mitt|)" +} +``` + +**You use `|pipes|` around item names and `(parentheses)` around your layers of nesting, if needed.** + +- Pipes tell Manual where to look for entire item names. +- Parentheses tell Manual exactly how you're grouping your logic, since there's a difference between "Hammer and Power Glove or Titan's Mitt" and "(Hammer and Power Glove) or Titan's Mitt". + - The former essentially evaluates to "Hammer and either Power Glove or Titan's Mitt", while the latter is very explicit about what the logic should be and evaluates correctly. + - There's no theoretical limit to how many parentheses you can use, but try to not get past the practical limit of how many sets of parentheses you can reliably keep track of. + +### Additional Examples of Boolean Logic + +Boss 1 Requires Ladder and Gloves, OR Sword and Shield, OR Bow and Quiver and Arrow (separate items): a simple case of various successful item sets. It's a few sets of ANDs separated by ORs. + +```json +{ + "name": "Boss 1", + "requires": "(|Ladder| and |Gloves|) or (|Sword| and |Shield|) or (|Bow| and |Quiver| and |Arrow|)" +} +``` + +Boss 2 simply requires one heart, a way to strike it (Sword, Spear or Club) and a way to dodge it (Double Jump, Dash or Slide): we're looking at different sets, and picking one item from which. It's many ORs inside a big set of ANDs. + +```json +{ + "name": "Boss 2", + "requires": "|Heart| and (|Sword| or |Spear| or |Club|) and (|Double Jump| or |Dash| or |Slide|)" +} +``` + +Now, say the final boss is a big dragon with a glaring weakness to Blizzard. However, if you don't have blizzard, you will need a spear for its reach and a way to dodge it, which is one of the three mobility from before. This is an OR (the mobility), inside an AND (Spear and Mobility), inside an OR (Blizzard it or fight it legitimately). Layered logic is as such: + +```json +{ + "name": "Final Boss", + "requires": "|Blizzard| or (|Spear| and (|Double Jump| or |Dash| or |Slide|))", + "victory": true +} +``` + +## Item Counts + +As demonstrated in the [Making Items: Count](making/items.md#count) docs, you can configure an item to have more than one copy of that item in the world's item pool. Sometimes, you want to use multiple copies of an item as a requirement for accessing a location or region, and Manual supports this as well. + +The way to do this is a short suffix added to the end of any required item name separated by a colon, like this: `|Coin:25|`. + +- That will tell Manual that the location/region requires 25 of that Coin item. + +Now that we know how to require multiple of an item, we can revise our Boss 2 example from above to make the boss a little easier to handle in-logic: + +> Boss 2 simply requires **FIVE hearts**, a way to strike it (Sword, Spear or Club) and a way to dodge it (Double Jump, Dash or Slide): we're looking at different sets, and picking one item from which. It's many ORs inside a big set of ANDs. +> +> ```json +>{ +> "name": "Boss 2", +> "requires": "|Heart:5| and (|Sword| or |Spear| or |Club|) and (|Double Jump| or |Dash| or |Slide|)" +>} +> ``` + +In addition to specific item counts, you can also specify a broad relative amount like "all of this item" or "half of this item", or even percentages of that item versus the total in the pool. We'll demonstrate those briefly below as well. + +- `|Coin:ALL|` will make a location/region require every `Coin` item in the world's item pool before being accessible. So, if you have 50 Coins in the pool, it will require all 50. (The "ALL" is not case sensitive, so it can be lowercase too.) +- `|Coin:HALF|` will make a location/region require half of the `Coin` items in the world's item pool before being accessible. So, if you have 50 Coins in the pool, it will require 25. (The "HALF" is not case sensitive, so it can be lowercase too.) +- `|Coin:90%|` will make a location/region require 90% of the `Coin` items in the world's item pool before being accessible. So, if you have 50 Coins in the pool, it will require 45. (Supports percentages between 0 and 100.) + +## Requiring Categories + +As demonstrated in the [Making Items: Category](making/items.md#categories) docs, you can configure an item to belong to a category, potentially with other related items. Sometimes, you want to use a category of items as a requirement for accessing a location or region, and Manual supports this as well. + +The way to do this is a short ampersand prefix added to the beginning of any required item name, like this: `|@Coins Category|` + +- That will tell Manual that the location/region requires 1 item from the "Coins Category" category. + +Additionally, you can use counts as described above for required categories, just as you would use them for required item names. Let's see the demonstrated counts from above in category form: + +- `|@Coins Category:ALL|` will make a location/region require every item in the `Coins Category` category before being accessible. So, if you have 50 items in the `Coins Category` category, it will require all 50. (The "ALL" is not case sensitive, so it can be lowercase too.) +- `|@Coins Category:HALF|` will make a location/region require half of the items in the `Coins Category` category before being accessible. So, if you have 50 items in the `Coins Category` category, it will require any 25 of them. (The "HALF" is not case sensitive, so it can be lowercase too.) +- `|@Coins Category:90%|` will make a location/region require 90% of the items in the `Coins Category` category before being accessible. So, if you have 50 items in the `Coins Category` category, it will require any 45 of them. (Supports percentages between 0 and 100.) + +## Requirement Functions + +Requirement functions are functions that you write in the Rules hook file and can use in requires in locations/regions. We do provide a couple of default ones as examples, but only a couple of generic ones for very specific cases (more on that below \*). In most cases, you'll be working with hooks to take advantage of requirement functions. + +You'd typically use requirement functions if you have requirements that are too cumbersome to type out by hand, have requirements that rely on some dynamic piece of information, or have requirements that don't fit into the templating syntax that Manual provides for requirements. + +The way to do this is using curly braces around the function name that you want to call, like this: `{myFunction()}` + +- Note the lack of pipes (`|`). Functions are processed entirely differently than items/categories used as requirements. +- Doing this will tell Manual that the function will either return a requires string to be processed, or will return true/false based on whether this requirement was met. + +Requirement functions can have no function arguments, or have any number of function arguments separated by commas. + +- Example with no function arguments: https://github.com/ManualForArchipelago/Manual/blob/main/src/hooks/Rules.py#L8-L15. +- Example with one argument, add str arguments to the end of the function for more: https://github.com/ManualForArchipelago/Manual/blob/main/src/hooks/Rules.py#L17-L24 + +Additionally, those functions can themselves return a dynamically-created requires string, which would then be processed normally in the spot where the function call was. + +- Example of a returned requires string: https://github.com/ManualForArchipelago/Manual/blob/main/src/hooks/Rules.py#L26-L29 + +## Bundled functions + +In addition to writing your own Requirement Functions, Manual comes with some helpful functions built in: + +### `ItemValue(ValueName:Count)` + +Checks if you've collected the specificed value of a value-based item. + +For Example, `{ItemValue(Coins:12)}` will check if the player has collect at least 12 coins worth of items + +### `OptOne(ItemName)` + +Requires an item only if that item exists. Useful if an item might have been disabled by a yaml option. + +### `OptAll(ItemName)` + +Takes an entire requires string, and applies the above check to each item inside it. + +For example, `requires: "{OptAll(|DisabledItem| and |@CategoryWithModifedCount:10|)} and |other items|"` will be transformed into `"|DisabledItem:0| and |@CategoryWithModifedCount:2| and |other items|"` + +### `YamlEnabled(option_name)` and `YamlDisabled(option_name)` + +These allow you to check yaml options within your logic. + +You might use this to allow glitches + +```json +{ + "name": "Item on Cliff", + "requires": "|Double Jump| or {YamlEnabled(allow_hard_glitches)}" +} +``` + +Or make key items optional + +```json +{ + "name": "Hidden Item in Pokemon", + "requires": "|Itemfinder| or {YamlDisabled(require_itemfinder)}" +} +``` + +You can even combine the two in complex ways + +```json +{ + "name": "This is probably a region", + "requires": "({YamlEnabled(easy_mode)} and |Gravity|) or ({YamlDisabled(easy_mode)} and |Jump| and |Blizzard| and |Water|)" +} +``` + +### `YamlCompare(option_name, comparator_symbol, value)` + +Verify that the result of the option called _option_name_'s value compared using the _comparator_symbol_ with the requested _value_ + +The comparator symbol can be any of the following: `==, !=, >=, <=, <, >` + +The value can be of any type that your option supports + +- Range: integer aka number +- Range with values aka NamedRange: integer or one of the value name in "values" +- Choice: either numerical or string representation of a value in the option's "values" +- Toggle: a boolean value represented by any of the following not case sensitive: + - True: "true", "on", 1 + - False: "false", "off", 0 + +The folowing example would check that the player.yaml value of the range option Example_Range is bigger than 5 or that the `Item A` item is present: + +```json +{ + "name": "Example Region", + "requires": "|Item A| or {YamlCompare(Example_Range, >, 5)}" +} +``` From e864e35892b2ad2f203e0f23e6d86be786713bc1 Mon Sep 17 00:00:00 2001 From: nicopop <6759630+nicopop@users.noreply.github.com> Date: Mon, 6 Jan 2025 21:36:53 -0500 Subject: [PATCH 05/15] added TextChoice support --- docs/syntax/requires.md | 1 + src/Rules.py | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/docs/syntax/requires.md b/docs/syntax/requires.md index d9a573e7..ac4b7fdc 100644 --- a/docs/syntax/requires.md +++ b/docs/syntax/requires.md @@ -181,6 +181,7 @@ The value can be of any type that your option supports - Range: integer aka number - Range with values aka NamedRange: integer or one of the value name in "values" - Choice: either numerical or string representation of a value in the option's "values" +- Choice with allow_custom_value: either numerical or string representation of a value in the option's "values" or a custom string - Toggle: a boolean value represented by any of the following not case sensitive: - True: "true", "on", 1 - False: "false", "off", 0 diff --git a/src/Rules.py b/src/Rules.py index ff6dbda3..fdcac9be 100644 --- a/src/Rules.py +++ b/src/Rules.py @@ -535,11 +535,21 @@ def YamlCompare(world: "ManualWorld", multiworld: MultiWorld, state: CollectionS elif issubclass(type(option), Toggle): value = int(convert_string_to_type(value, bool)) + + except KeyError as ex: + raise ValueError(f"YamlCompare failed to find the requested value in what the \"{option_name}\" option supports.\ + \nRaw error:\ + \n\n{type(ex).__name__}:{ex}") + except Exception as ex: raise TypeError(f"YamlCompare failed to convert the requested value to what a {type(option).__base__.__name__} option supports.\ \nCaused By:\ \n\n{type(ex).__name__}:{ex}") + if isinstance(value, str) and comparator not in ['==', '!=']: + #At this point if its still a string don't try and compare with strings using > < >= <= + raise ValueError(f'YamlCompare can only compare strings with either "==" or "!=" and you tried to do: "{option.value} {comparator} {value}"') + if skipCache: return comp_symbols[comparator](option.value, value) world.yaml_compare_rule_cache[cacheindex] = comp_symbols[comparator](option.value, value) From 6284a897cfd156ef5ad8472c2be3ff5d360ede6a Mon Sep 17 00:00:00 2001 From: nicopop <6759630+nicopop@users.noreply.github.com> Date: Mon, 6 Jan 2025 21:45:24 -0500 Subject: [PATCH 06/15] use the correct option_name for exception --- src/Rules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Rules.py b/src/Rules.py index fdcac9be..4e8c18fa 100644 --- a/src/Rules.py +++ b/src/Rules.py @@ -537,7 +537,7 @@ def YamlCompare(world: "ManualWorld", multiworld: MultiWorld, state: CollectionS value = int(convert_string_to_type(value, bool)) except KeyError as ex: - raise ValueError(f"YamlCompare failed to find the requested value in what the \"{option_name}\" option supports.\ + raise ValueError(f"YamlCompare failed to find the requested value in what the \"{initial_option_name}\" option supports.\ \nRaw error:\ \n\n{type(ex).__name__}:{ex}") From 4678e79431a97cdbc9af9959afd0e49f345c288a Mon Sep 17 00:00:00 2001 From: nicopop <6759630+nicopop@users.noreply.github.com> Date: Wed, 8 Jan 2025 21:36:42 -0500 Subject: [PATCH 07/15] correct doc line ending --- docs/syntax/requires.md | 392 ++++++++++++++++++++-------------------- 1 file changed, 196 insertions(+), 196 deletions(-) diff --git a/docs/syntax/requires.md b/docs/syntax/requires.md index ac4b7fdc..e81bb923 100644 --- a/docs/syntax/requires.md +++ b/docs/syntax/requires.md @@ -1,196 +1,196 @@ -# Requires for Locations and Regions - -_Let's start with the bad:_ - -If you don't put requires on your locations and/or regions, every location in your world will be in sphere 0. As in, any multiworld you play in will expect you to complete your entire world as soon as the multi starts. - -So it's really important to have requires! But what are they? - -Location/region requires tell the world what the logic is for accessing that location or region. Example: If you're playing Link to the Past and you have a region for Thieves Town, one of the dungeons in the Dark World. Your requires for that region would say that it requires Moon Pearl (for navigating the Dark World in human form) and either Hammer + Power Glove or just Titan's Mitt (for getting to the Dark World at all). Until you get the bare minimum of those items, that location will not be in logic, so the multi will not expect you to do that location yet. - -Okay, so we know what requires are. Let's talk about the different ways you can write out those requirements! - -## Boolean Logic (AND/OR) - -**Boolean logic is the default way to write requires in Manual.** It's called "boolean logic" because you're writing your logic much like you'd describe it normally: with a series of AND/OR combos. - -For example, from the example above about Link to the Past and Thieves Town in the Dark World, let's assume that the first chest location in the dungeon has no additional requirements. So, we'd describe our logic for that first chest location as being "Moon Pearl and (either (Hammer and Power Glove) or Titan's Mitt)", same as the region itself. In Manual's boolean logic syntax, that would be: - -```json -{ - "name": "First chest in Thieves Town", - "requires": "|Moon Pearl| and ((|Hammer| and |Power Glove|) or |Titan's Mitt|)" -} -``` - -**You use `|pipes|` around item names and `(parentheses)` around your layers of nesting, if needed.** - -- Pipes tell Manual where to look for entire item names. -- Parentheses tell Manual exactly how you're grouping your logic, since there's a difference between "Hammer and Power Glove or Titan's Mitt" and "(Hammer and Power Glove) or Titan's Mitt". - - The former essentially evaluates to "Hammer and either Power Glove or Titan's Mitt", while the latter is very explicit about what the logic should be and evaluates correctly. - - There's no theoretical limit to how many parentheses you can use, but try to not get past the practical limit of how many sets of parentheses you can reliably keep track of. - -### Additional Examples of Boolean Logic - -Boss 1 Requires Ladder and Gloves, OR Sword and Shield, OR Bow and Quiver and Arrow (separate items): a simple case of various successful item sets. It's a few sets of ANDs separated by ORs. - -```json -{ - "name": "Boss 1", - "requires": "(|Ladder| and |Gloves|) or (|Sword| and |Shield|) or (|Bow| and |Quiver| and |Arrow|)" -} -``` - -Boss 2 simply requires one heart, a way to strike it (Sword, Spear or Club) and a way to dodge it (Double Jump, Dash or Slide): we're looking at different sets, and picking one item from which. It's many ORs inside a big set of ANDs. - -```json -{ - "name": "Boss 2", - "requires": "|Heart| and (|Sword| or |Spear| or |Club|) and (|Double Jump| or |Dash| or |Slide|)" -} -``` - -Now, say the final boss is a big dragon with a glaring weakness to Blizzard. However, if you don't have blizzard, you will need a spear for its reach and a way to dodge it, which is one of the three mobility from before. This is an OR (the mobility), inside an AND (Spear and Mobility), inside an OR (Blizzard it or fight it legitimately). Layered logic is as such: - -```json -{ - "name": "Final Boss", - "requires": "|Blizzard| or (|Spear| and (|Double Jump| or |Dash| or |Slide|))", - "victory": true -} -``` - -## Item Counts - -As demonstrated in the [Making Items: Count](making/items.md#count) docs, you can configure an item to have more than one copy of that item in the world's item pool. Sometimes, you want to use multiple copies of an item as a requirement for accessing a location or region, and Manual supports this as well. - -The way to do this is a short suffix added to the end of any required item name separated by a colon, like this: `|Coin:25|`. - -- That will tell Manual that the location/region requires 25 of that Coin item. - -Now that we know how to require multiple of an item, we can revise our Boss 2 example from above to make the boss a little easier to handle in-logic: - -> Boss 2 simply requires **FIVE hearts**, a way to strike it (Sword, Spear or Club) and a way to dodge it (Double Jump, Dash or Slide): we're looking at different sets, and picking one item from which. It's many ORs inside a big set of ANDs. -> -> ```json ->{ -> "name": "Boss 2", -> "requires": "|Heart:5| and (|Sword| or |Spear| or |Club|) and (|Double Jump| or |Dash| or |Slide|)" ->} -> ``` - -In addition to specific item counts, you can also specify a broad relative amount like "all of this item" or "half of this item", or even percentages of that item versus the total in the pool. We'll demonstrate those briefly below as well. - -- `|Coin:ALL|` will make a location/region require every `Coin` item in the world's item pool before being accessible. So, if you have 50 Coins in the pool, it will require all 50. (The "ALL" is not case sensitive, so it can be lowercase too.) -- `|Coin:HALF|` will make a location/region require half of the `Coin` items in the world's item pool before being accessible. So, if you have 50 Coins in the pool, it will require 25. (The "HALF" is not case sensitive, so it can be lowercase too.) -- `|Coin:90%|` will make a location/region require 90% of the `Coin` items in the world's item pool before being accessible. So, if you have 50 Coins in the pool, it will require 45. (Supports percentages between 0 and 100.) - -## Requiring Categories - -As demonstrated in the [Making Items: Category](making/items.md#categories) docs, you can configure an item to belong to a category, potentially with other related items. Sometimes, you want to use a category of items as a requirement for accessing a location or region, and Manual supports this as well. - -The way to do this is a short ampersand prefix added to the beginning of any required item name, like this: `|@Coins Category|` - -- That will tell Manual that the location/region requires 1 item from the "Coins Category" category. - -Additionally, you can use counts as described above for required categories, just as you would use them for required item names. Let's see the demonstrated counts from above in category form: - -- `|@Coins Category:ALL|` will make a location/region require every item in the `Coins Category` category before being accessible. So, if you have 50 items in the `Coins Category` category, it will require all 50. (The "ALL" is not case sensitive, so it can be lowercase too.) -- `|@Coins Category:HALF|` will make a location/region require half of the items in the `Coins Category` category before being accessible. So, if you have 50 items in the `Coins Category` category, it will require any 25 of them. (The "HALF" is not case sensitive, so it can be lowercase too.) -- `|@Coins Category:90%|` will make a location/region require 90% of the items in the `Coins Category` category before being accessible. So, if you have 50 items in the `Coins Category` category, it will require any 45 of them. (Supports percentages between 0 and 100.) - -## Requirement Functions - -Requirement functions are functions that you write in the Rules hook file and can use in requires in locations/regions. We do provide a couple of default ones as examples, but only a couple of generic ones for very specific cases (more on that below \*). In most cases, you'll be working with hooks to take advantage of requirement functions. - -You'd typically use requirement functions if you have requirements that are too cumbersome to type out by hand, have requirements that rely on some dynamic piece of information, or have requirements that don't fit into the templating syntax that Manual provides for requirements. - -The way to do this is using curly braces around the function name that you want to call, like this: `{myFunction()}` - -- Note the lack of pipes (`|`). Functions are processed entirely differently than items/categories used as requirements. -- Doing this will tell Manual that the function will either return a requires string to be processed, or will return true/false based on whether this requirement was met. - -Requirement functions can have no function arguments, or have any number of function arguments separated by commas. - -- Example with no function arguments: https://github.com/ManualForArchipelago/Manual/blob/main/src/hooks/Rules.py#L8-L15. -- Example with one argument, add str arguments to the end of the function for more: https://github.com/ManualForArchipelago/Manual/blob/main/src/hooks/Rules.py#L17-L24 - -Additionally, those functions can themselves return a dynamically-created requires string, which would then be processed normally in the spot where the function call was. - -- Example of a returned requires string: https://github.com/ManualForArchipelago/Manual/blob/main/src/hooks/Rules.py#L26-L29 - -## Bundled functions - -In addition to writing your own Requirement Functions, Manual comes with some helpful functions built in: - -### `ItemValue(ValueName:Count)` - -Checks if you've collected the specificed value of a value-based item. - -For Example, `{ItemValue(Coins:12)}` will check if the player has collect at least 12 coins worth of items - -### `OptOne(ItemName)` - -Requires an item only if that item exists. Useful if an item might have been disabled by a yaml option. - -### `OptAll(ItemName)` - -Takes an entire requires string, and applies the above check to each item inside it. - -For example, `requires: "{OptAll(|DisabledItem| and |@CategoryWithModifedCount:10|)} and |other items|"` will be transformed into `"|DisabledItem:0| and |@CategoryWithModifedCount:2| and |other items|"` - -### `YamlEnabled(option_name)` and `YamlDisabled(option_name)` - -These allow you to check yaml options within your logic. - -You might use this to allow glitches - -```json -{ - "name": "Item on Cliff", - "requires": "|Double Jump| or {YamlEnabled(allow_hard_glitches)}" -} -``` - -Or make key items optional - -```json -{ - "name": "Hidden Item in Pokemon", - "requires": "|Itemfinder| or {YamlDisabled(require_itemfinder)}" -} -``` - -You can even combine the two in complex ways - -```json -{ - "name": "This is probably a region", - "requires": "({YamlEnabled(easy_mode)} and |Gravity|) or ({YamlDisabled(easy_mode)} and |Jump| and |Blizzard| and |Water|)" -} -``` - -### `YamlCompare(option_name, comparator_symbol, value)` - -Verify that the result of the option called _option_name_'s value compared using the _comparator_symbol_ with the requested _value_ - -The comparator symbol can be any of the following: `==, !=, >=, <=, <, >` - -The value can be of any type that your option supports - -- Range: integer aka number -- Range with values aka NamedRange: integer or one of the value name in "values" -- Choice: either numerical or string representation of a value in the option's "values" -- Choice with allow_custom_value: either numerical or string representation of a value in the option's "values" or a custom string -- Toggle: a boolean value represented by any of the following not case sensitive: - - True: "true", "on", 1 - - False: "false", "off", 0 - -The folowing example would check that the player.yaml value of the range option Example_Range is bigger than 5 or that the `Item A` item is present: - -```json -{ - "name": "Example Region", - "requires": "|Item A| or {YamlCompare(Example_Range, >, 5)}" -} -``` +# Requires for Locations and Regions + +_Let's start with the bad:_ + +If you don't put requires on your locations and/or regions, every location in your world will be in sphere 0. As in, any multiworld you play in will expect you to complete your entire world as soon as the multi starts. + +So it's really important to have requires! But what are they? + +Location/region requires tell the world what the logic is for accessing that location or region. Example: If you're playing Link to the Past and you have a region for Thieves Town, one of the dungeons in the Dark World. Your requires for that region would say that it requires Moon Pearl (for navigating the Dark World in human form) and either Hammer + Power Glove or just Titan's Mitt (for getting to the Dark World at all). Until you get the bare minimum of those items, that location will not be in logic, so the multi will not expect you to do that location yet. + +Okay, so we know what requires are. Let's talk about the different ways you can write out those requirements! + +## Boolean Logic (AND/OR) + +**Boolean logic is the default way to write requires in Manual.** It's called "boolean logic" because you're writing your logic much like you'd describe it normally: with a series of AND/OR combos. + +For example, from the example above about Link to the Past and Thieves Town in the Dark World, let's assume that the first chest location in the dungeon has no additional requirements. So, we'd describe our logic for that first chest location as being "Moon Pearl and (either (Hammer and Power Glove) or Titan's Mitt)", same as the region itself. In Manual's boolean logic syntax, that would be: + +```json +{ + "name": "First chest in Thieves Town", + "requires": "|Moon Pearl| and ((|Hammer| and |Power Glove|) or |Titan's Mitt|)" +} +``` + +**You use `|pipes|` around item names and `(parentheses)` around your layers of nesting, if needed.** + +- Pipes tell Manual where to look for entire item names. +- Parentheses tell Manual exactly how you're grouping your logic, since there's a difference between "Hammer and Power Glove or Titan's Mitt" and "(Hammer and Power Glove) or Titan's Mitt". + - The former essentially evaluates to "Hammer and either Power Glove or Titan's Mitt", while the latter is very explicit about what the logic should be and evaluates correctly. + - There's no theoretical limit to how many parentheses you can use, but try to not get past the practical limit of how many sets of parentheses you can reliably keep track of. + +### Additional Examples of Boolean Logic + +Boss 1 Requires Ladder and Gloves, OR Sword and Shield, OR Bow and Quiver and Arrow (separate items): a simple case of various successful item sets. It's a few sets of ANDs separated by ORs. + +```json +{ + "name": "Boss 1", + "requires": "(|Ladder| and |Gloves|) or (|Sword| and |Shield|) or (|Bow| and |Quiver| and |Arrow|)" +} +``` + +Boss 2 simply requires one heart, a way to strike it (Sword, Spear or Club) and a way to dodge it (Double Jump, Dash or Slide): we're looking at different sets, and picking one item from which. It's many ORs inside a big set of ANDs. + +```json +{ + "name": "Boss 2", + "requires": "|Heart| and (|Sword| or |Spear| or |Club|) and (|Double Jump| or |Dash| or |Slide|)" +} +``` + +Now, say the final boss is a big dragon with a glaring weakness to Blizzard. However, if you don't have blizzard, you will need a spear for its reach and a way to dodge it, which is one of the three mobility from before. This is an OR (the mobility), inside an AND (Spear and Mobility), inside an OR (Blizzard it or fight it legitimately). Layered logic is as such: + +```json +{ + "name": "Final Boss", + "requires": "|Blizzard| or (|Spear| and (|Double Jump| or |Dash| or |Slide|))", + "victory": true +} +``` + +## Item Counts + +As demonstrated in the [Making Items: Count](making/items.md#count) docs, you can configure an item to have more than one copy of that item in the world's item pool. Sometimes, you want to use multiple copies of an item as a requirement for accessing a location or region, and Manual supports this as well. + +The way to do this is a short suffix added to the end of any required item name separated by a colon, like this: `|Coin:25|`. + +- That will tell Manual that the location/region requires 25 of that Coin item. + +Now that we know how to require multiple of an item, we can revise our Boss 2 example from above to make the boss a little easier to handle in-logic: + +> Boss 2 simply requires **FIVE hearts**, a way to strike it (Sword, Spear or Club) and a way to dodge it (Double Jump, Dash or Slide): we're looking at different sets, and picking one item from which. It's many ORs inside a big set of ANDs. +> +> ```json +>{ +> "name": "Boss 2", +> "requires": "|Heart:5| and (|Sword| or |Spear| or |Club|) and (|Double Jump| or |Dash| or |Slide|)" +>} +> ``` + +In addition to specific item counts, you can also specify a broad relative amount like "all of this item" or "half of this item", or even percentages of that item versus the total in the pool. We'll demonstrate those briefly below as well. + +- `|Coin:ALL|` will make a location/region require every `Coin` item in the world's item pool before being accessible. So, if you have 50 Coins in the pool, it will require all 50. (The "ALL" is not case sensitive, so it can be lowercase too.) +- `|Coin:HALF|` will make a location/region require half of the `Coin` items in the world's item pool before being accessible. So, if you have 50 Coins in the pool, it will require 25. (The "HALF" is not case sensitive, so it can be lowercase too.) +- `|Coin:90%|` will make a location/region require 90% of the `Coin` items in the world's item pool before being accessible. So, if you have 50 Coins in the pool, it will require 45. (Supports percentages between 0 and 100.) + +## Requiring Categories + +As demonstrated in the [Making Items: Category](making/items.md#categories) docs, you can configure an item to belong to a category, potentially with other related items. Sometimes, you want to use a category of items as a requirement for accessing a location or region, and Manual supports this as well. + +The way to do this is a short ampersand prefix added to the beginning of any required item name, like this: `|@Coins Category|` + +- That will tell Manual that the location/region requires 1 item from the "Coins Category" category. + +Additionally, you can use counts as described above for required categories, just as you would use them for required item names. Let's see the demonstrated counts from above in category form: + +- `|@Coins Category:ALL|` will make a location/region require every item in the `Coins Category` category before being accessible. So, if you have 50 items in the `Coins Category` category, it will require all 50. (The "ALL" is not case sensitive, so it can be lowercase too.) +- `|@Coins Category:HALF|` will make a location/region require half of the items in the `Coins Category` category before being accessible. So, if you have 50 items in the `Coins Category` category, it will require any 25 of them. (The "HALF" is not case sensitive, so it can be lowercase too.) +- `|@Coins Category:90%|` will make a location/region require 90% of the items in the `Coins Category` category before being accessible. So, if you have 50 items in the `Coins Category` category, it will require any 45 of them. (Supports percentages between 0 and 100.) + +## Requirement Functions + +Requirement functions are functions that you write in the Rules hook file and can use in requires in locations/regions. We do provide a couple of default ones as examples, but only a couple of generic ones for very specific cases (more on that below \*). In most cases, you'll be working with hooks to take advantage of requirement functions. + +You'd typically use requirement functions if you have requirements that are too cumbersome to type out by hand, have requirements that rely on some dynamic piece of information, or have requirements that don't fit into the templating syntax that Manual provides for requirements. + +The way to do this is using curly braces around the function name that you want to call, like this: `{myFunction()}` + +- Note the lack of pipes (`|`). Functions are processed entirely differently than items/categories used as requirements. +- Doing this will tell Manual that the function will either return a requires string to be processed, or will return true/false based on whether this requirement was met. + +Requirement functions can have no function arguments, or have any number of function arguments separated by commas. + +- Example with no function arguments: https://github.com/ManualForArchipelago/Manual/blob/main/src/hooks/Rules.py#L8-L15. +- Example with one argument, add str arguments to the end of the function for more: https://github.com/ManualForArchipelago/Manual/blob/main/src/hooks/Rules.py#L17-L24 + +Additionally, those functions can themselves return a dynamically-created requires string, which would then be processed normally in the spot where the function call was. + +- Example of a returned requires string: https://github.com/ManualForArchipelago/Manual/blob/main/src/hooks/Rules.py#L26-L29 + +## Bundled functions + +In addition to writing your own Requirement Functions, Manual comes with some helpful functions built in: + +### `ItemValue(ValueName:Count)` + +Checks if you've collected the specificed value of a value-based item. + +For Example, `{ItemValue(Coins:12)}` will check if the player has collect at least 12 coins worth of items + +### `OptOne(ItemName)` + +Requires an item only if that item exists. Useful if an item might have been disabled by a yaml option. + +### `OptAll(ItemName)` + +Takes an entire requires string, and applies the above check to each item inside it. + +For example, `requires: "{OptAll(|DisabledItem| and |@CategoryWithModifedCount:10|)} and |other items|"` will be transformed into `"|DisabledItem:0| and |@CategoryWithModifedCount:2| and |other items|"` + +### `YamlEnabled(option_name)` and `YamlDisabled(option_name)` + +These allow you to check yaml options within your logic. + +You might use this to allow glitches + +```json +{ + "name": "Item on Cliff", + "requires": "|Double Jump| or {YamlEnabled(allow_hard_glitches)}" +} +``` + +Or make key items optional + +```json +{ + "name": "Hidden Item in Pokemon", + "requires": "|Itemfinder| or {YamlDisabled(require_itemfinder)}" +} +``` + +You can even combine the two in complex ways + +```json +{ + "name": "This is probably a region", + "requires": "({YamlEnabled(easy_mode)} and |Gravity|) or ({YamlDisabled(easy_mode)} and |Jump| and |Blizzard| and |Water|)" +} +``` + +### `YamlCompare(option_name, comparator_symbol, value)` + +Verify that the result of the option called _option_name_'s value compared using the _comparator_symbol_ with the requested _value_ + +The comparator symbol can be any of the following: `==, !=, >=, <=, <, >` + +The value can be of any type that your option supports + +- Range: integer aka number +- Range with values aka NamedRange: integer or one of the value name in "values" +- Choice: either numerical or string representation of a value in the option's "values" +- Choice with allow_custom_value: either numerical or string representation of a value in the option's "values" or a custom string +- Toggle: a boolean value represented by any of the following not case sensitive: + - True: "true", "on", 1 + - False: "false", "off", 0 + +The folowing example would check that the player.yaml value of the range option Example_Range is bigger than 5 or that the `Item A` item is present: + +```json +{ + "name": "Example Region", + "requires": "|Item A| or {YamlCompare(Example_Range, >, 5)}" +} +``` From a587b4de31be96bd13d5a35e3f641836eb3e23ff Mon Sep 17 00:00:00 2001 From: nicopop <6759630+nicopop@users.noreply.github.com> Date: Sat, 25 Jan 2025 14:04:59 -0500 Subject: [PATCH 08/15] convert YamlCompare to only use 1 argument --- docs/syntax/requires.md | 4 ++-- src/Rules.py | 33 ++++++++++++++++++++++++--------- src/data/locations.json | 4 ++-- src/data/regions.json | 2 +- 4 files changed, 29 insertions(+), 14 deletions(-) diff --git a/docs/syntax/requires.md b/docs/syntax/requires.md index ac4b7fdc..531e8189 100644 --- a/docs/syntax/requires.md +++ b/docs/syntax/requires.md @@ -170,7 +170,7 @@ You can even combine the two in complex ways } ``` -### `YamlCompare(option_name, comparator_symbol, value)` +### `YamlCompare(option_name comparator_symbol value)` Verify that the result of the option called _option_name_'s value compared using the _comparator_symbol_ with the requested _value_ @@ -191,6 +191,6 @@ The folowing example would check that the player.yaml value of the range option ```json { "name": "Example Region", - "requires": "|Item A| or {YamlCompare(Example_Range, >, 5)}" + "requires": "|Item A| or {YamlCompare(Example_Range > 5)}" } ``` diff --git a/src/Rules.py b/src/Rules.py index 88cc27c0..ff7898be 100644 --- a/src/Rules.py +++ b/src/Rules.py @@ -496,11 +496,11 @@ def YamlDisabled(world: "ManualWorld", multiworld: MultiWorld, state: Collection """Is a yaml option disabled?""" return not is_option_enabled(multiworld, player, param) -def YamlCompare(world: "ManualWorld", multiworld: MultiWorld, state: CollectionState, player: int, option_name: str, comparator: str, value:str, skipCache: bool = False) -> bool: +def YamlCompare(world: "ManualWorld", multiworld: MultiWorld, state: CollectionState, player: int, args: str, skipCache: bool = False) -> bool: """Is a yaml option's value compared using {comparator} to the requested value - \nFormat it like {YamlCompare(OptionName, ==, value)} + \nFormat it like {YamlCompare(OptionName==value)} \nWhere == can be any of the following: ==, !=, >=, <=, <, > - \nExample: {YamlCompare(Example_Range, >, 5)}""" + \nExample: {YamlCompare(Example_Range > 5)}""" comp_symbols = { #Maybe find a better name for this '==' : eq, '!=' : ne, @@ -509,18 +509,33 @@ def YamlCompare(world: "ManualWorld", multiworld: MultiWorld, state: CollectionS '<' : lt, '>' : gt, } - initial_option_name = str(option_name) #For exception messages + if '==' in args: + comparator = "==" + elif '!=' in args: + comparator = '!=' + elif '>=' in args: + comparator = '>=' + elif '<=' in args: + comparator = '<=' + elif '>' in args: + comparator = '>' + elif '<' in args: + comparator = '<' + else: + raise ValueError(f"Could not find a valid comparator in given string '{args}', it must be one of {comp_symbols.keys()}") + + option_name, value = args.split(comparator) + initial_option_name = str(option_name).strip() #For exception messages option_name = format_to_valid_identifier(option_name) - comparator = comparator.strip() value = value.strip() - if comparator not in comp_symbols.keys(): - raise KeyError(f"YamlCompare requested comparator '{comparator}' but its not in {comp_symbols.keys()}") - option = getattr(world.options, option_name, None) if option is None: - raise AttributeError(f"YamlCompare could not find an option called '{initial_option_name}' to compare against, its either missing on misspelt") + raise ValueError(f"YamlCompare could not find an option called '{initial_option_name}' to compare against, its either missing on misspelt") + + if not value: #empty string '' + raise ValueError(f"Could not find a valid value to compare against in given string '{args}'. \nThere must be a value to compare against after the comparator (in this case '{comparator}').") if not skipCache: cacheindex = option_name + str(list(comp_symbols.keys()).index(comparator)) + format_to_valid_identifier(value.lower()) diff --git a/src/data/locations.json b/src/data/locations.json index 68668086..1672f7ff 100644 --- a/src/data/locations.json +++ b/src/data/locations.json @@ -9,12 +9,12 @@ { "name": "Beat the Game - Chun-Li", "category": ["Starter Team", "Left Side"], - "requires": "|Chun-Li| or {YamlCompare(Example_Choice, ==, start)}" + "requires": "|Chun-Li| or {YamlCompare(Example_Choice ==)}" }, { "name": "Beat the Game - Akuma", "category": ["Starter Team", "Left Side"], - "requires": "|Akuma| or {YamlCompare(Example_Toggle, !=, True)}" + "requires": "|Akuma| or {YamlCompare(Example_Toggle != True)}" }, { diff --git a/src/data/regions.json b/src/data/regions.json index a7d1623d..35588238 100644 --- a/src/data/regions.json +++ b/src/data/regions.json @@ -2,7 +2,7 @@ "$schema": "https://github.com/ManualForArchipelago/Manual/raw/main/schemas/Manual.regions.schema.json", "ExampleRegion":{ "connects_to": ["Second Region"], - "requires": "|Ryu| or {YamlCompare(Example_Range, >=, example )}", + "requires": "|Ryu| or {YamlCompare(Example_Range >= example )}", "starting": true }, "Second Region": { From c9fab206ba5ff0bd4d56e5fbf51446db937bdcab Mon Sep 17 00:00:00 2001 From: nicopop <6759630+nicopop@users.noreply.github.com> Date: Sat, 25 Jan 2025 14:28:18 -0500 Subject: [PATCH 09/15] add error for unsupported option type --- src/Rules.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Rules.py b/src/Rules.py index ff7898be..2d03223f 100644 --- a/src/Rules.py +++ b/src/Rules.py @@ -562,6 +562,9 @@ def YamlCompare(world: "ManualWorld", multiworld: MultiWorld, state: CollectionS elif issubclass(type(option), Toggle): value = int(convert_string_to_type(value, bool)) + else: + raise ValueError(f"YamlCompare does not currently support Option of type {type(option)} \nAsk about it in #Manual-dev and it might be added.") + except KeyError as ex: raise ValueError(f"YamlCompare failed to find the requested value in what the \"{initial_option_name}\" option supports.\ \nRaw error:\ From 094531a666a8aef1809a3ad05561c9720a68da37 Mon Sep 17 00:00:00 2001 From: nicopop <6759630+nicopop@users.noreply.github.com> Date: Sat, 25 Jan 2025 17:54:27 -0500 Subject: [PATCH 10/15] got inspired by #126 and added inverted result --- src/Rules.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/Rules.py b/src/Rules.py index 2d03223f..d49ac734 100644 --- a/src/Rules.py +++ b/src/Rules.py @@ -1,6 +1,6 @@ from typing import TYPE_CHECKING, Optional from enum import IntEnum -from operator import eq, ne, ge, le, lt, gt +from operator import eq, ge, le, lt, gt from .Regions import regionMap from .hooks import Rules @@ -503,20 +503,27 @@ def YamlCompare(world: "ManualWorld", multiworld: MultiWorld, state: CollectionS \nExample: {YamlCompare(Example_Range > 5)}""" comp_symbols = { #Maybe find a better name for this '==' : eq, - '!=' : ne, + '!=' : eq, #reverse_result starts true (for optimization) '>=' : ge, '<=' : le, + '=': eq, '<' : lt, '>' : gt, } + + reverse_result = False + if '==' in args: - comparator = "==" + comparator = '==' elif '!=' in args: comparator = '!=' + reverse_result = True #is in reality == but reversed elif '>=' in args: comparator = '>=' elif '<=' in args: comparator = '<=' + elif '=' in args: + comparator = '=' elif '>' in args: comparator = '>' elif '<' in args: @@ -528,6 +535,12 @@ def YamlCompare(world: "ManualWorld", multiworld: MultiWorld, state: CollectionS initial_option_name = str(option_name).strip() #For exception messages option_name = format_to_valid_identifier(option_name) + + if option_name.startswith('!'): + reverse_result = not reverse_result + option_name = option_name.lstrip('!') + initial_option_name = initial_option_name.lstrip('!') + value = value.strip() option = getattr(world.options, option_name, None) @@ -538,7 +551,7 @@ def YamlCompare(world: "ManualWorld", multiworld: MultiWorld, state: CollectionS raise ValueError(f"Could not find a valid value to compare against in given string '{args}'. \nThere must be a value to compare against after the comparator (in this case '{comparator}').") if not skipCache: - cacheindex = option_name + str(list(comp_symbols.keys()).index(comparator)) + format_to_valid_identifier(value.lower()) + cacheindex = option_name + '_' + comp_symbols[comparator].__name__ + '_' + format_to_valid_identifier(value.lower()) if not hasattr(world, 'yaml_compare_rule_cache'): #Cache made for optimization purposes world.yaml_compare_rule_cache = dict[str,bool]() @@ -583,4 +596,6 @@ def YamlCompare(world: "ManualWorld", multiworld: MultiWorld, state: CollectionS return comp_symbols[comparator](option.value, value) world.yaml_compare_rule_cache[cacheindex] = comp_symbols[comparator](option.value, value) + if reverse_result: + return not world.yaml_compare_rule_cache[cacheindex] return world.yaml_compare_rule_cache[cacheindex] From 5dab64194e22cba4a49e80f5064fdb12acebd815 Mon Sep 17 00:00:00 2001 From: nicopop <6759630+nicopop@users.noreply.github.com> Date: Sat, 25 Jan 2025 20:30:12 -0500 Subject: [PATCH 11/15] update str value compatible comparator check --- src/Rules.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Rules.py b/src/Rules.py index d49ac734..be2c296c 100644 --- a/src/Rules.py +++ b/src/Rules.py @@ -588,9 +588,9 @@ def YamlCompare(world: "ManualWorld", multiworld: MultiWorld, state: CollectionS \nCaused By:\ \n\n{type(ex).__name__}:{ex}") - if isinstance(value, str) and comparator not in ['==', '!=']: + if isinstance(value, str) and comp_symbols[comparator].__name__ != 'eq': #At this point if its still a string don't try and compare with strings using > < >= <= - raise ValueError(f'YamlCompare can only compare strings with either "==" or "!=" and you tried to do: "{option.value} {comparator} {value}"') + raise ValueError(f'YamlCompare can only compare strings with either "=="/"=" or "!=" and you tried to do: "{option.value} {comparator} {value}"') if skipCache: return comp_symbols[comparator](option.value, value) From d3f700cc667e26ef3379ffbfea33e8fecc97f4cb Mon Sep 17 00:00:00 2001 From: nicopop <6759630+nicopop@users.noreply.github.com> Date: Tue, 28 Jan 2025 20:55:29 -0500 Subject: [PATCH 12/15] rework of the return of the function --- src/Rules.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/Rules.py b/src/Rules.py index be2c296c..c1f3c7f1 100644 --- a/src/Rules.py +++ b/src/Rules.py @@ -590,12 +590,15 @@ def YamlCompare(world: "ManualWorld", multiworld: MultiWorld, state: CollectionS if isinstance(value, str) and comp_symbols[comparator].__name__ != 'eq': #At this point if its still a string don't try and compare with strings using > < >= <= - raise ValueError(f'YamlCompare can only compare strings with either "=="/"=" or "!=" and you tried to do: "{option.value} {comparator} {value}"') + raise ValueError(f'YamlCompare can only compare strings with one of the following: {[s for s, v in comp_symbols.items() if v.__name__ == 'eq']} and you tried to do: "{option.value} {comparator} {value}"') - if skipCache: - return comp_symbols[comparator](option.value, value) - world.yaml_compare_rule_cache[cacheindex] = comp_symbols[comparator](option.value, value) + result = comp_symbols[comparator](option.value, value) + + if not skipCache: + world.yaml_compare_rule_cache[cacheindex] = result + + else: #if exists and not skipCache + result = world.yaml_compare_rule_cache[cacheindex] + + return not result if reverse_result else result - if reverse_result: - return not world.yaml_compare_rule_cache[cacheindex] - return world.yaml_compare_rule_cache[cacheindex] From 2922029cd15c8a4f67b1bd90b4c81493a1bd5e94 Mon Sep 17 00:00:00 2001 From: nicopop <6759630+nicopop@users.noreply.github.com> Date: Tue, 28 Jan 2025 20:57:55 -0500 Subject: [PATCH 13/15] better comments --- src/Rules.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Rules.py b/src/Rules.py index c1f3c7f1..824be0d0 100644 --- a/src/Rules.py +++ b/src/Rules.py @@ -503,21 +503,22 @@ def YamlCompare(world: "ManualWorld", multiworld: MultiWorld, state: CollectionS \nExample: {YamlCompare(Example_Range > 5)}""" comp_symbols = { #Maybe find a better name for this '==' : eq, - '!=' : eq, #reverse_result starts true (for optimization) + '!=' : eq, #complement of == '>=' : ge, '<=' : le, - '=': eq, + '=': eq, #Alternate to be like yaml_option '<' : lt, '>' : gt, } reverse_result = False + #Find the comparator symbol to split the string with and for logs if '==' in args: comparator = '==' elif '!=' in args: comparator = '!=' - reverse_result = True #is in reality == but reversed + reverse_result = True #complement of == thus reverse by default elif '>=' in args: comparator = '>=' elif '<=' in args: @@ -536,6 +537,7 @@ def YamlCompare(world: "ManualWorld", multiworld: MultiWorld, state: CollectionS initial_option_name = str(option_name).strip() #For exception messages option_name = format_to_valid_identifier(option_name) + # Detect !reversing of result like yaml_option if option_name.startswith('!'): reverse_result = not reverse_result option_name = option_name.lstrip('!') @@ -550,10 +552,10 @@ def YamlCompare(world: "ManualWorld", multiworld: MultiWorld, state: CollectionS if not value: #empty string '' raise ValueError(f"Could not find a valid value to compare against in given string '{args}'. \nThere must be a value to compare against after the comparator (in this case '{comparator}').") - if not skipCache: + if not skipCache: #Cache made for optimization purposes cacheindex = option_name + '_' + comp_symbols[comparator].__name__ + '_' + format_to_valid_identifier(value.lower()) - if not hasattr(world, 'yaml_compare_rule_cache'): #Cache made for optimization purposes + if not hasattr(world, 'yaml_compare_rule_cache'): world.yaml_compare_rule_cache = dict[str,bool]() if skipCache or world.yaml_compare_rule_cache.get(cacheindex, None) is None: From 20c22a1947700adb3a299baba5c04b787729430d Mon Sep 17 00:00:00 2001 From: nicopop <6759630+nicopop@users.noreply.github.com> Date: Tue, 28 Jan 2025 21:01:22 -0500 Subject: [PATCH 14/15] make < > complements of >= and <= respectively --- src/Rules.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Rules.py b/src/Rules.py index 824be0d0..d4304b38 100644 --- a/src/Rules.py +++ b/src/Rules.py @@ -507,8 +507,8 @@ def YamlCompare(world: "ManualWorld", multiworld: MultiWorld, state: CollectionS '>=' : ge, '<=' : le, '=': eq, #Alternate to be like yaml_option - '<' : lt, - '>' : gt, + '<' : ge, #complement of >= + '>' : le, #complement of <= } reverse_result = False @@ -525,10 +525,12 @@ def YamlCompare(world: "ManualWorld", multiworld: MultiWorld, state: CollectionS comparator = '<=' elif '=' in args: comparator = '=' - elif '>' in args: - comparator = '>' elif '<' in args: comparator = '<' + reverse_result = True #complement of >= + elif '>' in args: + comparator = '>' + reverse_result = True #complement of <= else: raise ValueError(f"Could not find a valid comparator in given string '{args}', it must be one of {comp_symbols.keys()}") From ea12578f8fa7cda71830704e1edca95cd7c42f8b Mon Sep 17 00:00:00 2001 From: nicopop <6759630+nicopop@users.noreply.github.com> Date: Wed, 29 Jan 2025 15:03:05 -0500 Subject: [PATCH 15/15] small tweaks --- docs/syntax/requires.md | 2 +- src/Rules.py | 2 +- src/data/locations.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/syntax/requires.md b/docs/syntax/requires.md index 531e8189..e473e43f 100644 --- a/docs/syntax/requires.md +++ b/docs/syntax/requires.md @@ -174,7 +174,7 @@ You can even combine the two in complex ways Verify that the result of the option called _option_name_'s value compared using the _comparator_symbol_ with the requested _value_ -The comparator symbol can be any of the following: `==, !=, >=, <=, <, >` +The comparator symbol can be any of the following: `== or =, !=, >=, <=, <, >` The value can be of any type that your option supports diff --git a/src/Rules.py b/src/Rules.py index d4304b38..a22ca296 100644 --- a/src/Rules.py +++ b/src/Rules.py @@ -1,6 +1,6 @@ from typing import TYPE_CHECKING, Optional from enum import IntEnum -from operator import eq, ge, le, lt, gt +from operator import eq, ge, le from .Regions import regionMap from .hooks import Rules diff --git a/src/data/locations.json b/src/data/locations.json index 1672f7ff..afbcf69e 100644 --- a/src/data/locations.json +++ b/src/data/locations.json @@ -9,7 +9,7 @@ { "name": "Beat the Game - Chun-Li", "category": ["Starter Team", "Left Side"], - "requires": "|Chun-Li| or {YamlCompare(Example_Choice ==)}" + "requires": "|Chun-Li| or {YamlCompare(Example_Choice == 1)}" }, { "name": "Beat the Game - Akuma",