diff --git a/src/mars_patcher/common_types.py b/src/mars_patcher/common_types.py new file mode 100644 index 0000000..6be881b --- /dev/null +++ b/src/mars_patcher/common_types.py @@ -0,0 +1,12 @@ +from typing import Annotated + +from typing_extensions import TypeAlias + +from mars_patcher.auto_generated_types import Areaid, Typeu8 + +AreaId: TypeAlias = Areaid +RoomId: TypeAlias = Typeu8 + +AreaRoomPair = tuple[AreaId, RoomId] + +MinimapId: TypeAlias = Annotated[int, "0 <= value < 10"] diff --git a/src/mars_patcher/constants/minimap_tiles.py b/src/mars_patcher/constants/minimap_tiles.py index c0b866b..a8302b4 100644 --- a/src/mars_patcher/constants/minimap_tiles.py +++ b/src/mars_patcher/constants/minimap_tiles.py @@ -1,326 +1,541 @@ -# Format +from __future__ import annotations + +from enum import Enum +from typing import NamedTuple + +from typing_extensions import Self + +# String Format # __ # Chunk 1 (tile edges) -# - W: wall -# - D: door -# - S: shortcut -# - x: none +# - see Edge and ColoredDoor for values # Chunk 2 (tile corners) # - C: corner pixel # - x: none # Chunk 3 (tile content) -# - N: navigation room -# - S: save room -# - R: recharge room -# - H: hidden recharge room -# - D: data room -# - I: item -# - B: boss -# - G: gunship -# - P: gunship edge -# - x: none +# - see Content for values + + +# Edges +class Edge(Enum): + EMPTY = "x" + WALL = "W" + SHORTCUT = "S" + DOOR = "D" + + @property + def is_door(self) -> bool: + return self == Edge.DOOR + + +class ColoredDoor(Enum): + BLUE = "B" + GREEN = "G" + YELLOW = "Y" + RED = "R" + + @property + def is_door(self) -> bool: + return True + + # Aliases + L1 = BLUE + L2 = GREEN + L3 = YELLOW + L4 = RED + + +class TileEdges(NamedTuple): + top: Edge = Edge.WALL + left: Edge | ColoredDoor = Edge.WALL + right: Edge | ColoredDoor = Edge.WALL + bottom: Edge = Edge.WALL + + @property + def as_str(self) -> str: + return f"{self.top.value}{self.left.value}{self.right.value}{self.bottom.value}" + + @classmethod + def from_str(cls, value: str) -> Self: + if len(value) != 4: + raise ValueError(f"'{value}' is not a valid TileEdges string") + top, left, right, bottom = tuple(value) + + def any_edge_from_value(v: str) -> Edge | ColoredDoor: + try: + return Edge(v) + except ValueError: + pass + + try: + return ColoredDoor(v) + except ValueError: + raise ValueError(f"{repr(v)} is not a valid Edge or ColoredDoor") + + return cls( + top=Edge(top), + left=any_edge_from_value(left), + right=any_edge_from_value(right), + bottom=Edge(bottom), + ) + + def h_flip(self) -> TileEdges: + return TileEdges( + top=self.top, + left=self.right, + right=self.left, + bottom=self.bottom, + ) + + def v_flip(self) -> TileEdges: + return TileEdges( + top=self.bottom, + left=self.left, + right=self.right, + bottom=self.top, + ) + +# Corners +class TileCorners(NamedTuple): + top_left: bool = False + top_right: bool = False + bottom_left: bool = False + bottom_right: bool = False + + @property + def as_str(self) -> str: + def s(corner: bool) -> str: + return "C" if corner else "x" + + return f"{s(self.top_left)}{s(self.top_right)}{s(self.bottom_left)}{s(self.bottom_right)}" + + @classmethod + def from_str(cls, value: str) -> Self: + if len(value) != 4: + raise ValueError(f"'{value}' is not a valid TileCorners string") + tl, tr, bl, br = tuple(value) + return cls( + top_left=(tl == "C"), + top_right=(tr == "C"), + bottom_left=(bl == "C"), + bottom_right=(br == "C"), + ) + + def h_flip(self) -> TileCorners: + return TileCorners( + top_left=self.top_right, + top_right=self.top_left, + bottom_left=self.bottom_right, + bottom_right=self.bottom_left, + ) + + def v_flip(self) -> TileCorners: + return TileCorners( + top_left=self.bottom_left, + top_right=self.bottom_right, + bottom_left=self.top_left, + bottom_right=self.top_right, + ) + + +# Contents +class Content(Enum): + EMPTY = "x" + NAVIGATION = "N" + SAVE = "S" + RECHARGE = "R" + HIDDEN_RECHARGE = "H" + DATA = "D" + ITEM = "I" + OBTAINED_ITEM = "O" + BOSS = "B" + GUNSHIP = "G" + GUNSHIP_EDGE = "P" + + @property + def can_h_flip(self) -> bool: + exclude = { + Content.NAVIGATION, + Content.SAVE, + Content.RECHARGE, + Content.HIDDEN_RECHARGE, + Content.DATA, + Content.GUNSHIP, + Content.GUNSHIP_EDGE, + } + return self not in exclude + + @property + def can_v_flip(self) -> bool: + exclude = { + Content.NAVIGATION, + Content.SAVE, + Content.RECHARGE, + Content.HIDDEN_RECHARGE, + Content.GUNSHIP, + Content.GUNSHIP_EDGE, + Content.BOSS, + } + return self not in exclude + + +# Tile +class MapTile(NamedTuple): + edges: TileEdges = TileEdges() + corners: TileCorners = TileCorners() + content: Content = Content.EMPTY + + @property + def as_str(self) -> str: + return f"{self.edges.as_str}_{self.corners.as_str}_{self.content.value}" + + @classmethod + def from_str(cls, value: str) -> Self: + if len(value) != 11: + raise ValueError(f"'{value}' is not a valid MapTile string") + edges, corners, content = value.split("_") + return cls( + edges=TileEdges.from_str(edges), + corners=TileCorners.from_str(corners), + content=Content(content), + ) + + def h_flip(self) -> MapTile: + if not self.content.can_h_flip: + raise ValueError(f"Cannot h_flip tile with contents {self.content}") + + return MapTile( + edges=self.edges.h_flip(), + corners=self.corners.h_flip(), + content=self.content, + ) + + def v_flip(self) -> MapTile: + if not self.content.can_v_flip: + raise ValueError(f"Cannot v_flip tile with contents {self.content}") + + return MapTile( + edges=self.edges.v_flip(), + corners=self.corners.v_flip(), + content=self.content, + ) + + +# Constants COLORED_DOOR_TILES = { - 0x005: "WDxx_xxxx_x", - 0x006: "DDxx_xxxx_x", - 0x007: "DDxx_xxxC_x", - 0x008: "DDDD_xxxx_x", - 0x009: "DDDx_xxxx_x", - 0x00A: "WDxx_xxxx_x", - 0x00B: "DDxx_xxxx_x", - 0x00C: "DDxx_xxxC_x", - 0x00D: "DDDD_xxxx_x", - 0x00E: "DDDx_xxxx_x", - 0x00F: "WDxx_xxxx_x", - 0x010: "DDxx_xxxx_x", - 0x011: "DDxx_xxxC_x", - 0x012: "DDDD_xxxx_x", - 0x013: "DDDx_xxxx_x", - 0x014: "WDxx_xxxx_x", - 0x015: "DDxx_xxxx_x", - 0x016: "DDxx_xxxC_x", - 0x017: "DDDD_xxxx_x", - 0x018: "DDDx_xxxx_x", - 0x019: "DDDx_xxxx_x", - 0x01A: "DDDx_xxxx_x", - 0x01B: "DDDx_xxxx_x", - 0x01C: "DDDx_xxxx_x", - 0x01D: "DDDx_xxxx_x", - 0x01E: "DDDx_xxxx_x", - 0x01F: "DDDD_xxxx_x", - 0x025: "xDxx_xxxx_x", - 0x026: "xDxx_xCxC_x", - 0x027: "DDWD_xxxx_x", - 0x028: "DDDx_xxxx_x", - 0x029: "DDDW_xxxx_x", - 0x02A: "xDxx_xxxx_x", - 0x02B: "xDxx_xCxC_x", - 0x02C: "DDWD_xxxx_x", - 0x02D: "DDDx_xxxx_x", - 0x02E: "DDDW_xxxx_x", - 0x02F: "xDxx_xxxx_x", - 0x030: "xDxx_xCxC_x", - 0x031: "DDWD_xxxx_x", - 0x032: "DDDx_xxxx_x", - 0x033: "DDDW_xxxx_x", - 0x034: "xDxx_xxxx_x", - 0x035: "xDxx_xCxC_x", - 0x036: "DDWD_xxxx_x", - 0x037: "DDDx_xxxx_x", - 0x038: "DDDW_xxxx_x", - 0x039: "DDDW_xxxx_x", - 0x03A: "DDDW_xxxx_x", - 0x03B: "DDDW_xxxx_x", - 0x03C: "DDDW_xxxx_x", - 0x03D: "DDDW_xxxx_x", - 0x03E: "DDDW_xxxx_x", - 0x03F: "DDDD_xxxx_x", - 0x045: "WDxW_xxxx_x", - 0x046: "DDxD_xxxx_x", - 0x047: "DDxW_xxxx_x", - 0x048: "WDWW_xxxx_x", - 0x049: "WDDW_xxxx_x", - 0x04A: "WDxW_xxxx_x", - 0x04B: "DDxD_xxxx_x", - 0x04C: "DDxW_xxxx_x", - 0x04D: "WDWW_xxxx_x", - 0x04E: "WDDW_xxxx_x", - 0x04F: "WDxW_xxxx_x", - 0x050: "DDxD_xxxx_x", - 0x051: "DDxW_xxxx_x", - 0x052: "WDWW_xxxx_x", - 0x053: "WDDW_xxxx_x", - 0x054: "WDxW_xxxx_x", - 0x055: "DDxD_xxxx_x", - 0x056: "DDxW_xxxx_x", - 0x057: "WDWW_xxxx_x", - 0x058: "WDDW_xxxx_x", - 0x059: "WDDW_xxxx_x", - 0x05A: "WDDW_xxxx_x", - 0x05B: "WDDW_xxxx_x", - 0x05C: "WDDW_xxxx_x", - 0x05D: "WDDW_xxxx_x", - 0x05E: "WDDW_xxxx_x", - 0x05F: "DDDD_xxxx_x", - 0x065: "DDWx_xxxx_x", - 0x066: "WDDx_xxxx_x", - 0x068: "WDWx_xxxx_x", - 0x069: "WDDx_xxxx_x", - 0x06A: "DDWx_xxxx_x", - 0x06B: "WDDx_xxxx_x", - 0x06C: "WDxx_xxxC_x", - 0x06D: "WDWx_xxxx_x", - 0x06E: "WDDx_xxxx_x", - 0x06F: "DDWx_xxxx_x", - 0x070: "WDDx_xxxx_x", - 0x072: "WDWx_xxxx_x", - 0x073: "WDDx_xxxx_x", - 0x074: "DDWx_xxxx_x", - 0x075: "WDDx_xxxx_x", - 0x077: "WDWx_xxxx_x", - 0x078: "WDDx_xxxx_x", - 0x079: "WDDx_xxxx_x", - 0x07A: "WDDx_xxxx_x", - 0x07B: "WDDx_xxxx_x", - 0x07C: "WDDx_xxxx_x", - 0x07D: "WDDx_xxxx_x", - 0x07E: "WDDx_xxxx_x", - 0x07F: "DDDD_xxxx_x", - 0x085: "DDWW_xxxx_x", - 0x086: "xDDx_xxxx_x", - 0x088: "xDWx_xxxx_x", - 0x089: "xDDx_xxxx_x", - 0x08A: "DDWW_xxxx_x", - 0x08B: "xDDx_xxxx_x", - 0x08D: "xDWx_xxxx_x", - 0x08E: "xDDx_xxxx_x", - 0x08F: "DDWW_xxxx_x", - 0x090: "xDDx_xxxx_x", - 0x092: "xDWx_xxxx_x", - 0x093: "xDDx_xxxx_x", - 0x094: "DDWW_xxxx_x", - 0x095: "xDDx_xxxx_x", - 0x096: "DDDD_xxxx_x", - 0x097: "xDWx_xxxx_x", - 0x098: "xDDx_xxxx_x", - 0x099: "xDDx_xxxx_x", - 0x09A: "xDDx_xxxx_x", - 0x09B: "xDDx_xxxx_x", - 0x09C: "xDDx_xxxx_x", - 0x09D: "xDDx_xxxx_x", - 0x09E: "xDDx_xxxx_x", - 0x09F: "DDDD_xxxx_x", - 0x0E6: "WDDW_xxxx_x", - 0x0E7: "DDDW_xxxx_x", - 0x0E8: "DDDD_xxxx_x", - 0x0EA: "DDxx_xxxC_x", - 0x0EB: "WDDW_xxxx_x", - 0x0EC: "DDDW_xxxx_x", - 0x0ED: "DDDD_xxxx_x", - 0x0EF: "DDxx_xxxC_x", - 0x0F0: "WDDW_xxxx_x", - 0x0F1: "DDDW_xxxx_x", - 0x0F2: "DDDD_xxxx_x", - 0x0F4: "DDxx_xxxC_x", - 0x0F5: "WDDW_xxxx_x", - 0x0F6: "DDDW_xxxx_x", - 0x0F7: "DDDD_xxxx_x", - 0x0F8: "WDxx_xxxC_x", - 0x128: "WDDW_xxxx_R", - 0x140: "WDWW_xxxx_R", - 0x141: "WWDW_xxxx_R", - 0x142: "WDDW_xxxx_R", - 0x143: "WDWW_xxxx_R", - 0x144: "WWDW_xxxx_R", - 0x145: "WDDW_xxxx_R", - 0x146: "WDWW_xxxx_R", - 0x147: "WWDW_xxxx_R", - 0x148: "WDDW_xxxx_R", - 0x149: "WDWW_xxxx_R", - 0x14A: "WWDW_xxxx_R", - 0x14B: "WDDW_xxxx_R", - 0x14C: "WDWW_xxxx_N", - 0x14D: "WWDW_xxxx_N", - 0x14E: "WDDW_xxxx_N", - 0x14F: "WDWW_xxxx_N", - 0x150: "WWDW_xxxx_N", - 0x151: "WDDW_xxxx_N", - 0x152: "WDWW_xxxx_N", - 0x153: "WWDW_xxxx_N", - 0x154: "WDDW_xxxx_N", - 0x155: "WDWW_xxxx_N", - 0x156: "WWDW_xxxx_N", - 0x157: "WDDW_xxxx_N", - 0x15E: "WDDW_xxxx_R", - 0x160: "WDWW_xxxx_D", - 0x161: "WWDW_xxxx_D", - 0x162: "WDDW_xxxx_D", - 0x163: "WDWW_xxxx_D", - 0x164: "WWDW_xxxx_D", - 0x165: "WDDW_xxxx_D", - 0x166: "WDWW_xxxx_D", - 0x167: "WWDW_xxxx_D", - 0x168: "WDDW_xxxx_D", - 0x169: "WDWW_xxxx_D", - 0x16A: "WWDW_xxxx_D", - 0x16B: "WDDW_xxxx_D", - 0x17E: "WDDW_xxxx_S", - 0x17F: "WDDW_xxxx_S", - 0x198: "WDWW_xxxx_I", - 0x199: "WDWW_xxxx_O", - 0x19E: "WDWW_xxxx_I", - 0x19F: "WDWW_xxxx_O", - 0x1AC: "WDDW_xxxx_I", - 0x1AD: "WDDW_xxxx_O", + 0x005: MapTile.from_str("WBxx_xxxx_x"), + 0x006: MapTile.from_str("DBxx_xxxx_x"), + 0x007: MapTile.from_str("DBxx_xxxC_x"), + 0x008: MapTile.from_str("DBBD_xxxx_x"), + 0x009: MapTile.from_str("DBBx_xxxx_x"), + 0x025: MapTile.from_str("xBxx_xxxx_x"), + 0x026: MapTile.from_str("xBxx_xCxC_x"), + 0x027: MapTile.from_str("DBWD_xxxx_x"), + 0x028: MapTile.from_str("DBDx_xxxx_x"), + 0x029: MapTile.from_str("DBBW_xxxx_x"), + 0x045: MapTile.from_str("WBxW_xxxx_x"), + 0x046: MapTile.from_str("DBxD_xxxx_x"), + 0x047: MapTile.from_str("DBxW_xxxx_x"), + 0x048: MapTile.from_str("WBWW_xxxx_x"), + 0x049: MapTile.from_str("WBBW_xxxx_x"), + 0x065: MapTile.from_str("DBWx_xxxx_x"), + 0x066: MapTile.from_str("WBDx_xxxx_x"), + 0x068: MapTile.from_str("WBWx_xxxx_x"), + 0x069: MapTile.from_str("WBBx_xxxx_x"), + 0x085: MapTile.from_str("DBWW_xxxx_x"), + 0x086: MapTile.from_str("xBDx_xxxx_x"), + 0x088: MapTile.from_str("xBWx_xxxx_x"), + 0x089: MapTile.from_str("xBBx_xxxx_x"), + 0x00A: MapTile.from_str("WGxx_xxxx_x"), + 0x00B: MapTile.from_str("DGxx_xxxx_x"), + 0x00C: MapTile.from_str("DGxx_xxxC_x"), + 0x00D: MapTile.from_str("DGGD_xxxx_x"), + 0x00E: MapTile.from_str("DGGx_xxxx_x"), + 0x02A: MapTile.from_str("xGxx_xxxx_x"), + 0x02B: MapTile.from_str("xGxx_xCxC_x"), + 0x02C: MapTile.from_str("DGWD_xxxx_x"), + 0x02D: MapTile.from_str("DGDx_xxxx_x"), + 0x02E: MapTile.from_str("DGGW_xxxx_x"), + 0x04A: MapTile.from_str("WGxW_xxxx_x"), + 0x04B: MapTile.from_str("DGxD_xxxx_x"), + 0x04C: MapTile.from_str("DGxW_xxxx_x"), + 0x04D: MapTile.from_str("WGWW_xxxx_x"), + 0x04E: MapTile.from_str("WGGW_xxxx_x"), + 0x06A: MapTile.from_str("DGWx_xxxx_x"), + 0x06B: MapTile.from_str("WGDx_xxxx_x"), + 0x06D: MapTile.from_str("WGWx_xxxx_x"), + 0x06E: MapTile.from_str("WGGx_xxxx_x"), + 0x08A: MapTile.from_str("DGWW_xxxx_x"), + 0x08B: MapTile.from_str("xGDx_xxxx_x"), + 0x08D: MapTile.from_str("xGWx_xxxx_x"), + 0x08E: MapTile.from_str("xGGx_xxxx_x"), + 0x00F: MapTile.from_str("WRxx_xxxx_x"), + 0x010: MapTile.from_str("DRxx_xxxx_x"), + 0x011: MapTile.from_str("DRxx_xxxC_x"), + 0x012: MapTile.from_str("DRRD_xxxx_x"), + 0x013: MapTile.from_str("DRRx_xxxx_x"), + 0x02F: MapTile.from_str("xRxx_xxxx_x"), + 0x030: MapTile.from_str("xRxx_xCxC_x"), + 0x031: MapTile.from_str("DRWD_xxxx_x"), + 0x032: MapTile.from_str("DRDx_xxxx_x"), + 0x033: MapTile.from_str("DRRW_xxxx_x"), + 0x04F: MapTile.from_str("WRxW_xxxx_x"), + 0x050: MapTile.from_str("DRxD_xxxx_x"), + 0x051: MapTile.from_str("DRxW_xxxx_x"), + 0x052: MapTile.from_str("WRWW_xxxx_x"), + 0x053: MapTile.from_str("WRRW_xxxx_x"), + 0x06F: MapTile.from_str("DRWx_xxxx_x"), + 0x070: MapTile.from_str("WRDx_xxxx_x"), + 0x072: MapTile.from_str("WRWx_xxxx_x"), + 0x073: MapTile.from_str("WRRx_xxxx_x"), + 0x08F: MapTile.from_str("DRWW_xxxx_x"), + 0x090: MapTile.from_str("xRDx_xxxx_x"), + 0x092: MapTile.from_str("xRWx_xxxx_x"), + 0x093: MapTile.from_str("xRRx_xxxx_x"), + 0x014: MapTile.from_str("WYxx_xxxx_x"), + 0x015: MapTile.from_str("DYxx_xxxx_x"), + 0x016: MapTile.from_str("DYxx_xxxC_x"), + 0x017: MapTile.from_str("DYYD_xxxx_x"), + 0x018: MapTile.from_str("DYYx_xxxx_x"), + 0x034: MapTile.from_str("xYxx_xxxx_x"), + 0x035: MapTile.from_str("xYxx_xCxC_x"), + 0x036: MapTile.from_str("DYWD_xxxx_x"), + 0x037: MapTile.from_str("DYDx_xxxx_x"), + 0x038: MapTile.from_str("DYYW_xxxx_x"), + 0x054: MapTile.from_str("WYxW_xxxx_x"), + 0x055: MapTile.from_str("DYxD_xxxx_x"), + 0x056: MapTile.from_str("DYxW_xxxx_x"), + 0x057: MapTile.from_str("WYWW_xxxx_x"), + 0x058: MapTile.from_str("WYYW_xxxx_x"), + 0x074: MapTile.from_str("DYWx_xxxx_x"), + 0x075: MapTile.from_str("WYDx_xxxx_x"), + 0x077: MapTile.from_str("WYWx_xxxx_x"), + 0x078: MapTile.from_str("WYYx_xxxx_x"), + 0x094: MapTile.from_str("DYWW_xxxx_x"), + 0x095: MapTile.from_str("xYDx_xxxx_x"), + 0x097: MapTile.from_str("xYWx_xxxx_x"), + 0x098: MapTile.from_str("xYYx_xxxx_x"), + 0x019: MapTile.from_str("DBGx_xxxx_x"), + 0x01A: MapTile.from_str("DBRx_xxxx_x"), + 0x01B: MapTile.from_str("DBYx_xxxx_x"), + 0x01C: MapTile.from_str("DGRx_xxxx_x"), + 0x01D: MapTile.from_str("DGYx_xxxx_x"), + 0x01E: MapTile.from_str("DRYx_xxxx_x"), + 0x039: MapTile.from_str("DBGW_xxxx_x"), + 0x03A: MapTile.from_str("DBRW_xxxx_x"), + 0x03B: MapTile.from_str("DBYW_xxxx_x"), + 0x03C: MapTile.from_str("DGRW_xxxx_x"), + 0x03D: MapTile.from_str("DGYW_xxxx_x"), + 0x03E: MapTile.from_str("DRYW_xxxx_x"), + 0x059: MapTile.from_str("WBGW_xxxx_x"), + 0x05A: MapTile.from_str("WBRW_xxxx_x"), + 0x05B: MapTile.from_str("WBYW_xxxx_x"), + 0x05C: MapTile.from_str("WGRW_xxxx_x"), + 0x05D: MapTile.from_str("WGYW_xxxx_x"), + 0x05E: MapTile.from_str("WRYW_xxxx_x"), + 0x079: MapTile.from_str("WBGx_xxxx_x"), + 0x07A: MapTile.from_str("WBRx_xxxx_x"), + 0x07B: MapTile.from_str("WBYx_xxxx_x"), + 0x07C: MapTile.from_str("WGRx_xxxx_x"), + 0x07D: MapTile.from_str("WGYx_xxxx_x"), + 0x07E: MapTile.from_str("WRYx_xxxx_x"), + 0x099: MapTile.from_str("xBGx_xxxx_x"), + 0x09A: MapTile.from_str("xBRx_xxxx_x"), + 0x09B: MapTile.from_str("xBYx_xxxx_x"), + 0x09C: MapTile.from_str("xGRx_xxxx_x"), + 0x09D: MapTile.from_str("xGYx_xxxx_x"), + 0x09E: MapTile.from_str("xRYx_xxxx_x"), + 0x01F: MapTile.from_str("DBGD_xxxx_x"), + 0x03F: MapTile.from_str("DBRD_xxxx_x"), + 0x05F: MapTile.from_str("DBYD_xxxx_x"), + 0x07F: MapTile.from_str("DGRD_xxxx_x"), + 0x09F: MapTile.from_str("DGYD_xxxx_x"), + 0x096: MapTile.from_str("DRYD_xxxx_x"), + 0x0E6: MapTile.from_str("WDBW_xxxx_x"), + 0x0E7: MapTile.from_str("DDBW_xxxx_x"), + 0x0E8: MapTile.from_str("DDBD_xxxx_x"), + 0x0EA: MapTile.from_str("DGxx_xxxC_x"), + 0x0EB: MapTile.from_str("WDGW_xxxx_x"), + 0x0EC: MapTile.from_str("DDGW_xxxx_x"), + 0x0ED: MapTile.from_str("DDGD_xxxx_x"), + 0x0EF: MapTile.from_str("DRxx_xxxC_x"), + 0x0F0: MapTile.from_str("WDRW_xxxx_x"), + 0x0F1: MapTile.from_str("DDRW_xxxx_x"), + 0x0F2: MapTile.from_str("DDRD_xxxx_x"), + 0x0F4: MapTile.from_str("DYxx_xxxC_x"), + 0x0F5: MapTile.from_str("WDYW_xxxx_x"), + 0x0F6: MapTile.from_str("DDYW_xxxx_x"), + 0x0F7: MapTile.from_str("DDYD_xxxx_x"), + 0x0F8: MapTile.from_str("WYxx_xxxC_x"), + 0x06C: MapTile.from_str("WGxx_xxxC_x"), + 0x140: MapTile.from_str("WBWW_xxxx_R"), + 0x141: MapTile.from_str("WWBW_xxxx_R"), + 0x142: MapTile.from_str("WBBW_xxxx_R"), + 0x14C: MapTile.from_str("WBWW_xxxx_N"), + 0x14D: MapTile.from_str("WWBW_xxxx_N"), + 0x14E: MapTile.from_str("WBBW_xxxx_N"), + 0x160: MapTile.from_str("WBWW_xxxx_D"), + 0x161: MapTile.from_str("WWBW_xxxx_D"), + 0x162: MapTile.from_str("WBBW_xxxx_D"), + 0x143: MapTile.from_str("WGWW_xxxx_R"), + 0x144: MapTile.from_str("WWGW_xxxx_R"), + 0x145: MapTile.from_str("WGGW_xxxx_R"), + 0x14F: MapTile.from_str("WGWW_xxxx_N"), + 0x150: MapTile.from_str("WWGW_xxxx_N"), + 0x151: MapTile.from_str("WGGW_xxxx_N"), + 0x163: MapTile.from_str("WGWW_xxxx_D"), + 0x164: MapTile.from_str("WWGW_xxxx_D"), + 0x165: MapTile.from_str("WGGW_xxxx_D"), + 0x146: MapTile.from_str("WRWW_xxxx_R"), + 0x147: MapTile.from_str("WWRW_xxxx_R"), + 0x148: MapTile.from_str("WRRW_xxxx_R"), + 0x152: MapTile.from_str("WRWW_xxxx_N"), + 0x153: MapTile.from_str("WWRW_xxxx_N"), + 0x154: MapTile.from_str("WRRW_xxxx_N"), + 0x166: MapTile.from_str("WRWW_xxxx_D"), + 0x167: MapTile.from_str("WWRW_xxxx_D"), + 0x168: MapTile.from_str("WRRW_xxxx_D"), + 0x149: MapTile.from_str("WYWW_xxxx_R"), + 0x14A: MapTile.from_str("WWYW_xxxx_R"), + 0x14B: MapTile.from_str("WYYW_xxxx_R"), + 0x155: MapTile.from_str("WYWW_xxxx_N"), + 0x156: MapTile.from_str("WWYW_xxxx_N"), + 0x157: MapTile.from_str("WYYW_xxxx_N"), + 0x169: MapTile.from_str("WYWW_xxxx_D"), + 0x16A: MapTile.from_str("WWYW_xxxx_D"), + 0x16B: MapTile.from_str("WYYW_xxxx_D"), + 0x128: MapTile.from_str("WRDW_xxxx_R"), + 0x15E: MapTile.from_str("WDYW_xxxx_R"), + 0x17E: MapTile.from_str("WRRW_xxxx_S"), + 0x17F: MapTile.from_str("WYDW_xxxx_S"), + 0x198: MapTile.from_str("WBWW_xxxx_I"), + 0x199: MapTile.from_str("WBWW_xxxx_O"), + 0x19E: MapTile.from_str("WGWW_xxxx_I"), + 0x19F: MapTile.from_str("WGWW_xxxx_O"), + 0x1AC: MapTile.from_str("WYYW_xxxx_I"), + 0x1AD: MapTile.from_str("WYYW_xxxx_O"), } + NORMAL_DOOR_TILES = { - "WWxx_xxxx_x": 0x000, - "Wxxx_xxxx_x": 0x001, - "DWxx_xxxx_x": 0x002, - "Dxxx_xxxx_x": 0x003, - "DWxx_xxxC_x": 0x004, - "xWxx_xxxx_x": 0x020, - "xDxx_xxxx_x": 0x021, - "Wxxx_xxCC_x": 0x022, - "xWxx_xCxC_x": 0x023, - "DWWD_xxxx_x": 0x024, - "WWxW_xxxx_x": 0x040, - "WxxW_xxxx_x": 0x041, - "DWxD_xxxx_x": 0x042, - "DWxW_xxxx_x": 0x043, - "DxxD_xxxx_x": 0x044, - "DWWx_xxxx_x": 0x060, - "WDWx_xxxx_x": 0x061, - "WDDx_xxxx_x": 0x062, - "WWWx_xxxx_x": 0x063, - "DxxW_xxxx_x": 0x064, - "DWWW_xxxx_x": 0x080, - "xDWx_xxxx_x": 0x081, - "xDDx_xxxx_x": 0x082, - "xWWx_xxxx_x": 0x083, - "WDDW_xxxx_x": 0x084, - "WWWW_xxxx_x": 0x087, - "DDxW_xxxx_x": 0x0E0, - "DDxD_xxxx_x": 0x0E1, - "DDWx_xxxx_x": 0x0E2, - "DDDx_xxxx_x": 0x0E3, - "WWxx_xxxC_x": 0x0E4, - "xWxx_xxxC_x": 0x0E5, - "Dxxx_xxCC_x": 0x100, - "xDxx_xCxC_x": 0x101, - "DDxx_xxxC_x": 0x102, - # "DWxx_xxxC_x": 0x103, # dupe of 0x004 - "WDxx_xxxC_x": 0x104, - "xDxx_xCxx_x": 0x105, - "DDWW_xxxx_x": 0x120, - "DDDW_xxxx_x": 0x121, - "DDWD_xxxx_x": 0x122, - "DDDD_xxxx_x": 0x123, - "WDWW_xxxx_x": 0x124, - "WDxx_xxxx_x": 0x125, - "xxxx_xxxx_x": 0x126, - "Wxxx_xxxC_x": 0x127, - "WDxW_xxxx_P": 0x129, - "WxWW_xxxx_G": 0x12A, - "WDxW_xxxx_x": 0x12B, - "DDxx_xxxx_x": 0x12C, - "WDWW_xxxx_H": 0x138, - "WWDW_xxxx_H": 0x139, - "WDDW_xxxx_H": 0x13A, - "WDWW_xxxx_R": 0x158, - "WWDW_xxxx_R": 0x159, - "WDDW_xxxx_R": 0x15A, - "WDWW_xxxx_D": 0x15B, - "WWDW_xxxx_D": 0x15C, - "WDDW_xxxx_D": 0x15D, - "WDWW_xxxx_N": 0x178, - "WWDW_xxxx_N": 0x179, - "WDDW_xxxx_N": 0x17A, - "WDWW_xxxx_S": 0x17B, - "WWDW_xxxx_S": 0x17C, - "WDDW_xxxx_S": 0x17D, - "WDWW_xxxx_I": 0x180, - "WDWW_xxxx_O": 0x181, - "WWxW_xxxx_I": 0x182, - "WWxW_xxxx_O": 0x183, - "WDDW_xxxx_I": 0x184, - "WDDW_xxxx_O": 0x185, - "WWxx_xxxx_I": 0x186, - "WWxx_xxxx_O": 0x187, - "WDxW_xxxx_I": 0x188, - "WDxW_xxxx_O": 0x189, - "xWxx_xxxx_I": 0x18A, - "xWxx_xxxx_O": 0x18B, - "xDxx_xxxx_I": 0x18C, - "xDxx_xxxx_O": 0x18D, - "WDxx_xxxx_I": 0x18E, - "WDxx_xxxx_O": 0x18F, - "Wxxx_xxxx_I": 0x190, - "Wxxx_xxxx_O": 0x191, - "WxxW_xxxx_I": 0x192, - "WxxW_xxxx_O": 0x193, - "WWWx_xxxx_I": 0x194, - "WWWx_xxxx_O": 0x195, - "DDDW_xxxx_I": 0x196, - "DDDW_xxxx_O": 0x197, - "xDDW_xxxx_I": 0x19A, - "xDDW_xxxx_O": 0x19B, - "WWDx_xxxx_I": 0x19C, - "WWDx_xxxx_O": 0x19D, - "xWWD_xxxx_I": 0x1A0, - "xWWD_xxxx_O": 0x1A1, - "WWWD_xxxx_I": 0x1A2, - "WWWD_xxxx_O": 0x1A3, - "xxxx_xxxx_I": 0x1A4, - "xxxx_xxxx_O": 0x1A5, - "WWxD_xxxx_I": 0x1A6, - "WWxD_xxxx_O": 0x1A7, - "xWWx_xxxx_I": 0x1A8, - "xWWx_xxxx_O": 0x1A9, - "DDWW_xxxx_I": 0x1AA, - "DDWW_xxxx_O": 0x1AB, + 0x000: MapTile.from_str("WWxx_xxxx_x"), + 0x001: MapTile.from_str("Wxxx_xxxx_x"), + 0x002: MapTile.from_str("DWxx_xxxx_x"), + 0x003: MapTile.from_str("Dxxx_xxxx_x"), + 0x004: MapTile.from_str("DWxx_xxxC_x"), + 0x020: MapTile.from_str("xWxx_xxxx_x"), + 0x021: MapTile.from_str("xDxx_xxxx_x"), + 0x022: MapTile.from_str("Wxxx_xxCC_x"), + 0x023: MapTile.from_str("xWxx_xCxC_x"), + 0x024: MapTile.from_str("DWWD_xxxx_x"), + 0x040: MapTile.from_str("WWxW_xxxx_x"), + 0x041: MapTile.from_str("WxxW_xxxx_x"), + 0x042: MapTile.from_str("DWxD_xxxx_x"), + 0x043: MapTile.from_str("DWxW_xxxx_x"), + 0x044: MapTile.from_str("DxxD_xxxx_x"), + 0x060: MapTile.from_str("DWWx_xxxx_x"), + 0x061: MapTile.from_str("WDWx_xxxx_x"), + 0x062: MapTile.from_str("WDDx_xxxx_x"), + 0x063: MapTile.from_str("WWWx_xxxx_x"), + 0x064: MapTile.from_str("DxxW_xxxx_x"), + 0x080: MapTile.from_str("DWWW_xxxx_x"), + 0x081: MapTile.from_str("xDWx_xxxx_x"), + 0x082: MapTile.from_str("xDDx_xxxx_x"), + 0x083: MapTile.from_str("xWWx_xxxx_x"), + 0x084: MapTile.from_str("WDDW_xxxx_x"), + 0x087: MapTile.from_str("WWWW_xxxx_x"), + 0x0B4: MapTile.from_str("WSDW_xxxx_I"), + 0x0B6: MapTile.from_str("WSDW_xxxx_x"), + 0x0E0: MapTile.from_str("DDxW_xxxx_x"), + 0x0E1: MapTile.from_str("DDxD_xxxx_x"), + 0x0E2: MapTile.from_str("DDWx_xxxx_x"), + 0x0E3: MapTile.from_str("DDDx_xxxx_x"), + 0x0E4: MapTile.from_str("WWxx_xxxC_x"), + 0x0E5: MapTile.from_str("xWxx_xxxC_x"), + 0x100: MapTile.from_str("Dxxx_xxCC_x"), + 0x101: MapTile.from_str("xDxx_xCxC_x"), + 0x102: MapTile.from_str("DDxx_xxxC_x"), + 0x104: MapTile.from_str("WDxx_xxxC_x"), + 0x105: MapTile.from_str("xDxx_xCxx_x"), + 0x108: MapTile.from_str("xxDW_xxxx_B"), + 0x109: MapTile.from_str("WDxW_xxxx_B"), + 0x10D: MapTile.from_str("xxDx_xxxx_B"), + 0x10F: MapTile.from_str("xDxW_xxxx_B"), + 0x120: MapTile.from_str("DDWW_xxxx_x"), + 0x121: MapTile.from_str("DDDW_xxxx_x"), + 0x122: MapTile.from_str("DDWD_xxxx_x"), + 0x123: MapTile.from_str("DDDD_xxxx_x"), + 0x124: MapTile.from_str("WDWW_xxxx_x"), + 0x125: MapTile.from_str("WDxx_xxxx_x"), + 0x126: MapTile.from_str("xxxx_xxxx_x"), + 0x127: MapTile.from_str("Wxxx_xxxC_x"), + 0x129: MapTile.from_str("WDxW_xxxx_P"), + 0x12A: MapTile.from_str("WxWW_xxxx_G"), + 0x12B: MapTile.from_str("WDxW_xxxx_x"), + 0x12C: MapTile.from_str("DDxx_xxxx_x"), + 0x138: MapTile.from_str("WDWW_xxxx_H"), + 0x139: MapTile.from_str("WWDW_xxxx_H"), + 0x13A: MapTile.from_str("WDDW_xxxx_H"), + 0x158: MapTile.from_str("WDWW_xxxx_R"), + 0x159: MapTile.from_str("WWDW_xxxx_R"), + 0x15A: MapTile.from_str("WDDW_xxxx_R"), + 0x15B: MapTile.from_str("WDWW_xxxx_D"), + 0x15C: MapTile.from_str("WWDW_xxxx_D"), + 0x15D: MapTile.from_str("WDDW_xxxx_D"), + 0x178: MapTile.from_str("WDWW_xxxx_N"), + 0x179: MapTile.from_str("WWDW_xxxx_N"), + 0x17A: MapTile.from_str("WDDW_xxxx_N"), + 0x17B: MapTile.from_str("WDWW_xxxx_S"), + 0x17C: MapTile.from_str("WWDW_xxxx_S"), + 0x17D: MapTile.from_str("WDDW_xxxx_S"), + 0x180: MapTile.from_str("WDWW_xxxx_I"), + 0x181: MapTile.from_str("WDWW_xxxx_O"), + 0x182: MapTile.from_str("WWxW_xxxx_I"), + 0x183: MapTile.from_str("WWxW_xxxx_O"), + 0x184: MapTile.from_str("WDDW_xxxx_I"), + 0x185: MapTile.from_str("WDDW_xxxx_O"), + 0x186: MapTile.from_str("WWxx_xxxx_I"), + 0x187: MapTile.from_str("WWxx_xxxx_O"), + 0x188: MapTile.from_str("WDxW_xxxx_I"), + 0x189: MapTile.from_str("WDxW_xxxx_O"), + 0x18A: MapTile.from_str("xWxx_xxxx_I"), + 0x18B: MapTile.from_str("xWxx_xxxx_O"), + 0x18C: MapTile.from_str("xDxx_xxxx_I"), + 0x18D: MapTile.from_str("xDxx_xxxx_O"), + 0x18E: MapTile.from_str("WDxx_xxxx_I"), + 0x18F: MapTile.from_str("WDxx_xxxx_O"), + 0x190: MapTile.from_str("Wxxx_xxxx_I"), + 0x191: MapTile.from_str("Wxxx_xxxx_O"), + 0x192: MapTile.from_str("WxxW_xxxx_I"), + 0x193: MapTile.from_str("WxxW_xxxx_O"), + 0x194: MapTile.from_str("WWWx_xxxx_I"), + 0x195: MapTile.from_str("WWWx_xxxx_O"), + 0x196: MapTile.from_str("DDDW_xxxx_I"), + 0x197: MapTile.from_str("DDDW_xxxx_O"), + 0x19A: MapTile.from_str("xDDW_xxxx_I"), + 0x19B: MapTile.from_str("xDDW_xxxx_O"), + 0x19C: MapTile.from_str("WWDx_xxxx_I"), + 0x19D: MapTile.from_str("WWDx_xxxx_O"), + 0x1A0: MapTile.from_str("xWWD_xxxx_I"), + 0x1A1: MapTile.from_str("xWWD_xxxx_O"), + 0x1A2: MapTile.from_str("WWWD_xxxx_I"), + 0x1A3: MapTile.from_str("WWWD_xxxx_O"), + 0x1A4: MapTile.from_str("xxxx_xxxx_I"), + 0x1A5: MapTile.from_str("xxxx_xxxx_O"), + 0x1A6: MapTile.from_str("WWxD_xxxx_I"), + 0x1A7: MapTile.from_str("WWxD_xxxx_O"), + 0x1A8: MapTile.from_str("xWWx_xxxx_I"), + 0x1A9: MapTile.from_str("xWWx_xxxx_O"), + 0x1AA: MapTile.from_str("DDWW_xxxx_I"), + 0x1AB: MapTile.from_str("DDWW_xxxx_O"), } + +COLORED_DOOR_TILE_IDS = {tile: id for id, tile in COLORED_DOOR_TILES.items()} +NORMAL_DOOR_TILE_IDS = {tile: id for id, tile in NORMAL_DOOR_TILES.items()} + +ALL_DOOR_TILES = COLORED_DOOR_TILES | NORMAL_DOOR_TILES +ALL_DOOR_TILE_IDS = COLORED_DOOR_TILE_IDS | NORMAL_DOOR_TILE_IDS diff --git a/src/mars_patcher/door_locks.py b/src/mars_patcher/door_locks.py index 5037876..9c98014 100644 --- a/src/mars_patcher/door_locks.py +++ b/src/mars_patcher/door_locks.py @@ -1,14 +1,22 @@ +import logging +from collections import defaultdict from enum import Enum +from typing import Annotated, TypedDict from mars_patcher.auto_generated_types import MarsschemaDoorlocksItem +from mars_patcher.common_types import AreaId, AreaRoomPair, RoomId from mars_patcher.constants.game_data import ( area_doors_ptrs, hatch_lock_event_count, hatch_lock_events, - minimap_count, ) -from mars_patcher.constants.minimap_tiles import COLORED_DOOR_TILES, NORMAL_DOOR_TILES -from mars_patcher.minimap import MINIMAP_DIM, Minimap +from mars_patcher.constants.minimap_tiles import ( + ALL_DOOR_TILE_IDS, + ALL_DOOR_TILES, + ColoredDoor, + Edge, +) +from mars_patcher.minimap import Minimap from mars_patcher.rom import Rom from mars_patcher.room_entry import BlockLayer, RoomEntry @@ -58,43 +66,69 @@ class HatchLock(Enum): for val in vals: CLIP_TO_HATCH_LOCK[val] = lock + EXCLUDED_DOORS = { (0, 0xB4), # Restricted lab escape } +HatchSlot = Annotated[int, "0 <= value <= 5"] + +MinimapLocation = tuple[int, int, RoomId] +"""`(X, Y, RoomId)`""" + + +class MinimapLockChanges(TypedDict, total=False): + left: HatchLock + right: HatchLock + # TODO: # - Optimize by only loading rooms that contain doors to modify # - Split into more than one function for readability def set_door_locks(rom: Rom, data: list[MarsschemaDoorlocksItem]) -> None: door_locks = parse_door_lock_data(data) + # Go through all doors in game in order doors_ptrs = area_doors_ptrs(rom) - loaded_rooms: dict[tuple[int, int], RoomEntry] = {} + loaded_rooms: dict[AreaRoomPair, RoomEntry] = {} + # (AreaID, RoomID): (BG1, Clipdata) - loaded_bg1_and_clip: dict[tuple[int, int], tuple[BlockLayer, BlockLayer]] = {} + loaded_bg1_and_clip: dict[AreaRoomPair, tuple[BlockLayer, BlockLayer]] = {} + # (AreaID, RoomID): (CappedSlot, CaplessSlot) - orig_room_hatch_slots: dict[tuple[int, int], tuple[int, int]] = {} - new_room_hatch_slots: dict[tuple[int, int], tuple[int, int]] = {} - # (AreaID, RoomID): {OrigSlot: NewSlot} - hatch_slot_changes: dict[tuple[int, int], dict[int, int]] = {} + orig_room_hatch_slots: dict[AreaRoomPair, tuple[HatchSlot, HatchSlot]] = {} + new_room_hatch_slots: dict[AreaRoomPair, tuple[HatchSlot, HatchSlot]] = {} + + hatch_slot_changes: dict[AreaRoomPair, dict[HatchSlot, HatchSlot]] = {} + + def factory() -> dict: + return defaultdict(dict) + + # AreaID: {(MinimapX, MinimapY, RoomID): {"left" | "right": HatchLock}} + minimap_changes: dict[AreaId, dict[MinimapLocation, MinimapLockChanges]] = defaultdict(factory) + for area in range(7): area_addr = rom.read_ptr(doors_ptrs + area * 4) for door in range(256): door_addr = area_addr + door * 0xC door_type = rom.read_8(door_addr) + # Check if at end of list if door_type == 0: break + # Skip doors that mage marks as deleted room = rom.read_8(door_addr + 1) if room == 0xFF: continue + # Skip excluded doors and doors that aren't lockable hatches lock = door_locks.get((area, door)) if (area, door) in EXCLUDED_DOORS or door_type & 0xF != 4: - assert lock is None, f"Area {area} door {door} cannot have its lock changed" + if lock is not None: + logging.error(f"Area {area} door {door} cannot have its lock changed") continue + # Load room's BG1 and clipdata if not already loaded area_room = (area, room) room_entry = loaded_rooms.get(area_room) @@ -116,9 +150,11 @@ def set_door_locks(rom: Rom, data: list[MarsschemaDoorlocksItem]) -> None: x_exit = rom.read_8(door_addr + 7) facing_right = x_exit < 0x80 dx = 1 if facing_right else -1 + # Get hatch position hatch_x = rom.read_8(door_addr + 2) + dx hatch_y = rom.read_8(door_addr + 4) + # Get original hatch slot number capped_slot, capless_slot = orig_room_hatch_slots[area_room] clip_val = clip.get_block_value(hatch_x, hatch_y) @@ -132,6 +168,7 @@ def set_door_locks(rom: Rom, data: list[MarsschemaDoorlocksItem]) -> None: orig_hatch_slot = capless_slot capless_slot -= 1 orig_room_hatch_slots[area_room] = (capped_slot, capless_slot) + # Get new hatch slot number capped_slot, capless_slot = new_room_hatch_slots[area_room] if lock == HatchLock.LOCKED: @@ -147,44 +184,59 @@ def set_door_locks(rom: Rom, data: list[MarsschemaDoorlocksItem]) -> None: new_hatch_slot = capless_slot capless_slot -= 1 new_room_hatch_slots[area_room] = (capped_slot, capless_slot) + if new_hatch_slot != orig_hatch_slot: hatch_slot_changes[area_room][orig_hatch_slot] = new_hatch_slot + + # Map tiles + if lock is not None: + screen_offset_x = (hatch_x - 2) // 15 + screen_offset_y = (hatch_y - 2) // 10 + + minimap_x = room_entry.map_x + screen_offset_x + minimap_y = room_entry.map_y + screen_offset_y + + minimap_areas = [area] + if area == 0: + minimap_areas = [0, 9] # Main deck seemingly has two maps? + for minimap_area in minimap_areas: + map_tile = minimap_changes[minimap_area][minimap_x, minimap_y, room] + if facing_right: + map_tile["left"] = lock + else: + map_tile["right"] = lock + # Overwrite BG1 and clipdata if lock is None: # Even if a hatch's lock hasn't changed, its slot may have changed lock = CLIP_TO_HATCH_LOCK.get(clip_val) if lock is None: continue + bg1_val = BG1_VALUES[lock] if facing_right: bg1_val += 1 + clip_val = CLIP_VALUES[lock][new_hatch_slot] + for y in range(4): bg1.set_block_value(hatch_x, hatch_y + y, bg1_val) clip.set_block_value(hatch_x, hatch_y + y, clip_val) bg1_val += 0x10 + # Write BG1 and clipdata for each room for bg1, clip in loaded_bg1_and_clip.values(): bg1.write() clip.write() - fix_hatch_lock_events(rom, hatch_slot_changes) + fix_hatch_lock_events(rom, hatch_slot_changes) -def remove_door_colors_on_minimap(rom: Rom) -> None: - for id in range(minimap_count(rom)): - with Minimap(rom, id) as minimap: - for y in range(MINIMAP_DIM): - for x in range(MINIMAP_DIM): - tile, pal, h_flip, v_flip = minimap.get_tile_value(x, y) - tile_type = COLORED_DOOR_TILES.get(tile) - if tile_type is not None: - tile = NORMAL_DOOR_TILES[tile_type] - minimap.set_tile_value(x, y, tile, pal, h_flip, v_flip) + change_minimap_tiles(rom, minimap_changes) -def parse_door_lock_data(data: list[MarsschemaDoorlocksItem]) -> dict[tuple[int, int], HatchLock]: +def parse_door_lock_data(data: list[MarsschemaDoorlocksItem]) -> dict[AreaRoomPair, HatchLock]: """Returns a dictionary of `(AreaID, RoomID): HatchLock` from the input data.""" - door_locks: dict[tuple[int, int], HatchLock] = {} + door_locks: dict[AreaRoomPair, HatchLock] = {} for entry in data: area_door = (entry["Area"], entry["Door"]) lock = HATCH_LOCK_ENUMS[entry["LockType"]] @@ -193,7 +245,7 @@ def parse_door_lock_data(data: list[MarsschemaDoorlocksItem]) -> dict[tuple[int, def fix_hatch_lock_events( - rom: Rom, hatch_slot_changes: dict[tuple[int, int], dict[int, int]] + rom: Rom, hatch_slot_changes: dict[AreaRoomPair, dict[HatchSlot, HatchSlot]] ) -> None: hatch_locks_addr = hatch_lock_events(rom) count = hatch_lock_event_count(rom) @@ -214,3 +266,97 @@ def fix_hatch_lock_events( remain &= ~(1 << new_slot) new_flags |= hatch_flags & remain rom.write_8(addr + 3, new_flags) + + +def change_minimap_tiles( + rom: Rom, minimap_changes: dict[AreaId, dict[MinimapLocation, MinimapLockChanges]] +) -> None: + MAP_EDGES: dict[HatchLock, Edge | ColoredDoor] = { + HatchLock.OPEN: Edge.DOOR, + HatchLock.LEVEL_0: Edge.DOOR, + HatchLock.LEVEL_1: ColoredDoor.BLUE, + HatchLock.LEVEL_2: ColoredDoor.GREEN, + HatchLock.LEVEL_3: ColoredDoor.YELLOW, + HatchLock.LEVEL_4: ColoredDoor.RED, + HatchLock.LOCKED: Edge.DOOR, + # HatchLock.LOCKED: Edge.WALL, + } + + for area, area_map in minimap_changes.items(): + with Minimap(rom, area) as minimap: + for (x, y, room), tile_changes in area_map.items(): + tile_id, palette, h_flip, v_flip = minimap.get_tile_value(x, y) + + tile_data = ALL_DOOR_TILES[tile_id] + + # Account for h_flip before changing edges + left = tile_changes.get("left") + right = tile_changes.get("right") + if h_flip: + left, right = right, left + + # Replace edges + edges = tile_data.edges + if left is not None: + edges = edges._replace(left=MAP_EDGES[left]) + if right is not None: + edges = edges._replace(right=MAP_EDGES[right]) + og_new_tile_data = tile_data._replace(edges=edges) + new_tile_data = og_new_tile_data + + def tile_exists() -> bool: + return new_tile_data in ALL_DOOR_TILE_IDS + + if new_tile_data.content.can_h_flip and not tile_exists(): + # Try flipping horizontally + new_tile_data = og_new_tile_data.h_flip() + if tile_exists(): + h_flip = not h_flip + + if new_tile_data.content.can_v_flip and not tile_exists(): + # Try flipping vertically + new_tile_data = og_new_tile_data.v_flip() + if tile_exists(): + v_flip = not v_flip + + if ( + new_tile_data.content.can_h_flip + and new_tile_data.content.can_v_flip + and not tile_exists() + ): + # Try flipping it both ways + new_tile_data = og_new_tile_data.v_flip() + new_tile_data = new_tile_data.h_flip() + if tile_exists(): + v_flip = not v_flip + h_flip = not h_flip + + if not tile_exists(): + logging.debug( + "Could not edit map tile door icons for " + f"area {area} room {room:X}. ({x:X}, {y:X})." + ) + logging.debug(f" Desired tile: {og_new_tile_data.as_str}") + logging.debug(" Falling back to unlocked doors.") + + # Try replacing with open doors + if (left is not None) and tile_data.edges.left.is_door: + edges = edges._replace(left=Edge.DOOR) + if (right is not None) and tile_data.edges.right.is_door: + edges = edges._replace(right=Edge.DOOR) + new_tile_data = og_new_tile_data._replace(edges=edges) + + if tile_exists(): + logging.debug(" Still no luck. Using vanilla tile.") + + logging.debug("") + + if tile_exists(): + minimap.set_tile_value( + x, + y, + ALL_DOOR_TILE_IDS[new_tile_data], + palette, + h_flip, + v_flip, + ) diff --git a/src/mars_patcher/minimap.py b/src/mars_patcher/minimap.py index 403009f..14cdc94 100644 --- a/src/mars_patcher/minimap.py +++ b/src/mars_patcher/minimap.py @@ -8,6 +8,7 @@ if TYPE_CHECKING: from types import TracebackType + from mars_patcher.common_types import MinimapId from mars_patcher.rom import Rom MINIMAP_DIM = 32 @@ -16,7 +17,7 @@ class Minimap: """Class for reading/writing minimap data and setting tiles.""" - def __init__(self, rom: Rom, id: int): + def __init__(self, rom: Rom, id: MinimapId): self.rom = rom self.pointer = minimap_ptrs(rom) + (id * 4) addr = rom.read_ptr(self.pointer) diff --git a/src/mars_patcher/misc_patches.py b/src/mars_patcher/misc_patches.py index 9742dce..5e67833 100644 --- a/src/mars_patcher/misc_patches.py +++ b/src/mars_patcher/misc_patches.py @@ -89,3 +89,7 @@ def apply_anti_softlock_edits(rom: Rom) -> None: def apply_reveal_hidden_tiles(rom: Rom) -> None: rom.write_8(ReservedConstants.REVEAL_HIDDEN_TILES_ADDR, 1) + + +def apply_reveal_unexplored_doors(rom: Rom) -> None: + apply_patch_in_asm_path(rom, "unhidden_map_doors.ips") diff --git a/src/mars_patcher/patcher.py b/src/mars_patcher/patcher.py index a18b42f..2b05bda 100644 --- a/src/mars_patcher/patcher.py +++ b/src/mars_patcher/patcher.py @@ -7,7 +7,7 @@ from mars_patcher.connections import Connections from mars_patcher.credits import write_credits from mars_patcher.data import get_data_path -from mars_patcher.door_locks import remove_door_colors_on_minimap, set_door_locks +from mars_patcher.door_locks import set_door_locks from mars_patcher.item_patcher import ItemPatcher, set_required_metroid_count, set_tank_increments from mars_patcher.level_edits import apply_level_edits from mars_patcher.locations import LocationSettings @@ -121,11 +121,6 @@ def patch( conns = Connections(rom) conns.set_shortcut_connections(patch_data["SectorShortcuts"]) - # Door locks - if door_locks := patch_data.get("DoorLocks", []): - status_update("Writing door locks...", -1) - set_door_locks(rom, door_locks) - # Hints if nav_text := patch_data.get("NavigationText", {}): status_update("Writing navigation text...", -1) @@ -172,18 +167,24 @@ def patch( if patch_data.get("UnexploredMap"): apply_unexplored_map(rom) + # TODO: https://github.com/MetroidAdvRandomizerSystem/mars-fusion-asm/pull/217 + # if not "HideDoorsOnMinimap" in patch_data: + # apply_reveal_unexplored_doors(rom) + if patch_data.get("RevealHiddenTiles"): apply_reveal_hidden_tiles(rom) - if patch_data.get("DoorLocks") or "HideDoorsOnMinimap" in patch_data: - remove_door_colors_on_minimap(rom) - if "LevelEdits" in patch_data: apply_level_edits(rom, patch_data["LevelEdits"]) if "MinimapEdits" in patch_data: apply_minimap_edits(rom, patch_data["MinimapEdits"]) + # Door locks + if door_locks := patch_data.get("DoorLocks", []): + status_update("Writing door locks...", -1) + set_door_locks(rom, door_locks) + write_seed_hash(rom, patch_data["SeedHash"]) rom.save(output_path) diff --git a/src/mars_patcher/room_entry.py b/src/mars_patcher/room_entry.py index 4f55b31..e518cf2 100644 --- a/src/mars_patcher/room_entry.py +++ b/src/mars_patcher/room_entry.py @@ -52,6 +52,14 @@ def load_bg2(self) -> BlockLayer: def load_clip(self) -> BlockLayer: return BlockLayer(self.rom, self.clip_ptr()) + @property + def map_x(self) -> int: + return self.rom.read_8(self.addr + 0x35) + + @property + def map_y(self) -> int: + return self.rom.read_8(self.addr + 0x36) + class BlockLayer: def __enter__(self) -> BlockLayer: