diff --git a/worlds/lingo/__init__.py b/worlds/lingo/__init__.py index 2f935419..e35a1026 100644 --- a/worlds/lingo/__init__.py +++ b/worlds/lingo/__init__.py @@ -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): @@ -100,9 +100,9 @@ class LingoWorld(World): item = ALL_ITEM_TABLE[name] 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 - for painting_id in item.painting_ids)\ + if hasattr(self, "options") and self.options.shuffle_paintings and len(item.painting_ids) > 0 \ + 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 # go nowhere, then this item should not be progression. The Pilgrim Room painting is special and needs to be diff --git a/worlds/lingo/data/README.md b/worlds/lingo/data/README.md new file mode 100644 index 00000000..fe834cef --- /dev/null +++ b/worlds/lingo/data/README.md @@ -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. diff --git a/worlds/lingo/data/generated.dat b/worlds/lingo/data/generated.dat new file mode 100644 index 00000000..49ea60df Binary files /dev/null and b/worlds/lingo/data/generated.dat differ diff --git a/worlds/lingo/datatypes.py b/worlds/lingo/datatypes.py new file mode 100644 index 00000000..eb5c8797 --- /dev/null +++ b/worlds/lingo/datatypes.py @@ -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 diff --git a/worlds/lingo/items.py b/worlds/lingo/items.py index 9f8bf561..623cd79e 100644 --- a/worlds/lingo/items.py +++ b/worlds/lingo/items.py @@ -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. diff --git a/worlds/lingo/locations.py b/worlds/lingo/locations.py index 5903d603..e66ebac6 100644 --- a/worlds/lingo/locations.py +++ b/worlds/lingo/locations.py @@ -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): """ diff --git a/worlds/lingo/player_logic.py b/worlds/lingo/player_logic.py index 3a6eedfe..f5eb986a 100644 --- a/worlds/lingo/player_logic.py +++ b/worlds/lingo/player_logic.py @@ -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. diff --git a/worlds/lingo/regions.py b/worlds/lingo/regions.py index bdc42f42..464e9a14 100644 --- a/worlds/lingo/regions.py +++ b/worlds/lingo/regions.py @@ -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 diff --git a/worlds/lingo/rules.py b/worlds/lingo/rules.py index 481fab18..054c330c 100644 --- a/worlds/lingo/rules.py +++ b/worlds/lingo/rules.py @@ -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 diff --git a/worlds/lingo/static_logic.py b/worlds/lingo/static_logic.py index e9f82fb7..1da265df 100644 --- a/worlds/lingo/static_logic.py +++ b/worlds/lingo/static_logic.py @@ -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() diff --git a/worlds/lingo/test/TestDatafile.py b/worlds/lingo/test/TestDatafile.py new file mode 100644 index 00000000..9f4e9da0 --- /dev/null +++ b/worlds/lingo/test/TestDatafile.py @@ -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'") diff --git a/worlds/lingo/utils/__init__.py b/worlds/lingo/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/worlds/lingo/utils/pickle_static_data.py b/worlds/lingo/utils/pickle_static_data.py new file mode 100644 index 00000000..c7a2711d --- /dev/null +++ b/worlds/lingo/utils/pickle_static_data.py @@ -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)