Lingo: Pre-compile datafile to improve loading time (#2829)

This commit is contained in:
Star Rauchenberger 2024-03-15 04:26:00 -04:00 committed by GitHub
parent e534abeab0
commit afe7aea536
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 626 additions and 515 deletions

View File

@ -5,12 +5,12 @@ from logging import warning
from BaseClasses import Item, ItemClassification, Tutorial
from worlds.AutoWorld import WebWorld, World
from .datatypes import Room, RoomEntrance
from .items import ALL_ITEM_TABLE, LingoItem
from .locations import ALL_LOCATION_TABLE
from .options import LingoOptions
from .player_logic import LingoPlayerLogic
from .regions import create_regions
from .static_logic import Room, RoomEntrance
class LingoWebWorld(WebWorld):
@ -101,7 +101,7 @@ class LingoWorld(World):
classification = item.classification
if hasattr(self, "options") and self.options.shuffle_paintings and len(item.painting_ids) > 0 \
and len(item.door_ids) == 0 and all(painting_id not in self.player_logic.painting_mapping
and not item.has_doors and all(painting_id not in self.player_logic.painting_mapping
for painting_id in item.painting_ids) \
and "pilgrim_painting2" not in item.painting_ids:
# If this is a "door" that just moves one or more paintings, and painting shuffle is on and those paintings

View File

@ -0,0 +1,5 @@
# lingo data
The source of truth for the Lingo randomizer is (currently) the LL1.yaml and ids.yaml files located here. These files are used by the generator, the game client, and the tracker, in order to have logic that is consistent across them all.
The generator does not actually read in the yaml files. Instead, a compiled datafile called generated.dat is also located in this directory. If you update LL1.yaml and/or ids.yaml, you must also regenerate the datafile using `python worlds/lingo/utils/pickle_static_data.py`. A unit test will fail if you don't.

Binary file not shown.

67
worlds/lingo/datatypes.py Normal file
View File

@ -0,0 +1,67 @@
from typing import List, NamedTuple, Optional
class RoomAndDoor(NamedTuple):
room: Optional[str]
door: str
class RoomAndPanel(NamedTuple):
room: Optional[str]
panel: str
class RoomEntrance(NamedTuple):
room: str # source room
door: Optional[RoomAndDoor]
painting: bool
class Room(NamedTuple):
name: str
entrances: List[RoomEntrance]
class Door(NamedTuple):
name: str
item_name: str
location_name: Optional[str]
panels: Optional[List[RoomAndPanel]]
skip_location: bool
skip_item: bool
has_doors: bool
painting_ids: List[str]
event: bool
group: Optional[str]
include_reduce: bool
junk_item: bool
class Panel(NamedTuple):
required_rooms: List[str]
required_doors: List[RoomAndDoor]
required_panels: List[RoomAndPanel]
colors: List[str]
check: bool
event: bool
exclude_reduce: bool
achievement: bool
non_counting: bool
class Painting(NamedTuple):
id: str
room: str
enter_only: bool
exit_only: bool
required: bool
required_when_no_doors: bool
required_door: Optional[RoomAndDoor]
disable: bool
req_blocked: bool
req_blocked_when_no_doors: bool
class Progression(NamedTuple):
item_name: str
index: int

View File

@ -16,7 +16,7 @@ class ItemData(NamedTuple):
code: int
classification: ItemClassification
mode: Optional[str]
door_ids: List[str]
has_doors: bool
painting_ids: List[str]
def should_include(self, world: "LingoWorld") -> bool:
@ -61,7 +61,7 @@ def load_item_data():
door_mode = "doors"
else:
door_mode = "complex door"
door_groups.setdefault(door.group, []).extend(door.door_ids)
door_groups.setdefault(door.group, [])
if room_name in PROGRESSION_BY_ROOM and door_name in PROGRESSION_BY_ROOM[room_name]:
door_mode = "special"
@ -69,11 +69,11 @@ def load_item_data():
ALL_ITEM_TABLE[door.item_name] = \
ItemData(get_door_item_id(room_name, door_name),
ItemClassification.filler if door.junk_item else ItemClassification.progression, door_mode,
door.door_ids, door.painting_ids)
door.has_doors, door.painting_ids)
for group, group_door_ids in door_groups.items():
ALL_ITEM_TABLE[group] = ItemData(get_door_group_item_id(group),
ItemClassification.progression, "door group", group_door_ids, [])
ItemClassification.progression, "door group", True, [])
special_items: Dict[str, ItemClassification] = {
":)": ItemClassification.filler,
@ -88,11 +88,11 @@ def load_item_data():
for item_name, classification in special_items.items():
ALL_ITEM_TABLE[item_name] = ItemData(get_special_item_id(item_name), classification,
"special", [], [])
"special", False, [])
for item_name in PROGRESSIVE_ITEMS:
ALL_ITEM_TABLE[item_name] = ItemData(get_progressive_item_id(item_name),
ItemClassification.progression, "special", [], [])
ItemClassification.progression, "special", False, [])
# Initialize the item data at module scope.

View File

@ -2,7 +2,8 @@ from enum import Flag, auto
from typing import Dict, List, NamedTuple
from BaseClasses import Location
from .static_logic import DOORS_BY_ROOM, PANELS_BY_ROOM, RoomAndPanel, get_door_location_id, get_panel_location_id
from .datatypes import RoomAndPanel
from .static_logic import DOORS_BY_ROOM, PANELS_BY_ROOM, get_door_location_id, get_panel_location_id
class LocationClassification(Flag):
@ -20,14 +21,6 @@ class LocationData(NamedTuple):
panels: List[RoomAndPanel]
classification: LocationClassification
def panel_ids(self):
ids = set()
for panel in self.panels:
effective_room = self.room if panel.room is None else panel.room
panel_data = PANELS_BY_ROOM[effective_room][panel.panel]
ids = ids | set(panel_data.internal_ids)
return ids
class LingoLocation(Location):
"""

View File

@ -1,12 +1,12 @@
from enum import Enum
from typing import Dict, List, NamedTuple, Optional, Set, Tuple, TYPE_CHECKING
from .datatypes import Door, RoomAndDoor, RoomAndPanel
from .items import ALL_ITEM_TABLE
from .locations import ALL_LOCATION_TABLE, LocationClassification
from .options import LocationChecks, ShuffleDoors, VictoryCondition
from .static_logic import DOORS_BY_ROOM, Door, PAINTINGS, PAINTINGS_BY_ROOM, PAINTING_ENTRANCES, PAINTING_EXITS, \
PANELS_BY_ROOM, PROGRESSION_BY_ROOM, REQUIRED_PAINTING_ROOMS, REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS, RoomAndDoor, \
RoomAndPanel
from .static_logic import DOORS_BY_ROOM, PAINTINGS, PAINTING_ENTRANCES, PAINTING_EXITS, \
PANELS_BY_ROOM, PROGRESSION_BY_ROOM, REQUIRED_PAINTING_ROOMS, REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS
if TYPE_CHECKING:
from . import LingoWorld
@ -279,8 +279,9 @@ class LingoPlayerLogic:
# When painting shuffle is off, most Starting Room paintings give color hallways access. The Wondrous's
# painting does not, but it gives access to SHRINK and WELCOME BACK.
for painting_obj in PAINTINGS_BY_ROOM["Starting Room"]:
if not painting_obj.enter_only or painting_obj.required_door is None:
for painting_obj in PAINTINGS.values():
if not painting_obj.enter_only or painting_obj.required_door is None\
or painting_obj.room != "Starting Room":
continue
# If painting shuffle is on, we only want to consider paintings that actually go somewhere.

View File

@ -1,11 +1,12 @@
from typing import Dict, Optional, TYPE_CHECKING
from BaseClasses import Entrance, ItemClassification, Region
from .datatypes import Room, RoomAndDoor
from .items import LingoItem
from .locations import LingoLocation
from .player_logic import LingoPlayerLogic
from .rules import lingo_can_use_entrance, make_location_lambda
from .static_logic import ALL_ROOMS, PAINTINGS, Room, RoomAndDoor
from .static_logic import ALL_ROOMS, PAINTINGS
if TYPE_CHECKING:
from . import LingoWorld

View File

@ -1,8 +1,9 @@
from typing import TYPE_CHECKING
from BaseClasses import CollectionState
from .datatypes import RoomAndDoor
from .player_logic import AccessRequirements, LingoPlayerLogic, PlayerLocation
from .static_logic import PROGRESSION_BY_ROOM, PROGRESSIVE_ITEMS, RoomAndDoor
from .static_logic import PROGRESSION_BY_ROOM, PROGRESSIVE_ITEMS
if TYPE_CHECKING:
from . import LingoWorld

View File

@ -1,86 +1,16 @@
from typing import Dict, List, NamedTuple, Optional, Set
import os
import pkgutil
from io import BytesIO
from typing import Dict, List, Set
import Utils
import pickle
class RoomAndDoor(NamedTuple):
room: Optional[str]
door: str
class RoomAndPanel(NamedTuple):
room: Optional[str]
panel: str
class RoomEntrance(NamedTuple):
room: str # source room
door: Optional[RoomAndDoor]
painting: bool
class Room(NamedTuple):
name: str
entrances: List[RoomEntrance]
class Door(NamedTuple):
name: str
item_name: str
location_name: Optional[str]
panels: Optional[List[RoomAndPanel]]
skip_location: bool
skip_item: bool
door_ids: List[str]
painting_ids: List[str]
event: bool
group: Optional[str]
include_reduce: bool
junk_item: bool
class Panel(NamedTuple):
required_rooms: List[str]
required_doors: List[RoomAndDoor]
required_panels: List[RoomAndPanel]
colors: List[str]
check: bool
event: bool
internal_ids: List[str]
exclude_reduce: bool
achievement: bool
non_counting: bool
class Painting(NamedTuple):
id: str
room: str
enter_only: bool
exit_only: bool
orientation: str
required: bool
required_when_no_doors: bool
required_door: Optional[RoomAndDoor]
disable: bool
move: bool
req_blocked: bool
req_blocked_when_no_doors: bool
class Progression(NamedTuple):
item_name: str
index: int
ROOMS: Dict[str, Room] = {}
PANELS: Dict[str, Panel] = {}
DOORS: Dict[str, Door] = {}
PAINTINGS: Dict[str, Painting] = {}
from .datatypes import Door, Painting, Panel, Progression, Room
ALL_ROOMS: List[Room] = []
DOORS_BY_ROOM: Dict[str, Dict[str, Door]] = {}
PANELS_BY_ROOM: Dict[str, Dict[str, Panel]] = {}
PAINTINGS_BY_ROOM: Dict[str, List[Painting]] = {}
PAINTINGS: Dict[str, Painting] = {}
PROGRESSIVE_ITEMS: List[str] = []
PROGRESSION_BY_ROOM: Dict[str, Dict[str, Progression]] = {}
@ -98,61 +28,7 @@ DOOR_ITEM_IDS: Dict[str, Dict[str, int]] = {}
DOOR_GROUP_ITEM_IDS: Dict[str, int] = {}
PROGRESSIVE_ITEM_IDS: Dict[str, int] = {}
def load_static_data():
global PAINTING_EXITS, SPECIAL_ITEM_IDS, PANEL_LOCATION_IDS, DOOR_LOCATION_IDS, DOOR_ITEM_IDS, \
DOOR_GROUP_ITEM_IDS, PROGRESSIVE_ITEM_IDS
try:
from importlib.resources import files
except ImportError:
from importlib_resources import files
from . import data
# Load in all item and location IDs. These are broken up into groups based on the type of item/location.
with files(data).joinpath("ids.yaml").open() as file:
config = Utils.parse_yaml(file)
if "special_items" in config:
for item_name, item_id in config["special_items"].items():
SPECIAL_ITEM_IDS[item_name] = item_id
if "panels" in config:
for room_name in config["panels"].keys():
PANEL_LOCATION_IDS[room_name] = {}
for panel_name, location_id in config["panels"][room_name].items():
PANEL_LOCATION_IDS[room_name][panel_name] = location_id
if "doors" in config:
for room_name in config["doors"].keys():
DOOR_LOCATION_IDS[room_name] = {}
DOOR_ITEM_IDS[room_name] = {}
for door_name, door_data in config["doors"][room_name].items():
if "location" in door_data:
DOOR_LOCATION_IDS[room_name][door_name] = door_data["location"]
if "item" in door_data:
DOOR_ITEM_IDS[room_name][door_name] = door_data["item"]
if "door_groups" in config:
for item_name, item_id in config["door_groups"].items():
DOOR_GROUP_ITEM_IDS[item_name] = item_id
if "progression" in config:
for item_name, item_id in config["progression"].items():
PROGRESSIVE_ITEM_IDS[item_name] = item_id
# Process the main world file.
with files(data).joinpath("LL1.yaml").open() as file:
config = Utils.parse_yaml(file)
for room_name, room_data in config.items():
process_room(room_name, room_data)
PAINTING_EXITS = len(PAINTING_EXIT_ROOMS)
HASHES: Dict[str, str] = {}
def get_special_item_id(name: str):
@ -197,363 +73,39 @@ def get_progressive_item_id(name: str):
return PROGRESSIVE_ITEM_IDS[name]
def process_entrance(source_room, doors, room_obj):
global PAINTING_ENTRANCES, PAINTING_EXIT_ROOMS
# If the value of an entrance is just True, that means that the entrance is always accessible.
if doors is True:
room_obj.entrances.append(RoomEntrance(source_room, None, False))
elif isinstance(doors, dict):
# If the value of an entrance is a dictionary, that means the entrance requires a door to be accessible, is a
# painting-based entrance, or both.
if "painting" in doors and "door" not in doors:
PAINTING_EXIT_ROOMS.add(room_obj.name)
PAINTING_ENTRANCES += 1
room_obj.entrances.append(RoomEntrance(source_room, None, True))
else:
if "painting" in doors and doors["painting"]:
PAINTING_EXIT_ROOMS.add(room_obj.name)
PAINTING_ENTRANCES += 1
room_obj.entrances.append(RoomEntrance(source_room, RoomAndDoor(
doors["room"] if "room" in doors else None,
doors["door"]
), doors["painting"] if "painting" in doors else False))
else:
# If the value of an entrance is a list, then there are multiple possible doors that can give access to the
# entrance.
for door in doors:
if "painting" in door and door["painting"]:
PAINTING_EXIT_ROOMS.add(room_obj.name)
PAINTING_ENTRANCES += 1
room_obj.entrances.append(RoomEntrance(source_room, RoomAndDoor(
door["room"] if "room" in door else None,
door["door"]
), door["painting"] if "painting" in door else False))
def process_panel(room_name, panel_name, panel_data):
global PANELS, PANELS_BY_ROOM
full_name = f"{room_name} - {panel_name}"
# required_room can either be a single room or a list of rooms.
if "required_room" in panel_data:
if isinstance(panel_data["required_room"], list):
required_rooms = panel_data["required_room"]
else:
required_rooms = [panel_data["required_room"]]
else:
required_rooms = []
# required_door can either be a single door or a list of doors. For convenience, the room key for each door does not
# need to be specified if the door is in this room.
required_doors = list()
if "required_door" in panel_data:
if isinstance(panel_data["required_door"], dict):
door = panel_data["required_door"]
required_doors.append(RoomAndDoor(
door["room"] if "room" in door else None,
door["door"]
))
else:
for door in panel_data["required_door"]:
required_doors.append(RoomAndDoor(
door["room"] if "room" in door else None,
door["door"]
))
# required_panel can either be a single panel or a list of panels. For convenience, the room key for each panel does
# not need to be specified if the panel is in this room.
required_panels = list()
if "required_panel" in panel_data:
if isinstance(panel_data["required_panel"], dict):
other_panel = panel_data["required_panel"]
required_panels.append(RoomAndPanel(
other_panel["room"] if "room" in other_panel else None,
other_panel["panel"]
))
else:
for other_panel in panel_data["required_panel"]:
required_panels.append(RoomAndPanel(
other_panel["room"] if "room" in other_panel else None,
other_panel["panel"]
))
# colors can either be a single color or a list of colors.
if "colors" in panel_data:
if isinstance(panel_data["colors"], list):
colors = panel_data["colors"]
else:
colors = [panel_data["colors"]]
else:
colors = []
if "check" in panel_data:
check = panel_data["check"]
else:
check = False
if "event" in panel_data:
event = panel_data["event"]
else:
event = False
if "achievement" in panel_data:
achievement = True
else:
achievement = False
if "exclude_reduce" in panel_data:
exclude_reduce = panel_data["exclude_reduce"]
else:
exclude_reduce = False
if "non_counting" in panel_data:
non_counting = panel_data["non_counting"]
else:
non_counting = False
if "id" in panel_data:
if isinstance(panel_data["id"], list):
internal_ids = panel_data["id"]
else:
internal_ids = [panel_data["id"]]
else:
internal_ids = []
panel_obj = Panel(required_rooms, required_doors, required_panels, colors, check, event, internal_ids,
exclude_reduce, achievement, non_counting)
PANELS[full_name] = panel_obj
PANELS_BY_ROOM[room_name][panel_name] = panel_obj
def process_door(room_name, door_name, door_data):
global DOORS, DOORS_BY_ROOM
# The item name associated with a door can be explicitly specified in the configuration. If it is not, it is
# generated from the room and door name.
if "item_name" in door_data:
item_name = door_data["item_name"]
else:
item_name = f"{room_name} - {door_name}"
if "skip_location" in door_data:
skip_location = door_data["skip_location"]
else:
skip_location = False
if "skip_item" in door_data:
skip_item = door_data["skip_item"]
else:
skip_item = False
if "event" in door_data:
event = door_data["event"]
else:
event = False
if "include_reduce" in door_data:
include_reduce = door_data["include_reduce"]
else:
include_reduce = False
if "junk_item" in door_data:
junk_item = door_data["junk_item"]
else:
junk_item = False
if "group" in door_data:
group = door_data["group"]
else:
group = None
# panels is a list of panels. Each panel can either be a simple string (the name of a panel in the current room) or
# a dictionary specifying a panel in a different room.
if "panels" in door_data:
panels = list()
for panel in door_data["panels"]:
if isinstance(panel, dict):
panels.append(RoomAndPanel(panel["room"], panel["panel"]))
else:
panels.append(RoomAndPanel(None, panel))
else:
skip_location = True
panels = None
# The location name associated with a door can be explicitly specified in the configuration. If it is not, then the
# name is generated using a combination of all of the panels that would ordinarily open the door. This can get quite
# messy if there are a lot of panels, especially if panels from multiple rooms are involved, so in these cases it
# would be better to specify a name.
if "location_name" in door_data:
location_name = door_data["location_name"]
elif skip_location is False:
panel_per_room = dict()
for panel in panels:
panel_room_name = room_name if panel.room is None else panel.room
panel_per_room.setdefault(panel_room_name, []).append(panel.panel)
room_strs = list()
for door_room_str, door_panels_str in panel_per_room.items():
room_strs.append(door_room_str + " - " + ", ".join(door_panels_str))
location_name = " and ".join(room_strs)
else:
location_name = None
# The id field can be a single item, or a list of door IDs, in the event that the item for this logical door should
# open more than one actual in-game door.
if "id" in door_data:
if isinstance(door_data["id"], list):
door_ids = door_data["id"]
else:
door_ids = [door_data["id"]]
else:
door_ids = []
# The painting_id field can be a single item, or a list of painting IDs, in the event that the item for this logical
# door should move more than one actual in-game painting.
if "painting_id" in door_data:
if isinstance(door_data["painting_id"], list):
painting_ids = door_data["painting_id"]
else:
painting_ids = [door_data["painting_id"]]
else:
painting_ids = []
door_obj = Door(door_name, item_name, location_name, panels, skip_location, skip_item, door_ids,
painting_ids, event, group, include_reduce, junk_item)
DOORS[door_obj.item_name] = door_obj
DOORS_BY_ROOM[room_name][door_name] = door_obj
def process_painting(room_name, painting_data):
global PAINTINGS, PAINTINGS_BY_ROOM, REQUIRED_PAINTING_ROOMS, REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS
# Read in information about this painting and store it in an object.
painting_id = painting_data["id"]
if "orientation" in painting_data:
orientation = painting_data["orientation"]
else:
orientation = ""
if "disable" in painting_data:
disable_painting = painting_data["disable"]
else:
disable_painting = False
if "required" in painting_data:
required_painting = painting_data["required"]
if required_painting:
REQUIRED_PAINTING_ROOMS.append(room_name)
else:
required_painting = False
if "move" in painting_data:
move_painting = painting_data["move"]
else:
move_painting = False
if "required_when_no_doors" in painting_data:
rwnd = painting_data["required_when_no_doors"]
if rwnd:
REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS.append(room_name)
else:
rwnd = False
if "exit_only" in painting_data:
exit_only = painting_data["exit_only"]
else:
exit_only = False
if "enter_only" in painting_data:
enter_only = painting_data["enter_only"]
else:
enter_only = False
if "req_blocked" in painting_data:
req_blocked = painting_data["req_blocked"]
else:
req_blocked = False
if "req_blocked_when_no_doors" in painting_data:
req_blocked_when_no_doors = painting_data["req_blocked_when_no_doors"]
else:
req_blocked_when_no_doors = False
required_door = None
if "required_door" in painting_data:
door = painting_data["required_door"]
required_door = RoomAndDoor(
door["room"] if "room" in door else room_name,
door["door"]
)
painting_obj = Painting(painting_id, room_name, enter_only, exit_only, orientation,
required_painting, rwnd, required_door, disable_painting, move_painting, req_blocked,
req_blocked_when_no_doors)
PAINTINGS[painting_id] = painting_obj
PAINTINGS_BY_ROOM[room_name].append(painting_obj)
def process_progression(room_name, progression_name, progression_doors):
global PROGRESSIVE_ITEMS, PROGRESSION_BY_ROOM
# Progressive items are configured as a list of doors.
PROGRESSIVE_ITEMS.append(progression_name)
progression_index = 1
for door in progression_doors:
if isinstance(door, Dict):
door_room = door["room"]
door_door = door["door"]
else:
door_room = room_name
door_door = door
room_progressions = PROGRESSION_BY_ROOM.setdefault(door_room, {})
room_progressions[door_door] = Progression(progression_name, progression_index)
progression_index += 1
def process_room(room_name, room_data):
global ROOMS, ALL_ROOMS
room_obj = Room(room_name, [])
if "entrances" in room_data:
for source_room, doors in room_data["entrances"].items():
process_entrance(source_room, doors, room_obj)
if "panels" in room_data:
PANELS_BY_ROOM[room_name] = dict()
for panel_name, panel_data in room_data["panels"].items():
process_panel(room_name, panel_name, panel_data)
if "doors" in room_data:
DOORS_BY_ROOM[room_name] = dict()
for door_name, door_data in room_data["doors"].items():
process_door(room_name, door_name, door_data)
if "paintings" in room_data:
PAINTINGS_BY_ROOM[room_name] = []
for painting_data in room_data["paintings"]:
process_painting(room_name, painting_data)
if "progression" in room_data:
for progression_name, progression_doors in room_data["progression"].items():
process_progression(room_name, progression_name, progression_doors)
ROOMS[room_name] = room_obj
ALL_ROOMS.append(room_obj)
def load_static_data_from_file():
global PAINTING_ENTRANCES, PAINTING_EXITS
class RenameUnpickler(pickle.Unpickler):
def find_class(self, module, name):
renamed_module = module
if module == "datatypes":
renamed_module = "worlds.lingo.datatypes"
return super(RenameUnpickler, self).find_class(renamed_module, name)
file = pkgutil.get_data(__name__, os.path.join("data", "generated.dat"))
pickdata = RenameUnpickler(BytesIO(file)).load()
HASHES.update(pickdata["HASHES"])
PAINTINGS.update(pickdata["PAINTINGS"])
ALL_ROOMS.extend(pickdata["ALL_ROOMS"])
DOORS_BY_ROOM.update(pickdata["DOORS_BY_ROOM"])
PANELS_BY_ROOM.update(pickdata["PANELS_BY_ROOM"])
PROGRESSIVE_ITEMS.extend(pickdata["PROGRESSIVE_ITEMS"])
PROGRESSION_BY_ROOM.update(pickdata["PROGRESSION_BY_ROOM"])
PAINTING_ENTRANCES = pickdata["PAINTING_ENTRANCES"]
PAINTING_EXIT_ROOMS.update(pickdata["PAINTING_EXIT_ROOMS"])
PAINTING_EXITS = pickdata["PAINTING_EXITS"]
REQUIRED_PAINTING_ROOMS.extend(pickdata["REQUIRED_PAINTING_ROOMS"])
REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS.extend(pickdata["REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS"])
SPECIAL_ITEM_IDS.update(pickdata["SPECIAL_ITEM_IDS"])
PANEL_LOCATION_IDS.update(pickdata["PANEL_LOCATION_IDS"])
DOOR_LOCATION_IDS.update(pickdata["DOOR_LOCATION_IDS"])
DOOR_ITEM_IDS.update(pickdata["DOOR_ITEM_IDS"])
DOOR_GROUP_ITEM_IDS.update(pickdata["DOOR_GROUP_ITEM_IDS"])
PROGRESSIVE_ITEM_IDS.update(pickdata["PROGRESSIVE_ITEM_IDS"])
# Initialize the static data at module scope.
load_static_data()
load_static_data_from_file()

View File

@ -0,0 +1,16 @@
import os
import unittest
from worlds.lingo.static_logic import HASHES
from worlds.lingo.utils.pickle_static_data import hash_file
class TestDatafile(unittest.TestCase):
def test_check_hashes(self) -> None:
ll1_file_hash = hash_file(os.path.join(os.path.dirname(__file__), "..", "data", "LL1.yaml"))
ids_file_hash = hash_file(os.path.join(os.path.dirname(__file__), "..", "data", "ids.yaml"))
self.assertEqual(ll1_file_hash, HASHES["LL1.yaml"],
"LL1.yaml hash does not match generated.dat. Please regenerate using 'python worlds/lingo/utils/pickle_static_data.py'")
self.assertEqual(ids_file_hash, HASHES["ids.yaml"],
"ids.yaml hash does not match generated.dat. Please regenerate using 'python worlds/lingo/utils/pickle_static_data.py'")

View File

View File

@ -0,0 +1,475 @@
from typing import Dict, List, Set
import os
import sys
sys.path.append(os.path.join("worlds", "lingo"))
sys.path.append(".")
sys.path.append("..")
from datatypes import Door, Painting, Panel, Progression, Room, RoomAndDoor, RoomAndPanel, RoomEntrance
import hashlib
import pickle
import sys
import Utils
ALL_ROOMS: List[Room] = []
DOORS_BY_ROOM: Dict[str, Dict[str, Door]] = {}
PANELS_BY_ROOM: Dict[str, Dict[str, Panel]] = {}
PAINTINGS: Dict[str, Painting] = {}
PROGRESSIVE_ITEMS: List[str] = []
PROGRESSION_BY_ROOM: Dict[str, Dict[str, Progression]] = {}
PAINTING_ENTRANCES: int = 0
PAINTING_EXIT_ROOMS: Set[str] = set()
PAINTING_EXITS: int = 0
REQUIRED_PAINTING_ROOMS: List[str] = []
REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS: List[str] = []
SPECIAL_ITEM_IDS: Dict[str, int] = {}
PANEL_LOCATION_IDS: Dict[str, Dict[str, int]] = {}
DOOR_LOCATION_IDS: Dict[str, Dict[str, int]] = {}
DOOR_ITEM_IDS: Dict[str, Dict[str, int]] = {}
DOOR_GROUP_ITEM_IDS: Dict[str, int] = {}
PROGRESSIVE_ITEM_IDS: Dict[str, int] = {}
def hash_file(path):
md5 = hashlib.md5()
with open(path, 'rb') as f:
content = f.read()
content = content.replace(b'\r\n', b'\n')
md5.update(content)
return md5.hexdigest()
def load_static_data(ll1_path, ids_path):
global PAINTING_EXITS, SPECIAL_ITEM_IDS, PANEL_LOCATION_IDS, DOOR_LOCATION_IDS, DOOR_ITEM_IDS, \
DOOR_GROUP_ITEM_IDS, PROGRESSIVE_ITEM_IDS
# Load in all item and location IDs. These are broken up into groups based on the type of item/location.
with open(ids_path, "r") as file:
config = Utils.parse_yaml(file)
if "special_items" in config:
for item_name, item_id in config["special_items"].items():
SPECIAL_ITEM_IDS[item_name] = item_id
if "panels" in config:
for room_name in config["panels"].keys():
PANEL_LOCATION_IDS[room_name] = {}
for panel_name, location_id in config["panels"][room_name].items():
PANEL_LOCATION_IDS[room_name][panel_name] = location_id
if "doors" in config:
for room_name in config["doors"].keys():
DOOR_LOCATION_IDS[room_name] = {}
DOOR_ITEM_IDS[room_name] = {}
for door_name, door_data in config["doors"][room_name].items():
if "location" in door_data:
DOOR_LOCATION_IDS[room_name][door_name] = door_data["location"]
if "item" in door_data:
DOOR_ITEM_IDS[room_name][door_name] = door_data["item"]
if "door_groups" in config:
for item_name, item_id in config["door_groups"].items():
DOOR_GROUP_ITEM_IDS[item_name] = item_id
if "progression" in config:
for item_name, item_id in config["progression"].items():
PROGRESSIVE_ITEM_IDS[item_name] = item_id
# Process the main world file.
with open(ll1_path, "r") as file:
config = Utils.parse_yaml(file)
for room_name, room_data in config.items():
process_room(room_name, room_data)
PAINTING_EXITS = len(PAINTING_EXIT_ROOMS)
def process_entrance(source_room, doors, room_obj):
global PAINTING_ENTRANCES, PAINTING_EXIT_ROOMS
# If the value of an entrance is just True, that means that the entrance is always accessible.
if doors is True:
room_obj.entrances.append(RoomEntrance(source_room, None, False))
elif isinstance(doors, dict):
# If the value of an entrance is a dictionary, that means the entrance requires a door to be accessible, is a
# painting-based entrance, or both.
if "painting" in doors and "door" not in doors:
PAINTING_EXIT_ROOMS.add(room_obj.name)
PAINTING_ENTRANCES += 1
room_obj.entrances.append(RoomEntrance(source_room, None, True))
else:
if "painting" in doors and doors["painting"]:
PAINTING_EXIT_ROOMS.add(room_obj.name)
PAINTING_ENTRANCES += 1
room_obj.entrances.append(RoomEntrance(source_room, RoomAndDoor(
doors["room"] if "room" in doors else None,
doors["door"]
), doors["painting"] if "painting" in doors else False))
else:
# If the value of an entrance is a list, then there are multiple possible doors that can give access to the
# entrance.
for door in doors:
if "painting" in door and door["painting"]:
PAINTING_EXIT_ROOMS.add(room_obj.name)
PAINTING_ENTRANCES += 1
room_obj.entrances.append(RoomEntrance(source_room, RoomAndDoor(
door["room"] if "room" in door else None,
door["door"]
), door["painting"] if "painting" in door else False))
def process_panel(room_name, panel_name, panel_data):
global PANELS_BY_ROOM
full_name = f"{room_name} - {panel_name}"
# required_room can either be a single room or a list of rooms.
if "required_room" in panel_data:
if isinstance(panel_data["required_room"], list):
required_rooms = panel_data["required_room"]
else:
required_rooms = [panel_data["required_room"]]
else:
required_rooms = []
# required_door can either be a single door or a list of doors. For convenience, the room key for each door does not
# need to be specified if the door is in this room.
required_doors = list()
if "required_door" in panel_data:
if isinstance(panel_data["required_door"], dict):
door = panel_data["required_door"]
required_doors.append(RoomAndDoor(
door["room"] if "room" in door else None,
door["door"]
))
else:
for door in panel_data["required_door"]:
required_doors.append(RoomAndDoor(
door["room"] if "room" in door else None,
door["door"]
))
# required_panel can either be a single panel or a list of panels. For convenience, the room key for each panel does
# not need to be specified if the panel is in this room.
required_panels = list()
if "required_panel" in panel_data:
if isinstance(panel_data["required_panel"], dict):
other_panel = panel_data["required_panel"]
required_panels.append(RoomAndPanel(
other_panel["room"] if "room" in other_panel else None,
other_panel["panel"]
))
else:
for other_panel in panel_data["required_panel"]:
required_panels.append(RoomAndPanel(
other_panel["room"] if "room" in other_panel else None,
other_panel["panel"]
))
# colors can either be a single color or a list of colors.
if "colors" in panel_data:
if isinstance(panel_data["colors"], list):
colors = panel_data["colors"]
else:
colors = [panel_data["colors"]]
else:
colors = []
if "check" in panel_data:
check = panel_data["check"]
else:
check = False
if "event" in panel_data:
event = panel_data["event"]
else:
event = False
if "achievement" in panel_data:
achievement = True
else:
achievement = False
if "exclude_reduce" in panel_data:
exclude_reduce = panel_data["exclude_reduce"]
else:
exclude_reduce = False
if "non_counting" in panel_data:
non_counting = panel_data["non_counting"]
else:
non_counting = False
panel_obj = Panel(required_rooms, required_doors, required_panels, colors, check, event, exclude_reduce,
achievement, non_counting)
PANELS_BY_ROOM[room_name][panel_name] = panel_obj
def process_door(room_name, door_name, door_data):
global DOORS_BY_ROOM
# The item name associated with a door can be explicitly specified in the configuration. If it is not, it is
# generated from the room and door name.
if "item_name" in door_data:
item_name = door_data["item_name"]
else:
item_name = f"{room_name} - {door_name}"
if "skip_location" in door_data:
skip_location = door_data["skip_location"]
else:
skip_location = False
if "skip_item" in door_data:
skip_item = door_data["skip_item"]
else:
skip_item = False
if "event" in door_data:
event = door_data["event"]
else:
event = False
if "include_reduce" in door_data:
include_reduce = door_data["include_reduce"]
else:
include_reduce = False
if "junk_item" in door_data:
junk_item = door_data["junk_item"]
else:
junk_item = False
if "group" in door_data:
group = door_data["group"]
else:
group = None
# panels is a list of panels. Each panel can either be a simple string (the name of a panel in the current room) or
# a dictionary specifying a panel in a different room.
if "panels" in door_data:
panels = list()
for panel in door_data["panels"]:
if isinstance(panel, dict):
panels.append(RoomAndPanel(panel["room"], panel["panel"]))
else:
panels.append(RoomAndPanel(None, panel))
else:
skip_location = True
panels = None
# The location name associated with a door can be explicitly specified in the configuration. If it is not, then the
# name is generated using a combination of all of the panels that would ordinarily open the door. This can get quite
# messy if there are a lot of panels, especially if panels from multiple rooms are involved, so in these cases it
# would be better to specify a name.
if "location_name" in door_data:
location_name = door_data["location_name"]
elif skip_location is False:
panel_per_room = dict()
for panel in panels:
panel_room_name = room_name if panel.room is None else panel.room
panel_per_room.setdefault(panel_room_name, []).append(panel.panel)
room_strs = list()
for door_room_str, door_panels_str in panel_per_room.items():
room_strs.append(door_room_str + " - " + ", ".join(door_panels_str))
location_name = " and ".join(room_strs)
else:
location_name = None
# The id field can be a single item, or a list of door IDs, in the event that the item for this logical door should
# open more than one actual in-game door.
has_doors = "id" in door_data
# The painting_id field can be a single item, or a list of painting IDs, in the event that the item for this logical
# door should move more than one actual in-game painting.
if "painting_id" in door_data:
if isinstance(door_data["painting_id"], list):
painting_ids = door_data["painting_id"]
else:
painting_ids = [door_data["painting_id"]]
else:
painting_ids = []
door_obj = Door(door_name, item_name, location_name, panels, skip_location, skip_item, has_doors,
painting_ids, event, group, include_reduce, junk_item)
DOORS_BY_ROOM[room_name][door_name] = door_obj
def process_painting(room_name, painting_data):
global PAINTINGS, REQUIRED_PAINTING_ROOMS, REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS
# Read in information about this painting and store it in an object.
painting_id = painting_data["id"]
if "disable" in painting_data:
disable_painting = painting_data["disable"]
else:
disable_painting = False
if "required" in painting_data:
required_painting = painting_data["required"]
if required_painting:
REQUIRED_PAINTING_ROOMS.append(room_name)
else:
required_painting = False
if "required_when_no_doors" in painting_data:
rwnd = painting_data["required_when_no_doors"]
if rwnd:
REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS.append(room_name)
else:
rwnd = False
if "exit_only" in painting_data:
exit_only = painting_data["exit_only"]
else:
exit_only = False
if "enter_only" in painting_data:
enter_only = painting_data["enter_only"]
else:
enter_only = False
if "req_blocked" in painting_data:
req_blocked = painting_data["req_blocked"]
else:
req_blocked = False
if "req_blocked_when_no_doors" in painting_data:
req_blocked_when_no_doors = painting_data["req_blocked_when_no_doors"]
else:
req_blocked_when_no_doors = False
required_door = None
if "required_door" in painting_data:
door = painting_data["required_door"]
required_door = RoomAndDoor(
door["room"] if "room" in door else room_name,
door["door"]
)
painting_obj = Painting(painting_id, room_name, enter_only, exit_only,
required_painting, rwnd, required_door, disable_painting, req_blocked,
req_blocked_when_no_doors)
PAINTINGS[painting_id] = painting_obj
def process_progression(room_name, progression_name, progression_doors):
global PROGRESSIVE_ITEMS, PROGRESSION_BY_ROOM
# Progressive items are configured as a list of doors.
PROGRESSIVE_ITEMS.append(progression_name)
progression_index = 1
for door in progression_doors:
if isinstance(door, Dict):
door_room = door["room"]
door_door = door["door"]
else:
door_room = room_name
door_door = door
room_progressions = PROGRESSION_BY_ROOM.setdefault(door_room, {})
room_progressions[door_door] = Progression(progression_name, progression_index)
progression_index += 1
def process_room(room_name, room_data):
global ALL_ROOMS
room_obj = Room(room_name, [])
if "entrances" in room_data:
for source_room, doors in room_data["entrances"].items():
process_entrance(source_room, doors, room_obj)
if "panels" in room_data:
PANELS_BY_ROOM[room_name] = dict()
for panel_name, panel_data in room_data["panels"].items():
process_panel(room_name, panel_name, panel_data)
if "doors" in room_data:
DOORS_BY_ROOM[room_name] = dict()
for door_name, door_data in room_data["doors"].items():
process_door(room_name, door_name, door_data)
if "paintings" in room_data:
for painting_data in room_data["paintings"]:
process_painting(room_name, painting_data)
if "progression" in room_data:
for progression_name, progression_doors in room_data["progression"].items():
process_progression(room_name, progression_name, progression_doors)
ALL_ROOMS.append(room_obj)
if __name__ == '__main__':
if len(sys.argv) == 1:
ll1_path = os.path.join("worlds", "lingo", "data", "LL1.yaml")
ids_path = os.path.join("worlds", "lingo", "data", "ids.yaml")
output_path = os.path.join("worlds", "lingo", "data", "generated.dat")
elif len(sys.argv) != 4:
print("")
print("Usage: python worlds/lingo/utils/pickle_static_data.py [args]")
print("Arguments:")
print(" - Path to LL1.yaml")
print(" - Path to ids.yaml")
print(" - Path to output file")
exit()
else:
ll1_path = sys.argv[1]
ids_path = sys.argv[2]
output_path = sys.argv[3]
load_static_data(ll1_path, ids_path)
hashes = {
"LL1.yaml": hash_file(ll1_path),
"ids.yaml": hash_file(ids_path),
}
pickdata = {
"HASHES": hashes,
"PAINTINGS": PAINTINGS,
"ALL_ROOMS": ALL_ROOMS,
"DOORS_BY_ROOM": DOORS_BY_ROOM,
"PANELS_BY_ROOM": PANELS_BY_ROOM,
"PROGRESSIVE_ITEMS": PROGRESSIVE_ITEMS,
"PROGRESSION_BY_ROOM": PROGRESSION_BY_ROOM,
"PAINTING_ENTRANCES": PAINTING_ENTRANCES,
"PAINTING_EXIT_ROOMS": PAINTING_EXIT_ROOMS,
"PAINTING_EXITS": PAINTING_EXITS,
"REQUIRED_PAINTING_ROOMS": REQUIRED_PAINTING_ROOMS,
"REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS": REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS,
"SPECIAL_ITEM_IDS": SPECIAL_ITEM_IDS,
"PANEL_LOCATION_IDS": PANEL_LOCATION_IDS,
"DOOR_LOCATION_IDS": DOOR_LOCATION_IDS,
"DOOR_ITEM_IDS": DOOR_ITEM_IDS,
"DOOR_GROUP_ITEM_IDS": DOOR_GROUP_ITEM_IDS,
"PROGRESSIVE_ITEM_IDS": PROGRESSIVE_ITEM_IDS,
}
with open(output_path, "wb") as file:
pickle.dump(pickdata, file)