Initial Commit
This commit is contained in:
commit
fa38469735
30 files changed
+6902
No files matched your search
+370
@@ -0,0 +1,370 @@
|
||||
import ast
|
||||
import csv
|
||||
import pkgutil
|
||||
import json
|
||||
import re
|
||||
|
||||
from BaseClasses import MultiWorld, Item, ItemClassification
|
||||
from enum import IntEnum
|
||||
from typing import Optional, List, Union, get_args, get_origin, Any
|
||||
from types import GenericAlias
|
||||
from worlds.AutoWorld import World
|
||||
from .hooks.Helpers import before_is_category_enabled, before_is_item_enabled, before_is_location_enabled, before_is_event_enabled
|
||||
|
||||
|
||||
# blatantly copied from the minecraft ap world because why not
|
||||
def load_data_file(*args) -> dict:
|
||||
fname = "/".join(["data", *args])
|
||||
|
||||
try:
|
||||
filedata = json.loads(pkgutil.get_data(__name__, fname).decode())
|
||||
except:
|
||||
filedata = []
|
||||
|
||||
return filedata
|
||||
|
||||
def load_data_csv(*args) -> list[dict]:
|
||||
fname = "/".join(["data", *args])
|
||||
|
||||
try:
|
||||
lines = pkgutil.get_data(__name__, fname).decode().splitlines()
|
||||
except:
|
||||
lines = []
|
||||
filedata = list(csv.DictReader(lines))
|
||||
|
||||
return filedata
|
||||
|
||||
def is_option_enabled(multiworld: MultiWorld, player: int, name: str) -> bool:
|
||||
return get_option_value(multiworld, player, name) > 0
|
||||
|
||||
def get_option_value(multiworld: MultiWorld, player: int, name: str) -> Union[int, dict]:
|
||||
option = getattr(multiworld.worlds[player].options, name, None)
|
||||
if option is None:
|
||||
return 0
|
||||
|
||||
return option.value
|
||||
|
||||
def clamp(value, min, max):
|
||||
"""Returns value clamped to the inclusive range of min and max"""
|
||||
if value < min:
|
||||
return min
|
||||
elif value > max:
|
||||
return max
|
||||
else:
|
||||
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)
|
||||
|
||||
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
|
||||
if option_name.startswith("!"):
|
||||
option_name = option_name[1:]
|
||||
required = False
|
||||
|
||||
option_name = format_to_valid_identifier(option_name)
|
||||
if is_option_enabled(multiworld, player, option_name) != required:
|
||||
return False
|
||||
return True
|
||||
|
||||
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."""
|
||||
item = multiworld.worlds[player].item_name_to_item.get(item_name, {})
|
||||
if not item:
|
||||
return False
|
||||
|
||||
return is_item_enabled(multiworld, player, item)
|
||||
|
||||
def is_item_enabled(multiworld: MultiWorld, player: int, item: dict[str, Any]) -> 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)
|
||||
|
||||
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."""
|
||||
location = multiworld.worlds[player].location_name_to_location.get(location_name, {})
|
||||
if not location:
|
||||
return False
|
||||
|
||||
return is_location_enabled(multiworld, player, location)
|
||||
|
||||
def is_location_enabled(multiworld: MultiWorld, player: int, location: dict[str, Any]) -> bool:
|
||||
"""Check if a location has been disabled by a yaml option."""
|
||||
if getattr(multiworld, "generation_is_fake", False):
|
||||
# When UT is doing a fake gen, give it everything
|
||||
return True
|
||||
|
||||
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)
|
||||
|
||||
def is_event_enabled(multiworld: MultiWorld, player: int, event: dict[str, Any]) -> bool:
|
||||
"""Check if an event has been disabled by a yaml option."""
|
||||
hook_result = before_is_event_enabled(multiworld, player, event)
|
||||
if hook_result is not None:
|
||||
return hook_result
|
||||
|
||||
return _is_manualobject_enabled(multiworld, player, event)
|
||||
|
||||
def _is_manualobject_enabled(multiworld: MultiWorld, player: int, object: dict[str, 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
|
||||
|
||||
def get_items_for_player(multiworld: MultiWorld, player: int, includePrecollected: bool = False) -> List[Item]:
|
||||
"""Return list of items of a player including placed items"""
|
||||
items = [i for i in multiworld.get_items() if i.player == player]
|
||||
if includePrecollected:
|
||||
items.extend(multiworld.precollected_items.get(player, []))
|
||||
return items
|
||||
|
||||
def reset_specific_item_value_cache_for_player(world: World, value: str, player: Optional[int] = None) -> dict[str, int]:
|
||||
if player is None:
|
||||
player = world.player
|
||||
return world.item_values[player].pop(value, {})
|
||||
|
||||
def reset_item_value_cache_for_player(world: World, player: Optional[int] = None):
|
||||
if player is None:
|
||||
player = world.player
|
||||
world.item_values[player] = {}
|
||||
|
||||
def get_items_with_value(world: World, multiworld: MultiWorld, value: str, player: Optional[int] = None, skipCache: bool = False) -> dict[str, int]:
|
||||
"""Return a dict of every items with a specific value type present in their respective 'value' dict\n
|
||||
Output in the format 'Item Name': 'value count'\n
|
||||
Keep a cache of the result, it can be skipped with 'skipCache == True'\n
|
||||
To force a Reset of the player's cache of a value use either reset_specific_item_value_cache_for_player or reset_item_value_cache_for_player
|
||||
"""
|
||||
if player is None:
|
||||
player = world.player
|
||||
|
||||
player_items = get_items_for_player(multiworld, player, True)
|
||||
# Just a small check to prevent caching {} if items don't exist yet
|
||||
if not player_items:
|
||||
return {value: -1}
|
||||
|
||||
value = value.lower().strip()
|
||||
|
||||
if not skipCache:
|
||||
if not hasattr(world, 'item_values'): #Cache of just the item values
|
||||
world.item_values = {}
|
||||
|
||||
if not world.item_values.get(player):
|
||||
world.item_values[player] = {}
|
||||
|
||||
if value not in world.item_values.get(player, {}).keys() or skipCache:
|
||||
item_with_values = {i.name: world.item_name_to_item[i.name]['value'].get(value, 0)
|
||||
for i in player_items if i.code is not None
|
||||
and i.name in world.item_name_groups.get(f'has_{value}_value', [])}
|
||||
if skipCache:
|
||||
return item_with_values
|
||||
world.item_values[player][value] = item_with_values
|
||||
return world.item_values[player].get(value)
|
||||
|
||||
|
||||
def filter_used_regions(player_regions: dict|list) -> set:
|
||||
"""Return a set of regions that are actually used in Generation. It includes region that have no locations but are required by other regions\n
|
||||
The dict version of the player_regions must be in the format: dict(region name str: region)
|
||||
"""
|
||||
used_regions = set()
|
||||
|
||||
if isinstance(player_regions, list):
|
||||
player_regions = {r.name: r for r in player_regions}
|
||||
|
||||
#Grab all the player's regions and take note of those with locations
|
||||
for region in player_regions.values():
|
||||
if region.locations:
|
||||
used_regions.add(region)
|
||||
|
||||
#Check every known region with location for parent regions
|
||||
checked_parent = []
|
||||
for region in set(used_regions):
|
||||
def checkParent(parent_region):
|
||||
if parent_region.name in checked_parent: #dont check a region twice
|
||||
return
|
||||
checked_parent.append(parent_region.name)
|
||||
used_regions.add(parent_region)
|
||||
for entrance in parent_region.entrances:
|
||||
if player_regions.get(entrance.parent_region.name):
|
||||
checkParent(entrance.parent_region)
|
||||
return
|
||||
checkParent(region)
|
||||
return used_regions
|
||||
|
||||
def convert_to_long_string(input: str | list[str]) -> str:
|
||||
"""Verify that the input is a str. If it's a list[str] then it combine them into a str in a way that works with yaml template/website options descriptions"""
|
||||
if not isinstance(input, str):
|
||||
return str.join("\n ", input)
|
||||
return input
|
||||
|
||||
def format_to_valid_identifier(input: str) -> str:
|
||||
"""Make sure the input is a valid python identifier"""
|
||||
from keyword import iskeyword
|
||||
input = input.strip()
|
||||
if input.isidentifier() and not iskeyword(input):
|
||||
return input
|
||||
|
||||
if iskeyword(input):
|
||||
input = "_" + input
|
||||
# if its already a valid keyword no need to check all its characters
|
||||
return input
|
||||
|
||||
if input[:1].isdecimal():
|
||||
input = "_" + input
|
||||
|
||||
input = "".join([c if f"_{c}".isidentifier() else "_" for c in input])
|
||||
|
||||
return input
|
||||
|
||||
def remove_specific_item(source: list[Item], item: Item) -> Item:
|
||||
"""Remove and return an item from a list in a more precise way, base AP only check for name and player id before removing.
|
||||
\nThis checks that the item IS the exact same in the list.
|
||||
\nRaise ValueError if the item is not in the list."""
|
||||
# Inspired by https://stackoverflow.com/a/58761459
|
||||
for i in range(len(source)): # check all elements of the list like a normal remove does
|
||||
if item is source[i]:
|
||||
return source.pop(i)
|
||||
|
||||
# if we reach here we didn't get any item
|
||||
raise ValueError(f"Item '{item.name}' could not be found in source list")
|
||||
|
||||
class ProgItemsCat(IntEnum):
|
||||
VALUE = 1
|
||||
CATEGORY = 2
|
||||
|
||||
def format_state_prog_items_key(category: str|ProgItemsCat ,key: str) -> str:
|
||||
"""Convert the inputted key to the format used in state.has(key) to check/set the count of an item_value.
|
||||
Using either one of the predefined categories or a custom string.
|
||||
|
||||
Example: Coin -> MANUAL_VALUE_coin
|
||||
"""
|
||||
if isinstance(category, str):
|
||||
cat_key = format_to_valid_identifier(category.upper())
|
||||
else:
|
||||
cat_key = category.name
|
||||
|
||||
return f"MANUAL_{cat_key}_{format_to_valid_identifier(key.lower())}"
|
||||
|
||||
def convert_string_to_itemclassification(string: str) -> ItemClassification:
|
||||
def stringCheck(string):
|
||||
if string.isdigit():
|
||||
true_class = ItemClassification(int(string))
|
||||
elif string.startswith('0b'):
|
||||
true_class = ItemClassification(int(string, base=0))
|
||||
else:
|
||||
true_class = ItemClassification[string]
|
||||
return true_class
|
||||
|
||||
if "+" in string or "," in string:
|
||||
true_class = ItemClassification.filler
|
||||
for substring in re.split(r'[+,]', string):
|
||||
true_class |= stringCheck(substring.strip())
|
||||
|
||||
else:
|
||||
true_class = stringCheck(string)
|
||||
return true_class
|
||||
|
||||
def convert_string_to_type(input: str, target_type: type) -> Any:
|
||||
"""Take a string and attempt to convert it to {target_type}
|
||||
\ntarget_type can be a single type(ex. str), an union (int|str), an Optional type (Optional[str]) or a combo of any of those (Optional[int|str])
|
||||
\nSpecial logic:
|
||||
- When target_type is Optional or contains None: it will check if input.lower() is "none"
|
||||
- When target_type contains bool: it will check if input.lower() is "true", "1", "false" or "0"
|
||||
- If bool is the last type in target_type it also run the input directly through bool(input) if previous fails
|
||||
\nif you want this to possibly fail without Exceptions include str in target_type, your input should get returned if all the other conversions fails
|
||||
"""
|
||||
def checktype(target_type, found_types: list):
|
||||
if issubclass(type(target_type), type): #is it a single type (str, list, etc)
|
||||
if target_type not in found_types:
|
||||
found_types.append(target_type)
|
||||
|
||||
elif issubclass(type(target_type), GenericAlias): #is it something like list[str] and dict{str:int}
|
||||
if target_type not in found_types and get_origin(target_type) not in found_types: #dont add 'dict[str]' if we already search for 'dict'
|
||||
found_types.append(target_type)
|
||||
|
||||
elif issubclass(type(target_type), type(str|int)) \
|
||||
or issubclass(type(target_type), type(Union[str|int])): #Support both version of Union, and Optional and other alike
|
||||
for arg in get_args(target_type):
|
||||
checktype(arg, found_types)
|
||||
|
||||
else:
|
||||
raise Exception(f"'{value}' cannot be converted to {target_type} since its not a supported type \nAsk about it in #Manual-support and it might be added.")
|
||||
|
||||
found_types = []
|
||||
checktype(target_type, found_types)
|
||||
|
||||
if str in found_types: #do it last
|
||||
found_types.remove(str)
|
||||
found_types.append(str)
|
||||
|
||||
value = input.strip()
|
||||
i = 0
|
||||
errors = []
|
||||
for value_type in found_types:
|
||||
i += 1
|
||||
if issubclass(value_type, type(None)):
|
||||
if value.lower() == 'none':
|
||||
return None
|
||||
errors.append(str(value_type) + ": value was not 'none'")
|
||||
|
||||
elif issubclass(value_type, bool):
|
||||
if value.lower() in ['true', '1', 'on']:
|
||||
return True
|
||||
|
||||
elif value.lower() in ['false', '0', 'off']:
|
||||
return False
|
||||
|
||||
else:
|
||||
if i == len(found_types):
|
||||
return value_type(value) #if its the last type might as well try and convert to bool
|
||||
errors.append(str(value_type) + ": value was not in either ['true', '1', 'on'] or ['false', '0', 'off']")
|
||||
|
||||
elif issubclass(value_type, list) or issubclass(value_type, dict) \
|
||||
or issubclass(value_type, set) or issubclass(type(value_type), GenericAlias):
|
||||
try:
|
||||
try:
|
||||
converted_value = ast.literal_eval(value)
|
||||
except ValueError as e:
|
||||
# The ValueError from ast when the string cannot be evaluated as a literal is usually something like
|
||||
# "malformed node or string on line 1: <ast.Name object at 0x000001AEBBCC7590>", which is not
|
||||
# helpful, so re-raise with a better exception message.
|
||||
raise ValueError(f"'{value}' could not be evaluated as a literal") from e
|
||||
|
||||
compareto = get_origin(value_type) if issubclass(type(value_type), GenericAlias) else value_type
|
||||
if issubclass(compareto, type(converted_value)):
|
||||
return converted_value
|
||||
else:
|
||||
errors.append(str(value_type) + f": value '{value}' was not a valid {str(compareto)}")
|
||||
except Exception as e:
|
||||
errors.append(str(value_type) + ": " + str(e))
|
||||
continue
|
||||
else:
|
||||
try:
|
||||
return value_type(value)
|
||||
|
||||
except Exception as e:
|
||||
errors.append(str(value_type) + ": " + str(e))
|
||||
continue
|
||||
|
||||
newline = "\n"
|
||||
raise Exception(f"'{value}' could not be converted to {target_type}, here's the conversion failure message(s):\n\n{newline.join([' - ' + str(validation_error) for validation_error in errors])}\n\n")
|
||||
Reference in new issue
Block a user