Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
32 changes: 30 additions & 2 deletions docs/syntax/requires.md
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -166,3 +168,29 @@ 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: `== or =, !=, >=, <=, <, >`

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)}"
}
```
113 changes: 113 additions & 0 deletions src/Rules.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from typing import TYPE_CHECKING, Optional
from enum import IntEnum
from operator import eq, ge, le

from .Regions import regionMap
from .hooks import Rules
Expand All @@ -8,6 +9,7 @@
from BaseClasses import MultiWorld, CollectionState
from worlds.AutoWorld import World
from worlds.generic.Rules import set_rule, add_rule
from Options import Choice, Toggle, Range, NamedRange

import re
import math
Expand Down Expand Up @@ -493,3 +495,114 @@ 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, 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)}
\nWhere == can be any of the following: ==, !=, >=, <=, <, >
\nExample: {YamlCompare(Example_Range > 5)}"""
comp_symbols = { #Maybe find a better name for this
'==' : eq,
'!=' : eq, #complement of ==
'>=' : ge,
'<=' : le,
'=': eq, #Alternate to be like yaml_option
'<' : ge, #complement of >=
'>' : le, #complement of <=
}

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 #complement of == thus reverse by default
elif '>=' in args:
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()}")

option_name, value = args.split(comparator)

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('!')
initial_option_name = initial_option_name.lstrip('!')

value = value.strip()

option = getattr(world.options, option_name, None)
if option is None:
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: #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'):
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))

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:\
\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 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 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}"')

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

6 changes: 3 additions & 3 deletions src/data/locations.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 == 1)}"
},
{
"name": "Beat the Game - Akuma",
"category": ["Starter Team", "Left Side"],
"requires": "|Akuma|"
"requires": "|Akuma| or {YamlCompare(Example_Toggle != True)}"
},

{
Expand Down
1 change: 1 addition & 0 deletions src/data/regions.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down