Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 26 additions & 7 deletions linklink/Data.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import logging
from typing import Any

from .DataValidation import DataValidation, ValidationError
from .Helpers import load_data_file as helpers_load_data_file

from .hooks.Data import \
after_load_game_file, \
after_load_item_file, after_load_location_file, \
after_load_event_file, \
after_load_region_file, after_load_category_file, \
after_load_option_file, after_load_meta_file

Expand Down Expand Up @@ -36,13 +38,14 @@ def load(self):
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
game_table: dict[str, Any] = ManualFile('game.json', dict).load() #dict
item_table: list[dict[str, Any]] = convert_to_list(ManualFile('items.json', list).load(), 'data') #list
location_table: list[dict[str, Any]] = convert_to_list(ManualFile('locations.json', list).load(), 'data') #list
event_table: list[dict[str, Any]] = convert_to_list(ManualFile('events.json', list).load(), 'data') #list
region_table: dict[str, Any] = ManualFile('regions.json', dict).load() #dict
category_table: dict[str, Any] = ManualFile('categories.json', dict).load() #dict
option_table: dict[str, Any] = ManualFile('options.json', dict).load() #dict
meta_table: dict[str, Any] = ManualFile('meta.json', dict).load() #dict

# Removal of schemas in root of tables
region_table.pop('$schema', '')
Expand All @@ -52,6 +55,7 @@ def load(self):
game_table = after_load_game_file(game_table)
item_table = after_load_item_file(item_table)
location_table = after_load_location_file(location_table)
event_table = after_load_event_file(event_table)
region_table = after_load_region_file(region_table)
category_table = after_load_category_file(category_table)
option_table = after_load_option_file(option_table)
Expand All @@ -61,8 +65,23 @@ def load(self):
DataValidation.game_table = game_table
DataValidation.item_table = item_table
DataValidation.location_table = location_table
DataValidation.event_table = event_table
DataValidation.region_table = region_table

# might as well save this for other uses in tests
DataValidation.location_name_to_location = {l.get("name", f"unknown location {key}"): l for key, l in enumerate(DataValidation.location_table)}
# since "copy_location" just changes data its handled here to simplify things
for key, event in enumerate(event_table):
if "copy_location" in event:
if event["copy_location"] not in DataValidation.location_name_to_location.keys():
raise KeyError(f"Event {event.get('name', f'unnamed event #{key}')} tried to copy a location named {event['copy_location']} but its misspelled or does not exist.")

event_table[key] = DataValidation.location_name_to_location[event["copy_location"]] | event
DataValidation.event_table[key] = event_table[key]

DataValidation.item_table_with_events = DataValidation.item_table + DataValidation.event_table
DataValidation.location_table_with_events = DataValidation.location_table + DataValidation.event_table

validation_errors = []

# check that json files are not just invalid json
Expand Down
181 changes: 132 additions & 49 deletions linklink/DataValidation.py

Large diffs are not rendered by default.

5 changes: 1 addition & 4 deletions linklink/Game.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
from .Data import game_table

if 'creator' in game_table:
game_table['player'] = game_table['creator']

game_name = "Manual_%s_%s" % (game_table["game"], game_table["player"])
game_name = "Manual_%s_%s" % (game_table["game"], game_table["creator"])
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

Expand Down
72 changes: 60 additions & 12 deletions linklink/Helpers.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
import ast
import csv
import os
import pkgutil
import json
import re

from BaseClasses import MultiWorld, Item
from BaseClasses import MultiWorld, Item, ItemClassification
from enum import IntEnum
from typing import Optional, List, TYPE_CHECKING, Union, get_args, get_origin, Any
from typing import Optional, List, Union, get_args, get_origin, Any
from types import GenericAlias
from worlds.AutoWorld import World
from .hooks.Helpers import before_is_category_enabled, before_is_item_enabled, before_is_location_enabled
from .hooks.Helpers import before_is_category_enabled, before_is_item_enabled, before_is_location_enabled, before_is_event_enabled

if TYPE_CHECKING:
from .Items import ManualItem
from .Locations import ManualLocation

# blatantly copied from the minecraft ap world because why not
def load_data_file(*args) -> dict:
Expand Down Expand Up @@ -87,7 +84,7 @@ def is_item_name_enabled(multiworld: MultiWorld, player: int, item_name: str) ->

return is_item_enabled(multiworld, player, item)

def is_item_enabled(multiworld: MultiWorld, player: int, item: "ManualItem") -> bool:
def is_item_enabled(multiworld: MultiWorld, player: int, item: dict[str, Any]) -> bool:
"""Check if an item has been disabled by a yaml option."""
hook_result = before_is_item_enabled(multiworld, player, item)
if hook_result is not None:
Expand All @@ -103,15 +100,23 @@ def is_location_name_enabled(multiworld: MultiWorld, player: int, location_name:

return is_location_enabled(multiworld, player, location)

def is_location_enabled(multiworld: MultiWorld, player: int, location: "ManualLocation") -> bool:
def is_location_enabled(multiworld: MultiWorld, player: int, location: dict[str, Any]) -> bool:
"""Check if a location has been disabled by a yaml option."""
hook_result = before_is_location_enabled(multiworld, player, location)
if hook_result is not None:
return hook_result

return _is_manualobject_enabled(multiworld, player, location)

def _is_manualobject_enabled(multiworld: MultiWorld, player: int, object: Any) -> bool:
def is_event_enabled(multiworld: MultiWorld, player: int, event: dict[str, Any]) -> bool:
"""Check if an event has been disabled by a yaml option."""
hook_result = before_is_event_enabled(multiworld, player, event)
if hook_result is not None:
return hook_result

return _is_manualobject_enabled(multiworld, player, event)

def _is_manualobject_enabled(multiworld: MultiWorld, player: int, object: dict[str, Any]) -> bool:
"""Internal method: Check if a Manual Object has any category disabled by a yaml option.
\nPlease use the proper is_'item/location'_enabled or is_'item/location'_name_enabled methods instead.
"""
Expand Down Expand Up @@ -210,10 +215,34 @@ def convert_to_long_string(input: str | list[str]) -> str:

def format_to_valid_identifier(input: str) -> str:
"""Make sure the input is a valid python identifier"""
from keyword import iskeyword
input = input.strip()
if input[:1].isdigit():
if input.isidentifier() and not iskeyword(input):
return input

if iskeyword(input):
input = "_" + input
return input.replace(" ", "_")
# if its already a valid keyword no need to check all its characters
return input

if input[:1].isdecimal():
input = "_" + input

input = "".join([c if f"_{c}".isidentifier() else "_" for c in input])

return input

def remove_specific_item(source: list[Item], item: Item) -> Item:
"""Remove and return an item from a list in a more precise way, base AP only check for name and player id before removing.
\nThis checks that the item IS the exact same in the list.
\nRaise ValueError if the item is not in the list."""
# Inspired by https://stackoverflow.com/a/58761459
for i in range(len(source)): # check all elements of the list like a normal remove does
if item is source[i]:
return source.pop(i)

# if we reach here we didn't get any item
raise ValueError(f"Item '{item.name}' could not be found in source list")

class ProgItemsCat(IntEnum):
VALUE = 1
Expand All @@ -232,6 +261,25 @@ def format_state_prog_items_key(category: str|ProgItemsCat ,key: str) -> str:

return f"MANUAL_{cat_key}_{format_to_valid_identifier(key.lower())}"

def convert_string_to_itemclassification(string: str) -> ItemClassification:
def stringCheck(string):
if string.isdigit():
true_class = ItemClassification(int(string))
elif string.startswith('0b'):
true_class = ItemClassification(int(string, base=0))
else:
true_class = ItemClassification[string]
return true_class

if "+" in string or "," in string:
true_class = ItemClassification.filler
for substring in re.split(r'[+,]', string):
true_class |= stringCheck(substring.strip())

else:
true_class = stringCheck(string)
return true_class

def convert_string_to_type(input: str, target_type: type) -> Any:
"""Take a string and attempt to convert it to {target_type}
\ntarget_type can be a single type(ex. str), an union (int|str), an Optional type (Optional[str]) or a combo of any of those (Optional[int|str])
Expand Down
8 changes: 4 additions & 4 deletions linklink/Items.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from BaseClasses import Item
from .Data import item_table
from .Game import filler_item_name, starting_index
from .Game import filler_item_name, starting_index, game_name


######################
Expand Down Expand Up @@ -34,11 +34,11 @@
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_name = item.get("name", f"Unnamed Item {item['id']}")
item_id_to_name[item["id"]] = item_name
item_name_to_item[item_name] = item

Expand Down Expand Up @@ -70,4 +70,4 @@


class ManualItem(Item):
game = "Manual"
game = game_name
43 changes: 34 additions & 9 deletions linklink/Locations.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from BaseClasses import Location
from .Data import location_table
from .Game import starting_index
from .Data import location_table, event_table
from .Game import starting_index, game_name
from typing import Any


######################
Expand Down Expand Up @@ -44,26 +45,50 @@
victory_names.append("__Manual Game Complete__")

location_id_to_name: dict[int, str] = {}
location_name_to_location: dict[str, dict] = {}
location_name_to_location: dict[str, dict[str, Any]] = {}
location_name_groups: dict[str, list[str]] = {}
event_name_to_event: dict[str, dict[str, Any]] = {}

for item in location_table:
location_id_to_name[item["id"]] = item["name"]
location_name_to_location[item["name"]] = item
for loc in location_table:
loc_name = loc.get("name", f"Unnamed Location {loc['id']}")
location_id_to_name[loc["id"]] = loc_name
location_name_to_location[loc_name] = loc

for c in item.get("category", []):
for c in loc.get("category", []):
if c not in location_name_groups:
location_name_groups[c] = []
location_name_groups[c].append(item["name"])
location_name_groups[c].append(loc_name)


# location_id_to_name[None] = "__Manual Game Complete__"
location_name_to_id = {name: id for id, name in location_id_to_name.items()}

id = 0
for key, event in enumerate(event_table):
event_name = f"{id}_{event['name']}".upper().replace(" ", "_")
while event_name in location_name_to_location:
id += 1
event_name = f"{id}_{event['name']}".upper().replace(" ", "_")
if "location_name" in event:
if event["location_name"] in location_name_to_location:
raise Exception(f"Cannot define event {event['location_name']} with the same name as a location.")
event_name_to_event[event["location_name"]] = event
else:
event_name_to_event[event_name] = event
event_name_to_event[event_name]["location_name"] = event_name
event_table[key]["location_name"] = event_name
if 'visible' not in event:
event_name_to_event[event_name]['visible'] = False
event_table[key]['visible'] = False
if 'region' not in event:
event_name_to_event[event_name]['region'] = "Manual"
event_table[key]['region'] = "Manual"
id += 1

######################
# Location classes
######################


class ManualLocation(Location):
game = "Manual"
game = game_name
2 changes: 1 addition & 1 deletion linklink/MANUAL_VERSION.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
manual_stable_20250813
manual_stable_20260319
Loading