diff --git a/src/Data.py b/src/Data.py index 87bd036f..d1093642 100644 --- a/src/Data.py +++ b/src/Data.py @@ -1,4 +1,8 @@ import logging +import os +import json +import zipfile +from argparse import Namespace from .DataValidation import DataValidation, ValidationError from .Helpers import load_data_file as helpers_load_data_file @@ -20,67 +24,95 @@ def convert_to_list(data, property_name: str) -> list: return data class ManualFile: + source_path: any filename: str data_type: dict|list - def __init__(self, filename, data_type): + def __init__(self, source_path, filename, data_type): + self.source_path = source_path 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 = ManualFile('game.json', dict).load() #dict -item_table = convert_to_list(ManualFile('items.json', list).load(), 'data') #list -location_table = convert_to_list(ManualFile('locations.json', list).load(), 'data') #list -region_table = ManualFile('regions.json', dict).load() #dict -category_table = ManualFile('categories.json', dict).load() #dict -option_table = ManualFile('options.json', dict).load() #dict -meta_table = 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) -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.region_table = region_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.") + def load(self, safe=False): + if safe: + try: + return json.loads(zipfile.Path( + self.source_path, + f"{os.path.splitext(self.source_path.name)[0]}/data/{self.filename}" + ).open().read()) + except: + return self.data_type() + else: + contents = helpers_load_data_file(self.filename, source=self.source_path) + + if not contents and type(contents) != self.data_type: + return self.data_type() + + return contents + +def get_data_Namespace(path="", safe=False) -> Namespace: + # TODO add path handling so it can be retargeted to a different source than self + ret = Namespace() + + ret.game_table = ManualFile(path, 'game.json', dict).load(safe=safe) #dict + ret.item_table = convert_to_list(ManualFile(path, 'items.json', list).load(safe=safe), 'data') #list + ret.location_table = convert_to_list(ManualFile(path, 'locations.json', list).load(safe=safe), 'data') #list + ret.region_table = ManualFile(path, 'regions.json', dict).load(safe=safe) #dict + ret.category_table = ManualFile(path, 'categories.json', dict).load(safe=safe) #dict + ret.option_table = ManualFile(path, 'options.json', dict).load(safe=safe) #dict + ret.meta_table = ManualFile(path, 'meta.json', dict).load(safe=safe) #dict + + # Removal of schemas in root of tables + ret.region_table.pop('$schema', '') + ret.category_table.pop('$schema', '') + + # hooks + ret.game_table = after_load_game_file(ret.game_table) + ret.item_table = after_load_item_file(ret.item_table) + ret.location_table = after_load_location_file(ret.location_table) + ret.region_table = after_load_region_file(ret.region_table) + ret.category_table = after_load_category_file(ret.category_table) + ret.option_table = after_load_option_file(ret.option_table) + ret.meta_table = after_load_meta_file(ret.meta_table) + + # seed all of the tables for validation + ret.data_validation = DataValidation() + ret.data_validation.game_table = ret.game_table + ret.data_validation.item_table = ret.item_table + ret.data_validation.location_table = ret.location_table + ret.data_validation.region_table = ret.region_table + + validation_errors = [] + + # check that json files are not just invalid json + try: ret.data_validation.checkForGameBeingInvalidJSON() + except ValidationError as e: validation_errors.append(e) + + try: ret.data_validation.checkForItemsBeingInvalidJSON() + except ValidationError as e: validation_errors.append(e) + + try: ret.data_validation.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.") + + from .Game import parse_gamedata + parse_gamedata(ret) + from .Items import parse_itemdata + parse_itemdata(ret) + from .Locations import parse_locationdata + parse_locationdata(ret) + from .Meta import parse_metadata + parse_metadata(ret) + from .Regions import parse_regiondata + parse_regiondata(ret) + + return ret diff --git a/src/DataValidation.py b/src/DataValidation.py index 279d9f02..e19cfdd5 100644 --- a/src/DataValidation.py +++ b/src/DataValidation.py @@ -9,15 +9,19 @@ class ValidationError(Exception): pass class DataValidation(): - game_table = {} - item_table = [] - location_table = [] - region_table = {} - - - @staticmethod - def checkItemNamesInLocationRequires(): - for location in DataValidation.location_table: + game_table: dict + item_table: list + location_table: list + region_table: dict + + def __init__(self): + self.game_table = {} + self.item_table = [] + self.location_table = [] + self.region_table = {} + + def checkItemNamesInLocationRequires(self): + for location in self.location_table: if "requires" not in location: continue @@ -39,7 +43,7 @@ def checkItemNamesInLocationRequires(): if len(item_parts) > 1: item_name = item_parts[0] - item_exists = len([item["name"] for item in DataValidation.item_table if item["name"] == item_name]) > 0 + item_exists = len([item["name"] for item in self.item_table if item["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"])) @@ -60,7 +64,7 @@ def checkItemNamesInLocationRequires(): if len(or_item_parts) > 1: or_item_name = or_item_parts[0] - item_exists = len([item["name"] for item in DataValidation.item_table if item["name"] == or_item_name]) > 0 + item_exists = len([item["name"] for item in self.item_table if item["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"])) @@ -71,15 +75,14 @@ def checkItemNamesInLocationRequires(): if len(item_parts) > 1: item_name = item_parts[0] - item_exists = len([item["name"] for item in DataValidation.item_table if item["name"] == item_name]) > 0 + item_exists = len([item["name"] for item in self.item_table if item["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(): - for region_name in DataValidation.region_table: - region = DataValidation.region_table[region_name] + def checkItemNamesInRegionRequires(self): + for region_name in self.region_table: + region = self.region_table[region_name] if "requires" not in region: continue @@ -102,7 +105,7 @@ def checkItemNamesInRegionRequires(): if len(item_parts) > 1: item_name = item_parts[0] - item_exists = len([item["name"] for item in DataValidation.item_table if item["name"] == item_name]) > 0 + item_exists = len([item["name"] for item in self.item_table if item["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)) @@ -123,7 +126,7 @@ def checkItemNamesInRegionRequires(): if len(or_item_parts) > 1: or_item_name = or_item_parts[0] - item_exists = len([item["name"] for item in DataValidation.item_table if item["name"] == or_item_name]) > 0 + item_exists = len([item["name"] for item in self.item_table if item["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)) @@ -134,25 +137,23 @@ def checkItemNamesInRegionRequires(): if len(item_parts) > 1: item_name = item_parts[0] - item_exists = len([item["name"] for item in DataValidation.item_table if item["name"] == item_name]) > 0 + item_exists = len([item["name"] for item in self.item_table if item["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: + def checkRegionNamesInLocations(self): + for location in self.location_table: 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 + region_exists = len([name for name in self.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 checkItemsThatShouldBeRequired(): - for item in DataValidation.item_table: + def checkItemsThatShouldBeRequired(self): + for item in self.item_table: # if the item is already progression, no need to check if "progression" in item and item["progression"]: continue @@ -162,7 +163,7 @@ def checkItemsThatShouldBeRequired(): continue # check location requires for the presence of item name - for location in DataValidation.location_table: + for location in self.location_table: if "requires" not in location: continue @@ -178,8 +179,8 @@ def checkItemsThatShouldBeRequired(): raise ValidationError("Item %s is required by location %s, but the item is not marked as progression." % (item["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] + for region_name in self.region_table: + region = self.region_table[region_name] if "requires" not in region: continue @@ -208,8 +209,7 @@ def _checkLocationRequiresForItemValueWithRegex(values_requested: dict[str, int] return values_requested - @staticmethod - def preFillCheckIfEnoughItemsForValue(world: World, multiworld: MultiWorld): + def preFillCheckIfEnoughItemsForValue(self,world: World, multiworld: MultiWorld): from .Helpers import get_items_with_value, get_items_for_player, filter_used_regions player = world.player values_requested = {} @@ -226,23 +226,23 @@ def preFillCheckIfEnoughItemsForValue(world: World, multiworld: MultiWorld): #Check used regions (and their parent(s)) for ItemValue requirement for region in used_regions: - manualregion = DataValidation.region_table.get(region.name, {}) + manualregion = self.region_table.get(region.name, {}) if manualregion: if manualregion.get("requires"): - DataValidation._checkLocationRequiresForItemValueWithRegex(values_requested, json.dumps(manualregion["requires"])) + self._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)) + self._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)) + self._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"])) + self._checkLocationRequiresForItemValueWithRegex(values_requested, json.dumps(manualLocation["requires"])) # compare whats available vs requested but only if there's anything requested if values_requested: @@ -262,51 +262,46 @@ def preFillCheckIfEnoughItemsForValue(world: World, multiworld: MultiWorld): 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] + def checkRegionsConnectingToOtherRegions(self): + for region_name in self.region_table: + region = self.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 + region_exists = len([name for name in self.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 checkForDuplicateItemNames(): - for item in DataValidation.item_table: - name_count = len([i for i in DataValidation.item_table if i["name"] == item["name"]]) + def checkForDuplicateItemNames(self): + for item in self.item_table: + name_count = len([i for i in self.item_table if i["name"] == item["name"]]) if name_count > 1: raise ValidationError("Item %s is defined more than once." % (item["name"])) - @staticmethod - def checkForDuplicateLocationNames(): - for location in DataValidation.location_table: - name_count = len([l for l in DataValidation.location_table if l["name"] == location["name"]]) + def checkForDuplicateLocationNames(self): + for location in self.location_table: + name_count = len([l for l in self.location_table if l["name"] == location["name"]]) if name_count > 1: raise ValidationError("Location %s is defined more than once." % (location["name"])) - @staticmethod - def checkForDuplicateRegionNames(): + def checkForDuplicateRegionNames(self): # this currently does nothing because the region name is a dict key, which will never be non-unique / limited to 1 - for region_name in DataValidation.region_table: - name_count = len([r for r in DataValidation.region_table if r == region_name]) + for region_name in self.region_table: + name_count = len([r for r in self.region_table if r == region_name]) if name_count > 1: raise ValidationError("Region %s is defined more than once." % (region_name)) - @staticmethod - def checkStartingItemsForValidItemsAndCategories(): - if "starting_items" not in DataValidation.game_table: + def checkStartingItemsForValidItemsAndCategories(self): + if "starting_items" not in self.game_table: return - starting_items = DataValidation.game_table["starting_items"] + starting_items = self.game_table["starting_items"] for starting_block in starting_items: if "items" in starting_block and "item_categories" in starting_block: @@ -314,17 +309,16 @@ def checkStartingItemsForValidItemsAndCategories(): if "items" in starting_block: for item_name in starting_block["items"]: - if not item_name in [item["name"] for item in DataValidation.item_table]: + if not item_name in [item["name"] for item in self.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: + if len([item for item in self.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)): + def checkStartingItemsForBadSyntax(self): + if not (starting_items := self.game_table.get("starting_items", False)): return for starting_block in starting_items: @@ -337,9 +331,8 @@ def checkStartingItemsForBadSyntax(): 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: + def checkPlacedItemsAndCategoriesForBadSyntax(self): + for location in self.location_table: place_item = location.get("place_item", False) place_item_category = location.get("place_item_category", False) @@ -352,9 +345,8 @@ def checkPlacedItemsAndCategoriesForBadSyntax(): 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: + def checkPlacedItemsForValidItems(self): + for location in self.location_table: if not (place_item := location.get("place_item", False)): continue @@ -363,12 +355,11 @@ def checkPlacedItemsForValidItems(): continue for item_name in place_item: - if not item_name in [item["name"] for item in DataValidation.item_table]: + if not item_name in [item["name"] for item in self.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: + def checkPlacedItemCategoriesForValidItemCategories(self): + for location in self.location_table: if not (place_item_category := location.get("place_item_category", False)): continue @@ -377,104 +368,100 @@ def checkPlacedItemCategoriesForValidItemCategories(): 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: + if len([item for item in self.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: + def checkForGameBeingInvalidJSON(self): + if len(self.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: + def checkForItemsBeingInvalidJSON(self): + if len(self.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: + def checkForLocationsBeingInvalidJSON(self): + if len(self.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 "starting" in DataValidation.region_table[region] and not DataValidation.region_table[region]["starting"]]) > 0 + def checkForNonStartingRegionsThatAreUnreachable(self): + using_starting_regions = len([region for region in self.region_table if "starting" in self.region_table[region] and not self.region_table[region]["starting"]]) > 0 if not using_starting_regions: return - nonstarting_regions = [region for region in DataValidation.region_table if "starting" in DataValidation.region_table[region] and not DataValidation.region_table[region]["starting"]] + nonstarting_regions = [region for region in self.region_table if "starting" in self.region_table[region] and not self.region_table[region]["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"]] + regions_that_connect_to = [region for region in self.region_table if "connects_to" in self.region_table[region] and nonstarter in self.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(self, world: World, multiworld: MultiWorld): + validation_errors = [] -def runPreFillDataValidation(world: World, multiworld: MultiWorld): - validation_errors = [] + # check if there is enough items with values + try: self.preFillCheckIfEnoughItemsForValue(world, multiworld) + except ValidationError as e: validation_errors.append(e) - # check if there is enough items with values - try: DataValidation.preFillCheckIfEnoughItemsForValue(world, multiworld) - except ValidationError as e: validation_errors.append(e) + if validation_errors: + newline = "\n" + raise Exception(f"\nValidationError(s) for pre_fill of player {world.player}: \n\n{newline.join([' - ' + str(validation_error) for validation_error in validation_errors])}\n\n") - if validation_errors: - newline = "\n" - raise Exception(f"\nValidationError(s) for pre_fill of player {world.player}: \n\n{newline.join([' - ' + str(validation_error) for validation_error in validation_errors])}\n\n") -# Called during stage_assert_generate -def runGenerationDataValidation() -> None: - validation_errors = [] + # Called during stage_assert_generate + def runGenerationDataValidation(self) -> None: + validation_errors = [] - # check that requires have correct item names in locations and regions - try: DataValidation.checkItemNamesInLocationRequires() - except ValidationError as e: validation_errors.append(e) + # check that requires have correct item names in locations and regions + try: self.checkItemNamesInLocationRequires() + except ValidationError as e: validation_errors.append(e) - try: DataValidation.checkItemNamesInRegionRequires() - except ValidationError as e: validation_errors.append(e) + try: self.checkItemNamesInRegionRequires() + 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 region names are correct in locations + try: self.checkRegionNamesInLocations() + 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 items that are required by locations and regions are also marked required + try: self.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 that regions that are connected to are correct + try: self.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) + # check for duplicate names in items, locations, and regions + try: self.checkForDuplicateItemNames() + except ValidationError as e: validation_errors.append(e) - try: DataValidation.checkForDuplicateLocationNames() - except ValidationError as e: validation_errors.append(e) + try: self.checkForDuplicateLocationNames() + except ValidationError as e: validation_errors.append(e) - try: DataValidation.checkForDuplicateRegionNames() - except ValidationError as e: validation_errors.append(e) + try: self.checkForDuplicateRegionNames() + 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 are actually valid starting item definitions + try: self.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 starting items and starting item categories actually exist in the items json + try: self.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 that placed items are actually valid place item definitions + try: self.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) + # check placed item and item categories for valid options for each + try: self.checkPlacedItemsForValidItems() + except ValidationError as e: validation_errors.append(e) - try: DataValidation.checkPlacedItemCategoriesForValidItemCategories() - except ValidationError as e: validation_errors.append(e) + try: self.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: - raise Exception("\nValidationError(s): \n\n%s\n\n" % ("\n".join([' - ' + str(validation_error) for validation_error in validation_errors]))) + # check for regions that are set as non-starting regions and have no connectors to them (so are unreachable) + try: self.checkForNonStartingRegionsThatAreUnreachable() + except ValidationError as e: validation_errors.append(e) + if len(validation_errors) > 0: + raise Exception("\nValidationError(s): \n\n%s\n\n" % ("\n".join([' - ' + str(validation_error) for validation_error in validation_errors]))) diff --git a/src/Game.py b/src/Game.py index dc9e9a3d..9832585c 100644 --- a/src/Game.py +++ b/src/Game.py @@ -1,16 +1,18 @@ -from .Data import game_table +from argparse import Namespace -if 'creator' in game_table: - game_table['player'] = game_table['creator'] -game_name = "Manual_%s_%s" % (game_table["game"], game_table["player"]) -filler_item_name = game_table["filler_item_name"] if "filler_item_name" in game_table else "Filler" -starting_items = game_table["starting_items"] if "starting_items" in game_table else None +def parse_gamedata(ret: Namespace): + if 'creator' in ret.game_table: + ret.game_table['player'] = ret.game_table['creator'] -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 + ret.game_name = "Manual_%s_%s" % (ret.game_table["game"], ret.game_table["player"]) + ret.filler_item_name = ret.game_table["filler_item_name"] if "filler_item_name" in ret.game_table else "Filler" + ret.starting_items = ret.game_table["starting_items"] if "starting_items" in ret.game_table else None + + if "starting_index" in ret.game_table: + try: + ret.starting_index = int(ret.game_table["starting_index"]) + except ValueError: + raise Exception("The value of data/game.json:'starting_index' should be an int") + else: + ret.starting_index = 1 diff --git a/src/Helpers.py b/src/Helpers.py index 642287ff..ad0251e6 100644 --- a/src/Helpers.py +++ b/src/Helpers.py @@ -15,11 +15,13 @@ from .Locations import ManualLocation # blatantly copied from the minecraft ap world because why not -def load_data_file(*args) -> dict: +def load_data_file(*args, source="") -> dict: + if not source: + source = __name__ fname = os.path.join("data", *args) try: - filedata = json.loads(pkgutil.get_data(__name__, fname).decode()) + filedata = json.loads(pkgutil.get_data(source, fname).decode()) except: filedata = [] diff --git a/src/Items.py b/src/Items.py index d31c8102..afafefa0 100644 --- a/src/Items.py +++ b/src/Items.py @@ -1,67 +1,69 @@ -from BaseClasses import Item -from .Data import item_table -from .Game import filler_item_name, starting_index +from argparse import Namespace +from BaseClasses import Item -###################### -# Generate item lookups -###################### -item_id_to_name: dict[int, str] = {} -item_name_to_item: dict[str, dict] = {} -item_name_groups: dict[str, str] = {} -advancement_item_names: set[str] = set() -lastItemId = -1 - -count = starting_index - -# add the filler item to the list of items for lookup -if filler_item_name: - item_table.append({ - "name": filler_item_name - }) - -# 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"]] - - count += 1 - -for item in item_table: - item_name = item["name"] - 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] = [] - item_name_groups[c].append(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] = [] - item_name_groups[group_name].append(item_name) - -item_id_to_name[None] = "__Victory__" -item_name_to_id = {name: id for id, name in item_id_to_name.items()} +def parse_itemdata(ret: Namespace): + + ###################### + # Generate item lookups + ###################### + + ret.item_id_to_name: dict[int, str] = {} + ret.item_name_to_item: dict[str, dict] = {} + ret.item_name_groups: dict[str, str] = {} + advancement_item_names: set[str] = set() + lastItemId = -1 + + count = ret.starting_index + + # add the filler item to the list of items for lookup + if ret.filler_item_name: + ret.item_table.append({ + "name": ret.filler_item_name + }) + + # add sequential generated ids to the lists + for key, val in enumerate(ret.item_table): + if "id" in ret.item_table[key]: + item_id = ret.item_table[key]["id"] + if item_id >= count: + count = item_id + else: + raise ValueError(f"{ret.item_table[key]['name']} has an invalid ID. ID must be at least {count + 1}") + + ret.item_table[key]["id"] = count + ret.item_table[key]["progression"] = val["progression"] if "progression" in val else False + if isinstance(val.get("category", []), str): + ret.item_table[key]["category"] = [val["category"]] + + count += 1 + + for item in ret.item_table: + item_name = item["name"] + ret.item_id_to_name[item["id"]] = item_name + ret.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 ret.item_name_groups: + ret.item_name_groups[c] = [] + ret.item_name_groups[c].append(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 ret.item_name_groups: + ret.item_name_groups[group_name] = [] + ret.item_name_groups[group_name].append(item_name) + + ret.item_id_to_name[None] = "__Victory__" + ret.item_name_to_id = {name: id for id, name in ret.item_id_to_name.items()} ###################### diff --git a/src/Locations.py b/src/Locations.py index d94094d8..d4f81f70 100644 --- a/src/Locations.py +++ b/src/Locations.py @@ -1,64 +1,65 @@ +from argparse import Namespace + from BaseClasses import Location -from .Data import location_table -from .Game import starting_index -###################### -# Generate location lookups -###################### +def parse_locationdata(ret: Namespace): + ###################### + # Generate location lookups + ###################### -count = starting_index -victory_names: list[str] = [] + count = ret.starting_index + ret.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"]) + # add sequential generated ids to the lists + for key, _ in enumerate(ret.location_table): + if "victory" in ret.location_table[key] and ret.location_table[key]["victory"]: + ret.victory_names.append(ret.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}") + if "id" in ret.location_table[key]: + item_id = ret.location_table[key]["id"] + if item_id >= count: + count = item_id + else: + raise ValueError(f"{ret.location_table[key]['name']} has an invalid ID. ID must be at least {count + 1}") - location_table[key]["id"] = count + ret.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 "region" not in ret.location_table[key]: + ret.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"]] + if isinstance(ret.location_table[key].get("category", []), str): + ret.location_table[key]["category"] = [ret.location_table[key]["category"]] - count += 1 + 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__") + if not ret.victory_names: + # Add the game completion location, which will have the Victory item assigned to it automatically + ret.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 [] + }) + ret.victory_names.append("__Manual Game Complete__") -location_id_to_name: dict[int, str] = {} -location_name_to_location: dict[str, dict] = {} -location_name_groups: dict[str, list[str]] = {} + ret.location_id_to_name: dict[int, str] = {} + ret.location_name_to_location: dict[str, dict] = {} + ret.location_name_groups: dict[str, list[str]] = {} -for item in location_table: - location_id_to_name[item["id"]] = item["name"] - location_name_to_location[item["name"]] = item + for item in ret.location_table: + ret.location_id_to_name[item["id"]] = item["name"] + ret.location_name_to_location[item["name"]] = item - for c in item.get("category", []): - if c not in location_name_groups: - location_name_groups[c] = [] - location_name_groups[c].append(item["name"]) + for c in item.get("category", []): + if c not in ret.location_name_groups: + ret.location_name_groups[c] = [] + ret.location_name_groups[c].append(item["name"]) -# location_id_to_name[None] = "__Manual Game Complete__" -location_name_to_id = {name: id for id, name in location_id_to_name.items()} + # ret.location_id_to_name[None] = "__Manual Game Complete__" + ret.location_name_to_id = {name: id for id, name in ret.location_id_to_name.items()} ###################### # Location classes diff --git a/src/ManualClient.py b/src/ManualClient.py index 186395c7..1b7d792e 100644 --- a/src/ManualClient.py +++ b/src/ManualClient.py @@ -136,8 +136,11 @@ async def connection_closed(self): def suggested_game(self) -> str: if self.game: return self.game - from .Game import game_name # This will at least give us the name of a manual they've installed - return Utils.persistent_load().get("client", {}).get("last_manual_game", game_name) + ret = Utils.persistent_load().get("client", {}).get("last_manual_game") + if ret: + return ret + from . import manual_worlds + return manual_worlds[0].game # This will at least give us the name of a manual they've installed def get_location_by_name(self, name) -> dict[str, Any]: location = self.location_table.get(name) diff --git a/src/Meta.py b/src/Meta.py index 94bdd1a6..d2ee7e1e 100644 --- a/src/Meta.py +++ b/src/Meta.py @@ -1,7 +1,8 @@ +from argparse import Namespace from BaseClasses import Tutorial from worlds.AutoWorld import World, WebWorld -from .Data import meta_table +# from .Data import meta_table from .Helpers import convert_to_long_string ############## @@ -20,23 +21,23 @@ class ManualWeb(WebWorld): ###################################### # 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"]) +def set_world_description(data: Namespace, base_doc: str) -> str: + if data.meta_table.get("docs", {}).get("apworld_description"): + return convert_to_long_string(data.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"] +def set_world_webworld(data: Namespace, web: WebWorld) -> WebWorld: + # from .Options import make_options_group # TODO + if data.meta_table.get("docs", {}).get("web", {}): + Web_Config = data.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() + # 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: @@ -52,20 +53,22 @@ def set_world_webworld(web: WebWorld) -> WebWorld: tutorial.get("language", "English"), tutorial.get("file_name", "setup_en.md"), tutorial.get("link", "setup/en"), - tutorial.get("authors", [meta_table.get("creator", meta_table.get("player", "Unknown"))]) + tutorial.get("authors", [data.meta_table.get("creator", data.meta_table.get("player", "Unknown"))]) )) 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()) -enable_region_diagram = bool(meta_table.get("enable_region_diagram", False)) +def parse_metadata(ret: Namespace): + ################# + # Meta Properties + ################# + ret.world_description: str = set_world_description(ret, """ + 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. + """) + ret.world_webworld: ManualWeb = set_world_webworld(ret, ManualWeb()) + + ret.enable_region_diagram = bool(ret.meta_table.get("enable_region_diagram", False)) diff --git a/src/Options.py b/src/Options.py index 836cc5a5..a55a61bf 100644 --- a/src/Options.py +++ b/src/Options.py @@ -1,249 +1,254 @@ +from argparse import Namespace + 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 .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 .Locations import victory_names +# from .Items import item_table +# from .Game import starting_items from dataclasses import make_dataclass from typing import List 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 +def build_options(data: Namespace): + 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 -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 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()] + 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 = int(input, base=0) - else: - visibility = Visibility[input.lower()] + elif isinstance(input,str): + if input.startswith('0b'): + visibility = int(input, base=0) + else: + visibility = Visibility[input.lower()] + + elif isinstance(input, int): + visibility = input + return visibility - elif isinstance(input, int): - visibility = input - return visibility + def getOriginalOptionArguments(option: Option) -> 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 -def getOriginalOptionArguments(option: Option) -> 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 = {} + 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_option_groups = {} -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's default options -###################### + manual_options = before_options_defined({}) + manual_options["start_inventory_from_pool"] = StartInventoryPool -manual_options = before_options_defined({}) -manual_options["start_inventory_from_pool"] = StartInventoryPool + if len(data.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") -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 = {'option_' + v: i for i, v in enumerate(data.victory_names)} - goal = {'option_' + v: i for i, v in enumerate(victory_names)} + manual_options['goal'] = type('goal', (Choice,), dict(goal)) + manual_options['goal'].__doc__ = "Choose your victory condition." - manual_options['goal'] = type('goal', (Choice,), dict(goal)) - manual_options['goal'].__doc__ = "Choose your victory condition." + if any(item.get('trap') for item in data.item_table): + manual_options["filler_traps"] = FillerTrapPercent -if any(item.get('trap') for item in item_table): - manual_options["filler_traps"] = FillerTrapPercent + if data.game_table.get("death_link"): + manual_options["death_link"] = DeathLink -if game_table.get("death_link"): - manual_options["death_link"] = DeathLink + ###################### + # Option.json options + ###################### + + for option_name, option in data.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: Option = manual_options[option_name] + original_doc = str(original_option.__doc__) -###################### -# Option.json options -###################### + if issubclass(original_option, Toggle): + if option.get('default', None) is not None: + option_type = DefaultOnToggle if option['default'] else Toggle -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 original_option.__base__ != option_type: #only recreate if needed + args = getOriginalOptionArguments(original_option) + manual_options[option_name] = type(option_name, (option_type,), dict(args)) + logging.debug(f"Manual: Option.json converted option '{option_display_name}' into a {option_type}") - if manual_options.get(option_name): - original_option: Option = manual_options[option_name] - original_doc = str(original_option.__doc__) + 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 issubclass(original_option, Toggle): - if option.get('default', None) is not None: - option_type = DefaultOnToggle if option['default'] else Toggle + 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 - if original_option.__base__ != option_type: #only recreate if needed + 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) - manual_options[option_name] = type(option_name, (option_type,), dict(args)) - logging.debug(f"Manual: Option.json converted option '{option_display_name}' into a {option_type}") + args['special_range_names'] = {} + if original_option.__base__ == 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()}} + + manual_options[option_name] = type(option_name, (NamedRange,), dict(args)) + logging.debug(f"Manual: Option.json converted option '{option_display_name}' into a {NamedRange}") + + if option.get('display_name'): + manual_options[option_name].display_name = option['display_name'] + + elif option_name != option_display_name: + manual_options[option_name].display_name = option_display_name + + 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") + - 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 original_option.__base__ == 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()}} - - manual_options[option_name] = type(option_name, (NamedRange,), dict(args)) - logging.debug(f"Manual: Option.json converted option '{option_display_name}' into a {NamedRange}") - - if option.get('display_name'): - manual_options[option_name].display_name = option['display_name'] - - elif option_name != option_display_name: - manual_options[option_name].display_name = option_display_name - - 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)} - - if option_type == "Toggle": - value = option.get('default', False) - option_class = DefaultOnToggle if value else Toggle - - elif 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']) - - manual_options[option_name] = type(option_name, (option_class,), args ) - 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:] + supported_option_types = ["Toggle", "Choice", "Range"] + for option_name, option in data.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 option_name not in manual_options: - manual_options[option_name] = type(option_name, (DefaultOnToggle,), {"default": True}) - 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}) - 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) - - 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) + 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)} + + if option_type == "Toggle": + value = option.get('default', False) + option_class = DefaultOnToggle if value else Toggle + + elif 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']) + + manual_options[option_name] = type(option_name, (option_class,), args ) + 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 data.category_table: + for option_name in data.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}) + manual_options[option_name].__doc__ = "Should items/locations linked to this option be enabled?" + + if data.starting_items: + for data.starting_items in data.starting_items: + if data.starting_items.get("yaml_option"): + for option_name in data.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}) + manual_options[option_name].__doc__ = "Should items/locations linked to this option be enabled?" + + ###################### + # OptionGroups Creation + ###################### + + def make_options_group() -> list[OptionGroup]: # TODO this is weird + 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) + + 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) + + return manual_options_data diff --git a/src/Regions.py b/src/Regions.py index f7cb6035..1a4d9b11 100644 --- a/src/Regions.py +++ b/src/Regions.py @@ -1,32 +1,35 @@ +from argparse import Namespace + from BaseClasses import Entrance, MultiWorld, Region from .Helpers import is_category_enabled, is_location_enabled -from .Data import region_table -from .Locations import ManualLocation, location_name_to_location +# from .Data import region_table +from .Locations import ManualLocation # , location_name_to_location from worlds.AutoWorld import World -if not region_table: - region_table = {} +def parse_regiondata(ret: Namespace): + if not ret.region_table: + ret.region_table = {} -regionMap = { **region_table } -starting_regions = [ name for name in regionMap if "starting" in regionMap[name].keys() and regionMap[name]["starting"] ] + ret.regionMap = { **ret.region_table } + starting_regions = [ name for name in ret.regionMap if "starting" in ret.regionMap[name].keys() and ret.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 + if len(starting_regions) == 0: + starting_regions = ret.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 -} + ret.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]: + for region in world.regionMap: + if "connects_to" not in world.regionMap[region]: exit_array = None else: - exit_array = regionMap[region]["connects_to"] or None + exit_array = world.regionMap[region]["connects_to"] or None # safeguard for bad value at the end if not exit_array: @@ -47,9 +50,9 @@ def create_regions(world: World, multiworld: MultiWorld, player: int): menuConn.connect(multiworld.get_region("Manual", player)) # 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"]: + for region in world.regionMap: + if "connects_to" in world.regionMap[region] and world.regionMap[region]["connects_to"]: + for linkedRegion in world.regionMap[region]["connects_to"]: connection = multiworld.get_entrance(getConnectionName(region, linkedRegion), player) connection.connect(multiworld.get_region(linkedRegion, player)) @@ -60,7 +63,7 @@ def create_region(world: World, multiworld: MultiWorld, player: int, name: str, 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'): + if world.location_name_to_location[location].get('prehint'): world.options.start_location_hints.value.add(location) ret.locations.append(locationObj) if exits: diff --git a/src/Rules.py b/src/Rules.py index 6a75bb62..9e3f431d 100644 --- a/src/Rules.py +++ b/src/Rules.py @@ -1,7 +1,7 @@ from typing import TYPE_CHECKING, Optional from enum import IntEnum from worlds.generic.Rules import set_rule, add_rule -from .Regions import regionMap +# from .Regions import regionMap from .hooks import Rules from BaseClasses import MultiWorld, CollectionState @@ -269,19 +269,19 @@ def fullLocationOrRegionCheck(state: CollectionState, area: dict): used_location_names = [] # Region access rules - for region in regionMap.keys(): + for region in world.regionMap.keys(): used_location_names.extend([l.name for l in multiworld.get_region(region, player).locations]) if region != "Menu": for exitRegion in multiworld.get_region(region, player).exits: - def fullRegionCheck(state: CollectionState, region=regionMap[region]): + def fullRegionCheck(state: CollectionState, region=world.regionMap[region]): return fullLocationOrRegionCheck(state, region) add_rule(world.get_entrance(exitRegion.name), fullRegionCheck) - entrance_rules = regionMap[region].get("entrance_requires", {}) + entrance_rules = world.regionMap[region].get("entrance_requires", {}) for e in entrance_rules: entrance = world.get_entrance(f'{e}To{region}') add_rule(entrance, lambda state, rule={"requires": entrance_rules[e]}: fullLocationOrRegionCheck(state, rule)) - exit_rules = regionMap[region].get("exit_requires", {}) + exit_rules = world.regionMap[region].get("exit_requires", {}) for e in exit_rules: exit = world.get_entrance(f'{region}To{e}') add_rule(exit, lambda state, rule={"requires": exit_rules[e]}: fullLocationOrRegionCheck(state, rule)) @@ -293,7 +293,7 @@ def fullRegionCheck(state: CollectionState, region=regionMap[region]): locFromWorld = multiworld.get_location(location["name"], player) - locationRegion = regionMap[location["region"]] if "region" in location else None + locationRegion = world.regionMap[location["region"]] if "region" in location else None if locationRegion: locationRegion['name'] = location['region'] diff --git a/src/__init__.py b/src/__init__.py index 903152da..de958d76 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -2,7 +2,7 @@ import logging import os import json -from typing import Callable, Optional +from typing import Callable, Optional, ClassVar import webbrowser import requests @@ -10,17 +10,17 @@ from worlds.generic.Rules import forbid_items_for_player from worlds.LauncherComponents import Component, SuffixIdentifier, components, Type, launch_subprocess, icon_paths -from .Data import item_table, location_table, region_table, category_table, meta_table -from .Game import game_name, filler_item_name, starting_items -from .Meta import world_description, world_webworld, enable_region_diagram -from .Locations import location_id_to_name, location_name_to_id, location_name_to_location, location_name_groups, victory_names -from .Items import item_id_to_name, item_name_to_id, item_name_to_item, item_name_groups -from .DataValidation import runGenerationDataValidation, runPreFillDataValidation +from .Data import get_data_Namespace +# from .Game import game_name, filler_item_name, starting_items +# from .Meta import world_description, world_webworld, enable_region_diagram +# from .Locations import location_id_to_name, location_name_to_id, location_name_to_location, location_name_groups, victory_names +# from .Items import item_id_to_name, item_name_to_id, item_name_to_item, item_name_groups +from .DataValidation import DataValidation from .Regions import create_regions from .Items import ManualItem from .Rules import set_rules -from .Options import manual_options_data +from .Options import build_options # manual_options_data from .Helpers import is_item_enabled, get_option_value, get_items_for_player, resolve_yaml_option from BaseClasses import ItemClassification, Tutorial, Item @@ -37,38 +37,44 @@ before_extend_hint_information, after_extend_hint_information from .hooks.Data import hook_interpret_slot_data -class ManualWorld(World): - __doc__ = world_description - game: str = game_name - web = world_webworld - options_dataclass = manual_options_data - data_version = 2 - required_client_version = (0, 3, 4) +class ManualWorld(): + # __doc__ = data.world_description + # game: str = data.game_name + # web = data.world_webworld - # 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 - category_table = category_table + # options_dataclass = manual_options_data + # data_version = 2 + # required_client_version = (0, 3, 4) - 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 + # # These properties are set from the imports of the same name above. + # item_table = data.item_table + # location_table = data.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 + # category_table = data.category_table + # data = data - filler_item_name = filler_item_name + # item_id_to_name = data.item_id_to_name + # item_name_to_id = data.item_name_to_id + # item_name_to_item = data.item_name_to_item + # item_name_groups = data.item_name_groups - item_counts = {} - start_inventory = {} + # filler_item_name = data.filler_item_name - 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 + # item_counts = {} + # start_inventory = {} - # UT (the universal-est of trackers) can now generate without a YAML - ut_can_gen_without_yaml = True + # location_id_to_name = data.location_id_to_name + # location_name_to_id = data.location_name_to_id + # location_name_to_location = data.location_name_to_location + # location_name_groups = data.location_name_groups + # victory_names = data.victory_names + + # enable_region_diagram = data.enable_region_diagram + + # # UT (the universal-est of trackers) can now generate without a YAML + # ut_can_gen_without_yaml = True + + data_validation: ClassVar[DataValidation] def get_filler_item_name(self) -> str: return hook_get_filler_item_name(self, self.multiworld, self.player) or self.filler_item_name @@ -87,7 +93,7 @@ def interpret_slot_data(self, slot_data: dict[str, any]): @classmethod def stage_assert_generate(cls, multiworld) -> None: - runGenerationDataValidation() + cls.data_validation.runGenerationDataValidation() def create_regions(self): @@ -95,10 +101,10 @@ def create_regions(self): create_regions(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 = self.multiworld.get_location(self.victory_names[get_option_value(self.multiworld, self.player, 'goal')], self.player) location_game_complete.address = None - for unused_goal in [self.multiworld.get_location(name, self.player) for name in victory_names if name != location_game_complete.name]: + for unused_goal in [self.multiworld.get_location(name, self.player) for name in self.victory_names if name != location_game_complete.name]: unused_goal.parent_region.locations.remove(unused_goal) location_game_complete.place_locked_item( @@ -116,7 +122,7 @@ def create_items(self): # victory gets placed via place_locked_item at the victory location in create_regions if name == "__Victory__": continue # the game.json filler item name is added to the item lookup, so skip it until it's potentially needed later - if name == filler_item_name: continue # intentionally using the Game.py filler_item_name here because it's a non-Items item + if name == self.filler_item_name: continue # intentionally using the Game.py filler_item_name here because it's a non-Items item # ignoring this comment but using the classvar because i don't see why they should be different item = self.item_name_to_item[name] item_count = int(item.get("count", 1)) @@ -163,8 +169,8 @@ def create_items(self): items_started = [] - if starting_items: - for starting_item_block in starting_items: + if self.data.starting_items: + for starting_item_block in self.data.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 @@ -243,23 +249,23 @@ def generate_basic(self): before_generate_basic(self, self.multiworld, self.player) # Handle item forbidding - manual_locations_with_forbid = {location['name']: location for location in location_name_to_location.values() if "dont_place_item" in location or "dont_place_item_category" in location} + manual_locations_with_forbid = {location['name']: location for location in self.location_name_to_location.values() if "dont_place_item" in location or "dont_place_item_category" in location} locations_with_forbid = [l for l in self.multiworld.get_unfilled_locations(player=self.player) if l.name in manual_locations_with_forbid.keys()] for location in locations_with_forbid: manual_location = manual_locations_with_forbid[location.name] forbidden_item_names = [] if manual_location.get("dont_place_item"): - forbidden_item_names.extend([i["name"] for i in item_name_to_item.values() if i["name"] in manual_location["dont_place_item"]]) + forbidden_item_names.extend([i["name"] for i in self.item_name_to_item.values() if i["name"] in manual_location["dont_place_item"]]) if manual_location.get("dont_place_item_category"): - forbidden_item_names.extend([i["name"] for i in item_name_to_item.values() if "category" in i and set(i["category"]).intersection(manual_location["dont_place_item_category"])]) + forbidden_item_names.extend([i["name"] for i in self.item_name_to_item.values() if "category" in i and set(i["category"]).intersection(manual_location["dont_place_item_category"])]) if forbidden_item_names: forbid_items_for_player(location, set(forbidden_item_names), self.player) # Handle specific item placements using fill_restrictive - manual_locations_with_placements = {location['name']: location for location in location_name_to_location.values() if "place_item" in location or "place_item_category" in location} + manual_locations_with_placements = {location['name']: location for location in self.location_name_to_location.values() if "place_item" in location or "place_item_category" in location} locations_with_placements = [l for l in self.multiworld.get_unfilled_locations(player=self.player) if l.name in manual_locations_with_placements.keys()] for location in locations_with_placements: manual_location = manual_locations_with_placements[location.name] @@ -275,7 +281,7 @@ def generate_basic(self): place_messages.append('", "'.join(manual_location["place_item"])) if manual_location.get("place_item_category"): - eligible_item_names += [i["name"] for i in item_name_to_item.values() if "category" in i and set(i["category"]).intersection(manual_location["place_item_category"])] + eligible_item_names += [i["name"] for i in self.item_name_to_item.values() if "category" in i and set(i["category"]).intersection(manual_location["place_item_category"])] place_messages.append('", "'.join(manual_location["place_item_category"]) + " category(ies)") # Second we check for forbidden items names @@ -284,7 +290,7 @@ def generate_basic(self): forbid_messages.append('", "'.join(manual_location["dont_place_item"]) + ' items') if manual_location.get("dont_place_item_category"): - forbidden_item_names += [i["name"] for i in item_name_to_item.values() if "category" in i and set(i["category"]).intersection(manual_location["dont_place_item_category"])] + forbidden_item_names += [i["name"] for i in self.item_name_to_item.values() if "category" in i and set(i["category"]).intersection(manual_location["dont_place_item_category"])] 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 @@ -310,13 +316,13 @@ def generate_basic(self): after_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 enable_region_diagram: + if self.enable_region_diagram: from Utils import visualize_regions visualize_regions(self.multiworld.get_region("Menu", 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) + cls.data_validation.runPreFillDataValidation(self, self.multiworld) def fill_slot_data(self): slot_data = before_fill_slot_data({}, self, self.multiworld, self.player) @@ -437,10 +443,59 @@ def client_data(self): 'items': self.item_name_to_item, 'locations': self.location_name_to_location, # todo: extract connections out of multiworld.get_regions() instead, in case hooks have modified the regions. - 'regions': region_table, - 'categories': category_table + 'regions': self.data.region_table, + 'categories': self.data.category_table } + +from worlds import user_folder + +manual_worlds = [] + +for entry in os.scandir(user_folder): + if entry.is_file() and entry.name.endswith(".manuworld"): + data = get_data_Namespace(path=entry, safe=True) # TODO write the other codepath + manual_options_data = build_options(data) + world_name = data.game_name + + manual_worlds.append(type(world_name, (ManualWorld, World, ), { + "__doc__": data.world_description, + "game": data.game_name, + "web": data.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": data.item_table, + "location_table": data.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 + "category_table": data.category_table, + "data": data, + + "item_id_to_name": data.item_id_to_name, + "item_name_to_id": data.item_name_to_id, + "item_name_to_item": data.item_name_to_item, + "item_name_groups": data.item_name_groups, + + "filler_item_name": data.filler_item_name, + + "item_counts": {}, + "start_inventory": {}, + + "location_id_to_name": data.location_id_to_name, + "location_name_to_id": data.location_name_to_id, + "location_name_to_location": data.location_name_to_location, + "location_name_groups": data.location_name_groups, + "victory_names": data.victory_names, + + "enable_region_diagram": data.enable_region_diagram, + + # UT (the universal-est of trackers) can now generate without a YAML + "ut_can_gen_without_yaml": True, + })) + + ### # Non-world client methods ### diff --git a/src/hooks/World.py b/src/hooks/World.py index eaaf7d15..78e1eaec 100644 --- a/src/hooks/World.py +++ b/src/hooks/World.py @@ -9,7 +9,7 @@ # 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 +# from ..Data import game_table, item_table, location_table, region_table # this will likely break some hooks unfortnite # 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