Initial Commit
This commit is contained in:
commit
fa38469735
30 files changed
+6902
No files matched your search
@@ -0,0 +1,109 @@
|
|||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from .DataValidation import DataValidation, ValidationError
|
||||||
|
from .Helpers import load_data_file as helpers_load_data_file
|
||||||
|
|
||||||
|
from .hooks.Data import \
|
||||||
|
after_load_game_file, \
|
||||||
|
after_load_item_file, after_load_location_file, \
|
||||||
|
after_load_event_file, \
|
||||||
|
after_load_region_file, after_load_category_file, \
|
||||||
|
after_load_option_file, after_load_meta_file
|
||||||
|
|
||||||
|
# blatantly copied from the minecraft ap world because why not
|
||||||
|
def load_data_file(*args) -> dict:
|
||||||
|
logging.warning("Deprecated usage of importing load_data_file from Data.py uses the one from Helper.py instead")
|
||||||
|
return helpers_load_data_file(*args)
|
||||||
|
|
||||||
|
def convert_to_list(data, property_name: str) -> list:
|
||||||
|
if isinstance(data, dict):
|
||||||
|
data = data.get(property_name, [])
|
||||||
|
return data
|
||||||
|
|
||||||
|
class ManualFile:
|
||||||
|
filename: str
|
||||||
|
data_type: dict|list
|
||||||
|
|
||||||
|
def __init__(self, filename, data_type):
|
||||||
|
self.filename = filename
|
||||||
|
self.data_type = data_type
|
||||||
|
|
||||||
|
def load(self):
|
||||||
|
contents = helpers_load_data_file(self.filename)
|
||||||
|
|
||||||
|
if not contents and type(contents) != self.data_type:
|
||||||
|
return self.data_type()
|
||||||
|
|
||||||
|
return contents
|
||||||
|
|
||||||
|
|
||||||
|
game_table: dict[str, Any] = ManualFile('game.json', dict).load() #dict
|
||||||
|
item_table: list[dict[str, Any]] = convert_to_list(ManualFile('items.json', list).load(), 'data') #list
|
||||||
|
location_table: list[dict[str, Any]] = convert_to_list(ManualFile('locations.json', list).load(), 'data') #list
|
||||||
|
event_table: list[dict[str, Any]] = convert_to_list(ManualFile('events.json', list).load(), 'data') #list
|
||||||
|
region_table: dict[str, Any] = ManualFile('regions.json', dict).load() #dict
|
||||||
|
category_table: dict[str, Any] = ManualFile('categories.json', dict).load() #dict
|
||||||
|
option_table: dict[str, Any] = ManualFile('options.json', dict).load() #dict
|
||||||
|
meta_table: dict[str, Any] = ManualFile('meta.json', dict).load() #dict
|
||||||
|
|
||||||
|
# Removal of schemas in root of tables
|
||||||
|
region_table.pop('$schema', '')
|
||||||
|
category_table.pop('$schema', '')
|
||||||
|
|
||||||
|
# hooks
|
||||||
|
game_table = after_load_game_file(game_table)
|
||||||
|
item_table = after_load_item_file(item_table)
|
||||||
|
location_table = after_load_location_file(location_table)
|
||||||
|
event_table = after_load_event_file(event_table)
|
||||||
|
region_table = after_load_region_file(region_table)
|
||||||
|
category_table = after_load_category_file(category_table)
|
||||||
|
option_table = after_load_option_file(option_table)
|
||||||
|
meta_table = after_load_meta_file(meta_table)
|
||||||
|
|
||||||
|
# seed all of the tables for validation
|
||||||
|
DataValidation.game_table = game_table
|
||||||
|
DataValidation.item_table = item_table
|
||||||
|
DataValidation.location_table = location_table
|
||||||
|
DataValidation.event_table = event_table
|
||||||
|
DataValidation.region_table = region_table
|
||||||
|
|
||||||
|
# Add UT's glitch item if required
|
||||||
|
if game_table.get("glitches_item_name"):
|
||||||
|
DataValidation.event_table = DataValidation.event_table + [{"name": game_table["glitches_item_name"]}]
|
||||||
|
|
||||||
|
# might as well save this for other uses in tests
|
||||||
|
DataValidation.location_name_to_location = {l.get("name", f"unknown location {key}"): l for key, l in enumerate(DataValidation.location_table)}
|
||||||
|
# since "copy_location" just changes data its handled here to simplify things
|
||||||
|
for key, event in enumerate(event_table):
|
||||||
|
if "copy_location" in event:
|
||||||
|
if event["copy_location"] not in DataValidation.location_name_to_location.keys():
|
||||||
|
raise KeyError(f"Event {event.get('name', f'unnamed event #{key}')} tried to copy a location named {event['copy_location']} but its misspelled or does not exist.")
|
||||||
|
|
||||||
|
event_table[key] = DataValidation.location_name_to_location[event["copy_location"]] | event
|
||||||
|
DataValidation.event_table[key] = event_table[key]
|
||||||
|
|
||||||
|
DataValidation.item_table_with_events = DataValidation.item_table + DataValidation.event_table
|
||||||
|
DataValidation.location_table_with_events = DataValidation.location_table + DataValidation.event_table
|
||||||
|
|
||||||
|
validation_errors = []
|
||||||
|
|
||||||
|
# check that json files are not just invalid json
|
||||||
|
try: DataValidation.checkForGameBeingInvalidJSON()
|
||||||
|
except ValidationError as e: validation_errors.append(e)
|
||||||
|
|
||||||
|
try: DataValidation.checkForItemsBeingInvalidJSON()
|
||||||
|
except ValidationError as e: validation_errors.append(e)
|
||||||
|
|
||||||
|
try: DataValidation.checkForLocationsBeingInvalidJSON()
|
||||||
|
except ValidationError as e: validation_errors.append(e)
|
||||||
|
|
||||||
|
|
||||||
|
############
|
||||||
|
# If there are any validation errors, display all of them at once
|
||||||
|
############
|
||||||
|
|
||||||
|
if len(validation_errors) > 0:
|
||||||
|
logging.error("\nValidationError(s): \n\n%s\n\n" % ("\n".join([' - ' + str(validation_error) for validation_error in validation_errors])))
|
||||||
|
print("\n\nYou can close this window.\n")
|
||||||
|
keeping_terminal_open = input("If you are running from a terminal, press Ctrl-C followed by ENTER to break execution.")
|
||||||
@@ -0,0 +1,573 @@
|
|||||||
|
import logging
|
||||||
|
import re
|
||||||
|
import json
|
||||||
|
from worlds.AutoWorld import World
|
||||||
|
from BaseClasses import MultiWorld, ItemClassification
|
||||||
|
from typing import Any, Counter
|
||||||
|
|
||||||
|
from .Helpers import convert_string_to_itemclassification
|
||||||
|
|
||||||
|
class ValidationError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class DataValidation():
|
||||||
|
game_table: dict[str, Any] = {}
|
||||||
|
item_table: list[dict[str, Any]] = []
|
||||||
|
item_table_with_events: list[dict[str, Any]] = []
|
||||||
|
event_table: list[dict[str, Any]] = []
|
||||||
|
location_table: list[dict[str, Any]] = []
|
||||||
|
region_table: dict[str, Any] = {}
|
||||||
|
location_table_with_events: list[dict[str, Any]] = []
|
||||||
|
location_name_to_location: dict[str, dict[str, Any]] = {}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def checkItemNamesInLocationRequires():
|
||||||
|
from .Rules import ITEM_REGEX
|
||||||
|
for location in DataValidation.location_table_with_events:
|
||||||
|
if "requires" not in location:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if isinstance(location["requires"], str):
|
||||||
|
# parse user written statement into list of each item
|
||||||
|
for match in ITEM_REGEX.finditer(location["requires"]):
|
||||||
|
is_category = bool(match.group(1))
|
||||||
|
item_name = match.group(2)
|
||||||
|
item_count = (str(match.group(3) or "1")).lstrip(':').strip()
|
||||||
|
|
||||||
|
if not item_count.isnumeric() and item_count.lower() not in ["all", "half"] and not item_count.endswith('%'):
|
||||||
|
item_name = match.group(0).strip("|")
|
||||||
|
logging.debug(f'Invalid item_count "{item_count}" found, reverting to initial item_name "{item_name}"')
|
||||||
|
|
||||||
|
# if it's a category, validate that the category exists
|
||||||
|
if is_category:
|
||||||
|
item_category_exists = len([item for item in DataValidation.item_table_with_events if item_name in item.get('category', [])]) > 0
|
||||||
|
|
||||||
|
if not item_category_exists:
|
||||||
|
raise ValidationError("Item category %s is required by location %s but is misspelled or does not exist." % (item_name, location.get("name")))
|
||||||
|
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
item_exists = len([item.get("name") for item in DataValidation.item_table_with_events if item.get("name") == item_name]) > 0
|
||||||
|
|
||||||
|
if not item_exists:
|
||||||
|
raise ValidationError("Item %s is required by location %s but is misspelled or does not exist." % (item_name, location.get("name")))
|
||||||
|
|
||||||
|
else: # item access is in dict form
|
||||||
|
for item in location["requires"]:
|
||||||
|
# if the require entry is an object with "or" or a list of items, treat it as a standalone require of its own
|
||||||
|
if (isinstance(item, dict) and "or" in item and isinstance(item["or"], list)) or (isinstance(item, list)):
|
||||||
|
or_items = item
|
||||||
|
|
||||||
|
if isinstance(item, dict):
|
||||||
|
or_items = item["or"]
|
||||||
|
|
||||||
|
for or_item in or_items:
|
||||||
|
or_item_parts = or_item.split(":")
|
||||||
|
or_item_name = or_item
|
||||||
|
|
||||||
|
if len(or_item_parts) > 1:
|
||||||
|
or_item_name = or_item_parts[0]
|
||||||
|
|
||||||
|
item_exists = len([item.get("name") for item in DataValidation.item_table_with_events if item.get("name") == or_item_name]) > 0
|
||||||
|
|
||||||
|
if not item_exists:
|
||||||
|
raise ValidationError("Item %s is required by location %s but is misspelled or does not exist." % (or_item_name, location["name"]))
|
||||||
|
else:
|
||||||
|
item_parts = item.split(":")
|
||||||
|
item_name = item
|
||||||
|
|
||||||
|
if len(item_parts) > 1:
|
||||||
|
item_name = item_parts[0]
|
||||||
|
|
||||||
|
item_exists = len([item.get("name") for item in DataValidation.item_table_with_events if item.get("name") == item_name]) > 0
|
||||||
|
|
||||||
|
if not item_exists:
|
||||||
|
raise ValidationError("Item %s is required by location %s but is misspelled or does not exist." % (item_name, location["name"]))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def checkItemNamesInRegionRequires():
|
||||||
|
from .Rules import ITEM_REGEX
|
||||||
|
for region_name in DataValidation.region_table:
|
||||||
|
region = DataValidation.region_table[region_name]
|
||||||
|
|
||||||
|
if "requires" not in region:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if isinstance(region["requires"], str):
|
||||||
|
# parse user written statement into list of each item
|
||||||
|
for match in ITEM_REGEX.finditer(region["requires"]):
|
||||||
|
is_category = bool(match.group(1))
|
||||||
|
item_name = match.group(2)
|
||||||
|
item_count = (str(match.group(3) or "1")).lstrip(':').strip()
|
||||||
|
|
||||||
|
if not item_count.isnumeric() and item_count.lower() not in ["all", "half"] and not item_count.endswith('%'):
|
||||||
|
item_name = match.group(0).strip("|")
|
||||||
|
logging.debug(f'Invalid item_count "{item_count}" found, reverting to initial item_name "{item_name}"')
|
||||||
|
|
||||||
|
# if it's a category, validate that the category exists
|
||||||
|
if is_category:
|
||||||
|
item_category_exists = len([item for item in DataValidation.item_table_with_events if item_name in item.get('category', [])]) > 0
|
||||||
|
|
||||||
|
if not item_category_exists:
|
||||||
|
raise ValidationError("Item category %s is required by region %s but is misspelled or does not exist." % (item_name, region_name))
|
||||||
|
|
||||||
|
else:
|
||||||
|
item_exists = len([item.get("name") for item in DataValidation.item_table_with_events if item.get("name") == item_name]) > 0
|
||||||
|
|
||||||
|
if not item_exists:
|
||||||
|
raise ValidationError("Item %s is required by region %s but is misspelled or does not exist." % (item_name, region_name))
|
||||||
|
|
||||||
|
else: # item access is in dict form
|
||||||
|
for item in region["requires"]:
|
||||||
|
# if the require entry is an object with "or" or a list of items, treat it as a standalone require of its own
|
||||||
|
if (isinstance(item, dict) and "or" in item and isinstance(item["or"], list)) or (isinstance(item, list)):
|
||||||
|
or_items = item
|
||||||
|
|
||||||
|
if isinstance(item, dict):
|
||||||
|
or_items = item["or"]
|
||||||
|
|
||||||
|
for or_item in or_items:
|
||||||
|
or_item_parts = or_item.split(":")
|
||||||
|
or_item_name = or_item
|
||||||
|
|
||||||
|
if len(or_item_parts) > 1:
|
||||||
|
or_item_name = or_item_parts[0]
|
||||||
|
|
||||||
|
item_exists = len([item.get("name") for item in DataValidation.item_table_with_events if item.get("name") == or_item_name]) > 0
|
||||||
|
|
||||||
|
if not item_exists:
|
||||||
|
raise ValidationError("Item %s is required by region %s but is misspelled or does not exist." % (or_item_name, region_name))
|
||||||
|
else:
|
||||||
|
item_parts = item.split(":")
|
||||||
|
item_name = item
|
||||||
|
|
||||||
|
if len(item_parts) > 1:
|
||||||
|
item_name = item_parts[0]
|
||||||
|
|
||||||
|
item_exists = len([item.get("name") for item in DataValidation.item_table_with_events if item.get("name") == item_name]) > 0
|
||||||
|
|
||||||
|
if not item_exists:
|
||||||
|
raise ValidationError("Item %s is required by region %s but is misspelled or does not exist." % (item_name, region_name))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def checkRegionNamesInLocations():
|
||||||
|
for location in DataValidation.location_table_with_events:
|
||||||
|
if "region" not in location or location["region"] in ["Menu", "Manual"]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
region_exists = len([name for name in DataValidation.region_table if name == location["region"]]) > 0
|
||||||
|
|
||||||
|
if not region_exists:
|
||||||
|
raise ValidationError("Region %s is set for location %s, but the region is misspelled or does not exist." % (location["region"], location["name"]))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def checkItemsHasValidClassificationCount():
|
||||||
|
for item in DataValidation.item_table:
|
||||||
|
if not item.get("classification_count"):
|
||||||
|
continue
|
||||||
|
for cat, count in item["classification_count"].items():
|
||||||
|
cat = str(cat)
|
||||||
|
if count == 0:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
convert_string_to_itemclassification(cat)
|
||||||
|
|
||||||
|
except KeyError as ex:
|
||||||
|
raise ValidationError(f"Item '{item.get('name', '')}''s classification_count '{cat}' is misspelled or does not exist.\n Valid names are {', '.join(ItemClassification.__members__.keys())} \n\n{type(ex).__name__}:{ex}")
|
||||||
|
except Exception as ex:
|
||||||
|
raise ValidationError(f"Item '{item.get('name', '')}''s classification_count '{cat}' was improperly defined\n\n{type(ex).__name__}:{ex}")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def checkItemsThatShouldBeRequired():
|
||||||
|
for item in DataValidation.item_table:
|
||||||
|
# if the item is already progression, no need to check
|
||||||
|
if item.get("progression"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# progression_skip_balancing is also progression, so no check needed
|
||||||
|
if item.get("progression_skip_balancing"):
|
||||||
|
continue
|
||||||
|
# if any of the advanced type is already progression then no check needed
|
||||||
|
if item.get("classification_count"):
|
||||||
|
has_progression = False
|
||||||
|
for cat, count in item["classification_count"].items():
|
||||||
|
cat = str(cat)
|
||||||
|
if count == 0:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
true_class = convert_string_to_itemclassification(cat)
|
||||||
|
|
||||||
|
except:
|
||||||
|
# Skip since this validation error is dealt with in checkItemsHasValidClassificationCount
|
||||||
|
true_class = ItemClassification.filler
|
||||||
|
if ItemClassification.progression in true_class:
|
||||||
|
has_progression = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if has_progression:
|
||||||
|
continue
|
||||||
|
# check location requires for the presence of item name
|
||||||
|
for location in DataValidation.location_table_with_events:
|
||||||
|
if "requires" not in location:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# convert to json so we don't have to guess the data type
|
||||||
|
location_requires = json.dumps(location["requires"])
|
||||||
|
|
||||||
|
# if boolean, else legacy
|
||||||
|
if isinstance(location_requires, str):
|
||||||
|
if '|{}|'.format(item.get("name")) in location_requires:
|
||||||
|
raise ValidationError("Item %s is required by location %s, but the item is not marked as progression." % (item.get("name"), location["name"]))
|
||||||
|
else:
|
||||||
|
if item.get("name") in location_requires:
|
||||||
|
raise ValidationError("Item %s is required by location %s, but the item is not marked as progression." % (item.get("name"), location["name"]))
|
||||||
|
|
||||||
|
# check region requires for the presence of item name
|
||||||
|
for region_name in DataValidation.region_table:
|
||||||
|
region = DataValidation.region_table[region_name]
|
||||||
|
|
||||||
|
if "requires" not in region:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# convert to json so we don't have to guess the data type
|
||||||
|
region_requires = json.dumps(region["requires"])
|
||||||
|
|
||||||
|
# if boolean, else legacy
|
||||||
|
if isinstance(region_requires, str):
|
||||||
|
if '|{}|'.format(item.get("name")) in region_requires:
|
||||||
|
raise ValidationError("Item %s is required by region %s, but the item is not marked as progression." % (item.get("name"), region_name))
|
||||||
|
else:
|
||||||
|
if item.get("name") in region_requires:
|
||||||
|
raise ValidationError("Item %s is required by region %s, but the item is not marked as progression." % (item.get("name"), region_name))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _checkLocationRequiresForItemValueWithRegex(values_requested: dict[str, int], requires) -> dict[str, int]:
|
||||||
|
if isinstance(requires, str) and 'ItemValue' in requires:
|
||||||
|
for result in re.findall(r'\{ItemValue\(([^:]*)\:(.*?)\)\}', requires):
|
||||||
|
value = result[0].lower().strip()
|
||||||
|
count = int(result[1].split(",")[0])
|
||||||
|
if not values_requested.get(value):
|
||||||
|
values_requested[value] = count
|
||||||
|
else:
|
||||||
|
values_requested[value] = max(values_requested[value], count)
|
||||||
|
return values_requested
|
||||||
|
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def preFillCheckIfEnoughItemsForValue(world: World, multiworld: MultiWorld):
|
||||||
|
from .Helpers import get_items_with_value, get_items_for_player, filter_used_regions
|
||||||
|
player = world.player
|
||||||
|
values_requested = {}
|
||||||
|
player_regions = []
|
||||||
|
|
||||||
|
#Grab all the player's regions
|
||||||
|
for region in multiworld.regions:
|
||||||
|
if region.player != player:
|
||||||
|
continue
|
||||||
|
player_regions.append(region)
|
||||||
|
|
||||||
|
used_regions = filter_used_regions(player_regions)
|
||||||
|
used_regions_names = {r.name for r in set(used_regions)}
|
||||||
|
|
||||||
|
#Check used regions (and their parent(s)) for ItemValue requirement
|
||||||
|
for region in used_regions:
|
||||||
|
manualregion = DataValidation.region_table.get(region.name, {})
|
||||||
|
if manualregion:
|
||||||
|
if manualregion.get("requires"):
|
||||||
|
DataValidation._checkLocationRequiresForItemValueWithRegex(values_requested, json.dumps(manualregion["requires"]))
|
||||||
|
|
||||||
|
for region_entrance, require in manualregion.get('entrance_requires', {}).items():
|
||||||
|
if region_entrance in used_regions_names:
|
||||||
|
DataValidation._checkLocationRequiresForItemValueWithRegex(values_requested, json.dumps(require))
|
||||||
|
|
||||||
|
for region_exit, require in manualregion.get('exit_requires', {}).items():
|
||||||
|
if region_exit in used_regions_names:
|
||||||
|
DataValidation._checkLocationRequiresForItemValueWithRegex(values_requested, json.dumps(require))
|
||||||
|
|
||||||
|
for location in region.locations:
|
||||||
|
manualLocation = world.location_name_to_location.get(location.name, {})
|
||||||
|
if "requires" in manualLocation and manualLocation["requires"]:
|
||||||
|
DataValidation._checkLocationRequiresForItemValueWithRegex(values_requested, json.dumps(manualLocation["requires"]))
|
||||||
|
|
||||||
|
# compare whats available vs requested but only if there's anything requested
|
||||||
|
if values_requested:
|
||||||
|
errors = []
|
||||||
|
existing_items = [item for item in get_items_for_player(multiworld, player, True) if
|
||||||
|
item.code is not None and ItemClassification.progression in item.classification]
|
||||||
|
for value, val_count in values_requested.items():
|
||||||
|
items_value = get_items_with_value(world, multiworld, value, player)
|
||||||
|
found_count = 0
|
||||||
|
if items_value:
|
||||||
|
for item in existing_items:
|
||||||
|
if item.name in items_value:
|
||||||
|
found_count += items_value[item.name]
|
||||||
|
|
||||||
|
if found_count < val_count:
|
||||||
|
errors.append(f" '{value}': {found_count} out of the {val_count} {value} worth of progression items required can be found.")
|
||||||
|
if errors:
|
||||||
|
raise ValidationError("There are not enough progression items for the following value(s): \n" + "\n".join(errors))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def checkRegionsConnectingToOtherRegions():
|
||||||
|
for region_name in DataValidation.region_table:
|
||||||
|
region = DataValidation.region_table[region_name]
|
||||||
|
|
||||||
|
if "connects_to" not in region:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for connecting_region in region["connects_to"]:
|
||||||
|
region_exists = len([name for name in DataValidation.region_table if name == connecting_region]) > 0
|
||||||
|
|
||||||
|
if not region_exists:
|
||||||
|
raise ValidationError("Region %s connects to a region %s, which is misspelled or does not exist." % (region_name, connecting_region))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def checkForMissingItemNames():
|
||||||
|
missing_name_count = len([i for i in DataValidation.item_table if not i.get("name")])
|
||||||
|
|
||||||
|
if missing_name_count > 0:
|
||||||
|
raise ValidationError("At least one of your items is missing the 'name' field.")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def checkForDuplicateItemNames():
|
||||||
|
names: Counter[str] = Counter()
|
||||||
|
problems: list[str] = []
|
||||||
|
for item in DataValidation.item_table:
|
||||||
|
name = item.get("name")
|
||||||
|
if name is not None:
|
||||||
|
# Counter don't require checking that the key exists
|
||||||
|
names[name] += 1
|
||||||
|
if names[name] > 1:
|
||||||
|
problems.append(name)
|
||||||
|
if problems:
|
||||||
|
if len(problems) == 1:
|
||||||
|
raise ValidationError(f"Item {problems[0]} is defined more than once.")
|
||||||
|
else:
|
||||||
|
separator = '", "'
|
||||||
|
raise ValidationError(f"The following Items are defined more than once.\n \"{separator.join(problems)}\"")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def checkForMissingLocationNames():
|
||||||
|
missing_name_count = len([l for l in DataValidation.location_table if not l.get("name")])
|
||||||
|
|
||||||
|
if missing_name_count > 0:
|
||||||
|
raise ValidationError("At least one of your locations is missing the 'name' field.")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def checkForDuplicateLocationNames():
|
||||||
|
names: Counter[str] = Counter()
|
||||||
|
problems: list[str] = []
|
||||||
|
for location in DataValidation.location_table:
|
||||||
|
name = location.get("name")
|
||||||
|
if name is not None:
|
||||||
|
# Counter don't require checking that the key exists
|
||||||
|
names[name] += 1
|
||||||
|
if names[name] > 1:
|
||||||
|
problems.append(name)
|
||||||
|
if problems:
|
||||||
|
if len(problems) == 1:
|
||||||
|
raise ValidationError(f"Location {problems[0]} is defined more than once.")
|
||||||
|
else:
|
||||||
|
separator = '", "'
|
||||||
|
raise ValidationError(f"The following Locations are defined more than once.\n \"{separator.join(problems)}\"")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def checkForInvalidRegionNames():
|
||||||
|
# check for regions that Manual defines itself; currently limited to "Menu" and "Manual"
|
||||||
|
for region_name in ["Menu", "Manual"]:
|
||||||
|
if region_name in DataValidation.region_table:
|
||||||
|
raise ValidationError(f"You cannot define a '{region_name}' region because Manual already defines a region with the same name.")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def checkStartingItemsForValidItemsAndCategories():
|
||||||
|
if "starting_items" not in DataValidation.game_table:
|
||||||
|
return
|
||||||
|
|
||||||
|
starting_items = DataValidation.game_table["starting_items"]
|
||||||
|
|
||||||
|
for starting_block in starting_items:
|
||||||
|
if "items" in starting_block and "item_categories" in starting_block:
|
||||||
|
raise ValidationError("One of your starting item definitions has both 'items' and 'item_categories' defined, but only one will be applied.")
|
||||||
|
|
||||||
|
if "items" in starting_block:
|
||||||
|
for item_name in starting_block["items"]:
|
||||||
|
if not item_name in [item.get("name") for item in DataValidation.item_table]:
|
||||||
|
raise ValidationError("Item %s is set as a starting item, but is misspelled or is not defined." % (item_name))
|
||||||
|
|
||||||
|
if "item_categories" in starting_block:
|
||||||
|
for category_name in starting_block["item_categories"]:
|
||||||
|
if len([item for item in DataValidation.item_table if "category" in item and category_name in item["category"]]) == 0:
|
||||||
|
raise ValidationError("Item category %s is set as a starting item category, but is misspelled or is not defined on any items." % (category_name))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def checkStartingItemsForBadSyntax():
|
||||||
|
if not (starting_items := DataValidation.game_table.get("starting_items", False)):
|
||||||
|
return
|
||||||
|
|
||||||
|
for starting_block in starting_items:
|
||||||
|
if type(starting_block) is not dict or len(starting_block.keys()) == 0:
|
||||||
|
raise ValidationError("One of your starting item definitions is not a valid dictionary.\n Each definition must be inside {}, as demonstrated in the Manual documentation.")
|
||||||
|
|
||||||
|
valid_keys = ["items", "item_categories", "random", "if_previous_item", "_comment", "yaml_option"] # _comment is provided by schema
|
||||||
|
invalid_keys = [f'"{key}"' for key in starting_block.keys() if key not in valid_keys]
|
||||||
|
|
||||||
|
if len(invalid_keys) > 0:
|
||||||
|
raise ValidationError("One of your starting item definitions is invalid and may have unexpected results.\n The invalid starting item definition specifies the following incorrect keys: {}".format(", ".join(invalid_keys)))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def checkPlacedItemsAndCategoriesForBadSyntax():
|
||||||
|
for location in DataValidation.location_table:
|
||||||
|
place_item = location.get("place_item", False)
|
||||||
|
place_item_category = location.get("place_item_category", False)
|
||||||
|
|
||||||
|
if not place_item and not place_item_category:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if place_item and type(place_item) is not list:
|
||||||
|
raise ValidationError("One of your location has an incorrectly formatted place_item.\n The items, even just one, must be inside [].")
|
||||||
|
|
||||||
|
if place_item_category and type(place_item_category) is not list:
|
||||||
|
raise ValidationError("One of your location has an incorrectly formatted place_item_category.\n The categories, even just one, must be inside [].")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def checkPlacedItemsForValidItems():
|
||||||
|
for location in DataValidation.location_table:
|
||||||
|
if not (place_item := location.get("place_item", False)):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# don't bother checking for valid items if the syntax is wrong
|
||||||
|
if type(place_item) is not list:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for item_name in place_item:
|
||||||
|
if not item_name in [item.get("name") for item in DataValidation.item_table]:
|
||||||
|
raise ValidationError("Item %s is placed (using place_item) on a location, but is misspelled or is not defined." % (item_name))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def checkPlacedItemCategoriesForValidItemCategories():
|
||||||
|
for location in DataValidation.location_table:
|
||||||
|
if not (place_item_category := location.get("place_item_category", False)):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# don't bother checking for valid item categories if the syntax is wrong
|
||||||
|
if type(place_item_category) is not list:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for category_name in place_item_category:
|
||||||
|
if len([item for item in DataValidation.item_table if "category" in item and category_name in item["category"]]) == 0:
|
||||||
|
raise ValidationError("Item category %s is placed (using place_item_category) on a location, but is misspelled or is not defined." % (category_name))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def checkForGameBeingInvalidJSON():
|
||||||
|
if len(DataValidation.game_table) == 0:
|
||||||
|
raise ValidationError("No settings were found in your game.json. This likely indicates that your JSON is incorrectly formatted. Use https://jsonlint.com/ to validate your JSON files.")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def checkForItemsBeingInvalidJSON():
|
||||||
|
if len(DataValidation.item_table) == 0:
|
||||||
|
raise ValidationError("No items were found in your items.json. This likely indicates that your JSON is incorrectly formatted. Use https://jsonlint.com/ to validate your JSON files.")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def checkForLocationsBeingInvalidJSON():
|
||||||
|
if len(DataValidation.location_table) == 0:
|
||||||
|
raise ValidationError("No locations were found in your locations.json. This likely indicates that your JSON is incorrectly formatted. Use https://jsonlint.com/ to validate your JSON files.")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def checkForNonStartingRegionsThatAreUnreachable():
|
||||||
|
using_starting_regions = len([region for region in DataValidation.region_table if DataValidation.region_table[region].get("starting")]) > 0
|
||||||
|
|
||||||
|
if not using_starting_regions:
|
||||||
|
return
|
||||||
|
|
||||||
|
nonstarting_regions = [region for region in DataValidation.region_table if not DataValidation.region_table[region].get("starting")]
|
||||||
|
|
||||||
|
for nonstarter in nonstarting_regions:
|
||||||
|
regions_that_connect_to = [region for region in DataValidation.region_table if "connects_to" in DataValidation.region_table[region] and nonstarter in DataValidation.region_table[region]["connects_to"]]
|
||||||
|
|
||||||
|
if len(regions_that_connect_to) == 0:
|
||||||
|
raise ValidationError("The region '%s' is set as a non-starting region, but has no regions that connect to it. It will be inaccessible." % nonstarter)
|
||||||
|
|
||||||
|
|
||||||
|
def runPreFillDataValidation(world: World, multiworld: MultiWorld):
|
||||||
|
validation_errors: list[ValidationError] = []
|
||||||
|
|
||||||
|
# check if there is enough items with values
|
||||||
|
try: DataValidation.preFillCheckIfEnoughItemsForValue(world, multiworld)
|
||||||
|
except ValidationError as e: validation_errors.append(e)
|
||||||
|
|
||||||
|
if validation_errors:
|
||||||
|
heading = f"ValidationError(s) for pre_fill of {world.game}:";
|
||||||
|
newline = "\n"
|
||||||
|
raise Exception(f"\n\n{heading} \n\n{newline.join([' - ' + str(validation_error) for validation_error in validation_errors])}\n\n")
|
||||||
|
|
||||||
|
# Called during stage_assert_generate
|
||||||
|
def runGenerationDataValidation(cls) -> None:
|
||||||
|
validation_errors: list[ValidationError] = []
|
||||||
|
|
||||||
|
try: DataValidation.checkForMissingItemNames()
|
||||||
|
except ValidationError as e: validation_errors.append(e)
|
||||||
|
|
||||||
|
try: DataValidation.checkForMissingLocationNames()
|
||||||
|
except ValidationError as e: validation_errors.append(e)
|
||||||
|
|
||||||
|
# check that requires have correct item names in locations and regions
|
||||||
|
try: DataValidation.checkItemNamesInLocationRequires()
|
||||||
|
except ValidationError as e: validation_errors.append(e)
|
||||||
|
|
||||||
|
try: DataValidation.checkItemNamesInRegionRequires()
|
||||||
|
except ValidationError as e: validation_errors.append(e)
|
||||||
|
|
||||||
|
# check that region names are valid (i.e., not already defined, etc.)
|
||||||
|
try: DataValidation.checkForInvalidRegionNames()
|
||||||
|
except ValidationError as e: validation_errors.append(e)
|
||||||
|
|
||||||
|
# check that region names are correct in locations
|
||||||
|
try: DataValidation.checkRegionNamesInLocations()
|
||||||
|
except ValidationError as e: validation_errors.append(e)
|
||||||
|
|
||||||
|
# check that any classification_count used in items are valid
|
||||||
|
try: DataValidation.checkItemsHasValidClassificationCount()
|
||||||
|
except ValidationError as e: validation_errors.append(e)
|
||||||
|
|
||||||
|
# check that items that are required by locations and regions are also marked required
|
||||||
|
try: DataValidation.checkItemsThatShouldBeRequired()
|
||||||
|
except ValidationError as e: validation_errors.append(e)
|
||||||
|
|
||||||
|
# check that regions that are connected to are correct
|
||||||
|
try: DataValidation.checkRegionsConnectingToOtherRegions()
|
||||||
|
except ValidationError as e: validation_errors.append(e)
|
||||||
|
|
||||||
|
# check for duplicate names in items, locations, and regions
|
||||||
|
try: DataValidation.checkForDuplicateItemNames()
|
||||||
|
except ValidationError as e: validation_errors.append(e)
|
||||||
|
|
||||||
|
try: DataValidation.checkForDuplicateLocationNames()
|
||||||
|
except ValidationError as e: validation_errors.append(e)
|
||||||
|
|
||||||
|
# check that starting items are actually valid starting item definitions
|
||||||
|
try: DataValidation.checkStartingItemsForBadSyntax()
|
||||||
|
except ValidationError as e: validation_errors.append(e)
|
||||||
|
|
||||||
|
# check that starting items and starting item categories actually exist in the items json
|
||||||
|
try: DataValidation.checkStartingItemsForValidItemsAndCategories()
|
||||||
|
except ValidationError as e: validation_errors.append(e)
|
||||||
|
|
||||||
|
# check that placed items are actually valid place item definitions
|
||||||
|
try: DataValidation.checkPlacedItemsAndCategoriesForBadSyntax()
|
||||||
|
except ValidationError as e: validation_errors.append(e)
|
||||||
|
|
||||||
|
# check placed item and item categories for valid options for each
|
||||||
|
try: DataValidation.checkPlacedItemsForValidItems()
|
||||||
|
except ValidationError as e: validation_errors.append(e)
|
||||||
|
|
||||||
|
try: DataValidation.checkPlacedItemCategoriesForValidItemCategories()
|
||||||
|
except ValidationError as e: validation_errors.append(e)
|
||||||
|
|
||||||
|
# check for regions that are set as non-starting regions and have no connectors to them (so are unreachable)
|
||||||
|
try: DataValidation.checkForNonStartingRegionsThatAreUnreachable()
|
||||||
|
except ValidationError as e: validation_errors.append(e)
|
||||||
|
|
||||||
|
if len(validation_errors) > 0:
|
||||||
|
heading = f"ValidationError(s) in {cls.game}:";
|
||||||
|
|
||||||
|
raise Exception("\n\n%s \n\n%s\n\n" % (heading, "\n".join([' - ' + str(validation_error) for validation_error in validation_errors])))
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
from .Data import game_table
|
||||||
|
|
||||||
|
game_name: str = "Manual_%s_%s" % (game_table["game"], game_table["creator"])
|
||||||
|
filler_item_name: str | list[str] = game_table["filler_item_name"] if "filler_item_name" in game_table else "Filler"
|
||||||
|
glitches_item_name: str | None = game_table["glitches_item_name"] if "glitches_item_name" in game_table else None
|
||||||
|
starting_items: list[dict] | None = game_table["starting_items"] if "starting_items" in game_table else None
|
||||||
|
|
||||||
|
if "starting_index" in game_table:
|
||||||
|
try:
|
||||||
|
starting_index = int(game_table["starting_index"])
|
||||||
|
except ValueError:
|
||||||
|
raise Exception("The value of data/game.json:'starting_index' should be an int")
|
||||||
|
else:
|
||||||
|
starting_index = 1
|
||||||
|
|
||||||
|
unused_goals_are_locations = game_table.get("unused_goals_are_locations", False)
|
||||||
+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")
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
from BaseClasses import Item
|
||||||
|
from .Data import item_table
|
||||||
|
from .Game import filler_item_name, starting_index, game_name
|
||||||
|
|
||||||
|
|
||||||
|
######################
|
||||||
|
# Generate item lookups
|
||||||
|
######################
|
||||||
|
|
||||||
|
item_id_to_name: dict[int, str] = {}
|
||||||
|
item_name_to_item: dict[str, dict] = {}
|
||||||
|
item_name_groups: dict[str, set[str]] = {}
|
||||||
|
advancement_item_names: set[str] = set()
|
||||||
|
lastItemId = -1
|
||||||
|
|
||||||
|
count = starting_index
|
||||||
|
|
||||||
|
filler_found: set[str] = set()
|
||||||
|
|
||||||
|
# add sequential generated ids to the lists
|
||||||
|
for key, val in enumerate(item_table):
|
||||||
|
if "id" in item_table[key]:
|
||||||
|
item_id = item_table[key]["id"]
|
||||||
|
if item_id >= count:
|
||||||
|
count = item_id
|
||||||
|
else:
|
||||||
|
raise ValueError(f"{item_table[key]['name']} has an invalid ID. ID must be at least {count + 1}")
|
||||||
|
|
||||||
|
item_table[key]["id"] = count
|
||||||
|
item_table[key]["progression"] = val["progression"] if "progression" in val else False
|
||||||
|
if isinstance(val.get("category", []), str):
|
||||||
|
item_table[key]["category"] = [val["category"]]
|
||||||
|
if isinstance(filler_item_name, list):
|
||||||
|
if item_table[key].get("name") in filler_item_name:
|
||||||
|
filler_found.add(item_table[key].get("name"))
|
||||||
|
elif isinstance(filler_item_name, str):
|
||||||
|
if item_table[key].get("name") == filler_item_name:
|
||||||
|
filler_found.add(item_table[key].get("name"))
|
||||||
|
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
# add the filler item to the list of items for lookup
|
||||||
|
if isinstance(filler_item_name, list):
|
||||||
|
for generic_filler in filler_item_name:
|
||||||
|
if generic_filler not in filler_found:
|
||||||
|
item_table.append({
|
||||||
|
"name": generic_filler,
|
||||||
|
"id": count,
|
||||||
|
})
|
||||||
|
count += 1
|
||||||
|
elif isinstance(filler_item_name, str) and not filler_found:
|
||||||
|
item_table.append({
|
||||||
|
"name": filler_item_name,
|
||||||
|
"id": count,
|
||||||
|
})
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
for item in item_table:
|
||||||
|
item_name = item.get("name", f"Unnamed Item {item['id']}")
|
||||||
|
item_id_to_name[item["id"]] = item_name
|
||||||
|
item_name_to_item[item_name] = item
|
||||||
|
|
||||||
|
if item["id"] is not None:
|
||||||
|
lastItemId = max(lastItemId, item["id"])
|
||||||
|
|
||||||
|
for c in item.get("category", []):
|
||||||
|
if c not in item_name_groups:
|
||||||
|
item_name_groups[c] = set()
|
||||||
|
item_name_groups[c].add(item_name)
|
||||||
|
|
||||||
|
#Just lowercase the values here to remove all the .lower.strip down the line
|
||||||
|
item['value'] = {k.lower().strip(): v
|
||||||
|
for k, v in item.get('value', {}).items()}
|
||||||
|
|
||||||
|
for v in item.get("value", {}).keys():
|
||||||
|
group_name = f"has_{v}_value"
|
||||||
|
if group_name not in item_name_groups:
|
||||||
|
item_name_groups[group_name] = set()
|
||||||
|
item_name_groups[group_name].add(item_name)
|
||||||
|
|
||||||
|
item_id_to_name[None] = "__Victory__"
|
||||||
|
item_name_to_id = {name: id for id, name in item_id_to_name.items()}
|
||||||
|
|
||||||
|
|
||||||
|
######################
|
||||||
|
# Item classes
|
||||||
|
######################
|
||||||
|
|
||||||
|
|
||||||
|
class ManualItem(Item):
|
||||||
|
game = game_name
|
||||||
+100
@@ -0,0 +1,100 @@
|
|||||||
|
from BaseClasses import Location
|
||||||
|
from .Data import location_table, event_table
|
||||||
|
from .Game import starting_index, game_name
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
######################
|
||||||
|
# Generate location lookups
|
||||||
|
######################
|
||||||
|
|
||||||
|
count = starting_index
|
||||||
|
victory_names: list[str] = []
|
||||||
|
|
||||||
|
# add sequential generated ids to the lists
|
||||||
|
for key, _ in enumerate(location_table):
|
||||||
|
if "victory" in location_table[key] and location_table[key]["victory"]:
|
||||||
|
victory_names.append(location_table[key]["name"])
|
||||||
|
|
||||||
|
if "id" in location_table[key]:
|
||||||
|
item_id = location_table[key]["id"]
|
||||||
|
if item_id >= count:
|
||||||
|
count = item_id
|
||||||
|
else:
|
||||||
|
raise ValueError(f"{location_table[key]['name']} has an invalid ID. ID must be at least {count + 1}")
|
||||||
|
|
||||||
|
location_table[key]["id"] = count
|
||||||
|
|
||||||
|
if "region" not in location_table[key]:
|
||||||
|
location_table[key]["region"] = "Manual" # all locations are in the same region for Manual
|
||||||
|
|
||||||
|
if isinstance(location_table[key].get("category", []), str):
|
||||||
|
location_table[key]["category"] = [location_table[key]["category"]]
|
||||||
|
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
if not victory_names:
|
||||||
|
# Add the game completion location, which will have the Victory item assigned to it automatically
|
||||||
|
location_table.append({
|
||||||
|
"id": count + 1,
|
||||||
|
"name": "__Manual Game Complete__",
|
||||||
|
"region": "Manual",
|
||||||
|
"requires": []
|
||||||
|
# "category": custom_victory_location["category"] if "category" in custom_victory_location else []
|
||||||
|
})
|
||||||
|
victory_names.append("__Manual Game Complete__")
|
||||||
|
|
||||||
|
location_id_to_name: dict[int, str] = {}
|
||||||
|
location_name_to_location: dict[str, dict[str, Any]] = {}
|
||||||
|
location_name_groups: dict[str, set[str]] = {}
|
||||||
|
event_name_to_event: dict[str, dict[str, Any]] = {}
|
||||||
|
event_name_groups: dict[str, set[str]] = {}
|
||||||
|
|
||||||
|
for loc in location_table:
|
||||||
|
loc_name = loc.get("name", f"Unnamed Location {loc['id']}")
|
||||||
|
location_id_to_name[loc["id"]] = loc_name
|
||||||
|
location_name_to_location[loc_name] = loc
|
||||||
|
|
||||||
|
for c in loc.get("category", []):
|
||||||
|
if c not in location_name_groups:
|
||||||
|
location_name_groups[c] = set()
|
||||||
|
location_name_groups[c].add(loc_name)
|
||||||
|
|
||||||
|
|
||||||
|
# location_id_to_name[None] = "__Manual Game Complete__"
|
||||||
|
location_name_to_id = {name: id for id, name in location_id_to_name.items()}
|
||||||
|
|
||||||
|
id = 0
|
||||||
|
for key, event in enumerate(event_table):
|
||||||
|
event_name = f"{id}_{event['name']}".upper().replace(" ", "_")
|
||||||
|
while event_name in location_name_to_location:
|
||||||
|
id += 1
|
||||||
|
event_name = f"{id}_{event['name']}".upper().replace(" ", "_")
|
||||||
|
if "location_name" in event:
|
||||||
|
if event["location_name"] in location_name_to_location:
|
||||||
|
raise Exception(f"Cannot define event {event['location_name']} with the same name as a location.")
|
||||||
|
event_name_to_event[event["location_name"]] = event
|
||||||
|
else:
|
||||||
|
event_name_to_event[event_name] = event
|
||||||
|
event_name_to_event[event_name]["location_name"] = event_name
|
||||||
|
event_table[key]["location_name"] = event_name
|
||||||
|
if 'visible' not in event:
|
||||||
|
event_name_to_event[event_name]['visible'] = False
|
||||||
|
event_table[key]['visible'] = False
|
||||||
|
if 'region' not in event:
|
||||||
|
event_name_to_event[event_name]['region'] = "Manual"
|
||||||
|
event_table[key]['region'] = "Manual"
|
||||||
|
for c in event.get("category", []):
|
||||||
|
if c not in event_name_groups:
|
||||||
|
event_name_groups[c] = set()
|
||||||
|
event_name_groups[c].add(event['name'])
|
||||||
|
|
||||||
|
id += 1
|
||||||
|
|
||||||
|
######################
|
||||||
|
# Location classes
|
||||||
|
######################
|
||||||
|
|
||||||
|
|
||||||
|
class ManualLocation(Location):
|
||||||
|
game = game_name
|
||||||
+1547
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,70 @@
|
|||||||
|
|
||||||
|
from BaseClasses import Tutorial
|
||||||
|
from enum import Enum
|
||||||
|
from worlds.AutoWorld import World, WebWorld
|
||||||
|
from .Data import game_table, meta_table
|
||||||
|
from .Helpers import convert_to_long_string
|
||||||
|
|
||||||
|
##############
|
||||||
|
# Meta Classes
|
||||||
|
##############
|
||||||
|
class ManualWeb(WebWorld):
|
||||||
|
tutorials = [Tutorial(
|
||||||
|
"Multiworld Setup Guide",
|
||||||
|
"A guide to setting up manual game integration for Archipelago multiworld games.",
|
||||||
|
"English",
|
||||||
|
"setup_en.md",
|
||||||
|
"setup/en",
|
||||||
|
["Fuzzy"]
|
||||||
|
)]
|
||||||
|
|
||||||
|
######################################
|
||||||
|
# Convert meta.json data to properties
|
||||||
|
######################################
|
||||||
|
def set_world_description(base_doc: str) -> str:
|
||||||
|
if meta_table.get("docs", {}).get("apworld_description"):
|
||||||
|
return convert_to_long_string(meta_table["docs"]["apworld_description"])
|
||||||
|
|
||||||
|
return base_doc
|
||||||
|
|
||||||
|
|
||||||
|
def set_world_webworld(web: WebWorld) -> WebWorld:
|
||||||
|
from .Options import make_options_group
|
||||||
|
if meta_table.get("docs", {}).get("web", {}):
|
||||||
|
Web_Config = meta_table["docs"]["web"]
|
||||||
|
|
||||||
|
web.theme = Web_Config.get("theme", web.theme)
|
||||||
|
web.game_info_languages = Web_Config.get("game_info_languages", web.game_info_languages)
|
||||||
|
web.options_presets = Web_Config.get("options_presets", web.options_presets)
|
||||||
|
web.options_page = Web_Config.get("options_page", web.options_page)
|
||||||
|
web.option_groups = make_options_group()
|
||||||
|
if hasattr(web, 'bug_report_page'):
|
||||||
|
web.bug_report_page = Web_Config.get("bug_report_page", web.bug_report_page)
|
||||||
|
else:
|
||||||
|
web.bug_report_page = Web_Config.get("bug_report_page", None)
|
||||||
|
|
||||||
|
if Web_Config.get("tutorials", []):
|
||||||
|
tutorials = []
|
||||||
|
for tutorial in Web_Config.get("tutorials", []):
|
||||||
|
# Converting json to Tutorials
|
||||||
|
tutorials.append(Tutorial(
|
||||||
|
tutorial.get("name", "Multiworld Setup Guide"),
|
||||||
|
tutorial.get("description", "A guide to setting up manual game integration for Archipelago multiworld games."),
|
||||||
|
tutorial.get("language", "English"),
|
||||||
|
tutorial.get("file_name", "setup_en.md"),
|
||||||
|
tutorial.get("link", "setup/en"),
|
||||||
|
tutorial.get("authors", [game_table.get("creator")])
|
||||||
|
))
|
||||||
|
web.tutorials = tutorials
|
||||||
|
return web
|
||||||
|
|
||||||
|
#################
|
||||||
|
# Meta Properties
|
||||||
|
#################
|
||||||
|
world_description: str = set_world_description("""
|
||||||
|
Manual games allow you to set custom check locations and custom item names that will be rolled into a multiworld.
|
||||||
|
This allows any variety of game -- PC, console, board games, Microsoft Word memes... really anything -- to be part of a multiworld randomizer.
|
||||||
|
The key component to including these games is some level of manual restriction. Since the items are not actually withheld from the player,
|
||||||
|
the player must manually refrain from using these gathered items until the tracker shows that they have been acquired or sent.
|
||||||
|
""")
|
||||||
|
world_webworld: ManualWeb = set_world_webworld(ManualWeb())
|
||||||
+267
@@ -0,0 +1,267 @@
|
|||||||
|
import sys
|
||||||
|
from Options import PerGameCommonOptions, FreeText, Toggle, DefaultOnToggle, Choice, TextChoice, Range, NamedRange, DeathLink, \
|
||||||
|
OptionGroup, StartInventoryPool, Visibility, item_and_loc_options, Option
|
||||||
|
from .hooks.Options import before_options_defined, after_options_defined, before_option_groups_created, after_option_groups_created
|
||||||
|
from .Data import category_table, game_table, option_table
|
||||||
|
from .Helpers import convert_to_long_string, format_to_valid_identifier
|
||||||
|
from .Locations import victory_names
|
||||||
|
from .Items import item_table
|
||||||
|
from .Game import starting_items
|
||||||
|
|
||||||
|
from dataclasses import make_dataclass
|
||||||
|
from typing import List, Any, Type
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
class FillerTrapPercent(Range):
|
||||||
|
"""How many fillers will be replaced with traps. 0 means no additional traps, 100 means all fillers are traps."""
|
||||||
|
range_end = 100
|
||||||
|
|
||||||
|
class GenerateRegionDiagram(Toggle):
|
||||||
|
"""Generate a region diagram."""
|
||||||
|
visibility = Visibility.none # Hidden option
|
||||||
|
|
||||||
|
class ManualDeathLink(DeathLink):
|
||||||
|
"""When you die, everyone who enabled death link dies. Of course, the reverse is true too."""
|
||||||
|
|
||||||
|
def createChoiceOptions(values: dict, aliases: dict) -> dict:
|
||||||
|
values = {'option_' + i: v for i, v in values.items()}
|
||||||
|
aliases = {'alias_' + i: v for i, v in aliases.items()}
|
||||||
|
return {**values, **aliases}
|
||||||
|
|
||||||
|
def convertOptionVisibility(input) -> Visibility:
|
||||||
|
visibility = Visibility.all
|
||||||
|
if isinstance(input, list):
|
||||||
|
visibility = Visibility.none
|
||||||
|
for type in input:
|
||||||
|
visibility |= Visibility[type.lower()]
|
||||||
|
|
||||||
|
elif isinstance(input,str):
|
||||||
|
if input.startswith('0b'):
|
||||||
|
visibility = Visibility(int(input, base=0))
|
||||||
|
else:
|
||||||
|
visibility = Visibility[input.lower()]
|
||||||
|
|
||||||
|
elif isinstance(input, int):
|
||||||
|
visibility = Visibility(input)
|
||||||
|
return visibility
|
||||||
|
|
||||||
|
def getOriginalOptionArguments(option: Type[Option[Any]]) -> dict:
|
||||||
|
args = {}
|
||||||
|
args['default'] = option.default
|
||||||
|
if hasattr(option, 'display_name'): args['display'] = option.display_name
|
||||||
|
args['rich_text_doc'] = option.rich_text_doc
|
||||||
|
args['default'] = option.default
|
||||||
|
args['visibility'] = option.visibility
|
||||||
|
return args
|
||||||
|
|
||||||
|
manual_option_groups: dict[str, List[Type[Option[Any]]]] = {}
|
||||||
|
def addOptionToGroup(option_name: str, group: str):
|
||||||
|
if group not in manual_option_groups.keys():
|
||||||
|
manual_option_groups[group] = []
|
||||||
|
if manual_options.get(option_name) and manual_options[option_name] not in manual_option_groups[group]:
|
||||||
|
manual_option_groups[group].append(manual_options[option_name])
|
||||||
|
|
||||||
|
######################
|
||||||
|
# Manual's default options
|
||||||
|
######################
|
||||||
|
|
||||||
|
manual_options: dict[str, Type[Option[Any]]] = before_options_defined({})
|
||||||
|
manual_options["start_inventory_from_pool"] = StartInventoryPool
|
||||||
|
manual_options["generate_region_diagram"] = GenerateRegionDiagram
|
||||||
|
|
||||||
|
if len(victory_names) > 1:
|
||||||
|
if manual_options.get('goal'):
|
||||||
|
logging.warning("Existing Goal option found created via Hooks, it will be overwritten by Manual's generated Goal option.\nIf you want to support old yaml you will need to add alias in after_options_defined")
|
||||||
|
|
||||||
|
goal: dict[str, Any] = {'option_' + v: i for i, v in enumerate(victory_names)}
|
||||||
|
goal['__module__'] = __name__
|
||||||
|
|
||||||
|
manual_options['goal'] = type('goal', (Choice,), dict(goal))
|
||||||
|
manual_options['goal'].__doc__ = "Choose your victory condition."
|
||||||
|
|
||||||
|
|
||||||
|
if any(item.get('trap') for item in item_table):
|
||||||
|
manual_options["filler_traps"] = FillerTrapPercent
|
||||||
|
|
||||||
|
if game_table.get("death_link"):
|
||||||
|
manual_options["death_link"] = ManualDeathLink
|
||||||
|
|
||||||
|
|
||||||
|
######################
|
||||||
|
# Option.json options
|
||||||
|
######################
|
||||||
|
|
||||||
|
for option_name, option in option_table.get('core', {}).items():
|
||||||
|
if option_name.startswith('_'): #To allow commenting out options
|
||||||
|
continue
|
||||||
|
option_display_name = option_name
|
||||||
|
option_name = format_to_valid_identifier(option_name)
|
||||||
|
|
||||||
|
if manual_options.get(option_name):
|
||||||
|
original_option: Type[Option] = manual_options[option_name]
|
||||||
|
original_doc = str(original_option.__doc__)
|
||||||
|
|
||||||
|
if issubclass(original_option, Toggle):
|
||||||
|
if option.get('default', None) is not None:
|
||||||
|
option_type = DefaultOnToggle if option['default'] else Toggle
|
||||||
|
|
||||||
|
if original_option.__base__ != option_type: #only recreate if needed
|
||||||
|
args = getOriginalOptionArguments(original_option)
|
||||||
|
args['__module__'] = __name__
|
||||||
|
manual_options[option_name] = type(option_name, (option_type,), dict(args)) # Type checker doesn't like having a variable as a base for the type # type: ignore
|
||||||
|
logging.debug(f"Manual: Option.json converted option '{option_display_name}' into a {option_type}")
|
||||||
|
|
||||||
|
elif issubclass(original_option, Choice):
|
||||||
|
if option.get("values"):
|
||||||
|
raise Exception(f"You cannot modify the values of the '{option_display_name}' option since they cannot have their value changed by Option.json")
|
||||||
|
|
||||||
|
if option.get('aliases'):
|
||||||
|
for alias, value in option['aliases'].items():
|
||||||
|
original_option.aliases[alias] = value
|
||||||
|
original_option.options.update(original_option.aliases) #for an alias to be valid it must also be in options
|
||||||
|
|
||||||
|
logging.debug(f"Manual: Option.json modified option '{option_display_name}''s aliases")
|
||||||
|
|
||||||
|
elif issubclass(original_option, Range):
|
||||||
|
if option.get('values'): #let user add named values
|
||||||
|
args = getOriginalOptionArguments(original_option)
|
||||||
|
args['special_range_names'] = {}
|
||||||
|
if issubclass(original_option, NamedRange):
|
||||||
|
args['special_range_names'] = dict(original_option.special_range_names)
|
||||||
|
args['special_range_names']['default'] = option.get('default', args['special_range_names'].get('default', args['default']))
|
||||||
|
args['range_start'] = original_option.range_start
|
||||||
|
args['range_end'] = original_option.range_end
|
||||||
|
args['special_range_names'] = {**args['special_range_names'], **{l.lower(): v for l, v in option['values'].items()}}
|
||||||
|
|
||||||
|
args['__module__'] = __name__
|
||||||
|
manual_options[option_name] = type(option_name, (NamedRange,), dict(args))
|
||||||
|
logging.debug(f"Manual: Option.json converted option '{option_display_name}' into a {NamedRange}")
|
||||||
|
|
||||||
|
manual_options[option_name].display_name = option.get('display_name', option_display_name) # type: ignore
|
||||||
|
manual_options[option_name].__doc__ = convert_to_long_string(option.get('description', original_doc))
|
||||||
|
if option.get('rich_text_doc'):
|
||||||
|
manual_options[option_name].rich_text_doc = option["rich_text_doc"]
|
||||||
|
|
||||||
|
if option.get('default'):
|
||||||
|
manual_options[option_name].default = option['default']
|
||||||
|
|
||||||
|
if option.get('hidden'):
|
||||||
|
manual_options[option_name].visibility = Visibility.none
|
||||||
|
elif option.get('visibility'):
|
||||||
|
manual_options[option_name].visibility = convertOptionVisibility(option['visibility'])
|
||||||
|
else:
|
||||||
|
logging.debug(f"Manual: Option.json just tried to modify the option '{option_display_name}' but it doesn't currently exists")
|
||||||
|
|
||||||
|
|
||||||
|
supported_option_types = ["Toggle", "Choice", "Range"]
|
||||||
|
for option_name, option in option_table.get('user', {}).items():
|
||||||
|
if option_name.startswith('_'): #To allow commenting out options
|
||||||
|
continue
|
||||||
|
option_display_name = option_name
|
||||||
|
option_name = format_to_valid_identifier(option_name)
|
||||||
|
if manual_options.get(option_name):
|
||||||
|
logging.warning(f"Manual: An option with the name '{option_display_name}' cannot be added since it already exists in Manual Core Options. \nTo modify an existing option move it to the 'core' section of Option.json")
|
||||||
|
|
||||||
|
else:
|
||||||
|
option_type = option.get('type', "").title()
|
||||||
|
|
||||||
|
if option_type not in supported_option_types:
|
||||||
|
raise Exception(f'Option {option_display_name} in options.json has an invalid type of "{option["type"]}".\nIt must be one of the folowing: {supported_option_types}')
|
||||||
|
|
||||||
|
args = {'display_name': option.get('display_name', option_display_name)}
|
||||||
|
|
||||||
|
# Default to Toggle to prevent hypothetical Unbound option_class
|
||||||
|
option_class: Type[Option] = DefaultOnToggle if option.get('default', False) else Toggle
|
||||||
|
|
||||||
|
if option_type == "Choice":
|
||||||
|
args = {**args, **createChoiceOptions(option.get('values'), option.get('aliases', {}))}
|
||||||
|
option_class = TextChoice if option.get("allow_custom_value", False) else Choice
|
||||||
|
|
||||||
|
elif option_type == "Range":
|
||||||
|
args['range_start'] = option.get('range_start', 0)
|
||||||
|
args['range_end'] = option.get('range_end', 1)
|
||||||
|
if option.get('values'):
|
||||||
|
args['special_range_names'] = {l.lower(): v for l, v in option['values'].items()}
|
||||||
|
args['special_range_names']['default'] = option.get('default', args['range_start'])
|
||||||
|
option_class = NamedRange if option.get('values') else Range
|
||||||
|
|
||||||
|
if option.get('default'):
|
||||||
|
args['default'] = option['default']
|
||||||
|
|
||||||
|
if option.get('rich_text_doc',None) is not None:
|
||||||
|
args["rich_text_doc"] = option["rich_text_doc"]
|
||||||
|
|
||||||
|
if option.get('hidden'):
|
||||||
|
args['visibility'] = Visibility.none
|
||||||
|
elif option.get('visibility'):
|
||||||
|
args['visibility'] = convertOptionVisibility(option['visibility'])
|
||||||
|
|
||||||
|
args['__module__'] = __name__
|
||||||
|
|
||||||
|
manual_options[option_name] = type(option_name, (option_class,), args ) # Same as the first ignore above # type: ignore
|
||||||
|
manual_options[option_name].__doc__ = convert_to_long_string(option.get('description', "an Option"))
|
||||||
|
|
||||||
|
if option.get('group'):
|
||||||
|
addOptionToGroup(option_name, option['group'])
|
||||||
|
|
||||||
|
######################
|
||||||
|
# category and starting_items options
|
||||||
|
######################
|
||||||
|
|
||||||
|
for category in category_table:
|
||||||
|
for option_name in category_table[category].get("yaml_option", []):
|
||||||
|
if option_name[0] == "!":
|
||||||
|
option_name = option_name[1:]
|
||||||
|
option_name = format_to_valid_identifier(option_name)
|
||||||
|
if option_name not in manual_options:
|
||||||
|
manual_options[option_name] = type(option_name, (DefaultOnToggle,), {"default": True, "__module__": __name__})
|
||||||
|
manual_options[option_name].__doc__ = "Should items/locations linked to this option be enabled?"
|
||||||
|
|
||||||
|
if starting_items:
|
||||||
|
for starting_items in starting_items:
|
||||||
|
if starting_items.get("yaml_option"):
|
||||||
|
for option_name in starting_items["yaml_option"]:
|
||||||
|
if option_name[0] == "!":
|
||||||
|
option_name = option_name[1:]
|
||||||
|
option_name = format_to_valid_identifier(option_name)
|
||||||
|
if option_name not in manual_options:
|
||||||
|
manual_options[option_name] = type(option_name, (DefaultOnToggle,), {"default": True, "__module__": __name__})
|
||||||
|
manual_options[option_name].__doc__ = "Should items/locations linked to this option be enabled?"
|
||||||
|
|
||||||
|
######################
|
||||||
|
# OptionGroups Creation
|
||||||
|
######################
|
||||||
|
|
||||||
|
def make_options_group() -> list[OptionGroup]:
|
||||||
|
global manual_option_groups
|
||||||
|
manual_option_groups = before_option_groups_created(manual_option_groups)
|
||||||
|
option_groups: List[OptionGroup] = []
|
||||||
|
|
||||||
|
# For some reason, unless they are added manually, the base item and loc option don't get grouped as they should
|
||||||
|
base_item_loc_group = item_and_loc_options
|
||||||
|
|
||||||
|
if manual_option_groups:
|
||||||
|
if 'Item & Location Options' in manual_option_groups.keys():
|
||||||
|
base_item_loc_group = manual_option_groups.pop('Item & Location Options') #Put the custom options before the base AP options
|
||||||
|
base_item_loc_group.extend(item_and_loc_options)
|
||||||
|
|
||||||
|
if 'Game Options' in manual_option_groups.keys():
|
||||||
|
# Archipelago automatically assign ungrouped options to this group unless its defined so by deleting it here we let AP recreate it later
|
||||||
|
manual_option_groups.pop('Game Options')
|
||||||
|
|
||||||
|
for group, options in manual_option_groups.items():
|
||||||
|
option_groups.append(OptionGroup(group, options))
|
||||||
|
|
||||||
|
option_groups.append(OptionGroup('Item & Location Options', base_item_loc_group, True))
|
||||||
|
|
||||||
|
return after_option_groups_created(option_groups)
|
||||||
|
|
||||||
|
manual_options_data = make_dataclass('ManualOptionsClass', manual_options.items(), bases=(PerGameCommonOptions,))
|
||||||
|
after_options_defined(manual_options_data)
|
||||||
|
|
||||||
|
# Make the options available in this module for import, needed for WebWorld compatibility
|
||||||
|
this = sys.modules[__name__]
|
||||||
|
for name, obj in manual_options.items():
|
||||||
|
setattr(this, name, obj)
|
||||||
|
del this
|
||||||
+78
@@ -0,0 +1,78 @@
|
|||||||
|
from BaseClasses import Entrance, MultiWorld, Region, ItemClassification
|
||||||
|
from .Helpers import is_category_enabled, is_location_enabled, is_event_enabled
|
||||||
|
from .Data import region_table
|
||||||
|
from .Locations import ManualLocation, location_name_to_location
|
||||||
|
from .Items import ManualItem
|
||||||
|
from worlds.AutoWorld import World
|
||||||
|
|
||||||
|
|
||||||
|
if not region_table:
|
||||||
|
region_table = {}
|
||||||
|
|
||||||
|
regionMap = { **region_table }
|
||||||
|
starting_regions = [ name for name in regionMap if "starting" in regionMap[name].keys() and regionMap[name]["starting"] ]
|
||||||
|
|
||||||
|
if len(starting_regions) == 0:
|
||||||
|
starting_regions = region_table.keys() # the Manual region connects to all user-defined regions automatically if you specify no starting regions
|
||||||
|
|
||||||
|
regionMap["Manual"] = {
|
||||||
|
"requires": [],
|
||||||
|
"connects_to": starting_regions
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def create_regions(world: World, multiworld: MultiWorld, player: int):
|
||||||
|
# Create regions and assign locations to each region
|
||||||
|
for region in regionMap:
|
||||||
|
if "connects_to" not in regionMap[region]:
|
||||||
|
exit_array = None
|
||||||
|
else:
|
||||||
|
exit_array = regionMap[region]["connects_to"] or None
|
||||||
|
|
||||||
|
# safeguard for bad value at the end
|
||||||
|
if not exit_array:
|
||||||
|
exit_array = None
|
||||||
|
|
||||||
|
locations = []
|
||||||
|
for location in world.location_table:
|
||||||
|
if "region" in location and location["region"] == region:
|
||||||
|
if is_location_enabled(multiworld, player, location):
|
||||||
|
locations.append(location["name"])
|
||||||
|
|
||||||
|
new_region = create_region(world, multiworld, player, region, locations, exit_array)
|
||||||
|
multiworld.regions += [new_region]
|
||||||
|
|
||||||
|
# Link regions together
|
||||||
|
for region in regionMap:
|
||||||
|
if "connects_to" in regionMap[region] and regionMap[region]["connects_to"]:
|
||||||
|
for linkedRegion in regionMap[region]["connects_to"]:
|
||||||
|
connection = multiworld.get_entrance(getConnectionName(region, linkedRegion), player)
|
||||||
|
connection.connect(multiworld.get_region(linkedRegion, player))
|
||||||
|
|
||||||
|
def create_region(world: World, multiworld: MultiWorld, player: int, name: str, locations=None, exits=None):
|
||||||
|
ret = Region(name, player, multiworld)
|
||||||
|
|
||||||
|
if locations:
|
||||||
|
for location in locations:
|
||||||
|
loc_id = world.location_name_to_id.get(location, 0)
|
||||||
|
locationObj = ManualLocation(player, location, loc_id, ret)
|
||||||
|
if location_name_to_location[location].get('prehint'):
|
||||||
|
world.options.start_location_hints.value.add(location)
|
||||||
|
ret.locations.append(locationObj)
|
||||||
|
if exits:
|
||||||
|
for exit in exits:
|
||||||
|
ret.exits.append(Entrance(player, getConnectionName(name, exit), ret))
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def getConnectionName(entranceName: str, exitName: str):
|
||||||
|
return entranceName + "To" + exitName
|
||||||
|
|
||||||
|
def create_events(world: World, multiworld: MultiWorld, player: int):
|
||||||
|
for name, event in world.event_name_to_event.items():
|
||||||
|
if not is_event_enabled(multiworld, player, event):
|
||||||
|
continue
|
||||||
|
region = multiworld.get_region(event.get("region", "Manual"), player)
|
||||||
|
item = ManualItem(event["name"], ItemClassification.progression, None, player=player)
|
||||||
|
location = ManualLocation(player, name, None, region)
|
||||||
|
region.locations.append(location)
|
||||||
|
location.place_locked_item(item)
|
||||||
@@ -0,0 +1,845 @@
|
|||||||
|
import dataclasses
|
||||||
|
import inspect
|
||||||
|
from typing import TYPE_CHECKING, Any, Callable, Optional, Any
|
||||||
|
from enum import IntEnum
|
||||||
|
from operator import eq, ge, le
|
||||||
|
|
||||||
|
from .Regions import regionMap
|
||||||
|
from .hooks import Rules
|
||||||
|
from .Helpers import clamp, is_item_enabled, is_option_enabled, get_option_value, convert_string_to_type,\
|
||||||
|
format_to_valid_identifier, format_state_prog_items_key, ProgItemsCat
|
||||||
|
from .Game import game_name
|
||||||
|
|
||||||
|
from BaseClasses import MultiWorld, CollectionState, Entrance
|
||||||
|
from worlds.AutoWorld import World
|
||||||
|
from worlds.generic.Rules import set_rule, add_rule
|
||||||
|
from Options import Choice, Toggle, Range, NamedRange, NumericOption
|
||||||
|
from Utils import version_tuple
|
||||||
|
|
||||||
|
import re
|
||||||
|
import math
|
||||||
|
import logging
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from . import ManualWorld
|
||||||
|
|
||||||
|
# At some point in the future, we should depreciate the non-RB codepath. But that's not until at least 0.7.X
|
||||||
|
use_rulebuilder = version_tuple >= (0, 6, 7)
|
||||||
|
|
||||||
|
if TYPE_CHECKING and use_rulebuilder:
|
||||||
|
import rule_builder.rules
|
||||||
|
|
||||||
|
FUNCTION_REGEX = re.compile(r'\{(\w+)\((.*?)\)\}')
|
||||||
|
ITEM_REGEX = re.compile(r'\|(@?)([^|]+?)(\:[^:|]+)?\|')
|
||||||
|
AND_REGEX = re.compile(r'\s?\bAND\b\s?', re.IGNORECASE)
|
||||||
|
OR_REGEX = re.compile(r'\s?\bOR\b\s?', flags=re.IGNORECASE)
|
||||||
|
|
||||||
|
class LogicErrorSource(IntEnum):
|
||||||
|
INFIX_TO_POSTFIX = 1
|
||||||
|
"""includes more closing parentheses than opening (but not the opposite)"""
|
||||||
|
EVALUATE_POSTFIX = 2
|
||||||
|
""" includes missing pipes and missing value on either side of AND/OR"""
|
||||||
|
EVALUATE_STACK_SIZE = 3
|
||||||
|
"""includes missing curly brackets"""
|
||||||
|
|
||||||
|
def construct_logic_error(location_or_region: dict, source: LogicErrorSource) -> KeyError:
|
||||||
|
object_type = "location/region"
|
||||||
|
object_name = location_or_region.get("name", "Unknown")
|
||||||
|
|
||||||
|
if location_or_region.get("is_region", False) or "starting" in location_or_region or "connects_to" in location_or_region:
|
||||||
|
object_type = "region"
|
||||||
|
elif "region" in location_or_region or "category" in location_or_region:
|
||||||
|
object_type = "location"
|
||||||
|
|
||||||
|
if source == LogicErrorSource.INFIX_TO_POSTFIX:
|
||||||
|
source_text = "There may be mismatched parentheses, or other invalid syntax for the requires."
|
||||||
|
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."
|
||||||
|
else:
|
||||||
|
source_text = "This requires includes invalid syntax."
|
||||||
|
|
||||||
|
return KeyError(f"Invalid 'requires' for {object_type} '{object_name}': {source_text} (ERROR {source})")
|
||||||
|
|
||||||
|
def infix_to_postfix(expr: str, location: dict) -> str:
|
||||||
|
prec: dict[str, int] = {"&": 2, "|": 2, "!": 3}
|
||||||
|
stack: list[str] = []
|
||||||
|
postfix: str = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
for c in expr:
|
||||||
|
if c.isnumeric():
|
||||||
|
postfix += c
|
||||||
|
elif c in prec:
|
||||||
|
while stack and stack[-1] != "(" and prec[c] <= prec[stack[-1]]:
|
||||||
|
postfix += stack.pop()
|
||||||
|
stack.append(c)
|
||||||
|
elif c == "(":
|
||||||
|
stack.append(c)
|
||||||
|
elif c == ")":
|
||||||
|
while stack and stack[-1] != "(":
|
||||||
|
postfix += stack.pop()
|
||||||
|
stack.pop()
|
||||||
|
else:
|
||||||
|
# added this here since '|Chun-Li| or {YamlCompare(Example_Choice == 1)' is valid because of the 1 at the end
|
||||||
|
raise ValueError(f"Invalid Character '{c}' in expression '{expr}', it should be either a number or a parentheses")
|
||||||
|
|
||||||
|
while stack:
|
||||||
|
postfix += stack.pop()
|
||||||
|
except ValueError as ex:
|
||||||
|
text = str(ex)
|
||||||
|
if "'{'" in text or ")}" in text:
|
||||||
|
raise construct_logic_error(location, LogicErrorSource.EVALUATE_STACK_SIZE)
|
||||||
|
|
||||||
|
raise construct_logic_error(location, LogicErrorSource.EVALUATE_POSTFIX)
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
raise construct_logic_error(location, LogicErrorSource.INFIX_TO_POSTFIX)
|
||||||
|
|
||||||
|
return postfix
|
||||||
|
|
||||||
|
|
||||||
|
def evaluate_postfix(expr: str, location: dict) -> bool:
|
||||||
|
stack: list[bool] = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
for c in expr:
|
||||||
|
if c == "0":
|
||||||
|
stack.append(False)
|
||||||
|
elif c == "1":
|
||||||
|
stack.append(True)
|
||||||
|
elif c == "&":
|
||||||
|
op2 = stack.pop()
|
||||||
|
op1 = stack.pop()
|
||||||
|
stack.append(op1 and op2)
|
||||||
|
elif c == "|":
|
||||||
|
op2 = stack.pop()
|
||||||
|
op1 = stack.pop()
|
||||||
|
stack.append(op1 or op2)
|
||||||
|
elif c == "!":
|
||||||
|
op = stack.pop()
|
||||||
|
stack.append(not op)
|
||||||
|
except Exception:
|
||||||
|
raise construct_logic_error(location, LogicErrorSource.EVALUATE_POSTFIX)
|
||||||
|
|
||||||
|
if len(stack) != 1:
|
||||||
|
raise construct_logic_error(location, LogicErrorSource.EVALUATE_STACK_SIZE)
|
||||||
|
|
||||||
|
return stack.pop()
|
||||||
|
|
||||||
|
def set_rules(world: "ManualWorld", multiworld: MultiWorld, player: int):
|
||||||
|
def evaluate_nonnumeric_count(item_base: str, item_name: str, item_count: str, is_category: bool, area: dict) -> tuple[str, int]:
|
||||||
|
item_count = item_count.strip()
|
||||||
|
if item_count.isnumeric():
|
||||||
|
return item_name, int(item_count)
|
||||||
|
|
||||||
|
items_counts = world.get_item_counts(player, only_progression=True)
|
||||||
|
if is_category:
|
||||||
|
total_count = sum([items_counts.get(item, 0) for item in world.item_and_event_name_groups.get(item_name, set())])
|
||||||
|
else:
|
||||||
|
total_count = items_counts.get(item_name, 0)
|
||||||
|
if item_count.lower() == 'all':
|
||||||
|
count = total_count
|
||||||
|
elif item_count.lower() == 'half':
|
||||||
|
count = int(total_count / 2)
|
||||||
|
elif item_count.endswith('%') and len(item_count) > 1:
|
||||||
|
percent = clamp(float(item_count[:-1]) / 100, 0, 1)
|
||||||
|
count = math.ceil(total_count * percent)
|
||||||
|
# If invalid count assume its actually part of the item name
|
||||||
|
else:
|
||||||
|
item_name = item_base.strip("|")
|
||||||
|
count = 1
|
||||||
|
return item_name, count
|
||||||
|
|
||||||
|
def construct_rule_from_string(area: dict) -> "rule_builder.rules.Rule | None":
|
||||||
|
if not use_rulebuilder:
|
||||||
|
return None
|
||||||
|
|
||||||
|
import rule_builder.rules
|
||||||
|
|
||||||
|
requires_list = area.get('requires', '')
|
||||||
|
if requires_list == "":
|
||||||
|
return rule_builder.rules.True_()
|
||||||
|
|
||||||
|
def recursively_tokenize_manual_rule(partial: str) -> "rule_builder.rules.Rule | None":
|
||||||
|
if not partial:
|
||||||
|
return rule_builder.rules.True_()
|
||||||
|
rule: Rule | None = None
|
||||||
|
remaining = ''
|
||||||
|
partial = partial.strip()
|
||||||
|
if match := ITEM_REGEX.match(partial):
|
||||||
|
is_category = bool(match.group(1))
|
||||||
|
item_name = match.group(2)
|
||||||
|
item_count = (str(match.group(3) or "1")).lstrip(':')
|
||||||
|
|
||||||
|
if item_count.isnumeric():
|
||||||
|
count = int(item_count)
|
||||||
|
else:
|
||||||
|
item_name, count = evaluate_nonnumeric_count(match.group(0), item_name, item_count, is_category, area)
|
||||||
|
|
||||||
|
if is_category:
|
||||||
|
rule = rule_builder.rules.HasFromList(*world.item_and_event_name_groups.get(item_name, set()), count=count)
|
||||||
|
else:
|
||||||
|
rule = rule_builder.rules.Has(item_name, count)
|
||||||
|
remaining = partial[len(match.group(0)):]
|
||||||
|
elif match := FUNCTION_REGEX.match(partial):
|
||||||
|
func_name = match.group(1)
|
||||||
|
func_args = match.group(2).split(",")
|
||||||
|
if func_args == ['']:
|
||||||
|
func_args.pop()
|
||||||
|
|
||||||
|
rule_class = None
|
||||||
|
search = [
|
||||||
|
(rule_builder.rules.DEFAULT_RULES, func_name),
|
||||||
|
(globals(), func_name + "Rule"),
|
||||||
|
(globals(), func_name),
|
||||||
|
(Rules, func_name + "Rule"),
|
||||||
|
(Rules, func_name),
|
||||||
|
]
|
||||||
|
for ns, name in search:
|
||||||
|
if isinstance(ns, dict):
|
||||||
|
func = ns.get(name)
|
||||||
|
else:
|
||||||
|
func = getattr(ns, name, None)
|
||||||
|
|
||||||
|
if func and inspect.isclass(func) and issubclass(func, rule_builder.rules.Rule):
|
||||||
|
convert_req_function_args(None, func, func_args, area['name'], world)
|
||||||
|
rule_class = func
|
||||||
|
break
|
||||||
|
|
||||||
|
if func and inspect.signature(func).return_annotation is str:
|
||||||
|
# I'm assuming that functions that return strings don't need states.
|
||||||
|
convert_req_function_args(None, func, func_args, area['name'], world)
|
||||||
|
rule = recursively_tokenize_manual_rule(func(*func_args))
|
||||||
|
break
|
||||||
|
|
||||||
|
if rule is None:
|
||||||
|
if not rule_class:
|
||||||
|
logging.warning(f'Warning: Could not find Rule implementation of {func_name}.')
|
||||||
|
# By returning None, we're saying "This entire requires string can't be done with a Rule. Fall back to the pre-rb lambdas"
|
||||||
|
return None
|
||||||
|
|
||||||
|
rule = rule_class(*func_args)
|
||||||
|
remaining = partial[len(match.group(0)):]
|
||||||
|
elif partial[0] == "(":
|
||||||
|
func_founds: list[str] = []
|
||||||
|
for match in FUNCTION_REGEX.finditer(partial):
|
||||||
|
if match.group(0) not in partial:
|
||||||
|
# already done all of them
|
||||||
|
continue
|
||||||
|
|
||||||
|
func_founds.append(match.group(0))
|
||||||
|
# looks like : {{Function#0}}
|
||||||
|
id = len(func_founds) - 1
|
||||||
|
partial = partial.replace(match.group(0), f"{{{{Function#{id}}}}}")
|
||||||
|
|
||||||
|
inner = ''
|
||||||
|
queue = list(partial[1:])
|
||||||
|
stack = 1
|
||||||
|
while stack > 0:
|
||||||
|
if not queue:
|
||||||
|
raise construct_logic_error(area, LogicErrorSource.INFIX_TO_POSTFIX)
|
||||||
|
|
||||||
|
c = queue.pop(0)
|
||||||
|
if c == "(":
|
||||||
|
stack += 1
|
||||||
|
elif c == ")":
|
||||||
|
stack -= 1
|
||||||
|
else:
|
||||||
|
inner += c
|
||||||
|
remaining = "".join(queue)
|
||||||
|
|
||||||
|
for id, func in enumerate(func_founds):
|
||||||
|
remaining = remaining.replace(f"{{{{Function#{id}}}}}", func)
|
||||||
|
inner = inner.replace(f"{{{{Function#{id}}}}}", func)
|
||||||
|
|
||||||
|
rule = recursively_tokenize_manual_rule(inner)
|
||||||
|
else:
|
||||||
|
logging.warning(f'Warning: Could not convert {partial} into a Rule')
|
||||||
|
return None
|
||||||
|
|
||||||
|
if rule is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if match := OR_REGEX.match(remaining):
|
||||||
|
remaining = remaining[len(match.group(0)):]
|
||||||
|
right = recursively_tokenize_manual_rule(remaining)
|
||||||
|
if right is None:
|
||||||
|
return None
|
||||||
|
rule = rule | right
|
||||||
|
remaining = ''
|
||||||
|
elif match := AND_REGEX.match(remaining):
|
||||||
|
remaining = remaining[len(match.group(0)):]
|
||||||
|
right = recursively_tokenize_manual_rule(remaining)
|
||||||
|
if right is None:
|
||||||
|
return None
|
||||||
|
rule = rule & right
|
||||||
|
remaining = ''
|
||||||
|
|
||||||
|
if remaining:
|
||||||
|
raise ValueError(f'Unexpected token `{remaining}` in {area["name"]} rule')
|
||||||
|
return rule
|
||||||
|
|
||||||
|
return recursively_tokenize_manual_rule(requires_list)
|
||||||
|
|
||||||
|
# this is only called when the area (think, location or region) has a "requires" field that is a string
|
||||||
|
def checkRequireStringForArea(state: CollectionState, area: dict):
|
||||||
|
requires_list = area["requires"]
|
||||||
|
|
||||||
|
# Preparing some variables for exception messages
|
||||||
|
area_type = "region" if area.get("is_region",False) else "location"
|
||||||
|
area_name = area.get("name", f"unknown with these parameters: {area}")
|
||||||
|
|
||||||
|
if requires_list == "":
|
||||||
|
return True
|
||||||
|
|
||||||
|
def findAndRecursivelyExecuteFunctions(requires_list: str, recursionDepth: int = 0) -> str:
|
||||||
|
found_functions = FUNCTION_REGEX.findall(requires_list)
|
||||||
|
if found_functions:
|
||||||
|
if recursionDepth > world.rules_functions_maximum_recursion:
|
||||||
|
raise RecursionError(f'One or more functions in {area_type} "{area_name}"\'s requires looped too many time (maximum recursion is {world.rules_functions_maximum_recursion}) \
|
||||||
|
\n As of this Exception the following function(s) are waiting to run: {[f[0] for f in found_functions]} \
|
||||||
|
\n And the currently processed requires look like this: "{requires_list}"')
|
||||||
|
else:
|
||||||
|
for item in found_functions:
|
||||||
|
func_name = item[0]
|
||||||
|
func_args = item[1].split(",")
|
||||||
|
if func_args == ['']:
|
||||||
|
func_args.pop()
|
||||||
|
|
||||||
|
func = globals().get(func_name)
|
||||||
|
|
||||||
|
if func is None:
|
||||||
|
func = getattr(Rules, func_name, None)
|
||||||
|
|
||||||
|
if not callable(func):
|
||||||
|
raise ValueError(f'Invalid function "{func_name}" in {area_type} "{area_name}".')
|
||||||
|
|
||||||
|
convert_req_function_args(state, func, func_args, area_name, world)
|
||||||
|
try:
|
||||||
|
result = func(*func_args)
|
||||||
|
except Exception as ex:
|
||||||
|
raise RuntimeError(f'A call to the function "{func_name}" in {area_type} "{area_name}"\'s requires raised an Exception. \
|
||||||
|
\nUnless it was called by another function, it should look something like "{{{func_name}({item[1]})}}" in {area_type}s.json. \
|
||||||
|
\nFull error message: \
|
||||||
|
\n\n{type(ex).__name__}: {ex}')
|
||||||
|
if isinstance(result, bool):
|
||||||
|
requires_list = requires_list.replace("{" + func_name + "(" + item[1] + ")}", "1" if result else "0")
|
||||||
|
else:
|
||||||
|
requires_list = requires_list.replace("{" + func_name + "(" + item[1] + ")}", str(result))
|
||||||
|
|
||||||
|
requires_list = findAndRecursivelyExecuteFunctions(requires_list, recursionDepth + 1)
|
||||||
|
return requires_list
|
||||||
|
|
||||||
|
requires_list = findAndRecursivelyExecuteFunctions(requires_list)
|
||||||
|
|
||||||
|
# parse user written statement into list of each item
|
||||||
|
for match in ITEM_REGEX.finditer(requires_list):
|
||||||
|
item_base = match.group(0)
|
||||||
|
is_category = bool(match.group(1))
|
||||||
|
item_name = match.group(2)
|
||||||
|
item_count = match.group(3)
|
||||||
|
|
||||||
|
if item_base not in requires_list:
|
||||||
|
# previous instance of this item was already processed
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not item_count:
|
||||||
|
item_count = "1"
|
||||||
|
item_count = item_count.lstrip(':')
|
||||||
|
|
||||||
|
item_name, numeric_count = evaluate_nonnumeric_count(item_base, item_name, item_count, is_category, area)
|
||||||
|
|
||||||
|
if is_category:
|
||||||
|
found = state.has_from_list(world.item_and_event_name_groups.get(item_name, set()), player, numeric_count)
|
||||||
|
else:
|
||||||
|
found = state.has(item_name, player, numeric_count)
|
||||||
|
|
||||||
|
if found:
|
||||||
|
requires_list = requires_list.replace(item_base, "1")
|
||||||
|
else:
|
||||||
|
requires_list = requires_list.replace(item_base, "0")
|
||||||
|
|
||||||
|
requires_list = AND_REGEX.sub('&', requires_list, count=0)
|
||||||
|
requires_list = OR_REGEX.sub('|', requires_list, count=0)
|
||||||
|
|
||||||
|
requires_string = infix_to_postfix("".join(requires_list), area)
|
||||||
|
return (evaluate_postfix(requires_string, area))
|
||||||
|
|
||||||
|
# this is only called when the area (think, location or region) has a "requires" field that is a dict
|
||||||
|
def checkRequireDictForArea(state: CollectionState, area: dict):
|
||||||
|
canAccess = True
|
||||||
|
|
||||||
|
for item in area["requires"]:
|
||||||
|
# if the require entry is an object with "or" or a list of items, treat it as a standalone require of its own
|
||||||
|
if (isinstance(item, dict) and "or" in item and isinstance(item["or"], list)) or (isinstance(item, list)):
|
||||||
|
canAccessOr = True
|
||||||
|
or_items = item
|
||||||
|
|
||||||
|
if isinstance(item, dict):
|
||||||
|
or_items = item["or"]
|
||||||
|
|
||||||
|
for or_item in or_items:
|
||||||
|
or_item_parts = or_item.split(":")
|
||||||
|
or_item_name = or_item
|
||||||
|
or_item_count = 1
|
||||||
|
|
||||||
|
if len(or_item_parts) > 1:
|
||||||
|
or_item_name = or_item_parts[0]
|
||||||
|
or_item_count = int(or_item_parts[1])
|
||||||
|
|
||||||
|
if not state.has(or_item_name, player, or_item_count):
|
||||||
|
canAccessOr = False
|
||||||
|
|
||||||
|
if canAccessOr:
|
||||||
|
canAccess = True
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
item_parts = item.split(":")
|
||||||
|
item_name = item
|
||||||
|
item_count = 1
|
||||||
|
|
||||||
|
if len(item_parts) > 1:
|
||||||
|
item_name = item_parts[0]
|
||||||
|
item_count = int(item_parts[1])
|
||||||
|
|
||||||
|
if not state.has(item_name, player, item_count):
|
||||||
|
canAccess = False
|
||||||
|
|
||||||
|
return canAccess
|
||||||
|
|
||||||
|
# handle any type of checking needed, then ferry the check off to a dedicated method for that check
|
||||||
|
def fullLocationOrRegionCheck(state: CollectionState, area: dict):
|
||||||
|
# if it's not a usable object of some sort, default to true
|
||||||
|
if not area:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# don't require the "requires" key for locations and regions if they don't need to use it
|
||||||
|
if "requires" not in area.keys():
|
||||||
|
return True
|
||||||
|
|
||||||
|
if isinstance(area["requires"], str):
|
||||||
|
return checkRequireStringForArea(state, area)
|
||||||
|
else: # item access is in dict form
|
||||||
|
return checkRequireDictForArea(state, area)
|
||||||
|
|
||||||
|
used_location_names = []
|
||||||
|
# Region access rules
|
||||||
|
extra_entrance_rules = {}
|
||||||
|
for region in regionMap.keys():
|
||||||
|
entrance_rules = regionMap[region].get("entrance_requires", {})
|
||||||
|
for e in entrance_rules:
|
||||||
|
entrance = world.get_entrance(f'{e}To{region}')
|
||||||
|
area = {"requires": entrance_rules[e]}
|
||||||
|
extra_entrance_rules[entrance.name] = area
|
||||||
|
|
||||||
|
exit_rules = regionMap[region].get("exit_requires", {})
|
||||||
|
for e in exit_rules:
|
||||||
|
exit = world.get_entrance(f'{region}To{e}')
|
||||||
|
area = {"requires": exit_rules[e]}
|
||||||
|
extra_entrance_rules[exit.name] = area
|
||||||
|
|
||||||
|
for region in regionMap.keys():
|
||||||
|
used_location_names.extend([l.name for l in multiworld.get_region(region, player).locations])
|
||||||
|
for exitRegion in multiworld.get_region(region, player).entrances:
|
||||||
|
extra = extra_entrance_rules.get(exitRegion.name, {})
|
||||||
|
area = regionMap[region]
|
||||||
|
area["name"] = exitRegion.name
|
||||||
|
area['is_region'] = True
|
||||||
|
rb_rule = construct_rule_from_string(area)
|
||||||
|
if rb_rule is not None:
|
||||||
|
if extra:
|
||||||
|
rb_extra_rule = construct_rule_from_string(extra)
|
||||||
|
if rb_extra_rule is None:
|
||||||
|
raise ValueError(f'Unable to combine Rule and functions for {exitRegion.name}.')
|
||||||
|
rb_rule = rb_rule & rb_extra_rule
|
||||||
|
world.set_rule(world.get_entrance(exitRegion.name), rb_rule)
|
||||||
|
else:
|
||||||
|
def fullRegionCheck(state: CollectionState, region=area):
|
||||||
|
return fullLocationOrRegionCheck(state, region)
|
||||||
|
|
||||||
|
add_rule(world.get_entrance(exitRegion.name), fullRegionCheck)
|
||||||
|
if extra:
|
||||||
|
add_rule(world.get_entrance(exitRegion.name), lambda state, extra=extra: fullLocationOrRegionCheck(state, extra))
|
||||||
|
|
||||||
|
# Location access rules
|
||||||
|
for location in (world.location_table + world.event_table):
|
||||||
|
if "location_name" in location:
|
||||||
|
name = location["location_name"]
|
||||||
|
else:
|
||||||
|
name = location["name"]
|
||||||
|
if name not in used_location_names:
|
||||||
|
continue
|
||||||
|
|
||||||
|
locFromWorld = multiworld.get_location(name, player)
|
||||||
|
|
||||||
|
if "requires" in location:
|
||||||
|
rb_rule = construct_rule_from_string(location)
|
||||||
|
if rb_rule is not None:
|
||||||
|
world.set_rule(locFromWorld, rb_rule)
|
||||||
|
else:
|
||||||
|
def checkLocation(state: CollectionState, location=location):
|
||||||
|
locationCheck = fullLocationOrRegionCheck(state, location)
|
||||||
|
return locationCheck
|
||||||
|
|
||||||
|
set_rule(locFromWorld, checkLocation)
|
||||||
|
elif use_rulebuilder:
|
||||||
|
import rule_builder.rules
|
||||||
|
world.set_rule(locFromWorld, rule_builder.rules.True_())
|
||||||
|
else: # No location requires? It's accessible.
|
||||||
|
def allRegionsAccessible(state):
|
||||||
|
return True
|
||||||
|
|
||||||
|
set_rule(locFromWorld, allRegionsAccessible)
|
||||||
|
|
||||||
|
# Victory requirement
|
||||||
|
multiworld.completion_condition[player] = lambda state: state.has("__Victory__", player)
|
||||||
|
|
||||||
|
def convert_req_function_args(state: CollectionState | None, func, args: list[str | Any], areaName: str, world: World) -> None:
|
||||||
|
parameters = inspect.signature(func).parameters
|
||||||
|
knownParameters = [World, 'ManualWorld', MultiWorld, CollectionState]
|
||||||
|
index = -1
|
||||||
|
for parameter in parameters.values():
|
||||||
|
target_type = parameter.annotation
|
||||||
|
if str(target_type) == "collections.abc.Iterable[rule_builder.options.OptionFilter]":
|
||||||
|
# parameters >= this one are added by rule builder and do not need to be converted
|
||||||
|
break
|
||||||
|
|
||||||
|
index += 1
|
||||||
|
if target_type in knownParameters:
|
||||||
|
if target_type in [World, 'ManualWorld']:
|
||||||
|
args.insert(index, world)
|
||||||
|
elif target_type == MultiWorld:
|
||||||
|
args.insert(index, world.multiworld)
|
||||||
|
elif target_type == CollectionState and state is None:
|
||||||
|
raise ValueError('Function needs a state but none was available')
|
||||||
|
elif target_type == CollectionState:
|
||||||
|
args.insert(index, state)
|
||||||
|
continue
|
||||||
|
if parameter.name.lower() == "player":
|
||||||
|
args.insert(index, world.player)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if index < len(args) and args[index] != "":
|
||||||
|
value = args[index].strip()
|
||||||
|
else:
|
||||||
|
if parameter.default is not inspect.Parameter.empty:
|
||||||
|
if index < len(args):
|
||||||
|
args[index] = parameter.default
|
||||||
|
else:
|
||||||
|
args.insert(index, parameter.default)
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
if parameter.annotation is inspect.Parameter.empty:
|
||||||
|
raise Exception(f"A call of the \"{func.__name__}\" function in \"{areaName}\"'s requirement, asks for a value for its argument \"{parameter.name}\" but it's missing.")
|
||||||
|
else:
|
||||||
|
raise Exception(f"A call of the \"{func.__name__}\" function in \"{areaName}\"'s requirement, asks for a value of type {target_type} for its argument \"{parameter.name}\" but it's missing.")
|
||||||
|
|
||||||
|
if target_type == str or parameter.annotation is inspect.Parameter.empty: #Don't convert since its already a string or if we don't know the type to convert to
|
||||||
|
args[index] = value
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
value = convert_string_to_type(value, target_type)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise Exception(f"A call of the \"{func.__name__}\" function in \"{areaName}\"'s requirement, asks for a value of type {target_type}\nfor its argument \"{parameter.name}\" but its value \"{value}\" cannot be converted to {target_type} \nOriginal Error:'{e}'")
|
||||||
|
|
||||||
|
args[index] = value
|
||||||
|
|
||||||
|
|
||||||
|
def ItemValue(state: CollectionState, player: int, valueCount: str):
|
||||||
|
"""When passed a string with this format: 'valueName:int',
|
||||||
|
this function will check if the player has collect at least 'int' valueName worth of items\n
|
||||||
|
eg. {ItemValue(Coins:12)} will check if the player has collect at least 12 coins worth of items
|
||||||
|
"""
|
||||||
|
|
||||||
|
args: list[str] = valueCount.split(":")
|
||||||
|
if not len(args) == 2 or not args[1].isnumeric():
|
||||||
|
raise Exception(f"ItemValue needs a number after : so it looks something like 'ItemValue({args[0]}:12)'")
|
||||||
|
value_name = format_state_prog_items_key(ProgItemsCat.VALUE, args[0])
|
||||||
|
requested_count = int(args[1].strip())
|
||||||
|
return state.has(value_name, player, requested_count)
|
||||||
|
|
||||||
|
|
||||||
|
# Two useful functions to make require work if an item is disabled instead of making it inaccessible
|
||||||
|
def OptOne(world: "ManualWorld", item: str) -> str:
|
||||||
|
"""Check if the passed item (with or without ||) is enabled, then this returns |item:count|
|
||||||
|
where count is clamped to the maximum number of said item in the itempool.\n
|
||||||
|
Eg. requires: "{OptOne(|DisabledItem|)} and |other items|" become "|DisabledItem:0| and |other items|" if the item is disabled.
|
||||||
|
"""
|
||||||
|
if item == "":
|
||||||
|
return "" #Skip this function if item is left blank
|
||||||
|
|
||||||
|
items_counts = world.get_item_counts(only_progression=True)
|
||||||
|
|
||||||
|
require_category = False
|
||||||
|
|
||||||
|
if '@' in item[:2]:
|
||||||
|
require_category = True
|
||||||
|
|
||||||
|
item = item.lstrip('|@$').rstrip('|')
|
||||||
|
|
||||||
|
item_parts = item.split(":")
|
||||||
|
item_name = item
|
||||||
|
item_count = '1'
|
||||||
|
|
||||||
|
if len(item_parts) > 1:
|
||||||
|
item_name = item_parts[0]
|
||||||
|
item_count = item_parts[1]
|
||||||
|
|
||||||
|
if require_category:
|
||||||
|
if item_count.isnumeric():
|
||||||
|
#Only loop if we can use the result to clamp
|
||||||
|
category_items = [item for item in world.item_name_to_item.values() if "category" in item and item_name in item["category"]]
|
||||||
|
category_items_counts = sum([items_counts.get(category_item["name"], 0) for category_item in category_items])
|
||||||
|
item_count = clamp(int(item_count), 0, category_items_counts)
|
||||||
|
return f"|@{item_name}:{item_count}|"
|
||||||
|
else:
|
||||||
|
if item_count.isnumeric():
|
||||||
|
item_current_count = items_counts.get(item_name, 0)
|
||||||
|
item_count = clamp(int(item_count), 0, item_current_count)
|
||||||
|
return f"|{item_name}:{item_count}|"
|
||||||
|
|
||||||
|
# OptAll check the passed require string and loop every item to check if they're enabled,
|
||||||
|
def OptAll(world: "ManualWorld", requires: str) -> bool|str:
|
||||||
|
"""Check the passed require string and loop every item to check if they're enabled,
|
||||||
|
then returns the require string with items counts adjusted using OptOne\n
|
||||||
|
eg. requires: "{OptAll(|DisabledItem| and |@CategoryWithModifedCount:10|)} and |other items|"
|
||||||
|
become "|DisabledItem:0| and |@CategoryWithModifedCount:2| and |other items|" """
|
||||||
|
requires_list = requires
|
||||||
|
|
||||||
|
if requires_list == "":
|
||||||
|
return True
|
||||||
|
|
||||||
|
# parse user written statement into list of each item
|
||||||
|
for item in re.findall(r'\|[^|]+\|', requires):
|
||||||
|
itemScanned = OptOne(world, item)
|
||||||
|
requires_list = requires_list.replace(item, itemScanned)
|
||||||
|
|
||||||
|
return requires_list
|
||||||
|
|
||||||
|
# going to be deprecated to name consistently to other req functions, in pascal case
|
||||||
|
def canReachLocation(state: CollectionState, player: int, location: str):
|
||||||
|
logging.warning("The 'canReachLocation' requirement function is being renamed to 'CanReachLocation'. Use that instead, as the lowercase version will be deprecated.")
|
||||||
|
return CanReachLocation(state, player, location)
|
||||||
|
|
||||||
|
# Rule to expose the can_reach_location core function
|
||||||
|
def CanReachLocation(state: CollectionState, player: int, location: str) -> bool:
|
||||||
|
"""Can the player reach the given location?"""
|
||||||
|
if state.can_reach_location(location, player):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def OptionCount(world: "ManualWorld", item: str, option_name: str) -> str:
|
||||||
|
"""Set the required count of 'item' to be the value set in the player's yaml of the Numerical option 'option_name'."""
|
||||||
|
return _optionCountLogic(world, item, option_name )
|
||||||
|
|
||||||
|
def OptionCountPercent(world: "ManualWorld", item: str, option_name: str) -> str:
|
||||||
|
"""Set the required count of 'item' to be a percentage of it total count based on the player's yaml value for Numerical option 'option_name'."""
|
||||||
|
return _optionCountLogic(world, item, option_name, is_percent=True)
|
||||||
|
|
||||||
|
def _optionCountLogic(world: "ManualWorld", item: str, option_name: str, is_percent: bool = False) -> str:
|
||||||
|
option_name = option_name.strip()
|
||||||
|
option: NumericOption | None = getattr(world.options, option_name, None)
|
||||||
|
if option is None:
|
||||||
|
raise ValueError(f"Could not find an option named: {option_name}")
|
||||||
|
|
||||||
|
# Verification that the value is compatible
|
||||||
|
if not isinstance(option.value, int):
|
||||||
|
raise ValueError(f"Cannot use a value that is not a number. Got value of '{option.value}' from option {option_name}")
|
||||||
|
|
||||||
|
item = item.strip('|').strip()
|
||||||
|
return f"|{item}:{option.value}{'%' if is_percent else ''}|"
|
||||||
|
|
||||||
|
def YamlEnabled(multiworld: MultiWorld, player: int, param: str) -> bool:
|
||||||
|
"""Is a yaml option enabled?"""
|
||||||
|
return is_option_enabled(multiworld, player, param)
|
||||||
|
|
||||||
|
def YamlDisabled(multiworld: MultiWorld, player: int, param: str) -> bool:
|
||||||
|
"""Is a yaml option disabled?"""
|
||||||
|
return not is_option_enabled(multiworld, player, param)
|
||||||
|
|
||||||
|
def YamlCompare(world: "ManualWorld", 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
|
||||||
|
|
||||||
|
if use_rulebuilder:
|
||||||
|
from rule_builder.rules import Rule, Has, True_, False_
|
||||||
|
|
||||||
|
@dataclasses.dataclass()
|
||||||
|
class ItemValueRule(Rule["ManualWorld"], game=game_name):
|
||||||
|
valueCount: str
|
||||||
|
def _instantiate(self, world: "ManualWorld") -> Rule.Resolved:
|
||||||
|
args: list[str] = self.valueCount.split(":")
|
||||||
|
if not len(args) == 2 or not args[1].isnumeric():
|
||||||
|
raise Exception(f"ItemValue needs a number after : so it looks something like 'ItemValue({args[0]}:12)'")
|
||||||
|
value_name = format_state_prog_items_key(ProgItemsCat.VALUE, args[0])
|
||||||
|
requested_count = int(args[1].strip())
|
||||||
|
return Has(value_name, requested_count).resolve(world)
|
||||||
|
|
||||||
|
@dataclasses.dataclass()
|
||||||
|
class YamlEnabledRule(Rule["ManualWorld"], game=game_name):
|
||||||
|
yaml_option: str
|
||||||
|
def _instantiate(self, world: "ManualWorld") -> Rule.Resolved:
|
||||||
|
if getattr(world.options, self.yaml_option).value:
|
||||||
|
return True_().resolve(world)
|
||||||
|
else:
|
||||||
|
return False_().resolve(world)
|
||||||
|
|
||||||
|
@dataclasses.dataclass()
|
||||||
|
class YamlDisabledRule(Rule["ManualWorld"], game=game_name):
|
||||||
|
yaml_option: str
|
||||||
|
def _instantiate(self, world: "ManualWorld") -> Rule.Resolved:
|
||||||
|
if getattr(world.options, self.yaml_option).value:
|
||||||
|
return False_().resolve(world)
|
||||||
|
else:
|
||||||
|
return True_().resolve(world)
|
||||||
|
|
||||||
|
@dataclasses.dataclass()
|
||||||
|
class YamlCompareRule(Rule["ManualWorld"], game=game_name):
|
||||||
|
yaml_comparison: str
|
||||||
|
def _instantiate(self, world: "ManualWorld") -> Rule.Resolved:
|
||||||
|
if YamlCompare(world, self.yaml_comparison):
|
||||||
|
return True_().resolve(world)
|
||||||
|
else:
|
||||||
|
return False_().resolve(world)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# # This was an attempt to make a universal lambda caller rule. It's messy, complicated, and probably not worth it.
|
||||||
|
# # As you can see above, you are much better off implementing optimized Rules rather than trying to execute check-time access rules this way.
|
||||||
|
# # This code is left here both as a warning and also a starting point.
|
||||||
|
# # But as things currently stand, you're better off either letting the whole rule fall back to lambdas or writing a proper custom Rule.
|
||||||
|
# # This middleground is not a good idea.
|
||||||
|
# @dataclasses.dataclass()
|
||||||
|
# class FallbackLambdaRule(Rule["ManualWorld"], game=game_name):
|
||||||
|
# access_rule: Callable
|
||||||
|
# args: list[str | Any]
|
||||||
|
|
||||||
|
# def _instantiate(self, world: "ManualWorld") -> Rule.Resolved:
|
||||||
|
# return self.Resolved(self.access_rule, self.args, world)
|
||||||
|
|
||||||
|
# class Resolved(Rule.Resolved):
|
||||||
|
# access_rule: Callable
|
||||||
|
# args: list[str | Any]
|
||||||
|
# world: World
|
||||||
|
|
||||||
|
# @override
|
||||||
|
# def _evaluate(self, state: CollectionState) -> bool:
|
||||||
|
# args = self.args.copy()
|
||||||
|
# convert_req_function_args(state, self.access_rule, args, "", self.world)
|
||||||
|
# value = self.access_rule(*args)
|
||||||
|
# if isinstance(value, bool):
|
||||||
|
# return value
|
||||||
|
# name = self.access_rule.__name__
|
||||||
|
# raise ValueError(f'Unexpected return value {value} from {name}')
|
||||||
|
|
||||||
|
|
||||||
+636
@@ -0,0 +1,636 @@
|
|||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from typing import Callable, Optional, ClassVar, Counter, Any
|
||||||
|
import webbrowser
|
||||||
|
|
||||||
|
import Utils
|
||||||
|
from worlds.generic.Rules import forbid_items_for_player
|
||||||
|
from worlds.LauncherComponents import Component, SuffixIdentifier, components, Type, launch, icon_paths
|
||||||
|
|
||||||
|
from .Data import item_table, location_table, event_table, category_table
|
||||||
|
from .Game import game_name, filler_item_name, starting_items, unused_goals_are_locations, glitches_item_name
|
||||||
|
from .Meta import world_description, world_webworld
|
||||||
|
from .Locations import location_id_to_name, location_name_to_id, location_name_to_location, location_name_groups, victory_names, event_name_to_event, event_name_groups
|
||||||
|
from .Items import item_id_to_name, item_name_to_id, item_name_to_item, item_name_groups
|
||||||
|
from .DataValidation import runGenerationDataValidation, runPreFillDataValidation
|
||||||
|
|
||||||
|
from .Regions import create_regions, create_events
|
||||||
|
from .Items import ManualItem
|
||||||
|
from .Rules import set_rules
|
||||||
|
from .Options import manual_options_data
|
||||||
|
from .Helpers import is_item_enabled, get_option_value, remove_specific_item, resolve_yaml_option, format_state_prog_items_key, convert_string_to_itemclassification, ProgItemsCat
|
||||||
|
from .container import APManualFile
|
||||||
|
|
||||||
|
from BaseClasses import CollectionState, ItemClassification, Item, Location
|
||||||
|
from Options import PerGameCommonOptions
|
||||||
|
from worlds.AutoWorld import World
|
||||||
|
|
||||||
|
from .hooks.World import \
|
||||||
|
hook_get_filler_item_name, before_create_regions, after_create_regions, \
|
||||||
|
before_create_items_all, before_create_items_place_items, before_create_items_starting, before_create_items_filler, after_create_items, \
|
||||||
|
before_create_item, after_create_item, \
|
||||||
|
before_set_rules, after_set_rules, \
|
||||||
|
before_generate_basic, \
|
||||||
|
before_fill_slot_data, after_fill_slot_data, before_write_spoiler, \
|
||||||
|
before_extend_hint_information, after_extend_hint_information, \
|
||||||
|
after_collect_item, after_remove_item, before_generate_early, hook_interpret_slot_data
|
||||||
|
|
||||||
|
class ManualWorld(World):
|
||||||
|
__doc__ = world_description
|
||||||
|
game: ClassVar[str] = game_name
|
||||||
|
web = world_webworld
|
||||||
|
|
||||||
|
options_dataclass = manual_options_data
|
||||||
|
data_version = 2
|
||||||
|
required_client_version = (0, 3, 4)
|
||||||
|
|
||||||
|
# These properties are set from the imports of the same name above.
|
||||||
|
item_table = item_table
|
||||||
|
location_table = location_table # this is likely imported from Data instead of Locations because the Game Complete location should not be in here, but is used for lookups
|
||||||
|
event_table = event_table
|
||||||
|
category_table = category_table
|
||||||
|
|
||||||
|
item_id_to_name = item_id_to_name
|
||||||
|
item_name_to_id = item_name_to_id
|
||||||
|
item_name_to_item = item_name_to_item
|
||||||
|
item_name_groups = item_name_groups
|
||||||
|
|
||||||
|
filler_item_name = filler_item_name
|
||||||
|
|
||||||
|
item_counts: dict[int, Counter[str]] = {}
|
||||||
|
item_counts_progression: dict[int, Counter[str]] = {}
|
||||||
|
start_inventory = {}
|
||||||
|
|
||||||
|
location_id_to_name = location_id_to_name
|
||||||
|
location_name_to_id = location_name_to_id
|
||||||
|
location_name_to_location = location_name_to_location
|
||||||
|
location_name_groups = location_name_groups
|
||||||
|
victory_names = victory_names
|
||||||
|
|
||||||
|
event_name_to_event = event_name_to_event
|
||||||
|
event_name_groups = event_name_groups
|
||||||
|
item_and_event_name_groups: dict[str, set[str]] = dict(item_name_groups)
|
||||||
|
for name, category in event_name_groups.items():
|
||||||
|
if name in item_and_event_name_groups.keys():
|
||||||
|
item_and_event_name_groups[name] = item_name_groups[name].union(category)
|
||||||
|
else:
|
||||||
|
item_and_event_name_groups[name] = category
|
||||||
|
|
||||||
|
# UT (the universal-est of trackers) can now generate without a YAML
|
||||||
|
ut_can_gen_without_yaml = True
|
||||||
|
glitches_item_name: str | None = glitches_item_name
|
||||||
|
|
||||||
|
origin_region_name = "Manual"
|
||||||
|
|
||||||
|
def get_filler_item_name(self) -> str:
|
||||||
|
hook_result = hook_get_filler_item_name(self, self.multiworld, self.player)
|
||||||
|
if hook_result and isinstance(hook_result, str):
|
||||||
|
return hook_result
|
||||||
|
elif hook_result and isinstance(hook_result, list):
|
||||||
|
return self.random.choice(hook_result)
|
||||||
|
elif isinstance(filler_item_name, str):
|
||||||
|
return self.filler_item_name
|
||||||
|
else:
|
||||||
|
random_filler_name = self.random.choice(filler_item_name)
|
||||||
|
return random_filler_name
|
||||||
|
|
||||||
|
def interpret_slot_data(self, slot_data: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
#this is called by tools like UT
|
||||||
|
if not slot_data:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
regen = hook_interpret_slot_data(self, self.player, slot_data) or slot_data
|
||||||
|
return regen
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def stage_assert_generate(cls, multiworld) -> None:
|
||||||
|
runGenerationDataValidation(cls)
|
||||||
|
|
||||||
|
def generate_early(self) -> None:
|
||||||
|
before_generate_early(self, self.multiworld, self.player)
|
||||||
|
for item in item_table:
|
||||||
|
if item.get("local"):
|
||||||
|
if item.get("name") not in self.options.local_items.value:
|
||||||
|
self.options.local_items.value.add(item["name"])
|
||||||
|
|
||||||
|
if hasattr(self.multiworld, "re_gen_passthrough"):
|
||||||
|
slot_data = self.multiworld.re_gen_passthrough.get(self.game, {})
|
||||||
|
if slot_data:
|
||||||
|
for key, value in slot_data.items():
|
||||||
|
if hasattr(self.options, key):
|
||||||
|
getattr(self.options, key).value = value
|
||||||
|
|
||||||
|
def create_regions(self):
|
||||||
|
before_create_regions(self, self.multiworld, self.player)
|
||||||
|
|
||||||
|
create_regions(self, self.multiworld, self.player)
|
||||||
|
|
||||||
|
create_events(self, self.multiworld, self.player)
|
||||||
|
|
||||||
|
location_game_complete = self.multiworld.get_location(victory_names[get_option_value(self.multiworld, self.player, 'goal')], self.player)
|
||||||
|
location_game_complete.address = None
|
||||||
|
|
||||||
|
if not unused_goals_are_locations:
|
||||||
|
for unused_goal in [self.multiworld.get_location(name, self.player) for name in victory_names if name != location_game_complete.name]:
|
||||||
|
unused_goal.parent_region.locations.remove(unused_goal)
|
||||||
|
|
||||||
|
location_game_complete.place_locked_item(
|
||||||
|
ManualItem("__Victory__", ItemClassification.progression, None, player=self.player))
|
||||||
|
|
||||||
|
after_create_regions(self, self.multiworld, self.player)
|
||||||
|
|
||||||
|
def create_items(self):
|
||||||
|
# Generate item pool
|
||||||
|
pool: list[Item] = []
|
||||||
|
traps = []
|
||||||
|
configured_item_names = self.item_id_to_name.copy()
|
||||||
|
is_UT = bool(getattr(self.multiworld, "generation_is_fake", False))
|
||||||
|
|
||||||
|
items_config: dict[str, int|dict[ItemClassification | str | int, int]] = {}
|
||||||
|
for name in configured_item_names.values():
|
||||||
|
if name == "__Victory__": continue
|
||||||
|
# intentionally using the Game.py filler_item_name here because it can be a non-Items item
|
||||||
|
if isinstance(filler_item_name, str):
|
||||||
|
if name == filler_item_name: continue
|
||||||
|
elif isinstance(filler_item_name, list):
|
||||||
|
if name in filler_item_name: continue
|
||||||
|
|
||||||
|
item = self.item_name_to_item[name]
|
||||||
|
item_count = int(item.get("count", 1))
|
||||||
|
|
||||||
|
if item.get("trap"):
|
||||||
|
traps.append(name)
|
||||||
|
|
||||||
|
if not is_item_enabled(self.multiworld, self.player, item):
|
||||||
|
items_config[name] = 0
|
||||||
|
|
||||||
|
else:
|
||||||
|
if item.get("classification_count"):
|
||||||
|
items_config[name] = item["classification_count"]
|
||||||
|
|
||||||
|
else:
|
||||||
|
items_config[name] = item_count
|
||||||
|
|
||||||
|
items_config = before_create_items_all(items_config, self, self.multiworld, self.player)
|
||||||
|
|
||||||
|
for name, configs in items_config.items():
|
||||||
|
total_created = 0
|
||||||
|
if type(configs) is int:
|
||||||
|
total_created = configs
|
||||||
|
for _ in range(configs):
|
||||||
|
new_item = self.create_item(name)
|
||||||
|
pool.append(new_item)
|
||||||
|
elif type(configs) is dict:
|
||||||
|
for override, count in configs.items():
|
||||||
|
total_created += count
|
||||||
|
if isinstance(override, ItemClassification):
|
||||||
|
true_class = override
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
if isinstance(override, int):
|
||||||
|
true_class = ItemClassification(override)
|
||||||
|
else:
|
||||||
|
true_class = convert_string_to_itemclassification(override)
|
||||||
|
except Exception as ex:
|
||||||
|
raise Exception(f"Item override '{override}' for {name} improperly defined\n\n{type(ex).__name__}:{ex}")
|
||||||
|
|
||||||
|
for _ in range(count):
|
||||||
|
new_item = self.create_item(name, true_class)
|
||||||
|
pool.append(new_item)
|
||||||
|
else:
|
||||||
|
raise Exception(f"Item override for {name} improperly defined")
|
||||||
|
|
||||||
|
if total_created == 0 or is_UT: continue
|
||||||
|
|
||||||
|
item = self.item_name_to_item[name]
|
||||||
|
if item.get("early"): # Some or all early
|
||||||
|
if isinstance(item["early"],int) or (isinstance(item["early"],str) and item["early"].isnumeric()):
|
||||||
|
self.multiworld.early_items[self.player][name] = int(item["early"])
|
||||||
|
|
||||||
|
elif isinstance(item["early"],bool): #No need to deal with true vs false since false wont get here
|
||||||
|
self.multiworld.early_items[self.player][name] = total_created
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise Exception(f"Item {name}'s 'early' has an invalid value of '{item['early']}'. \nA boolean or an integer was expected.")
|
||||||
|
|
||||||
|
if item.get("local_early"): # Some or all local and early
|
||||||
|
if isinstance(item["local_early"],int) or (isinstance(item["local_early"],str) and item["local_early"].isnumeric()):
|
||||||
|
self.multiworld.local_early_items[self.player][name] = int(item["local_early"])
|
||||||
|
|
||||||
|
elif isinstance(item["local_early"],bool):
|
||||||
|
self.multiworld.local_early_items[self.player][name] = total_created
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise Exception(f"Item {name}'s 'local_early' has an invalid value of '{item['local_early']}'. \nA boolean or an integer was expected.")
|
||||||
|
|
||||||
|
# Handle item forbidding/placement
|
||||||
|
pool = before_create_items_place_items(pool, self, self.multiworld, self.player)
|
||||||
|
locations_with_forbid: list[Location] = []
|
||||||
|
locations_with_placements: list[Location] = []
|
||||||
|
if not is_UT:
|
||||||
|
for location in self.multiworld.get_unfilled_locations(player=self.player):
|
||||||
|
manual_location = self.location_name_to_location.get(location.name, {})
|
||||||
|
if manual_location.get("place_item") or manual_location.get("place_item_category"):
|
||||||
|
locations_with_placements.append(location)
|
||||||
|
elif manual_location.get("dont_place_item") or manual_location.get("dont_place_item_category"):
|
||||||
|
locations_with_forbid.append(location)
|
||||||
|
|
||||||
|
# Handle specific item forbidding using forbid_items_for_player
|
||||||
|
for location in locations_with_forbid:
|
||||||
|
manual_location = self.location_name_to_location.get(location.name, {})
|
||||||
|
forbidden_item_names: set[str] = set()
|
||||||
|
|
||||||
|
if manual_location.get("dont_place_item"):
|
||||||
|
forbidden_item_names |= set(manual_location["dont_place_item"])
|
||||||
|
|
||||||
|
if manual_location.get("dont_place_item_category"):
|
||||||
|
for cat in manual_location["dont_place_item_category"]:
|
||||||
|
forbidden_item_names |= self.item_name_groups.get(cat, set())
|
||||||
|
|
||||||
|
if forbidden_item_names:
|
||||||
|
forbid_items_for_player(location, set(forbidden_item_names), self.player)
|
||||||
|
|
||||||
|
# Handle specific item placements using place_locked_item
|
||||||
|
for location in locations_with_placements:
|
||||||
|
manual_location = self.location_name_to_location.get(location.name, {})
|
||||||
|
eligible_items = []
|
||||||
|
eligible_item_names: set[str] = set()
|
||||||
|
forbidden_item_names = set()
|
||||||
|
place_messages = []
|
||||||
|
forbid_messages = []
|
||||||
|
|
||||||
|
#First we get possible items names
|
||||||
|
if manual_location.get("place_item"):
|
||||||
|
eligible_item_names |= set(manual_location["place_item"])
|
||||||
|
place_messages.append('", "'.join(manual_location["place_item"]))
|
||||||
|
|
||||||
|
if manual_location.get("place_item_category"):
|
||||||
|
for cat in manual_location["place_item_category"]:
|
||||||
|
eligible_item_names |= self.item_name_groups.get(cat, set())
|
||||||
|
place_messages.append('", "'.join(manual_location["place_item_category"]) + " category(ies)")
|
||||||
|
|
||||||
|
# Second we check for forbidden items names
|
||||||
|
if manual_location.get("dont_place_item"):
|
||||||
|
forbidden_item_names |= set(manual_location["dont_place_item"])
|
||||||
|
forbid_messages.append('", "'.join(manual_location["dont_place_item"]) + ' items')
|
||||||
|
|
||||||
|
if manual_location.get("dont_place_item_category"):
|
||||||
|
for cat in manual_location["dont_place_item_category"]:
|
||||||
|
forbidden_item_names |= self.item_name_groups.get(cat, set())
|
||||||
|
forbid_messages.append('", "'.join(manual_location["dont_place_item_category"]) + ' category(ies)')
|
||||||
|
|
||||||
|
# If we forbid some names, check for those in the possible names and remove them
|
||||||
|
if forbidden_item_names:
|
||||||
|
eligible_item_names = {name for name in eligible_item_names if name not in forbidden_item_names}
|
||||||
|
|
||||||
|
if eligible_item_names:
|
||||||
|
eligible_items = [item for item in pool if item.name in eligible_item_names]
|
||||||
|
|
||||||
|
if len(eligible_items) == 0:
|
||||||
|
nl = "\n"
|
||||||
|
if forbidden_item_names:
|
||||||
|
raise Exception(f'Could not find a suitable item to place at "{manual_location["name"]}".\n No items that match "{f"{nl} or ".join(place_messages)}"\n Maybe because of forbidden "{f"{nl} or ".join(forbid_messages)}"')
|
||||||
|
raise Exception(f'Could not find a suitable item to place at "{manual_location["name"]}". \n No items that match "{f"{nl} or ".join(place_messages)}"')
|
||||||
|
|
||||||
|
item_to_place = self.random.choice(eligible_items)
|
||||||
|
location.place_locked_item(item_to_place)
|
||||||
|
|
||||||
|
# remove the item we're about to place from the pool so it isn't placed twice
|
||||||
|
remove_specific_item(pool, item_to_place)
|
||||||
|
|
||||||
|
# Handle game.json's starting_items
|
||||||
|
pool = before_create_items_starting(pool, self, self.multiworld, self.player)
|
||||||
|
|
||||||
|
items_started: list[Item] = []
|
||||||
|
|
||||||
|
if starting_items:
|
||||||
|
for starting_item_block in starting_items:
|
||||||
|
if not resolve_yaml_option(self.multiworld, self.player, starting_item_block):
|
||||||
|
continue
|
||||||
|
# if there's a condition on having a previous item, check for any of them
|
||||||
|
# if not found in items started, this starting item rule shouldn't execute, and check the next one
|
||||||
|
if "if_previous_item" in starting_item_block:
|
||||||
|
matching_items = [item for item in items_started if item.name in starting_item_block["if_previous_item"]]
|
||||||
|
|
||||||
|
if len(matching_items) == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# start with the full pool of items
|
||||||
|
items = pool
|
||||||
|
|
||||||
|
# if the setting lists specific item names, limit the items to just those
|
||||||
|
if "items" in starting_item_block:
|
||||||
|
items = [item for item in pool if item.name in starting_item_block["items"]]
|
||||||
|
|
||||||
|
# if the setting lists specific item categories, limit the items to ones that have any of those categories
|
||||||
|
if "item_categories" in starting_item_block:
|
||||||
|
items_in_categories = [item["name"] for item in self.item_name_to_item.values() if "category" in item and len(set(starting_item_block["item_categories"]).intersection(item["category"])) > 0]
|
||||||
|
items = [item for item in pool if item.name in items_in_categories]
|
||||||
|
|
||||||
|
self.random.shuffle(items)
|
||||||
|
|
||||||
|
# if the setting lists a specific number of random items that should be pulled, only use a subset equal to that number
|
||||||
|
if "random" in starting_item_block:
|
||||||
|
items = items[0:starting_item_block["random"]]
|
||||||
|
|
||||||
|
for starting_item in items:
|
||||||
|
items_started.append(starting_item)
|
||||||
|
self.multiworld.push_precollected(starting_item)
|
||||||
|
remove_specific_item(pool, starting_item)
|
||||||
|
|
||||||
|
self.start_inventory = {i.name: items_started.count(i) for i in items_started}
|
||||||
|
|
||||||
|
pool = before_create_items_filler(pool, self, self.multiworld, self.player)
|
||||||
|
pool = self.adjust_filler_items(pool, traps)
|
||||||
|
pool = after_create_items(pool, self, self.multiworld, self.player)
|
||||||
|
|
||||||
|
# need to put all of the items in the pool so we can have a full state for placement
|
||||||
|
self.multiworld.itempool += pool
|
||||||
|
|
||||||
|
# Preparing to count the items:
|
||||||
|
precollected_items = list(self.multiworld.precollected_items[self.player])
|
||||||
|
|
||||||
|
# UT doesn't precollect the exceptions so this can be skipped
|
||||||
|
if not is_UT:
|
||||||
|
# Filter Precollected items for those not in logic aka created by start_inventory(_from_pool)
|
||||||
|
precollected_exceptions = self.options.start_inventory.value + self.options.start_inventory_from_pool.value # type: ignore
|
||||||
|
for item, count in precollected_exceptions.items():
|
||||||
|
items_iter = iter([i for i in precollected_items if i.name == item])
|
||||||
|
for _ in range(count):
|
||||||
|
precollected_items.remove(next(items_iter))
|
||||||
|
# Placed items:
|
||||||
|
placed_pool: list[Item] = []
|
||||||
|
for location in self.multiworld.get_filled_locations(self.player):
|
||||||
|
if location.item is not None: # For type checkers
|
||||||
|
placed_pool.append(location.item)
|
||||||
|
|
||||||
|
real_pool = pool + precollected_items + placed_pool
|
||||||
|
self.item_counts[self.player] = self.get_item_counts(pool=real_pool)
|
||||||
|
self.item_counts_progression[self.player] = self.get_item_counts(pool=real_pool, only_progression=True)
|
||||||
|
|
||||||
|
def create_item(self, name: str, class_override: Optional['ItemClassification']=None) -> Item:
|
||||||
|
name = before_create_item(name, self, self.multiworld, self.player)
|
||||||
|
|
||||||
|
if name == self.glitches_item_name:
|
||||||
|
return ManualItem(name, class_override or ItemClassification.progression, None, self.player)
|
||||||
|
|
||||||
|
item = self.item_name_to_item[name]
|
||||||
|
classification: ItemClassification = ItemClassification.filler
|
||||||
|
if class_override is not None:
|
||||||
|
classification = class_override
|
||||||
|
|
||||||
|
elif item.get("classification_count"):
|
||||||
|
# This should only be run if create_item is called outside of create_items
|
||||||
|
not_prog_classes: list[ItemClassification] = []
|
||||||
|
progression_classes: list[ItemClassification] = []
|
||||||
|
for cat, count in item["classification_count"].items():
|
||||||
|
if count:
|
||||||
|
true_class = convert_string_to_itemclassification(cat)
|
||||||
|
if ItemClassification.progression in true_class:
|
||||||
|
progression_classes.append(true_class)
|
||||||
|
else:
|
||||||
|
not_prog_classes.append(true_class)
|
||||||
|
if progression_classes:
|
||||||
|
classification |= self.random.choice(progression_classes)
|
||||||
|
elif not_prog_classes:
|
||||||
|
classification |= not_prog_classes[0]
|
||||||
|
|
||||||
|
else:
|
||||||
|
if item.get("trap"):
|
||||||
|
classification |= ItemClassification.trap
|
||||||
|
|
||||||
|
if item.get("useful"):
|
||||||
|
classification |= ItemClassification.useful
|
||||||
|
|
||||||
|
if item.get("progression_skip_balancing"):
|
||||||
|
classification |= ItemClassification.progression_skip_balancing
|
||||||
|
elif item.get("progression"):
|
||||||
|
classification |= ItemClassification.progression
|
||||||
|
|
||||||
|
item_object = ManualItem(name, classification,
|
||||||
|
self.item_name_to_id[name], player=self.player)
|
||||||
|
|
||||||
|
item_object = after_create_item(item_object, self, self.multiworld, self.player)
|
||||||
|
|
||||||
|
return item_object
|
||||||
|
|
||||||
|
# Item Value need a tweaked collect and remove:
|
||||||
|
def collect(self, state: CollectionState, item: Item) -> bool:
|
||||||
|
change = super().collect(state, item)
|
||||||
|
manual_item = self.item_name_to_item.get(item.name, {})
|
||||||
|
if change and manual_item.get("value"):
|
||||||
|
for key, value in manual_item["value"].items():
|
||||||
|
state.prog_items[item.player][format_state_prog_items_key(ProgItemsCat.VALUE, key)] += int(value)
|
||||||
|
after_collect_item(self, state, change, item)
|
||||||
|
return change
|
||||||
|
|
||||||
|
def remove(self, state: CollectionState, item: Item) -> bool:
|
||||||
|
change = super().remove(state, item)
|
||||||
|
manual_item = self.item_name_to_item.get(item.name, {})
|
||||||
|
if change and manual_item.get("value"):
|
||||||
|
for key, value in manual_item["value"].items():
|
||||||
|
state.prog_items[item.player][format_state_prog_items_key(ProgItemsCat.VALUE, key)] -= int(value)
|
||||||
|
after_remove_item(self, state, change, item)
|
||||||
|
return change
|
||||||
|
|
||||||
|
def set_rules(self):
|
||||||
|
before_set_rules(self, self.multiworld, self.player)
|
||||||
|
|
||||||
|
set_rules(self, self.multiworld, self.player)
|
||||||
|
|
||||||
|
after_set_rules(self, self.multiworld, self.player)
|
||||||
|
|
||||||
|
def generate_basic(self):
|
||||||
|
before_generate_basic(self, self.multiworld, self.player)
|
||||||
|
|
||||||
|
# Enable this in Meta.json to generate a diagram of your manual. Only works on 0.4.4+
|
||||||
|
if get_option_value(self.multiworld, self.player, "generate_region_diagram"):
|
||||||
|
from Utils import visualize_regions
|
||||||
|
visualize_regions(self.multiworld.get_region("Manual", self.player), f"{self.game}_{self.player}.puml")
|
||||||
|
|
||||||
|
def pre_fill(self):
|
||||||
|
# DataValidation after all the hooks are done but before fill
|
||||||
|
runPreFillDataValidation(self, self.multiworld)
|
||||||
|
|
||||||
|
def fill_slot_data(self):
|
||||||
|
slot_data = before_fill_slot_data({}, self, self.multiworld, self.player)
|
||||||
|
|
||||||
|
# slot_data["DeathLink"] = bool(self.multiworld.death_link[self.player].value)
|
||||||
|
common_options = set(PerGameCommonOptions.type_hints.keys())
|
||||||
|
common_options |= set(["generate_region_diagram", "start_inventory_from_pool"])
|
||||||
|
for option_key, _ in self.options_dataclass.type_hints.items():
|
||||||
|
if option_key in common_options:
|
||||||
|
continue
|
||||||
|
slot_data[option_key] = get_option_value(self.multiworld, self.player, option_key)
|
||||||
|
|
||||||
|
slot_data["visible_events"] = {}
|
||||||
|
for _, event in self.event_name_to_event.items():
|
||||||
|
event_name = event["name"]
|
||||||
|
if event["visible"] and event_name not in slot_data["visible_events"]:
|
||||||
|
slot_data["visible_events"][event_name] = event.get("category", [])
|
||||||
|
elif event_name in slot_data["visible_events"]:
|
||||||
|
temp_list = event.get("category", []) + slot_data["visible_events"][event_name]
|
||||||
|
slot_data["visible_events"][event_name] = list(set(temp_list))
|
||||||
|
|
||||||
|
slot_data = after_fill_slot_data(slot_data, self, self.multiworld, self.player)
|
||||||
|
|
||||||
|
return slot_data
|
||||||
|
|
||||||
|
def generate_output(self, output_directory: str):
|
||||||
|
filename = f"{self.multiworld.get_out_file_name_base(self.player)}.apmanual"
|
||||||
|
zf_path = os.path.join(output_directory, filename)
|
||||||
|
|
||||||
|
apmanual = APManualFile(zf_path, player=self.player, player_name=self.player_name)
|
||||||
|
apmanual.write()
|
||||||
|
|
||||||
|
if get_option_value(self.multiworld, self.player, "generate_region_diagram"):
|
||||||
|
from Utils import visualize_regions
|
||||||
|
visualize_regions(self.multiworld.get_region("Manual", self.player), f"{self.game}_{self.player}_spoiler.puml")
|
||||||
|
|
||||||
|
|
||||||
|
def write_spoiler(self, spoiler_handle):
|
||||||
|
before_write_spoiler(self, self.multiworld, spoiler_handle)
|
||||||
|
|
||||||
|
def extend_hint_information(self, hint_data: dict[int, dict[int, str]]) -> None:
|
||||||
|
before_extend_hint_information(hint_data, self, self.multiworld, self.player)
|
||||||
|
|
||||||
|
for location in self.multiworld.get_locations(self.player):
|
||||||
|
if not location.address:
|
||||||
|
continue
|
||||||
|
if "hint_entrance" in self.location_name_to_location[location.name]:
|
||||||
|
if self.player not in hint_data:
|
||||||
|
hint_data.update({self.player: {}})
|
||||||
|
hint_data[self.player][location.address] = self.location_name_to_location[location.name]["hint_entrance"]
|
||||||
|
|
||||||
|
after_extend_hint_information(hint_data, self, self.multiworld, self.player)
|
||||||
|
|
||||||
|
###
|
||||||
|
# Non-standard AP world methods
|
||||||
|
###
|
||||||
|
|
||||||
|
rules_functions_maximum_recursion: int = 5
|
||||||
|
"""Default: 5\n
|
||||||
|
The maximum time a location/region's requirement can loop to check for functions\n
|
||||||
|
One thing to remember is the more you loop the longer generation will take. So probably leave it as is unless you really needs it."""
|
||||||
|
|
||||||
|
def add_filler_items(self, item_pool, traps):
|
||||||
|
Utils.deprecate("You're calling the deprecated add_filler_items() function. Use the adjust_filler_items() function instead.")
|
||||||
|
return self.adjust_filler_items(item_pool, traps)
|
||||||
|
|
||||||
|
def adjust_filler_items(self, item_pool, traps):
|
||||||
|
extras = len(self.multiworld.get_unfilled_locations(player=self.player)) - len(item_pool)
|
||||||
|
|
||||||
|
if extras > 0:
|
||||||
|
trap_percent = get_option_value(self.multiworld, self.player, "filler_traps")
|
||||||
|
if not traps:
|
||||||
|
trap_percent = 0
|
||||||
|
|
||||||
|
trap_count = extras * trap_percent // 100
|
||||||
|
filler_count = extras - trap_count
|
||||||
|
|
||||||
|
for _ in range(0, trap_count):
|
||||||
|
extra_item = self.create_item(self.random.choice(traps))
|
||||||
|
item_pool.append(extra_item)
|
||||||
|
|
||||||
|
for _ in range(0, filler_count):
|
||||||
|
extra_item = self.create_item(self.get_filler_item_name())
|
||||||
|
item_pool.append(extra_item)
|
||||||
|
elif extras < 0:
|
||||||
|
logging.warning(f"{self.game} has more items than locations. {abs(extras)} non-progression items will be removed at random.")
|
||||||
|
# Filler is only assigned if the item doesn't have any other tags, so it only has to be covered by itself.
|
||||||
|
# Skip Balancing is also not covered due to how it's only supported when paired with Progression.
|
||||||
|
# As a result, these cover every possible combination can be removed.
|
||||||
|
fillers = [item for item in item_pool if item.classification == ItemClassification.filler]
|
||||||
|
traps = [item for item in item_pool if item.classification == ItemClassification.trap]
|
||||||
|
useful = [item for item in item_pool if item.classification == ItemClassification.useful]
|
||||||
|
# Useful + Trap is classified separately so that it can have a unique priority ranking.
|
||||||
|
useful_traps = [item for item in item_pool if
|
||||||
|
ItemClassification.progression not in item.classification
|
||||||
|
and ItemClassification.useful in item.classification
|
||||||
|
and ItemClassification.trap in item.classification]
|
||||||
|
self.random.shuffle(fillers)
|
||||||
|
self.random.shuffle(traps)
|
||||||
|
self.random.shuffle(useful)
|
||||||
|
self.random.shuffle(useful_traps)
|
||||||
|
for _ in range(0, abs(extras)):
|
||||||
|
popped = None
|
||||||
|
if fillers:
|
||||||
|
popped = fillers.pop()
|
||||||
|
elif traps:
|
||||||
|
popped = traps.pop()
|
||||||
|
elif useful:
|
||||||
|
popped = useful.pop()
|
||||||
|
elif useful_traps:
|
||||||
|
popped = useful_traps.pop()
|
||||||
|
else:
|
||||||
|
logging.warning("Could not remove enough non-progression items from the pool.")
|
||||||
|
break
|
||||||
|
remove_specific_item(item_pool, popped)
|
||||||
|
|
||||||
|
return item_pool
|
||||||
|
|
||||||
|
def get_item_counts(self, player: Optional[int] = None, pool: list[Item] | None | bool = None, only_progression: bool = False) -> Counter[str]:
|
||||||
|
"""Returns the player real item counts.\n
|
||||||
|
If you provide an item pool using the pool argument, then it's item counts will be returned.
|
||||||
|
Otherwise, this function will only work after create_items, before then an empty Counter is returned.\n
|
||||||
|
The only_progression argument let you filter the items to only get the count of progression items."""
|
||||||
|
if player is None:
|
||||||
|
player = self.player
|
||||||
|
|
||||||
|
if isinstance(pool, bool):
|
||||||
|
Utils.deprecate("the 'reset' argument of get_item_counts has been deprecated to increase the stability of item counts.\
|
||||||
|
\nIt should be removed. If you require a new up to date count you can get it using the 'pool' argument.\
|
||||||
|
\nThat result wont be saved to world unless you override the values of world.item_counts_progression or world.item_counts depending on if you counted only the items with progresion or not.")
|
||||||
|
pool = None
|
||||||
|
|
||||||
|
if pool is not None:
|
||||||
|
return Counter([i.name for i in pool if not only_progression or i.advancement])
|
||||||
|
|
||||||
|
if only_progression:
|
||||||
|
return self.item_counts_progression.get(player, Counter())
|
||||||
|
else:
|
||||||
|
return self.item_counts.get(player, Counter())
|
||||||
|
|
||||||
|
|
||||||
|
###
|
||||||
|
# Non-world client methods
|
||||||
|
###
|
||||||
|
|
||||||
|
def launch_client(*args):
|
||||||
|
import CommonClient
|
||||||
|
from .ManualClient import launch as Main
|
||||||
|
|
||||||
|
if CommonClient.gui_enabled:
|
||||||
|
launch(Main, name="Manual client", args=args)
|
||||||
|
else:
|
||||||
|
Main(*args)
|
||||||
|
|
||||||
|
class VersionedComponent(Component):
|
||||||
|
def __init__(self, display_name: str, script_name: Optional[str] = None, func: Optional[Callable] = None, version: int = 0, file_identifier: Optional[Callable[[str], bool]] = None, icon: Optional[str] = None):
|
||||||
|
super().__init__(display_name=display_name, script_name=script_name, func=func, component_type=Type.CLIENT, file_identifier=file_identifier, icon=icon)
|
||||||
|
self.version = version
|
||||||
|
|
||||||
|
def add_client_to_launcher() -> None:
|
||||||
|
version = 2026_09_18 # YYYYMMDD
|
||||||
|
found = False
|
||||||
|
|
||||||
|
if "manual" not in icon_paths:
|
||||||
|
icon_paths["manual"] = Utils.user_path('data', 'manual.png')
|
||||||
|
|
||||||
|
discord_component = None
|
||||||
|
for c in components:
|
||||||
|
if c.display_name == "Manual Client":
|
||||||
|
found = True
|
||||||
|
if getattr(c, "version", 0) < version: # We have a newer version of the Manual Client than the one the last apworld added
|
||||||
|
c.version = version
|
||||||
|
c.func = launch_client
|
||||||
|
c.icon = "manual"
|
||||||
|
elif c.display_name == "Manual Discord Server":
|
||||||
|
discord_component = c
|
||||||
|
|
||||||
|
if not found:
|
||||||
|
components.append(VersionedComponent("Manual Client", "ManualClient", func=launch_client, version=version, file_identifier=SuffixIdentifier('.apmanual'), icon="manual"))
|
||||||
|
if not discord_component:
|
||||||
|
components.append(Component("Manual Discord Server", "ManualDiscord", func=lambda: webbrowser.open("https://discord.gg/hm4rQnTzQ5"), icon="discord", component_type=Type.ADJUSTER))
|
||||||
|
|
||||||
|
add_client_to_launcher()
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import json
|
||||||
|
import zipfile
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from worlds import Files
|
||||||
|
|
||||||
|
from .Data import region_table, category_table
|
||||||
|
from .Game import game_name
|
||||||
|
from .Items import item_name_to_item
|
||||||
|
from .Locations import location_name_to_location
|
||||||
|
|
||||||
|
|
||||||
|
class APManualFile(Files.APPlayerContainer):
|
||||||
|
game = game_name
|
||||||
|
patch_file_ending = ".apmanual"
|
||||||
|
|
||||||
|
items: dict[str, Any] = None
|
||||||
|
locations: dict[str, Any] = None
|
||||||
|
regions: dict[str, Any] = None
|
||||||
|
categories: dict[str, Any] = None
|
||||||
|
|
||||||
|
def __init__(self, *args: Any, **kwargs: Any):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def write_contents(self, opened_zipfile: zipfile.ZipFile):
|
||||||
|
super().write_contents(opened_zipfile)
|
||||||
|
opened_zipfile.writestr("items.json", json.dumps(item_name_to_item, indent=2))
|
||||||
|
opened_zipfile.writestr("locations.json", json.dumps(location_name_to_location, indent=2))
|
||||||
|
opened_zipfile.writestr("regions.json", json.dumps(region_table, indent=2))
|
||||||
|
opened_zipfile.writestr("categories.json", json.dumps(category_table, indent=2))
|
||||||
|
|
||||||
|
def read_contents(self, opened_zipfile: zipfile.ZipFile) -> dict[str, Any]:
|
||||||
|
manifest = super().read_contents(opened_zipfile)
|
||||||
|
self.items = json.loads(opened_zipfile.read("items.json"))
|
||||||
|
self.locations = json.loads(opened_zipfile.read("locations.json"))
|
||||||
|
self.regions = json.loads(opened_zipfile.read("regions.json"))
|
||||||
|
return manifest
|
||||||
|
|
||||||
|
def get_manifest(self) -> dict[str, Any]:
|
||||||
|
data = super().get_manifest()
|
||||||
|
data["items"] = self.items
|
||||||
|
data["locations"] = self.locations
|
||||||
|
data["regions"] = self.regions
|
||||||
|
return data
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://github.com/ManualForArchipelago/Manual/raw/main/schemas/Manual.categories.schema.json",
|
||||||
|
"Wizard Mode Available": {
|
||||||
|
"hidden": true,
|
||||||
|
"yaml_option": ["wizard_mode_available"]
|
||||||
|
},
|
||||||
|
"Starting": {
|
||||||
|
"hidden": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://github.com/ManualForArchipelago/Manual/raw/main/schemas/Manual.events.schema.json",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"name": "Marvel event",
|
||||||
|
"requires": "|@Right Side:all|",
|
||||||
|
"visible": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Capcom event",
|
||||||
|
"requires": "|@Left Side:all|"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Stars_event",
|
||||||
|
"copy_location": "Stars%"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "X-Men Assemble",
|
||||||
|
"category": ["Right Side"],
|
||||||
|
"requires": "((|Wolverine|) OR |X-23|) AND |Storm| and |Magneto:1|"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Thunderbolts and Lightning",
|
||||||
|
"requires": "{CanReachLocation(Beat the Game - Storm)} and {CanReachLocation(Beat the Game - Thor)}",
|
||||||
|
"region": "Third Region"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://github.com/ManualForArchipelago/Manual/raw/main/schemas/Manual.game.schema.json",
|
||||||
|
"game": "Cogmind",
|
||||||
|
"creator": "Luminiferous & Xintrosi",
|
||||||
|
"filler_item_name": "Filler",
|
||||||
|
"starting_items": [
|
||||||
|
{
|
||||||
|
"item_categories": ["Starting"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"death_link": false,
|
||||||
|
"starting_index": 1,
|
||||||
|
"unused_goals_are_locations": false
|
||||||
|
}
|
||||||
+565
@@ -0,0 +1,565 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://github.com/ManualForArchipelago/Manual/raw/main/schemas/Manual.items.schema.json",
|
||||||
|
"_mainComplexFloors-nvimCodeFolding": "{{{",
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"name": "-10 Materials",
|
||||||
|
"category": [
|
||||||
|
"Main Complex Floors",
|
||||||
|
"Starting"
|
||||||
|
],
|
||||||
|
"progression": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"name": "-9 Materials",
|
||||||
|
"category": [
|
||||||
|
"Main Complex Floors"
|
||||||
|
],
|
||||||
|
"progression": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"name": "-8 Materials",
|
||||||
|
"category": [
|
||||||
|
"Main Complex Floors"
|
||||||
|
],
|
||||||
|
"progression": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"name": "-7 Factory",
|
||||||
|
"category": [
|
||||||
|
"Main Complex Floors"
|
||||||
|
],
|
||||||
|
"progression": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"name": "-6 Factory",
|
||||||
|
"category": [
|
||||||
|
"Main Complex Floors"
|
||||||
|
],
|
||||||
|
"progression": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"name": "-5 Factory",
|
||||||
|
"category": [
|
||||||
|
"Main Complex Floors"
|
||||||
|
],
|
||||||
|
"progression": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"name": "-4 Factory",
|
||||||
|
"category": [
|
||||||
|
"Main Complex Floors"
|
||||||
|
],
|
||||||
|
"progression": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"name": "-3 Research",
|
||||||
|
"category": [
|
||||||
|
"Main Complex Floors"
|
||||||
|
],
|
||||||
|
"progression": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"name": "-2 Research",
|
||||||
|
"category": [
|
||||||
|
"Main Complex Floors"
|
||||||
|
],
|
||||||
|
"progression": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"name": "-1 Access",
|
||||||
|
"category": [
|
||||||
|
"Main Complex Floors"
|
||||||
|
],
|
||||||
|
"progression": true
|
||||||
|
},
|
||||||
|
"_mainComplexFloors-end": "}}}",
|
||||||
|
"_branchAreas-nvimCodeFolding": "{{{",
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"name": "Archives",
|
||||||
|
"category": [
|
||||||
|
"Branch Areas"
|
||||||
|
],
|
||||||
|
"progression": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"name": "Armory",
|
||||||
|
"category": [
|
||||||
|
"Branch Areas"
|
||||||
|
],
|
||||||
|
"progression": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": "2",
|
||||||
|
"name": "Lower Caves",
|
||||||
|
"category": [
|
||||||
|
"Branch Areas"
|
||||||
|
],
|
||||||
|
"progression": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": "2",
|
||||||
|
"name": "Upper Caves",
|
||||||
|
"category": [
|
||||||
|
"Branch Areas"
|
||||||
|
],
|
||||||
|
"progression": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"name": "Cetus",
|
||||||
|
"category": [
|
||||||
|
"Branch Areas"
|
||||||
|
],
|
||||||
|
"progression": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"name": "Command",
|
||||||
|
"category": [
|
||||||
|
"Branch Areas"
|
||||||
|
],
|
||||||
|
"filler": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"name": "Acces_0",
|
||||||
|
"category": [
|
||||||
|
"Branch Areas"
|
||||||
|
],
|
||||||
|
"filler": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"name": "DSF",
|
||||||
|
"category": [
|
||||||
|
"Branch Areas"
|
||||||
|
],
|
||||||
|
"filler": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"name": "Data Miner (Location)",
|
||||||
|
"category": [
|
||||||
|
"Branch Areas"
|
||||||
|
],
|
||||||
|
"progression": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"name": "Exiles (Location)",
|
||||||
|
"category": [
|
||||||
|
"Branch Areas"
|
||||||
|
],
|
||||||
|
"progression": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"name": "Extension",
|
||||||
|
"category": [
|
||||||
|
"Branch Areas"
|
||||||
|
],
|
||||||
|
"progression": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"name": "Garrison",
|
||||||
|
"category": [
|
||||||
|
"Branch Areas"
|
||||||
|
],
|
||||||
|
"progression": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"name": "Hub_04(d)",
|
||||||
|
"category": [
|
||||||
|
"Branch Areas"
|
||||||
|
],
|
||||||
|
"progression": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"name": "Lab",
|
||||||
|
"category": [
|
||||||
|
"Branch Areas"
|
||||||
|
],
|
||||||
|
"progression": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"name": "-10 Mines",
|
||||||
|
"category": [
|
||||||
|
"Branch Areas"
|
||||||
|
],
|
||||||
|
"progression": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"name": "-9 Mines",
|
||||||
|
"category": [
|
||||||
|
"Branch Areas"
|
||||||
|
],
|
||||||
|
"progression": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"name": "Quarantine",
|
||||||
|
"category": [
|
||||||
|
"Branch Areas"
|
||||||
|
],
|
||||||
|
"progression": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"name": "Recycling",
|
||||||
|
"category": [
|
||||||
|
"Branch Areas"
|
||||||
|
],
|
||||||
|
"progression": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"name": "Scraptown",
|
||||||
|
"category": [
|
||||||
|
"Branch Areas"
|
||||||
|
],
|
||||||
|
"progression": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"name": "Section 7",
|
||||||
|
"category": [
|
||||||
|
"Branch Areas"
|
||||||
|
],
|
||||||
|
"progression": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"name": "Storage",
|
||||||
|
"category": [
|
||||||
|
"Branch Areas"
|
||||||
|
],
|
||||||
|
"progression": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"name": "Subcaves",
|
||||||
|
"category": [
|
||||||
|
"Branch Areas"
|
||||||
|
],
|
||||||
|
"progression": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"name": "Testing",
|
||||||
|
"category": [
|
||||||
|
"Branch Areas"
|
||||||
|
],
|
||||||
|
"progression": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"name": "Warlord (Location)",
|
||||||
|
"category": [
|
||||||
|
"Branch Areas"
|
||||||
|
],
|
||||||
|
"progression": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"name": "Wastes Escape",
|
||||||
|
"category": [
|
||||||
|
"Branch Areas"
|
||||||
|
],
|
||||||
|
"useful": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"name": "Zhirov (Location)",
|
||||||
|
"category": [
|
||||||
|
"Branch Areas"
|
||||||
|
],
|
||||||
|
"useful": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"name": "Zion",
|
||||||
|
"category": [
|
||||||
|
"Branch Areas"
|
||||||
|
],
|
||||||
|
"progression": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"name": "Zion Deep Caves",
|
||||||
|
"category": [
|
||||||
|
"Branch Areas"
|
||||||
|
],
|
||||||
|
"progression": true
|
||||||
|
},
|
||||||
|
"_branchAreas-end": "}}}",
|
||||||
|
"_parts-nvimCodeFolding": "{{{",
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"name": "Rating 1 Parts",
|
||||||
|
"category": [
|
||||||
|
"Parts",
|
||||||
|
"Starting"
|
||||||
|
],
|
||||||
|
"progression": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"name": "Rating 2 Parts",
|
||||||
|
"category": [
|
||||||
|
"Parts"
|
||||||
|
],
|
||||||
|
"progression": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"name": "Rating 3 Parts",
|
||||||
|
"category": [
|
||||||
|
"Parts"
|
||||||
|
],
|
||||||
|
"progression": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"name": "Rating 4 Parts",
|
||||||
|
"category": [
|
||||||
|
"Parts"
|
||||||
|
],
|
||||||
|
"progression": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"name": "Rating 5 Parts",
|
||||||
|
"category": [
|
||||||
|
"Parts"
|
||||||
|
],
|
||||||
|
"progression": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"name": "Rating 6 Parts",
|
||||||
|
"category": [
|
||||||
|
"Parts"
|
||||||
|
],
|
||||||
|
"progression": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"name": "Rating 7 Parts",
|
||||||
|
"category": [
|
||||||
|
"Parts"
|
||||||
|
],
|
||||||
|
"progression": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"name": "Rating 8 Parts",
|
||||||
|
"category": [
|
||||||
|
"Parts"
|
||||||
|
],
|
||||||
|
"progression": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"name": "Rating 9 Parts",
|
||||||
|
"category": [
|
||||||
|
"Parts"
|
||||||
|
],
|
||||||
|
"progression": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"name": "Rating 10 Parts",
|
||||||
|
"category": [
|
||||||
|
"Parts"
|
||||||
|
],
|
||||||
|
"useful": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"name": "Exiles Prototypes",
|
||||||
|
"category": [
|
||||||
|
"Parts"
|
||||||
|
],
|
||||||
|
"useful": true
|
||||||
|
},
|
||||||
|
"_parts-end": "}}}",
|
||||||
|
"_evolution-nvimCodeFolding": "{{{",
|
||||||
|
{
|
||||||
|
"count": "8",
|
||||||
|
"name": "Evolution Slot",
|
||||||
|
"category": [
|
||||||
|
"Evolution",
|
||||||
|
"Starting"
|
||||||
|
],
|
||||||
|
"progression_skip_balancing": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": "18",
|
||||||
|
"name": "Evolution Slot",
|
||||||
|
"category": [
|
||||||
|
"Evolution"
|
||||||
|
],
|
||||||
|
"progression_skip_balancing": true
|
||||||
|
},
|
||||||
|
"_evolution-end": "}}}",
|
||||||
|
"_alignment-nvimCodeFolding": "{{{",
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"name": "Core Reset Matrix",
|
||||||
|
"category": [
|
||||||
|
"Alignment"
|
||||||
|
],
|
||||||
|
"useful": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"name": "FarCom",
|
||||||
|
"category": [
|
||||||
|
"Alignment"
|
||||||
|
],
|
||||||
|
"useful": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"name": "Imprinted",
|
||||||
|
"category": [
|
||||||
|
"Alignment"
|
||||||
|
],
|
||||||
|
"useful": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"name": "RIF",
|
||||||
|
"category": [
|
||||||
|
"Alignment"
|
||||||
|
],
|
||||||
|
"useful": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"name": "Revision 17",
|
||||||
|
"category": [
|
||||||
|
"Alignment"
|
||||||
|
],
|
||||||
|
"useful": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"name": "UFD",
|
||||||
|
"category": [
|
||||||
|
"Alignment"
|
||||||
|
],
|
||||||
|
"useful": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"name": "Warlord",
|
||||||
|
"category": [
|
||||||
|
"Alignment"
|
||||||
|
],
|
||||||
|
"useful": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"name": "Zhirov",
|
||||||
|
"category": [
|
||||||
|
"Alignment"
|
||||||
|
],
|
||||||
|
"useful": true
|
||||||
|
},
|
||||||
|
"_alignment-end": "}}}",
|
||||||
|
"_filler-nvimCodeFolding": "{{{",
|
||||||
|
{
|
||||||
|
"count": "4",
|
||||||
|
"name": "Free Item from unlocked Ratings",
|
||||||
|
"category": [
|
||||||
|
"Filler"
|
||||||
|
],
|
||||||
|
"filler": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"name": "GOLEM Unit",
|
||||||
|
"category": [
|
||||||
|
"Filler"
|
||||||
|
],
|
||||||
|
"filler": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"name": "Scrap Engine",
|
||||||
|
"category": [
|
||||||
|
"Filler"
|
||||||
|
],
|
||||||
|
"filler": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"name": "SIGIX Exoskeleton",
|
||||||
|
"category": [
|
||||||
|
"Filler"
|
||||||
|
],
|
||||||
|
"filler": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": "3",
|
||||||
|
"name": "CORE Restore",
|
||||||
|
"category": [
|
||||||
|
"Filler"
|
||||||
|
],
|
||||||
|
"filler": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": "1",
|
||||||
|
"name": "Integrity Restore (All Equipped Parts)",
|
||||||
|
"category": [
|
||||||
|
"Filler"
|
||||||
|
],
|
||||||
|
"filler": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"name": "Companion Sword",
|
||||||
|
"category": [
|
||||||
|
"Filler"
|
||||||
|
],
|
||||||
|
"filler": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"name": "Microwarp Drive",
|
||||||
|
"category": [
|
||||||
|
"Filler"
|
||||||
|
],
|
||||||
|
"filler": true
|
||||||
|
},
|
||||||
|
"_filler-end": "}}}",
|
||||||
|
"_mcGuffin-nvimCodeFolding": "{{{",
|
||||||
|
{
|
||||||
|
"count": "27",
|
||||||
|
"name": "Triangle",
|
||||||
|
"category": [
|
||||||
|
"McGuffin"
|
||||||
|
],
|
||||||
|
"progression": true
|
||||||
|
},
|
||||||
|
"_mcGuffin-end": "}}}"
|
||||||
|
]
|
||||||
|
|
||||||
@@ -0,0 +1,962 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://github.com/ManualForArchipelago/Manual/raw/main/schemas/Manual.locations.schema.json",
|
||||||
|
{
|
||||||
|
"name": "Exit to -9 Materials",
|
||||||
|
"region": "-10",
|
||||||
|
"category": [
|
||||||
|
"Main Complex"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Earned Evolution Slots (1 of 18)",
|
||||||
|
"region": "-10",
|
||||||
|
"category": [
|
||||||
|
"Evolution"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Earned Evolution Slots (2 of 18)",
|
||||||
|
"region": "-10",
|
||||||
|
"category": [
|
||||||
|
"Evolution"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Exit to -10 Mines",
|
||||||
|
"region": "-10",
|
||||||
|
"category": [
|
||||||
|
"Branch"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Exit to Subcaves",
|
||||||
|
"region": "-10",
|
||||||
|
"category": [
|
||||||
|
"Branch"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Exit to Exiles",
|
||||||
|
"region": "-10",
|
||||||
|
"category": [
|
||||||
|
"Branch"
|
||||||
|
],
|
||||||
|
"requires": "|-10 Mines|"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Acquire Subcaves Prototype",
|
||||||
|
"region": "-10",
|
||||||
|
"category": [
|
||||||
|
"Feat of Strength"
|
||||||
|
],
|
||||||
|
"requires": "|Subcaves|",
|
||||||
|
"place_item_category": [
|
||||||
|
"Filler"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Use FARCOM",
|
||||||
|
"category": [
|
||||||
|
"Alignment"
|
||||||
|
],
|
||||||
|
"requires": "|-10 Mines| AND |Exiles (Location)|",
|
||||||
|
"region": "-10"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Exit to -8 Materials",
|
||||||
|
"region": "-9",
|
||||||
|
"category": [
|
||||||
|
"Main Complex"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Pickup Rating 2 Normal Item",
|
||||||
|
"region": "-9",
|
||||||
|
"category": [
|
||||||
|
"Items"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Exit to Storage",
|
||||||
|
"region": "-9",
|
||||||
|
"category": [
|
||||||
|
"Branch"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Earned Evolution Slots (3 of 18)",
|
||||||
|
"region": "-9",
|
||||||
|
"category": [
|
||||||
|
"Evolution"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Earned Evolution Slots (4 of 18)",
|
||||||
|
"region": "-9",
|
||||||
|
"category": [
|
||||||
|
"Evolution"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Exit to -9 Mines",
|
||||||
|
"region": "-9",
|
||||||
|
"category": [
|
||||||
|
"Branch"
|
||||||
|
],
|
||||||
|
"requires": "|-9 Materials|"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Use a Recycling Unit to Recycle",
|
||||||
|
"region": "-9",
|
||||||
|
"category": [
|
||||||
|
"Feat of Strength"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Hack 3 Schematics",
|
||||||
|
"region": "-9",
|
||||||
|
"category": [
|
||||||
|
"Feat of Strength"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Analyze 3 Bots",
|
||||||
|
"region": "-9",
|
||||||
|
"category": [
|
||||||
|
"Feat of Strength"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Exit to Recycling",
|
||||||
|
"category": [
|
||||||
|
"Branch"
|
||||||
|
],
|
||||||
|
"requires": "|Storage| or |Subcaves|",
|
||||||
|
"region": "-9"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Exit to Scraptown",
|
||||||
|
"category": [
|
||||||
|
"Branch"
|
||||||
|
],
|
||||||
|
"requires": "|Recycling| AND (|Storage| or |Subcaves|)",
|
||||||
|
"region": "-9"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Join UFD",
|
||||||
|
"category": [
|
||||||
|
"Alignment"
|
||||||
|
],
|
||||||
|
"requires": "|Scraptown| AND |Recycling| AND (|Storage| OR |Subcaves|)",
|
||||||
|
"region": "-9"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Install 3 Botnets on the Same Floor",
|
||||||
|
"category": [
|
||||||
|
"Feat of Strength"
|
||||||
|
],
|
||||||
|
"requires": "|Rating 2 Parts|",
|
||||||
|
"region": "-9"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Destroy two swarmers in a single salvo",
|
||||||
|
"category": [
|
||||||
|
"Feat of Strength"
|
||||||
|
],
|
||||||
|
"requires": "|Rating 5 Parts|",
|
||||||
|
"region": "-9"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Exit to -7 Factory",
|
||||||
|
"region": "-8",
|
||||||
|
"category": [
|
||||||
|
"Main Complex"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Pickup Rating 3 Normal Item",
|
||||||
|
"region": "-8",
|
||||||
|
"category": [
|
||||||
|
"Items"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Seal a Garrison",
|
||||||
|
"region": "-8",
|
||||||
|
"category": [
|
||||||
|
"Feat of Strength"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Enter a Garrison",
|
||||||
|
"region": "-8",
|
||||||
|
"category": [
|
||||||
|
"Branch"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Install RIF 1",
|
||||||
|
"region": "-8",
|
||||||
|
"category": [
|
||||||
|
"Alignment"
|
||||||
|
],
|
||||||
|
"requires": "|Garrison|"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Earned Evolution Slots (5 of 18)",
|
||||||
|
"region": "-8",
|
||||||
|
"category": [
|
||||||
|
"Evolution"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Earned Evolution Slots (6 of 18)",
|
||||||
|
"region": "-8",
|
||||||
|
"category": [
|
||||||
|
"Evolution"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Unlock DSF",
|
||||||
|
"region": "-7",
|
||||||
|
"category": [
|
||||||
|
"Branch"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Pick up Rating 2* Prototype",
|
||||||
|
"region": "-8",
|
||||||
|
"category": [
|
||||||
|
"Items"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Scanalyze a rating 2+ part",
|
||||||
|
"region": "-8",
|
||||||
|
"category": [
|
||||||
|
"Feat of Strength"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Exit to -6 Factory",
|
||||||
|
"region": "-7",
|
||||||
|
"category": [
|
||||||
|
"Main Complex"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Pickup Rating 4 Normal Item",
|
||||||
|
"region": "-7",
|
||||||
|
"category": [
|
||||||
|
"Items"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Exit to -7 Caves",
|
||||||
|
"region": "-7",
|
||||||
|
"category": [
|
||||||
|
"Branch"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Exit to Data Miner",
|
||||||
|
"region": "-7",
|
||||||
|
"category": [
|
||||||
|
"Branch"
|
||||||
|
],
|
||||||
|
"requires": "|Lower Caves|"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Exit to Zion",
|
||||||
|
"region": "-7",
|
||||||
|
"category": [
|
||||||
|
"Branch"
|
||||||
|
],
|
||||||
|
"requires": "|Lower Caves|"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Exit to Zion Deep Caves",
|
||||||
|
"region": "-7",
|
||||||
|
"category": [
|
||||||
|
"Branch"
|
||||||
|
],
|
||||||
|
"requires": "|Zion| AND |Lower Caves|"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Install RIF 2",
|
||||||
|
"region": "-7",
|
||||||
|
"category": [
|
||||||
|
"Alignment"
|
||||||
|
],
|
||||||
|
"requires": "|Garrison|"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Meet Data Miner",
|
||||||
|
"region": "-7",
|
||||||
|
"category": [
|
||||||
|
"Alignment"
|
||||||
|
],
|
||||||
|
"requires": "|Lower Caves| AND |Data Miner (Location)|"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Become Imprinted",
|
||||||
|
"region": "-7",
|
||||||
|
"category": [
|
||||||
|
"Alignment"
|
||||||
|
],
|
||||||
|
"requires": "|Lower Caves| AND |Zion|"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Kill Z-Imprinter",
|
||||||
|
"category": [
|
||||||
|
"Feat of Strength"
|
||||||
|
],
|
||||||
|
"requires": "|Lower Caves| AND |Zion| AND |Zion Deep Caves|",
|
||||||
|
"region": "-7"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Fall down a Chute Trap",
|
||||||
|
"region": "-7",
|
||||||
|
"category": [
|
||||||
|
"Feat of Strength"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Escape the Wastes",
|
||||||
|
"region": "-7",
|
||||||
|
"category": [
|
||||||
|
"Feat of Strength"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Earned Evolution Slots (7 of 18)",
|
||||||
|
"region": "-7",
|
||||||
|
"category": [
|
||||||
|
"Evolution"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Earned Evolution Slots (8 of 18)",
|
||||||
|
"region": "-7",
|
||||||
|
"category": [
|
||||||
|
"Evolution"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Fall down a Chute Trap ON PURPOSE",
|
||||||
|
"region": "-7",
|
||||||
|
"category": [
|
||||||
|
"Feat of Strength"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Assimilate an Operator",
|
||||||
|
"region": "-7",
|
||||||
|
"category": [
|
||||||
|
"Feat of Strength"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Pick up Rating 3* Prototype",
|
||||||
|
"region": "-7",
|
||||||
|
"category": [
|
||||||
|
"Items"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Kill a GOLEM",
|
||||||
|
"region": "-7",
|
||||||
|
"category": [
|
||||||
|
"Feat of Strength"
|
||||||
|
],
|
||||||
|
"requires": "|Zion Deep Caves| AND |Zion| AND |Lower Caves|"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Hack 5 Schematics",
|
||||||
|
"region": "-7",
|
||||||
|
"category": [
|
||||||
|
"Feat of Strength"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Acquire a GOLEM Unit",
|
||||||
|
"category": [
|
||||||
|
"Feat of Strength"
|
||||||
|
],
|
||||||
|
"requires": "|Lower Caves| AND |Zion| AND |Zion Deep Caves|",
|
||||||
|
"region": "-7"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Acquire an Exiles Prototype",
|
||||||
|
"category": [
|
||||||
|
"Feat of Strength"
|
||||||
|
],
|
||||||
|
"requires": "(|-10 Mines| AND |Exiles (Location)|) OR (|Lower Caves| AND |Zion| AND |Zion Deep Caves| AND |-7 Factory| AND ((|Recycling| and |Subcaves|) or (|-8 Materials| and |-9 Materials|))",
|
||||||
|
"region": "-10"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Exit to -5 Factory",
|
||||||
|
"region": "-6",
|
||||||
|
"category": [
|
||||||
|
"Main Complex"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Pickup Rating 5 Normal Item",
|
||||||
|
"region": "-6",
|
||||||
|
"category": [
|
||||||
|
"Items"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Exit to -6 Caves",
|
||||||
|
"region": "-6",
|
||||||
|
"category": [
|
||||||
|
"Branch"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Exit to Archives",
|
||||||
|
"region": "-6",
|
||||||
|
"category": [
|
||||||
|
"Branch"
|
||||||
|
],
|
||||||
|
"requires": "|Cetus| AND |Extension|"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Exit to Cetus",
|
||||||
|
"region": "-6",
|
||||||
|
"category": [
|
||||||
|
"Branch"
|
||||||
|
],
|
||||||
|
"requires": "|Extension|"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Exit to Hub_04(d)",
|
||||||
|
"region": "-6",
|
||||||
|
"category": [
|
||||||
|
"Branch"
|
||||||
|
],
|
||||||
|
"requires": "|Archives| AND |Cetus| AND |Extension|"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Exit to Extension",
|
||||||
|
"region": "-6",
|
||||||
|
"category": [
|
||||||
|
"Branch"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Install RIF 3",
|
||||||
|
"region": "-6",
|
||||||
|
"category": [
|
||||||
|
"Alignment"
|
||||||
|
],
|
||||||
|
"requires": "|Garrison|"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Meet Revision 17",
|
||||||
|
"region": "-6",
|
||||||
|
"category": [
|
||||||
|
"Alignment"
|
||||||
|
],
|
||||||
|
"requires": "|Cetus| AND |Extension|"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Earned Evolution Slots (9 of 18)",
|
||||||
|
"region": "-6",
|
||||||
|
"category": [
|
||||||
|
"Evolution"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Earned Evolution Slots (10 of 18)",
|
||||||
|
"region": "-6",
|
||||||
|
"category": [
|
||||||
|
"Evolution"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Pick up Rating 4* Prototype",
|
||||||
|
"region": "-6",
|
||||||
|
"category": [
|
||||||
|
"Items"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Kill a Heavy",
|
||||||
|
"region": "-6",
|
||||||
|
"category": [
|
||||||
|
"Feat of Strength"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Kill a Cetus Guard",
|
||||||
|
"region": "-6",
|
||||||
|
"category": [
|
||||||
|
"Feat of Strength"
|
||||||
|
],
|
||||||
|
"requires": "|Cetus| AND |Extension|"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Force(Fedlink)",
|
||||||
|
"region": "-6",
|
||||||
|
"category": [
|
||||||
|
"Feat of Strength"
|
||||||
|
],
|
||||||
|
"requires": "|Scraptown| AND |UFD| AND |Recycling| AND (|Storage| OR |Subcaves|)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Exit to -4 Factory",
|
||||||
|
"region": "-5",
|
||||||
|
"category": [
|
||||||
|
"Main Complex"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Pickup Rating 6 Normal Item",
|
||||||
|
"region": "-5",
|
||||||
|
"category": [
|
||||||
|
"Items"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Exit to -5 Caves",
|
||||||
|
"region": "-5",
|
||||||
|
"category": [
|
||||||
|
"Branch"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Install RIF 4",
|
||||||
|
"region": "-5",
|
||||||
|
"category": [
|
||||||
|
"Alignment"
|
||||||
|
],
|
||||||
|
"requires": "|Garrison|"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Exit to Zhirov",
|
||||||
|
"region": "-5",
|
||||||
|
"category": [
|
||||||
|
"Branch"
|
||||||
|
],
|
||||||
|
"requires": "|Upper Caves|"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Earned Evolution Slots (11 of 18)",
|
||||||
|
"region": "-5",
|
||||||
|
"category": [
|
||||||
|
"Evolution"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Earned Evolution Slots (12 of 18)",
|
||||||
|
"region": "-5",
|
||||||
|
"category": [
|
||||||
|
"Evolution"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Pick up Rating 5* Prototype",
|
||||||
|
"region": "-5",
|
||||||
|
"category": [
|
||||||
|
"Items"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Scanalyze a rating 5+ part",
|
||||||
|
"region": "-5",
|
||||||
|
"category": [
|
||||||
|
"Feat of Strength"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Analyze 5 Bots",
|
||||||
|
"region": "-5",
|
||||||
|
"category": [
|
||||||
|
"Feat of Strength"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Bothack an army of 5+ Combat Bots",
|
||||||
|
"region": "-5",
|
||||||
|
"category": [
|
||||||
|
"Feat of Strength"
|
||||||
|
],
|
||||||
|
"requires": "|RIF:1| AND |Garrison|"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Exit to Warlord (Location)",
|
||||||
|
"category": [
|
||||||
|
"Branch"
|
||||||
|
],
|
||||||
|
"requires": "|Upper Caves|",
|
||||||
|
"region": "-5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Talk to Warlord",
|
||||||
|
"category": [
|
||||||
|
"Alignment"
|
||||||
|
],
|
||||||
|
"requires": "|Upper Caves| AND |Warlord (Location)|",
|
||||||
|
"region": "-5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Exit to -3 Research",
|
||||||
|
"region": "-4",
|
||||||
|
"category": [
|
||||||
|
"Main Complex"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Pickup Rating 7 Normal Item",
|
||||||
|
"region": "-4",
|
||||||
|
"category": [
|
||||||
|
"Items"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Exit to -4 Caves",
|
||||||
|
"region": "-4",
|
||||||
|
"category": [
|
||||||
|
"Branch"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Exit to Armory",
|
||||||
|
"region": "-4",
|
||||||
|
"category": [
|
||||||
|
"Branch"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Exit to Lab",
|
||||||
|
"region": "-4",
|
||||||
|
"category": [
|
||||||
|
"Branch"
|
||||||
|
],
|
||||||
|
"requires": "|Armory|"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Install RIF 5",
|
||||||
|
"region": "-4",
|
||||||
|
"category": [
|
||||||
|
"Alignment"
|
||||||
|
],
|
||||||
|
"requires": "|Garrison|"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Earned Evolution Slots (13 of 18)",
|
||||||
|
"region": "-4",
|
||||||
|
"category": [
|
||||||
|
"Evolution"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Earned Evolution Slots (14 of 18)",
|
||||||
|
"region": "-4",
|
||||||
|
"category": [
|
||||||
|
"Evolution"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "5% or Less core Exposure",
|
||||||
|
"region": "-4",
|
||||||
|
"category": [
|
||||||
|
"Feat of Strength"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Corrupt a Behemoth to Death",
|
||||||
|
"region": "-4",
|
||||||
|
"category": [
|
||||||
|
"Feat of Strength"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Blow up 4 explosive machines indirectly",
|
||||||
|
"region": "-4",
|
||||||
|
"category": [
|
||||||
|
"Feat of Strength"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Fabricate 3 things on one floor",
|
||||||
|
"region": "-4",
|
||||||
|
"category": [
|
||||||
|
"Feat of Strength"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Repair 3 items on one floor",
|
||||||
|
"region": "-4",
|
||||||
|
"category": [
|
||||||
|
"Feat of Strength"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Use a Launcher Loader in combat",
|
||||||
|
"region": "-4",
|
||||||
|
"category": [
|
||||||
|
"Feat of Strength"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Pick up Rating 6* Prototype",
|
||||||
|
"region": "-4",
|
||||||
|
"category": [
|
||||||
|
"Items"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Index(Machines) Successfully",
|
||||||
|
"category": [
|
||||||
|
"Feat of Strength"
|
||||||
|
],
|
||||||
|
"requires": "|Rating 6 Parts|",
|
||||||
|
"region": "-4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Purge alert three times in a single map",
|
||||||
|
"category": [
|
||||||
|
"Feat of Strength"
|
||||||
|
],
|
||||||
|
"requires": "|Rating 6 Parts|",
|
||||||
|
"region": "-4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Exit to -2 Research",
|
||||||
|
"region": "-3",
|
||||||
|
"category": [
|
||||||
|
"Main Complex"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Pickup Rating 8 Normal Item",
|
||||||
|
"region": "-3",
|
||||||
|
"category": [
|
||||||
|
"Items"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Exit to Quarantine",
|
||||||
|
"region": "-3",
|
||||||
|
"category": [
|
||||||
|
"Branch"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Exit to Testing",
|
||||||
|
"region": "-3",
|
||||||
|
"category": [
|
||||||
|
"Branch"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Earned Evolution Slots (15 of 18)",
|
||||||
|
"region": "-3",
|
||||||
|
"category": [
|
||||||
|
"Evolution"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Earned Evolution Slots (16 of 18)",
|
||||||
|
"region": "-3",
|
||||||
|
"category": [
|
||||||
|
"Evolution"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Pick up Rating 7* Prototype",
|
||||||
|
"region": "-3",
|
||||||
|
"category": [
|
||||||
|
"Items"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Exit to -1 Access",
|
||||||
|
"region": "-2",
|
||||||
|
"category": [
|
||||||
|
"Main Complex"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Pickup Rating 9 Normal Item",
|
||||||
|
"region": "-2",
|
||||||
|
"category": [
|
||||||
|
"Items"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Exit to Section 7",
|
||||||
|
"region": "-2",
|
||||||
|
"category": [
|
||||||
|
"Branch"
|
||||||
|
],
|
||||||
|
"requires": "|Quarantine| AND |Testing|"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Earned Evolution Slots (17 of 18)",
|
||||||
|
"region": "-2",
|
||||||
|
"category": [
|
||||||
|
"Evolution"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Earned Evolution Slots (18 of 18)",
|
||||||
|
"region": "-2",
|
||||||
|
"category": [
|
||||||
|
"Evolution"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Pick up Rating 8* Prototype",
|
||||||
|
"region": "-2",
|
||||||
|
"category": [
|
||||||
|
"Items"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Scanalyze a rating 8+ part",
|
||||||
|
"region": "-2",
|
||||||
|
"category": [
|
||||||
|
"Feat of Strength"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Pickup Rating 10 Normal Item",
|
||||||
|
"region": "-1",
|
||||||
|
"category": [
|
||||||
|
"Items"
|
||||||
|
],
|
||||||
|
"requires": [],
|
||||||
|
"place_item_category": [
|
||||||
|
"Filler"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Exit to Command",
|
||||||
|
"region": "-1",
|
||||||
|
"category": [
|
||||||
|
"Branch"
|
||||||
|
],
|
||||||
|
"requires": [],
|
||||||
|
"place_item_category": [
|
||||||
|
"Filler"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Exit to Access_0",
|
||||||
|
"region": "-1",
|
||||||
|
"category": [
|
||||||
|
"Branch"
|
||||||
|
],
|
||||||
|
"requires": [],
|
||||||
|
"place_item_category": [
|
||||||
|
"Filler"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Pick up Rating 9* Prototype",
|
||||||
|
"region": "-1",
|
||||||
|
"category": [
|
||||||
|
"Items"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Pick up Rating 10* Prototype",
|
||||||
|
"region": "-1",
|
||||||
|
"category": [
|
||||||
|
"Items"
|
||||||
|
],
|
||||||
|
"requires": [],
|
||||||
|
"place_item_category": [
|
||||||
|
"Filler"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": [],
|
||||||
|
"requires": "|Triangle:27|",
|
||||||
|
"name": "Victory!",
|
||||||
|
"victory": true,
|
||||||
|
"region": "-1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://raw.githubusercontent.com/ManualForArchipelago/Manual/main/schemas/Manual.meta.schema.json",
|
||||||
|
"_comment": [ "remove the _ before a non-comment/info property to enable it",
|
||||||
|
"For more info use json schemas to get a description and allowed value for every properties",
|
||||||
|
"https://github.com/ManualForArchipelago/Manual/blob/main/docs/resources/json-schemas-for-world-devs.md"],
|
||||||
|
"docs": {
|
||||||
|
"_comment": [ "For more info on the possible documentation properties check schema and/or the github",
|
||||||
|
"https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/world%20api.md#webworld-class"
|
||||||
|
],
|
||||||
|
"apworld_description": [
|
||||||
|
"Manual games allow you to set custom check locations and custom item names that will be rolled into a multiworld.",
|
||||||
|
"This allows any variety of game -- PC, console, board games, Microsoft Word memes... really anything -- to be part of a multiworld randomizer.",
|
||||||
|
"The key component to including these games is some level of manual restriction. Since the items are not actually withheld from the player,",
|
||||||
|
"the player must manually refrain from using these gathered items until the tracker shows that they have been acquired or sent."
|
||||||
|
],
|
||||||
|
"web": {
|
||||||
|
"_comment": "for more info check github https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/world%20api.md#webworld-class",
|
||||||
|
"theme": "grass",
|
||||||
|
"bug_report_page": "https://discord.gg/T5bcsVHByx",
|
||||||
|
"tutorials": [
|
||||||
|
{
|
||||||
|
"name": "Multiworld Setup Guide",
|
||||||
|
"description": "A guide to setting up manual game integration for Archipelago multiworld games.",
|
||||||
|
"language": "English",
|
||||||
|
"file_name": "setup_en.md",
|
||||||
|
"link": "setup/en",
|
||||||
|
"_authors": ["Fuzzy"],
|
||||||
|
"_comment": "if you don't include the authors it will use creator from game.json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"_options_presets": {
|
||||||
|
"Easy":{
|
||||||
|
"Option1": true,
|
||||||
|
"Option2": "potato"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://raw.githubusercontent.com/ManualForArchipelago/Manual/main/schemas/Manual.options.schema.json",
|
||||||
|
"_comment": "Add a _ before an option name to comment it out and it wont be added to the apworld",
|
||||||
|
"core": {
|
||||||
|
"goal":{
|
||||||
|
"_comment": [
|
||||||
|
"Using 'goal' as the name of an option let you override some of the generated goal parameters"
|
||||||
|
],
|
||||||
|
"display_name": "The End Objective",
|
||||||
|
"description": "How to End your Randomized playthrough.",
|
||||||
|
"aliases": {"easiest": 0},
|
||||||
|
"hidden": false
|
||||||
|
},
|
||||||
|
"death_link": {
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"filler_traps": {
|
||||||
|
"_comment": "Using values here let you set a 'recommended' filler_trap percentage or even a default percentage",
|
||||||
|
"values": {
|
||||||
|
"recommended": 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"_commented_out_option":{
|
||||||
|
"type": "Toggle",
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"Example_Toggle":{
|
||||||
|
"description": [
|
||||||
|
"This is a Toggle",
|
||||||
|
"A simple boolean, aka True or False set the initial value with \"default\""
|
||||||
|
],
|
||||||
|
"type": "Toggle",
|
||||||
|
"default": false,
|
||||||
|
"group": "Example Options"
|
||||||
|
},
|
||||||
|
"Example_Choice":{
|
||||||
|
"type": "Choice",
|
||||||
|
"description": [
|
||||||
|
"This is a Choice",
|
||||||
|
"Let the user pick from a list of values",
|
||||||
|
"allow_custom_value let a player define their own text value",
|
||||||
|
"Currently the only use for Choice in Manual outside of hooks is the goal"
|
||||||
|
],
|
||||||
|
"values": {
|
||||||
|
"start":0,
|
||||||
|
"test":1
|
||||||
|
},
|
||||||
|
"default": 0,
|
||||||
|
"allow_custom_value": false,
|
||||||
|
"group": "Example Options"
|
||||||
|
},
|
||||||
|
"Example_Range":{
|
||||||
|
"type": "Range",
|
||||||
|
"description": [
|
||||||
|
"This is a Range",
|
||||||
|
"Allow the player to specify a value between 'start' and 'end'",
|
||||||
|
"if you include a \"values\" you can define the label for certain values",
|
||||||
|
"Currently the only use for Range in Manual outside of hooks is 'filler_trap'"
|
||||||
|
],
|
||||||
|
"range_start": 0,
|
||||||
|
"default": 1,
|
||||||
|
"range_end": 5,
|
||||||
|
"values": {"example": 2},
|
||||||
|
"group": "Example Options"
|
||||||
|
},
|
||||||
|
|
||||||
|
"DLC_enabled":{
|
||||||
|
"display_name": "DLC Enabled",
|
||||||
|
"description": "Is the Dlc enabled?",
|
||||||
|
"type": "Toggle",
|
||||||
|
"visibility": [
|
||||||
|
"simple_ui",
|
||||||
|
"complex_ui",
|
||||||
|
"template"
|
||||||
|
],
|
||||||
|
"default": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://github.com/ManualForArchipelago/Manual/raw/main/schemas/Manual.regions.schema.json",
|
||||||
|
"-11": {
|
||||||
|
"connects_to": [
|
||||||
|
"-10"
|
||||||
|
],
|
||||||
|
"requires": [],
|
||||||
|
"starting": true
|
||||||
|
},
|
||||||
|
"-10": {
|
||||||
|
"connects_to": [
|
||||||
|
"-9"
|
||||||
|
],
|
||||||
|
"requires": []
|
||||||
|
},
|
||||||
|
"-9": {
|
||||||
|
"connects_to": [
|
||||||
|
"-8"
|
||||||
|
],
|
||||||
|
"requires": "|-9 Materials| AND |Evolution Slot:2| AND |Rating 2 Parts|"
|
||||||
|
},
|
||||||
|
"-8": {
|
||||||
|
"connects_to": [
|
||||||
|
"-7"
|
||||||
|
],
|
||||||
|
"requires": "(|-8 Materials| AND |Evolution Slot:4| AND |Rating 3 Parts|"
|
||||||
|
},
|
||||||
|
"-7": {
|
||||||
|
"connects_to": [
|
||||||
|
"-6"
|
||||||
|
],
|
||||||
|
"requires": "|-7 Factory| AND |Evolution Slot:6| AND |Rating 4 Parts|"
|
||||||
|
},
|
||||||
|
"-6": {
|
||||||
|
"connects_to": [
|
||||||
|
"-5"
|
||||||
|
],
|
||||||
|
"requires": "|-6 Factory| AND |Evolution Slot:8| AND |Rating 5 Parts|"
|
||||||
|
},
|
||||||
|
"-5": {
|
||||||
|
"connects_to": [
|
||||||
|
"-4"
|
||||||
|
],
|
||||||
|
"requires": "|-5 Factory| AND |Evolution Slot:10| AND |Rating 6 Parts|"
|
||||||
|
},
|
||||||
|
"-4": {
|
||||||
|
"connects_to": [
|
||||||
|
"-3"
|
||||||
|
],
|
||||||
|
"requires": "|-4 Factory| AND |Evolution Slot:12| AND |Rating 7 Parts|"
|
||||||
|
},
|
||||||
|
"-3": {
|
||||||
|
"connects_to": [
|
||||||
|
"-2"
|
||||||
|
],
|
||||||
|
"requires": "|-3 Research| AND |Evolution Slot:14| AND |Rating 8 Parts|"
|
||||||
|
},
|
||||||
|
"-2": {
|
||||||
|
"connects_to": [
|
||||||
|
"-1"
|
||||||
|
],
|
||||||
|
"requires": "|-2 Research| AND |Evolution Slot:16| AND |Rating 9 Parts|"
|
||||||
|
},
|
||||||
|
"-1": {
|
||||||
|
"connects_to": [],
|
||||||
|
"requires": "|-1 Access| AND |Evolution Slot:18|"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Manual Randomizer Setup Guide
|
||||||
|
|
||||||
|
## Required Software
|
||||||
|
|
||||||
|
- None
|
||||||
|
|
||||||
|
## Installation Procedures
|
||||||
|
|
||||||
|
Needs content.
|
||||||
|
|
||||||
|
## Joining a MultiWorld Game
|
||||||
|
|
||||||
|
Needs content.
|
||||||
|
|
||||||
|
## Multiplayer Manual
|
||||||
|
|
||||||
|
Needs content.
|
||||||
|
|
||||||
|
## Game Troubleshooting
|
||||||
|
|
||||||
|
Needs content.
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# called after the game.json file has been loaded
|
||||||
|
def after_load_game_file(game_table: dict) -> dict:
|
||||||
|
return game_table
|
||||||
|
# called after the items.json file has been loaded, before any item loading or processing has occurred
|
||||||
|
# if you need access to the items after processing to add ids, etc., you should use the hooks in World.py
|
||||||
|
def after_load_item_file(item_table: list) -> list:
|
||||||
|
return item_table
|
||||||
|
|
||||||
|
# NOTE: Progressive items are not currently supported in Manual. Once they are,
|
||||||
|
# this hook will provide the ability to meaningfully change those.
|
||||||
|
def after_load_progressive_item_file(progressive_item_table: list) -> list:
|
||||||
|
return progressive_item_table
|
||||||
|
|
||||||
|
# called after the locations.json file has been loaded, before any location loading or processing has occurred
|
||||||
|
# if you need access to the locations after processing to add ids, etc., you should use the hooks in World.py
|
||||||
|
def after_load_location_file(location_table: list) -> list:
|
||||||
|
return location_table
|
||||||
|
|
||||||
|
# called after the events.json file has been loaded, before any processing has occurred
|
||||||
|
# If you need access to the events after processing, you should use the hooks in World.py
|
||||||
|
def after_load_event_file(event_table: list) -> list:
|
||||||
|
return event_table
|
||||||
|
|
||||||
|
# called after the regions.json file has been loaded, before any location loading or processing has occurred
|
||||||
|
# if you need access to the locations after processing to add ids, etc., you should use the hooks in World.py
|
||||||
|
def after_load_region_file(region_table: dict) -> dict:
|
||||||
|
return region_table
|
||||||
|
|
||||||
|
# called after the categories.json file has been loaded
|
||||||
|
def after_load_category_file(category_table: dict) -> dict:
|
||||||
|
return category_table
|
||||||
|
|
||||||
|
# called after the categories.json file has been loaded
|
||||||
|
def after_load_option_file(option_table: dict) -> dict:
|
||||||
|
# option_table["core"] is the dictionary of modification of existing options
|
||||||
|
# option_table["user"] is the dictionary of custom options
|
||||||
|
return option_table
|
||||||
|
|
||||||
|
# called after the meta.json file has been loaded and just before the properties of the apworld are defined. You can use this hook to change what is displayed on the webhost
|
||||||
|
# for more info check https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/world%20api.md#webworld-class
|
||||||
|
def after_load_meta_file(meta_table: dict) -> dict:
|
||||||
|
return meta_table
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
from typing import Optional, Any
|
||||||
|
from BaseClasses import MultiWorld
|
||||||
|
|
||||||
|
|
||||||
|
# Use this if you want to override the default behavior of is_option_enabled
|
||||||
|
# Return True to enable the category, False to disable it, or None to use the default behavior
|
||||||
|
def before_is_category_enabled(multiworld: MultiWorld, player: int, category_name: str) -> Optional[bool]:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Use this if you want to override the default behavior of is_option_enabled
|
||||||
|
# Return True to enable the item, False to disable it, or None to use the default behavior
|
||||||
|
def before_is_item_enabled(multiworld: MultiWorld, player: int, item: dict[str, Any]) -> Optional[bool]:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Use this if you want to override the default behavior of is_option_enabled
|
||||||
|
# Return True to enable the location, False to disable it, or None to use the default behavior
|
||||||
|
def before_is_location_enabled(multiworld: MultiWorld, player: int, location: dict[str, Any]) -> Optional[bool]:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Use this if you want to override the default behavior of is_option_enabled
|
||||||
|
# Return True to enable the event, False to disable it, or None to use the default behavior
|
||||||
|
def before_is_event_enabled(multiworld: MultiWorld, player: int, event: dict[str, Any]) -> Optional[bool]:
|
||||||
|
return None
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# Object classes from AP that represent different types of options that you can create
|
||||||
|
from Options import Option, FreeText, NumericOption, Toggle, DefaultOnToggle, Choice, TextChoice, Range, NamedRange, OptionGroup, PerGameCommonOptions
|
||||||
|
# These helper methods allow you to determine if an option has been set, or what its value is, for any player in the multiworld
|
||||||
|
from ..Helpers import is_option_enabled, get_option_value
|
||||||
|
from typing import Type, Any
|
||||||
|
|
||||||
|
|
||||||
|
####################################################################
|
||||||
|
# NOTE: At the time that options are created, Manual has no concept of the multiworld or its own world.
|
||||||
|
# Options are defined before the world is even created.
|
||||||
|
#
|
||||||
|
# Example of creating your own option:
|
||||||
|
#
|
||||||
|
# class MakeThePlayerOP(Toggle):
|
||||||
|
# """Should the player be overpowered? Probably not, but you can choose for this to do... something!"""
|
||||||
|
# display_name = "Make me OP"
|
||||||
|
#
|
||||||
|
# options["make_op"] = MakeThePlayerOP
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# Then, to see if the option is set, you can call is_option_enabled or get_option_value.
|
||||||
|
#####################################################################
|
||||||
|
|
||||||
|
|
||||||
|
# To add an option, use the before_options_defined hook below and something like this:
|
||||||
|
# options["total_characters_to_win_with"] = TotalCharactersToWinWith
|
||||||
|
#
|
||||||
|
class TotalCharactersToWinWith(Range):
|
||||||
|
"""Instead of having to beat the game with all characters, you can limit locations to a subset of character victory locations."""
|
||||||
|
display_name = "Number of characters to beat the game with before victory"
|
||||||
|
range_start = 10
|
||||||
|
range_end = 50
|
||||||
|
default = 50
|
||||||
|
|
||||||
|
# This is called before any manual options are defined, in case you want to define your own with a clean slate or let Manual define over them
|
||||||
|
def before_options_defined(options: dict[str, Type[Option[Any]]]) -> dict[str, Type[Option[Any]]]:
|
||||||
|
return options
|
||||||
|
|
||||||
|
# This is called after any manual options are defined, in case you want to see what options are defined or want to modify the defined options
|
||||||
|
def after_options_defined(options: Type[PerGameCommonOptions]):
|
||||||
|
# To access a modifiable version of options check the dict in options.type_hints
|
||||||
|
# For example if you want to change DLC_enabled's display name you would do:
|
||||||
|
# options.type_hints["DLC_enabled"].display_name = "New Display Name"
|
||||||
|
|
||||||
|
# Here's an example on how to add your aliases to the generated goal
|
||||||
|
# options.type_hints['goal'].aliases.update({"example": 0, "second_alias": 1})
|
||||||
|
# options.type_hints['goal'].options.update({"example": 0, "second_alias": 1}) #for an alias to be valid it must also be in options
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Use this Hook if you want to add your Option to an Option group (existing or not)
|
||||||
|
def before_option_groups_created(groups: dict[str, list[Type[Option[Any]]]]) -> dict[str, list[Type[Option[Any]]]]:
|
||||||
|
# Uses the format groups['GroupName'] = [TotalCharactersToWinWith]
|
||||||
|
return groups
|
||||||
|
|
||||||
|
def after_option_groups_created(groups: list[OptionGroup]) -> list[OptionGroup]:
|
||||||
|
return groups
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
from typing import Optional
|
||||||
|
from worlds.AutoWorld import World
|
||||||
|
from ..Helpers import clamp, get_items_with_value
|
||||||
|
from BaseClasses import MultiWorld, CollectionState
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
# Sometimes you have a requirement that is just too messy or repetitive to write out with boolean logic.
|
||||||
|
# Define a function here, and you can use it in a requires string with {function_name()}.
|
||||||
|
def overfishedAnywhere(world: World, state: CollectionState, player: int):
|
||||||
|
"""Has the player collected all fish from any fishing log?"""
|
||||||
|
for cat, items in world.item_name_groups:
|
||||||
|
if cat.endswith("Fishing Log") and state.has_all(items, player):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
# You can also pass an argument to your function, like {function_name(15)}
|
||||||
|
# Note that all arguments are strings, so you'll need to convert them to ints if you want to do math.
|
||||||
|
def anyClassLevel(state: CollectionState, player: int, level: str):
|
||||||
|
"""Has the player reached the given level in any class?"""
|
||||||
|
for item in ["Figher Level", "Black Belt Level", "Thief Level", "Red Mage Level", "White Mage Level", "Black Mage Level"]:
|
||||||
|
if state.count(item, player) >= int(level):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
# You can also return a string from your function, and it will be evaluated as a requires string.
|
||||||
|
def requiresMelee():
|
||||||
|
"""Returns a requires string that checks if the player has unlocked the tank."""
|
||||||
|
return "|Figher Level:15| or |Black Belt Level:15| or |Thief Level:15|"
|
||||||
+207
@@ -0,0 +1,207 @@
|
|||||||
|
# Object classes from AP core, to represent an entire MultiWorld and this individual World that's part of it
|
||||||
|
from typing import Any
|
||||||
|
from worlds.AutoWorld import World
|
||||||
|
from BaseClasses import MultiWorld, CollectionState, Item
|
||||||
|
|
||||||
|
# Object classes from Manual -- extending AP core -- representing items and locations that are used in generation
|
||||||
|
from ..Items import ManualItem
|
||||||
|
from ..Locations import ManualLocation
|
||||||
|
|
||||||
|
# Raw JSON data from the Manual apworld, respectively:
|
||||||
|
# data/game.json, data/items.json, data/locations.json, data/regions.json
|
||||||
|
#
|
||||||
|
from ..Data import game_table, item_table, location_table, region_table
|
||||||
|
|
||||||
|
# These helper methods allow you to determine if an option has been set, or what its value is, for any player in the multiworld
|
||||||
|
from ..Helpers import is_option_enabled, get_option_value, format_state_prog_items_key, ProgItemsCat, remove_specific_item
|
||||||
|
|
||||||
|
# calling logging.info("message") anywhere below in this file will output the message to both console and log file
|
||||||
|
import logging
|
||||||
|
|
||||||
|
########################################################################################
|
||||||
|
## Order of method calls when the world generates:
|
||||||
|
## 1. create_regions - Creates regions and locations
|
||||||
|
## 2. create_items - Creates the item pool
|
||||||
|
## 3. set_rules - Creates rules for accessing regions and locations
|
||||||
|
## 4. generate_basic - Runs any post item pool options, like place item/category
|
||||||
|
## 5. pre_fill - Creates the victory location
|
||||||
|
##
|
||||||
|
## The create_item method is used by plando and start_inventory settings to create an item from an item name.
|
||||||
|
## The fill_slot_data method will be used to send data to the Manual client for later use, like deathlink.
|
||||||
|
########################################################################################
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Use this function to change the valid filler items to be created to replace item links or starting items.
|
||||||
|
# Default value is the `filler_item_name` from game.json
|
||||||
|
def hook_get_filler_item_name(world: World, multiworld: MultiWorld, player: int) -> list[str] | str | bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def before_generate_early(world: World, multiworld: MultiWorld, player: int) -> None:
|
||||||
|
"""
|
||||||
|
This is the earliest hook called during generation, before anything else is done.
|
||||||
|
Use it to check or modify incompatible options, or to set up variables for later use.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def before_create_regions(world: World, multiworld: MultiWorld, player: int):
|
||||||
|
"""
|
||||||
|
Called before regions and locations are created. Not clear why you'd want this, but it's here.
|
||||||
|
Victory location is included, but Victory event is not placed yet.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def after_create_regions(world: World, multiworld: MultiWorld, player: int):
|
||||||
|
"""
|
||||||
|
Called after regions and locations are created, in case you want to see or modify that information.
|
||||||
|
Victory location is included.
|
||||||
|
|
||||||
|
Use this hook to modify locations from the world
|
||||||
|
If you wish to remove them entirely, use Helpers.before_is_location_enabled instead.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Add your code here to calculate which locations to exclude
|
||||||
|
for region in multiworld.regions:
|
||||||
|
if region.player == player:
|
||||||
|
for location in region.locations:
|
||||||
|
# If you have other things you want to modify, you can do it here as well.
|
||||||
|
# The fields you might care about are: item_rule, always_allow, progress_type, and show_in_spoiler.
|
||||||
|
pass
|
||||||
|
|
||||||
|
# This hook allows you to access the item names & counts before the items are created. Use this to increase/decrease the amount of a specific item in the pool
|
||||||
|
# Valid item_config key/values:
|
||||||
|
# {"Item Name": 5} <- This will create qty 5 items using all the default settings
|
||||||
|
# {"Item Name": {"useful": 7}} <- This will create qty 7 items and force them to be classified as useful
|
||||||
|
# {"Item Name": {"progression": 2, "useful": 1}} <- This will create 3 items, with 2 classified as progression and 1 as useful
|
||||||
|
# {"Item Name": {0b0110: 5}} <- If you know the special flag for the item classes, you can also define non-standard options. This setup
|
||||||
|
# will create 5 items that are the "useful trap" class
|
||||||
|
# {"Item Name": {ItemClassification.useful: 5}} <- You can also use the classification directly
|
||||||
|
def before_create_items_all(item_config: dict[str, int|dict], world: World, multiworld: MultiWorld, player: int) -> dict[str, int|dict]:
|
||||||
|
return item_config
|
||||||
|
|
||||||
|
# The item pool before place_item(_category) are processed, in case you want to see the raw item pool at that stage
|
||||||
|
def before_create_items_place_items(item_pool: list, world: World, multiworld: MultiWorld, player: int) -> list:
|
||||||
|
return item_pool
|
||||||
|
|
||||||
|
# The item pool before starting items are processed, in case you want to see the raw item pool at that stage
|
||||||
|
def before_create_items_starting(item_pool: list, world: World, multiworld: MultiWorld, player: int) -> list:
|
||||||
|
return item_pool
|
||||||
|
|
||||||
|
# The item pool after starting items are processed but before filler is added, in case you want to see the raw item pool at that stage
|
||||||
|
def before_create_items_filler(item_pool: list, world: World, multiworld: MultiWorld, player: int) -> list:
|
||||||
|
# Use this hook to remove items from the item pool
|
||||||
|
itemNamesToRemove: list[str] = [] # List of item names
|
||||||
|
|
||||||
|
# Add your code here to calculate which items to remove.
|
||||||
|
#
|
||||||
|
# Because multiple copies of an item can exist, you need to add an item name
|
||||||
|
# to the list multiple times if you want to remove multiple copies of it.
|
||||||
|
|
||||||
|
for itemName in itemNamesToRemove:
|
||||||
|
item = next(i for i in item_pool if i.name == itemName)
|
||||||
|
remove_specific_item(item_pool, item)
|
||||||
|
|
||||||
|
return item_pool
|
||||||
|
|
||||||
|
# Some other useful hook options:
|
||||||
|
|
||||||
|
## Place an item at a specific location
|
||||||
|
# location = next(l for l in multiworld.get_unfilled_locations(player=player) if l.name == "Location Name")
|
||||||
|
# item_to_place = next(i for i in item_pool if i.name == "Item Name")
|
||||||
|
# location.place_locked_item(item_to_place)
|
||||||
|
# remove_specific_item(item_pool, item_to_place)
|
||||||
|
|
||||||
|
# The complete item pool prior to being set for generation is provided here, in case you want to make changes to it
|
||||||
|
def after_create_items(item_pool: list, world: World, multiworld: MultiWorld, player: int) -> list:
|
||||||
|
return item_pool
|
||||||
|
|
||||||
|
# Called before rules for accessing regions and locations are created. Not clear why you'd want this, but it's here.
|
||||||
|
def before_set_rules(world: World, multiworld: MultiWorld, player: int):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Called after rules for accessing regions and locations are created, in case you want to see or modify that information.
|
||||||
|
def after_set_rules(world: World, multiworld: MultiWorld, player: int):
|
||||||
|
# Use this hook to modify the access rules for a given location
|
||||||
|
|
||||||
|
def Example_Rule(state: CollectionState) -> bool:
|
||||||
|
# Calculated rules take a CollectionState object and return a boolean
|
||||||
|
# True if the player can access the location
|
||||||
|
# CollectionState is defined in BaseClasses
|
||||||
|
return True
|
||||||
|
|
||||||
|
## Common functions:
|
||||||
|
# location = world.get_location(location_name, player)
|
||||||
|
# location.access_rule = Example_Rule
|
||||||
|
|
||||||
|
## Combine rules:
|
||||||
|
# old_rule = location.access_rule
|
||||||
|
# location.access_rule = lambda state: old_rule(state) and Example_Rule(state)
|
||||||
|
# OR
|
||||||
|
# location.access_rule = lambda state: old_rule(state) or Example_Rule(state)
|
||||||
|
|
||||||
|
# The item name to create is provided before the item is created, in case you want to make changes to it
|
||||||
|
def before_create_item(item_name: str, world: World, multiworld: MultiWorld, player: int) -> str:
|
||||||
|
return item_name
|
||||||
|
|
||||||
|
# The item that was created is provided after creation, in case you want to modify the item
|
||||||
|
def after_create_item(item: ManualItem, world: World, multiworld: MultiWorld, player: int) -> ManualItem:
|
||||||
|
return item
|
||||||
|
|
||||||
|
# This method is run towards the end of pre-generation, before the place_item options have been handled and before AP generation occurs
|
||||||
|
def before_generate_basic(world: World, multiworld: MultiWorld, player: int):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# This method is run every time an item is added to the state, can be used to modify the value of an item.
|
||||||
|
# IMPORTANT! Any changes made in this hook must be cancelled/undone in after_remove_item
|
||||||
|
def after_collect_item(world: World, state: CollectionState, Changed: bool, item: Item):
|
||||||
|
# the following let you add to the Potato Item Value count
|
||||||
|
# if item.name == "Cooked Potato":
|
||||||
|
# state.prog_items[item.player][format_state_prog_items_key(ProgItemsCat.VALUE, "Potato")] += 1
|
||||||
|
pass
|
||||||
|
|
||||||
|
# This method is run every time an item is removed from the state, can be used to modify the value of an item.
|
||||||
|
# IMPORTANT! Any changes made in this hook must be first done in after_collect_item
|
||||||
|
def after_remove_item(world: World, state: CollectionState, Changed: bool, item: Item):
|
||||||
|
# the following let you undo the addition to the Potato Item Value count
|
||||||
|
# if item.name == "Cooked Potato":
|
||||||
|
# state.prog_items[item.player][format_state_prog_items_key(ProgItemsCat.VALUE, "Potato")] -= 1
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# This is called before slot data is set and provides an empty dict ({}), in case you want to modify it before Manual does
|
||||||
|
def before_fill_slot_data(slot_data: dict, world: World, multiworld: MultiWorld, player: int) -> dict:
|
||||||
|
return slot_data
|
||||||
|
|
||||||
|
# This is called after slot data is set and provides the slot data at the time, in case you want to check and modify it after Manual is done with it
|
||||||
|
def after_fill_slot_data(slot_data: dict, world: World, multiworld: MultiWorld, player: int) -> dict:
|
||||||
|
return slot_data
|
||||||
|
|
||||||
|
# This is called right at the end, in case you want to write stuff to the spoiler log
|
||||||
|
def before_write_spoiler(world: World, multiworld: MultiWorld, spoiler_handle) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# This is called when you want to add information to the hint text
|
||||||
|
def before_extend_hint_information(hint_data: dict[int, dict[int, str]], world: World, multiworld: MultiWorld, player: int) -> None:
|
||||||
|
|
||||||
|
### Example way to use this hook:
|
||||||
|
# if player not in hint_data:
|
||||||
|
# hint_data.update({player: {}})
|
||||||
|
# for location in multiworld.get_locations(player):
|
||||||
|
# if not location.address:
|
||||||
|
# continue
|
||||||
|
#
|
||||||
|
# use this section to calculate the hint string
|
||||||
|
#
|
||||||
|
# hint_data[player][location.address] = hint_string
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
def after_extend_hint_information(hint_data: dict[int, dict[int, str]], world: World, multiworld: MultiWorld, player: int) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def hook_interpret_slot_data(world: World, player: int, slot_data: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Called when Universal Tracker wants to perform a fake generation
|
||||||
|
Use this if you want to use or modify the slot_data for passed into re_gen_passthrough
|
||||||
|
"""
|
||||||
|
return slot_data
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# intentionally empty
|
||||||
Whitespace-only changes.
@@ -0,0 +1,6 @@
|
|||||||
|
from test.TestBase import WorldTestBase
|
||||||
|
from ..Game import game_name
|
||||||
|
|
||||||
|
|
||||||
|
class ManualTest(WorldTestBase):
|
||||||
|
game = game_name
|
||||||
Reference in new issue
Block a user