forked from ArchipelagoMW/Archipelago
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
21 changed files
with
1,228 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import json | ||
import os | ||
import pkgutil | ||
import sys | ||
|
||
from .DataValidation import DataValidation, ValidationError | ||
|
||
# blatantly copied from the minecraft ap world because why not | ||
def load_data_file(*args) -> dict: | ||
fname = os.path.join("data", *args) | ||
|
||
try: | ||
filedata = json.loads(pkgutil.get_data(__name__, fname).decode()) | ||
except: | ||
filedata = [] | ||
|
||
return filedata | ||
|
||
game_table = load_data_file('game.json') | ||
item_table = load_data_file('items.json') | ||
#progressive_item_table = load_data_file('progressive_items.json') | ||
progressive_item_table = {} | ||
location_table = load_data_file('locations.json') | ||
region_table = load_data_file('regions.json') | ||
|
||
# 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 | ||
|
||
try: | ||
# check that requires have correct item names in locations and regions | ||
DataValidation.checkItemNamesInLocationRequires() | ||
DataValidation.checkItemNamesInRegionRequires() | ||
|
||
# check that region names are correct in locations | ||
DataValidation.checkRegionNamesInLocations() | ||
|
||
# check that items that are required by locations and regions are also marked required | ||
DataValidation.checkItemsThatShouldBeRequired() | ||
|
||
# check that regions that are connected to are correct | ||
DataValidation.checkRegionsConnectingToOtherRegions() | ||
|
||
# check that the apworld creator didn't specify multiple victory conditions | ||
DataValidation.checkForMultipleVictoryLocations() | ||
except ValidationError as e: | ||
print("\nValidationError: %s\n\n" % (e)) | ||
print("You 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.") | ||
|
||
# show the apworld creator what their game name and filename should be | ||
# commenting this out for now because it mainly just causes confusion for users | ||
# DataValidation.outputExpectedGameNames() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,205 @@ | ||
import logging | ||
import re | ||
import json | ||
|
||
class ValidationError(Exception): | ||
pass | ||
|
||
class DataValidation(): | ||
game_table = {} | ||
item_table = [] | ||
location_table = [] | ||
region_table = {} | ||
|
||
@staticmethod | ||
def outputExpectedGameNames(): | ||
logging.warning("With the information in your game.json, ") | ||
logging.warning(" - The expected game name in your YAML is: Manual_%s_%s" % (DataValidation.game_table["game"], DataValidation.game_table["player"])) | ||
logging.warning(" - The expected apworld filename and directory is: manual_%s_%s" % (DataValidation.game_table["game"].lower(), DataValidation.game_table["player"].lower())) | ||
logging.warning("") | ||
logging.warning("") | ||
|
||
@staticmethod | ||
def checkItemNamesInLocationRequires(): | ||
for location in DataValidation.location_table: | ||
if "requires" not in location: | ||
continue | ||
|
||
if isinstance(location["requires"], str): | ||
# parse user written statement into list of each item | ||
reqires_raw = re.split('(\AND|\)|\(|OR|\|)', location["requires"]) | ||
remove_spaces = [x.strip() for x in reqires_raw] | ||
remove_empty = [x for x in remove_spaces if x != ''] | ||
requires_list = [x for x in remove_empty if x != '|'] | ||
|
||
for i, item in enumerate(requires_list): | ||
if item.lower() == "or" or item.lower() == "and" or item == ")" or item == "(": | ||
continue | ||
else: | ||
item_parts = item.split(":") | ||
item_name = item | ||
|
||
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 | ||
|
||
if not item_exists: | ||
raise ValidationError("Item %s is required by location %s but is misspelled or does not exist." % (item_name, location["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["name"] for item in DataValidation.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"])) | ||
else: | ||
item_parts = item.split(":") | ||
item_name = item | ||
|
||
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 | ||
|
||
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] | ||
|
||
if "requires" not in region: | ||
continue | ||
|
||
if isinstance(region["requires"], str): | ||
# parse user written statement into list of each item | ||
reqires_raw = re.split('(\AND|\)|\(|OR|\|)', region["requires"]) | ||
remove_spaces = [x.strip() for x in reqires_raw] | ||
remove_empty = [x for x in remove_spaces if x != ''] | ||
requires_list = [x for x in remove_empty if x != '|'] | ||
|
||
for i, item in enumerate(requires_list): | ||
if item.lower() == "or" or item.lower() == "and" or item == ")" or item == "(": | ||
continue | ||
else: | ||
item_parts = item.split(":") | ||
item_name = item | ||
|
||
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 | ||
|
||
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["name"] for item in DataValidation.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)) | ||
else: | ||
item_parts = item.split(":") | ||
item_name = item | ||
|
||
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 | ||
|
||
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: | ||
if "region" not in location: | ||
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 checkItemsThatShouldBeRequired(): | ||
for item in DataValidation.item_table: | ||
# if the item is already progression, no need to check | ||
if "progression" in item and item["progression"]: | ||
continue | ||
|
||
# check location requires for the presence of item name | ||
for location in DataValidation.location_table: | ||
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 item["name"] in location_requires: | ||
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] | ||
|
||
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 item["name"] in region_requires: | ||
raise ValidationError("Item %s is required by region %s, but the item is not marked as progression." % (item["name"], key)) | ||
|
||
@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 checkForMultipleVictoryLocations(): | ||
victory_count = len([location["name"] for location in DataValidation.location_table if "victory" in location and location["victory"]]) | ||
|
||
if victory_count > 1: | ||
raise ValidationError("There are %s victory locations defined, but there should only be 1." % (str(victory_count))) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
from .Data import game_table | ||
|
||
game_name = "Manual_%s_%s" % (game_table["game"], game_table["player"]) | ||
filler_item_name = game_table["filler_item_name"] | ||
|
||
# Programmatically generate starting indexes for items and locations based upon the game name and player name to aim for non-colliding indexes | ||
# Additionally, make this use as many characters of the game name as possible to avoid accidental id pool collisions | ||
# - It's assumed that the first two characters of a game and the last character *should* be fairly unique, but we use the remaining characters anyways to move the pool | ||
# - The player name is meant to be a small differentiator, so we just apply a flat multiplier for that | ||
|
||
# 100m + 70m + 10m, which should put all Manual games comfortably in the billions | ||
starting_index = (ord(game_table["game"][:1]) * 100000000) + \ | ||
(ord(game_table["game"][1:2]) * 70000000) + \ | ||
(ord(game_table["game"][-1:]) * 10000000) | ||
|
||
if len(game_table["game"]) > 3: | ||
for index in range(2, len(game_table["game"]) - 1): | ||
multiplier = 100000 | ||
|
||
starting_index += (ord(game_table["game"][index:index+1]) * multiplier) | ||
|
||
for index in range(0, len(game_table["player"])): | ||
starting_index += (ord(game_table["player"][index:index+1]) * 1000) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
from BaseClasses import Item | ||
from .Data import item_table, progressive_item_table | ||
from .Game import filler_item_name, starting_index | ||
|
||
###################### | ||
# Generate item lookups | ||
###################### | ||
|
||
item_id_to_name = {} | ||
item_name_to_item = {} | ||
advancement_item_names = set() | ||
lastItemId = -1 | ||
|
||
count = starting_index | ||
|
||
# add the filler item to the list of items for lookup | ||
item_table.append({ | ||
"name": filler_item_name | ||
}) | ||
|
||
# add sequential generated ids to the lists | ||
for key, val in enumerate(item_table): | ||
item_table[key]["id"] = count | ||
item_table[key]["progression"] = val["progression"] if "progression" in val else False | ||
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["progression"]: | ||
advancement_item_names.add(item_name) | ||
|
||
if item["id"] != None: | ||
lastItemId = max(lastItemId, item["id"]) | ||
|
||
progressive_item_list = {} | ||
|
||
for item in progressive_item_table: | ||
progressiveName = progressive_item_table[item] | ||
if progressiveName not in progressive_item_list: | ||
progressive_item_list[progressiveName] = [] | ||
progressive_item_list[progressiveName].append(item) | ||
|
||
for progressiveItemName in progressive_item_list.keys(): | ||
lastItemId += 1 | ||
generatedItem = {} | ||
generatedItem["id"] = lastItemId | ||
generatedItem["name"] = progressiveItemName | ||
generatedItem["progression"] = item_name_to_item[progressive_item_list[progressiveItemName][0]]["progression"] | ||
item_name_to_item[progressiveItemName] = generatedItem | ||
item_id_to_name[lastItemId] = progressiveItemName | ||
|
||
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 = "Manual" |
Oops, something went wrong.