From c93a78938855122f83322ddc60027c0391b95f44 Mon Sep 17 00:00:00 2001 From: biosp4rk <37962487+biosp4rk@users.noreply.github.com> Date: Fri, 29 Aug 2025 15:43:11 -0700 Subject: [PATCH 01/12] Minimap tile creator --- src/mars_patcher/constants/game_data.py | 14 + src/mars_patcher/constants/minimap_tiles.py | 147 ++++++- src/mars_patcher/door_locks.py | 69 ++-- src/mars_patcher/minimap_tile_creator.py | 437 ++++++++++++++++++++ 4 files changed, 619 insertions(+), 48 deletions(-) create mode 100644 src/mars_patcher/minimap_tile_creator.py diff --git a/src/mars_patcher/constants/game_data.py b/src/mars_patcher/constants/game_data.py index 365e027..302bf84 100644 --- a/src/mars_patcher/constants/game_data.py +++ b/src/mars_patcher/constants/game_data.py @@ -503,3 +503,17 @@ def minimap_count(rom: Rom) -> int: elif rom.game == Game.ZM: return 11 raise ValueError(rom.game) + + +def minimap_graphics(rom: Rom) -> int: + """Returns the address of the minimap tile graphics.""" + if rom.game == Game.MF: + if rom.region == Region.U: + return 0x561FA8 + elif rom.region == Region.E: + return 0x55B6E4 + elif rom.region == Region.J: + return 0x55D764 + elif rom.region == Region.C: + return 0x561FA8 + raise ValueError(rom.game, rom.region) diff --git a/src/mars_patcher/constants/minimap_tiles.py b/src/mars_patcher/constants/minimap_tiles.py index 481409b..f3cd910 100644 --- a/src/mars_patcher/constants/minimap_tiles.py +++ b/src/mars_patcher/constants/minimap_tiles.py @@ -145,6 +145,7 @@ def v_flip(self) -> TileCorners: # Contents class Content(Enum): EMPTY = "x" + EMPTY_RED_WALLS = "w" NAVIGATION = "N" SAVE = "S" RECHARGE = "R" @@ -152,14 +153,19 @@ class Content(Enum): DATA = "D" ITEM = "I" OBTAINED_ITEM = "O" - BOSS = "B" + BOSS_RIGHT_DOWNLOADED = "B-R-D" # Serris skeleton + BOSS_BOTTOM_LEFT_EXPLORED = "B-BL-E" # Serris + BOSS_TOP_LEFT_DOWNLOADED = "B-TL-D" # BOX 1 + BOSS_LEFT_EXPLORED = "B-L-E" # Mega-X + BOSS_TOP_RIGHT_BOTH = "B-TR-B" # Nightmare + BOSS_TOP_RIGHT_EXPLORED = "B-TR-E" # BOX 2 GUNSHIP = "G" GUNSHIP_EDGE = "P" SECURITY = "K" - AUXILLARY_POWER = "X" + AUXILLARY_POWER = "Y" ANIMALS = "A" - BOILER_PAD = "b" TUNNEL = "T" + BOILER_PAD = "L" @property def can_h_flip(self) -> bool: @@ -171,6 +177,9 @@ def can_h_flip(self) -> bool: Content.DATA, Content.GUNSHIP, Content.GUNSHIP_EDGE, + Content.SECURITY, + Content.AUXILLARY_POWER, + Content.BOILER_PAD, } return self not in exclude @@ -181,9 +190,18 @@ def can_v_flip(self) -> bool: Content.SAVE, Content.RECHARGE, Content.HIDDEN_RECHARGE, + Content.BOSS_RIGHT_DOWNLOADED, + Content.BOSS_BOTTOM_LEFT_EXPLORED, + Content.BOSS_TOP_LEFT_DOWNLOADED, + Content.BOSS_LEFT_EXPLORED, + Content.BOSS_TOP_RIGHT_BOTH, + Content.BOSS_TOP_RIGHT_EXPLORED, Content.GUNSHIP, Content.GUNSHIP_EDGE, - Content.BOSS, + Content.SECURITY, + Content.AUXILLARY_POWER, + Content.TUNNEL, + Content.BOILER_PAD, } return self not in exclude @@ -200,8 +218,6 @@ def as_str(self) -> str: @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), @@ -424,7 +440,6 @@ def v_flip(self) -> MapTile: 0x1AC: MapTile.from_str("WYYW_xxxx_I"), 0x1AD: MapTile.from_str("WYYW_xxxx_O"), # New Tiles - 0x10B: MapTile.from_str("xxGW_Cxxx_B"), 0x16C: MapTile.from_str("xWBW_xxxx_K"), 0x16D: MapTile.from_str("WWGW_xxxx_K"), 0x16E: MapTile.from_str("WWRW_xxxx_K"), @@ -462,6 +477,7 @@ def v_flip(self) -> MapTile: 0x084: MapTile.from_str("WDDW_xxxx_x"), 0x087: MapTile.from_str("WWWW_xxxx_x"), 0x0B4: MapTile.from_str("WSDW_xxxx_I"), + 0x0B5: MapTile.from_str("WSDW_xxxx_O"), 0x0B6: MapTile.from_str("WSDW_xxxx_x"), 0x0E0: MapTile.from_str("DDxW_xxxx_x"), 0x0E1: MapTile.from_str("DDxD_xxxx_x"), @@ -474,10 +490,10 @@ def v_flip(self) -> MapTile: 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"), + 0x108: MapTile.from_str("xxDW_xxxx_B-TL-D"), + 0x109: MapTile.from_str("WDxW_xxxx_B-R-D"), + 0x10D: MapTile.from_str("xxDx_xxxx_B-L-E"), + 0x10F: MapTile.from_str("xDxW_xxxx_B-TR-B"), 0x120: MapTile.from_str("DDWW_xxxx_x"), 0x121: MapTile.from_str("DDDW_xxxx_x"), 0x122: MapTile.from_str("DDWD_xxxx_x"), @@ -487,7 +503,6 @@ def v_flip(self) -> MapTile: 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"), @@ -545,15 +560,12 @@ def v_flip(self) -> MapTile: 0x1A9: MapTile.from_str("xWWx_xxxx_O"), 0x1AA: MapTile.from_str("DDWW_xxxx_I"), 0x1AB: MapTile.from_str("DDWW_xxxx_O"), - 0x0B5: MapTile.from_str("WSDW_xxxx_O"), # New Tiles - 0x0A4: MapTile.from_str("WDxW_xxxx_T"), - 0x0D4: MapTile.from_str("WSDW_xxxx_I"), - 0x0D5: MapTile.from_str("WSDW_xxxx_O"), - 0x10A: MapTile.from_str("WxDx_xxxx_B"), - 0x10C: MapTile.from_str("xDxW_xCxx_B"), - 0x170: MapTile.from_str("WWDx_xxxx_x"), - 0x172: MapTile.from_str("WDDW_xxxx_X"), + 0x0A4: MapTile.from_str("WDSW_xxxx_T"), + 0x10A: MapTile.from_str("WxDx_xxxx_B-BL-E"), + 0x10C: MapTile.from_str("xDxW_xCxx_B-TR-E"), + 0x170: MapTile.from_str("WWDx_xxxx_w"), + 0x172: MapTile.from_str("WDDW_xxxx_Y"), } COLORED_DOOR_TILE_IDS = {tile: id for id, tile in COLORED_DOOR_TILES.items()} @@ -561,3 +573,98 @@ def v_flip(self) -> MapTile: ALL_DOOR_TILES = COLORED_DOOR_TILES | NORMAL_DOOR_TILES ALL_DOOR_TILE_IDS = COLORED_DOOR_TILE_IDS | NORMAL_DOOR_TILE_IDS + +BLANK_TILE_IDS = [ + 0x067, + 0x071, + 0x076, + 0x087, + 0x08C, + 0x091, + 0x0A1, + 0x0B2, + 0x0B3, + 0x0B8, + 0x0B9, + 0x0BA, + 0x0BB, + 0x0BC, + 0x0BD, + 0x0BE, + 0x0C0, + 0x0C1, + 0x0D2, + 0x0D3, + 0x0D8, + 0x0D9, + 0x0DA, + 0x0DB, + 0x0DC, + 0x0DD, + 0x0DE, + 0x0E9, + 0x0EE, + 0x0F3, + 0x0F9, + 0x0FA, + 0x0FB, + 0x0FC, + 0x0FD, + 0x0FE, + 0x0FF, + 0x107, + 0x110, + 0x111, + 0x112, + 0x113, + 0x114, + 0x115, + 0x116, + 0x117, + 0x118, + 0x119, + 0x11A, + 0x11B, + 0x11C, + 0x11D, + 0x11E, + 0x11F, + 0x12D, + 0x12E, + 0x12F, + 0x130, + 0x131, + 0x132, + 0x133, + 0x134, + 0x135, + 0x136, + 0x137, + 0x13B, + 0x13C, + 0x13D, + 0x13E, + 0x13F, + 0x15F, + 0x173, + 0x174, + 0x175, + 0x176, + 0x177, + 0x1B0, + 0x1B1, + 0x1B2, + 0x1B3, + 0x1B4, + 0x1B5, + 0x1B6, + 0x1B7, + 0x1B8, + 0x1B9, + 0x1BA, + 0x1BB, + 0x1BC, + 0x1BD, + 0x1BE, + 0x1BF, +] diff --git a/src/mars_patcher/door_locks.py b/src/mars_patcher/door_locks.py index fecf624..ac80e4e 100644 --- a/src/mars_patcher/door_locks.py +++ b/src/mars_patcher/door_locks.py @@ -9,14 +9,17 @@ area_doors_ptrs, hatch_lock_event_count, hatch_lock_events, + minimap_graphics, ) from mars_patcher.constants.minimap_tiles import ( ALL_DOOR_TILE_IDS, ALL_DOOR_TILES, + BLANK_TILE_IDS, ColoredDoor, Edge, ) from mars_patcher.minimap import Minimap +from mars_patcher.minimap_tile_creator import create_tile from mars_patcher.rom import Rom from mars_patcher.room_entry import BlockLayer, RoomEntry @@ -197,15 +200,11 @@ def factory() -> dict: 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 + map_tile = minimap_changes[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: @@ -283,6 +282,8 @@ def change_minimap_tiles( # HatchLock.LOCKED: Edge.WALL, } + next_blank_tile = 0 + for area, area_map in minimap_changes.items(): with Minimap(rom, area) as minimap: for (x, y, room), tile_changes in area_map.items(): @@ -302,21 +303,21 @@ def change_minimap_tiles( 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 + orig_new_tile_data = tile_data._replace(edges=edges) + new_tile_data = orig_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() + new_tile_data = orig_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() + new_tile_data = orig_new_tile_data.v_flip() if tile_exists(): v_flip = not v_flip @@ -326,7 +327,7 @@ def tile_exists() -> bool: and not tile_exists() ): # Try flipping it both ways - new_tile_data = og_new_tile_data.v_flip() + new_tile_data = orig_new_tile_data.v_flip() new_tile_data = new_tile_data.h_flip() if tile_exists(): v_flip = not v_flip @@ -337,20 +338,32 @@ def tile_exists() -> bool: "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("") + logging.debug(f" Desired tile: {orig_new_tile_data.as_str}") + + if next_blank_tile < len(BLANK_TILE_IDS): + # Create a new tile and replace the graphics + gfx = create_tile(orig_new_tile_data) + tile_id = BLANK_TILE_IDS[next_blank_tile] + addr = minimap_graphics(rom) + tile_id * 32 + rom.write_bytes(addr, gfx) + + new_tile_data = orig_new_tile_data + ALL_DOOR_TILE_IDS[new_tile_data] = tile_id + logging.debug(f" Created new tile: 0x{tile_id:X}.") + next_blank_tile += 1 + else: + # No blank tiles remaining, try replacing with open doors + logging.debug(" No blank tiles available, trying 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 = orig_new_tile_data._replace(edges=edges) + + if not tile_exists(): + logging.debug(" Still no luck. Using vanilla tile.") + + logging.debug("") if tile_exists(): minimap.set_tile_value( diff --git a/src/mars_patcher/minimap_tile_creator.py b/src/mars_patcher/minimap_tile_creator.py new file mode 100644 index 0000000..f41f416 --- /dev/null +++ b/src/mars_patcher/minimap_tile_creator.py @@ -0,0 +1,437 @@ +from enum import Enum + +from mars_patcher.constants.minimap_tiles import ColoredDoor, Content, Edge, MapTile, TileEdges + +# Used for the edges of tiles, also used for boss icons that are always visible +COLOR_WHITE_OUTLINE = 1 +# Used for normal connections between tiles, this appears as a wall when the +# tile is unexplored. Also used for boss icons that disappear when explored +COLOR_CONNECTION_BG = 2 +# Used for the background of tiles, either gray (unexplored), pink (normal), +# or green (hidden) +COLOR_BG = 3 +# Used for the outline of blank tiles or tiles that don't fill in the whole square +COLOR_BLANK_OUTLINE = 4 +# Used for the background of blank tiles or tiles that don't fill in the whole square +COLOR_BLANK_BG = 5 +# Used for the white circle/dot representing items, this hides the item when +# the tile is unexplored +COLOR_WHITE_ITEM = 6 +# Used for tiles with a letter in them (navigation, save, etc.) +COLOR_YELLOW_LETTER = 7 + +# Used for colored pixels on the edge of tiles, including hatches and the +# outline of tiles with letters. These appear white when the tile is unexplored +COLOR_BLUE_EDGE = 8 +COLOR_GREEN_EDGE = 9 +COLOR_RED_EDGE = 10 +COLOR_YELLOW_EDGE = 11 + +COLOR_COUNT = 4 + +# Used for colored pixels not on the edge of tiles, including hatches and +# various icons. These are hidden (gray) when the tile is unexplored +COLOR_BLUE_HIDDEN = COLOR_BLUE_EDGE + COLOR_COUNT +COLOR_GREEN_HIDDEN = COLOR_GREEN_EDGE + COLOR_COUNT +COLOR_RED_HIDDEN = COLOR_RED_EDGE + COLOR_COUNT +COLOR_YELLOW_HIDDEN = COLOR_YELLOW_EDGE + COLOR_COUNT + +PIXELS_ITEM = [ + ". . . . . . . .", + ". . . . . . . .", + ". . . # # . . .", + ". . # . . # . .", + ". . # . . # . .", + ". . . # # . . .", + ". . . . . . . .", + ". . . . . . . .", +] +PIXELS_NAVIGATION = [ + ". . . . . . . .", + ". . . . . . . .", + ". . # . . # . .", + ". . # # . # . .", + ". . # . # # . .", + ". . # . . # . .", + ". . . . . . . .", + ". . . . . . . .", +] +PIXELS_SAVE = [ + ". . . . . . . .", + ". . . . . . . .", + ". . . # # # . .", + ". . # . . . . .", + ". . . # # . . .", + ". . . . . # . .", + ". . # # # . . .", + ". . . . . . . .", +] +PIXELS_RECHARGE = [ + ". . . . . . . .", + ". . . . . . . .", + ". . # # # . . .", + ". . # . . # . .", + ". . # # # . . .", + ". . # . . # . .", + ". . . . . . . .", + ". . . . . . . .", +] +PIXELS_DATA = [ + ". . . . . . . .", + ". . . . . . . .", + ". . # # # . . .", + ". . # . . # . .", + ". . # . . # . .", + ". . # # # . . .", + ". . . . . . . .", + ". . . . . . . .", +] +PIXELS_SECURITY = [ + ". . . . . . . .", + ". . . . . . . .", + ". . # . # . . .", + ". . # # . . . .", + ". . # . # . . .", + ". . # . # . . .", + ". . . . . . . .", + ". . . . . . . .", +] +PIXELS_AUXILIARY = [ + ". . . . . . . .", + ". . . . . . . .", + ". - # . # . . .", + ". - . # . . . .", + ". - # . # . . .", + ". - # . # . . .", + ". . . . . . . .", + ". . . . . . . .", +] +PIXELS_MAP = [ + ". . . . . . . .", + ". . . . . . . .", + ". # . . . # . .", + ". # # . # # . .", + ". # . # . # . .", + ". # . . . # . .", + ". . . . . . . .", + ". . . . . . . .", +] + +PIXELS_BOSS = [ + ". # . . # .", + ". # # # # .", + "# # # # # #", + "# . # # . #", + ". # # # # .", + ". # . . # .", +] + +PIXELS_TUNNEL = [ + "4 4 4 4 4 4 4 4", + "4 5 5 5 1 5 5 4", + "1 1 1 5 1 1 5 4", + "4 5 5 5 1 1 1 4", + "4 5 5 5 1 1 5 4", + "1 1 1 5 1 5 5 4", + "4 5 5 5 5 5 5 4", + "4 4 4 4 4 4 4 4", +] + +PIXELS_MAJOR = [ + ". . . . . . . .", + ". . . 6 6 . . .", + ". - 6 C C 6 - .", + ". 6 C 6 C C 6 .", + ". 6 C C C C 6 .", + ". - 6 C C 6 - .", + ". . . 6 6 . . .", + ". . . . . . . .", +] +PIXELS_CHOZO = [ + ". . . . . . . .", + ". . 6 6 6 . . .", + ". 6 E E E 6 - .", + ". E E 6 E E 6 .", + ". E E E E E 6 .", + ". E 6 E E E 6 .", + ". 6 6 E E E 6 .", + ". . . . . . . .", +] + +HAS_RED_OUTLINE = { + Content.EMPTY_RED_WALLS, + Content.NAVIGATION, + Content.SAVE, + Content.RECHARGE, + Content.HIDDEN_RECHARGE, + Content.DATA, + Content.SECURITY, +} + + +class TileSide(Enum): + TOP = 0 + LEFT = 1 + RIGHT = 2 + BOTTOM = 3 + + +def create_tile(tile: MapTile) -> bytearray: + gfx = bytearray([(COLOR_BG << 4) | COLOR_BG for _ in range(32)]) + + # Handle tunnels separately + if tile.content == Content.TUNNEL: + draw_tunnel(gfx, tile.edges) + return gfx + + # Corners + if tile.corners.top_left: + set_pixel(gfx, COLOR_WHITE_OUTLINE, 0, 0) + if tile.corners.top_right: + set_pixel(gfx, COLOR_WHITE_OUTLINE, 7, 0) + if tile.corners.bottom_left: + set_pixel(gfx, COLOR_WHITE_OUTLINE, 0, 7) + if tile.corners.bottom_right: + set_pixel(gfx, COLOR_WHITE_OUTLINE, 7, 7) + + # Edges + for edge, side in zip(tile.edges, TileSide): + if isinstance(edge, Edge): + if edge == Edge.WALL: + draw_wall(gfx, side) + elif edge == Edge.DOOR: + draw_connection(gfx, side) + elif isinstance(edge, ColoredDoor): + if edge == ColoredDoor.BLUE: + draw_hatch_mf(gfx, COLOR_BLUE_EDGE, side) + elif edge == ColoredDoor.GREEN: + draw_hatch_mf(gfx, COLOR_GREEN_EDGE, side) + elif edge == ColoredDoor.YELLOW: + draw_hatch_mf(gfx, COLOR_YELLOW_EDGE, side) + elif edge == ColoredDoor.RED: + draw_hatch_mf(gfx, COLOR_RED_EDGE, side) + # TODO: ZM hatch types + + # Content + # The following don't have doors near them so they aren't needed: + # Gunship, Animals, Boiler Pad + if tile.content in HAS_RED_OUTLINE: + draw_red_outline(gfx, tile.edges) + + if tile.content == Content.NAVIGATION: + draw_pixel_art(gfx, COLOR_YELLOW_LETTER, PIXELS_NAVIGATION) + elif tile.content == Content.SAVE: + draw_pixel_art(gfx, COLOR_YELLOW_LETTER, PIXELS_SAVE) + elif tile.content == Content.RECHARGE: + draw_pixel_art(gfx, COLOR_YELLOW_LETTER, PIXELS_RECHARGE) + elif tile.content == Content.HIDDEN_RECHARGE: + draw_pixel_art(gfx, COLOR_YELLOW_HIDDEN, PIXELS_RECHARGE) + elif tile.content == Content.DATA: + draw_pixel_art(gfx, COLOR_YELLOW_LETTER, PIXELS_DATA) + elif tile.content == Content.ITEM: + draw_pixel_art(gfx, COLOR_WHITE_ITEM, PIXELS_ITEM) + elif tile.content == Content.OBTAINED_ITEM: + draw_obtained_tank(gfx) + elif tile.content == Content.BOSS_RIGHT_DOWNLOADED: + draw_boss_room(gfx, COLOR_CONNECTION_BG, 2, 1) + elif tile.content == Content.BOSS_BOTTOM_LEFT_EXPLORED: + draw_boss_room(gfx, COLOR_WHITE_ITEM, 0, 2) + elif tile.content == Content.BOSS_TOP_LEFT_DOWNLOADED: + draw_boss_room(gfx, COLOR_CONNECTION_BG, 0, 0) + elif tile.content == Content.BOSS_LEFT_EXPLORED: + draw_boss_room(gfx, COLOR_WHITE_ITEM, 0, 1) + elif tile.content == Content.BOSS_TOP_RIGHT_BOTH: + draw_boss_room(gfx, COLOR_WHITE_OUTLINE, 2, 0) + elif tile.content == Content.BOSS_TOP_RIGHT_EXPLORED: + draw_boss_room(gfx, COLOR_WHITE_ITEM, 2, 0) + elif tile.content == Content.GUNSHIP_EDGE: + draw_gunship_edge(gfx) + elif tile.content == Content.SECURITY: + draw_pixel_art(gfx, COLOR_YELLOW_LETTER, PIXELS_SECURITY) + elif tile.content == Content.AUXILLARY_POWER: + draw_pixel_art(gfx, COLOR_YELLOW_LETTER, PIXELS_AUXILIARY) + elif tile.content != Content.EMPTY and tile.content != Content.EMPTY_RED_WALLS: + raise ValueError(f"No implementation to create tile content {tile.content}") + + return gfx + + +def set_pixel(gfx: bytearray, color: int, x: int, y: int) -> None: + index = (y * 8 + x) // 2 + if x % 2 == 0: + gfx[index] = (gfx[index] & 0xF0) | color + else: + gfx[index] = (gfx[index] & 0xF) | (color << 4) + + +def draw_pixel_art(gfx: bytearray, color: int, art: list[str]) -> None: + for y, row in enumerate(art): + for x, c in enumerate(row.replace(" ", "")): + if c != ".": + p = COLOR_BG if c == "-" else color + set_pixel(gfx, p, x, y) + + +def draw_colored_pixel_art(gfx: bytearray, art: list[str]) -> None: + for y, row in enumerate(art): + for x, c in enumerate(row.replace(" ", "")): + if c != ".": + color = COLOR_BG if c == "-" else int(c, 16) + set_pixel(gfx, color, x, y) + + +def draw_wall(gfx: bytearray, side: TileSide) -> None: + n = 0 if side == TileSide.TOP or side == TileSide.LEFT else 7 + if side == TileSide.TOP or side == TileSide.BOTTOM: + for x in range(8): + set_pixel(gfx, COLOR_WHITE_OUTLINE, x, n) + elif side == TileSide.LEFT or side == TileSide.RIGHT: + for y in range(8): + set_pixel(gfx, COLOR_WHITE_OUTLINE, n, y) + + +def draw_connection(gfx: bytearray, side: TileSide) -> None: + draw_wall(gfx, side) + + if side == TileSide.TOP: + set_pixel(gfx, COLOR_CONNECTION_BG, 3, 0) + set_pixel(gfx, COLOR_CONNECTION_BG, 4, 0) + elif side == TileSide.BOTTOM: + set_pixel(gfx, COLOR_CONNECTION_BG, 3, 7) + set_pixel(gfx, COLOR_CONNECTION_BG, 4, 7) + elif side == TileSide.LEFT: + set_pixel(gfx, COLOR_CONNECTION_BG, 0, 3) + set_pixel(gfx, COLOR_CONNECTION_BG, 0, 4) + elif side == TileSide.RIGHT: + set_pixel(gfx, COLOR_CONNECTION_BG, 7, 3) + set_pixel(gfx, COLOR_CONNECTION_BG, 7, 4) + + +def draw_hatch_mf(gfx: bytearray, color: int, side: TileSide) -> None: + draw_wall(gfx, side) + + inner = color + COLOR_COUNT + if side == TileSide.LEFT: + set_pixel(gfx, color, 0, 3) + set_pixel(gfx, color, 0, 4) + set_pixel(gfx, inner, 1, 2) + set_pixel(gfx, inner, 1, 3) + set_pixel(gfx, inner, 1, 4) + set_pixel(gfx, inner, 1, 5) + elif side == TileSide.RIGHT: + set_pixel(gfx, color, 7, 3) + set_pixel(gfx, color, 7, 4) + set_pixel(gfx, inner, 6, 2) + set_pixel(gfx, inner, 6, 3) + set_pixel(gfx, inner, 6, 4) + set_pixel(gfx, inner, 6, 5) + + +def draw_hatch_zm(gfx: bytearray, color: int, side: TileSide) -> None: + draw_wall(gfx, side) + + inner = color + COLOR_COUNT + if side == TileSide.LEFT: + set_pixel(gfx, color, 0, 3) + set_pixel(gfx, color, 0, 4) + set_pixel(gfx, COLOR_WHITE_ITEM, 1, 2) + set_pixel(gfx, inner, 1, 3) + set_pixel(gfx, inner, 1, 4) + set_pixel(gfx, COLOR_WHITE_ITEM, 1, 5) + elif side == TileSide.RIGHT: + set_pixel(gfx, color, 7, 3) + set_pixel(gfx, color, 7, 4) + set_pixel(gfx, COLOR_WHITE_ITEM, 6, 2) + set_pixel(gfx, inner, 6, 3) + set_pixel(gfx, inner, 6, 4) + set_pixel(gfx, COLOR_WHITE_ITEM, 6, 5) + + +def draw_obtained_tank(gfx: bytearray) -> None: + set_pixel(gfx, COLOR_WHITE_ITEM, 3, 3) + set_pixel(gfx, COLOR_WHITE_ITEM, 3, 4) + set_pixel(gfx, COLOR_WHITE_ITEM, 4, 3) + set_pixel(gfx, COLOR_WHITE_ITEM, 4, 4) + + +def draw_red_outline(gfx: bytearray, edges: TileEdges) -> None: + # NOTE: This code assumes the left and right sides are always walls or hatches + + # Check for top and bottom walls + if edges.top == Edge.WALL: + for x in range(1, 7): + set_pixel(gfx, COLOR_RED_EDGE, x, 0) + if edges.bottom == Edge.WALL: + for x in range(1, 7): + set_pixel(gfx, COLOR_RED_EDGE, x, 7) + + # Draw parts of left and right edges that are always there + for y in [0, 1, 6, 7]: + set_pixel(gfx, COLOR_RED_EDGE, 0, y) + set_pixel(gfx, COLOR_RED_EDGE, 7, y) + + # Check for left wall + if isinstance(edges.left, Edge) and edges.left == Edge.WALL: + for y in range(2, 6): + set_pixel(gfx, COLOR_RED_EDGE, 0, y) + else: + # Remove any inner hatch pixels + for y in range(2, 6): + set_pixel(gfx, COLOR_BG, 1, y) + + # Check for right wall + if isinstance(edges.right, Edge) and edges.right == Edge.WALL: + for y in range(2, 6): + set_pixel(gfx, COLOR_RED_EDGE, 7, y) + else: + # Remove any inner hatch pixels + for y in range(2, 6): + set_pixel(gfx, COLOR_BG, 6, y) + + +def draw_boss_room(gfx: bytearray, color: int, x_offset: int, y_offset: int) -> None: + # Offsets are relative to the icon being in the top left corner + for y, row in enumerate(PIXELS_BOSS): + for x, c in enumerate(row.replace(" ", "")): + if c == "#": + set_pixel(gfx, color, x + x_offset, y + y_offset) + + +def draw_gunship_edge(gfx: bytearray) -> None: + set_pixel(gfx, COLOR_YELLOW_LETTER, 7, 5) + + +def draw_tunnel(gfx: bytearray, edges: TileEdges) -> None: + # NOTE: Assumes one side has a door + if (isinstance(edges.left, Edge) and edges.left == Edge.DOOR) or isinstance( + edges.left, ColoredDoor + ): + # Door on left, arrow faces right + draw_colored_pixel_art(gfx, PIXELS_TUNNEL) + edge = edges.left + x = 0 + elif (isinstance(edges.right, Edge) and edges.left == Edge.DOOR) or isinstance( + edges.left, ColoredDoor + ): + # Door on right, arrow faces left + flipped = [reversed(row) for row in PIXELS_TUNNEL] + draw_colored_pixel_art(gfx, flipped) + edge = edges.right + x = 7 + else: + raise ValueError("Tunnel does not have any doors") + + color = -1 + if isinstance(edge, ColoredDoor): + if edge == ColoredDoor.BLUE: + color = COLOR_BLUE_EDGE + elif edge == ColoredDoor.GREEN: + color = COLOR_GREEN_EDGE + elif edge == ColoredDoor.YELLOW: + color = COLOR_YELLOW_EDGE + elif edge == ColoredDoor.RED: + color = COLOR_RED_EDGE + + if color != -1: + set_pixel(gfx, color, x, 3) + set_pixel(gfx, color, x, 4) From 8ef6528f957150f3fc20a5a93fe40cf840f40d2c Mon Sep 17 00:00:00 2001 From: biosp4rk <37962487+biosp4rk@users.noreply.github.com> Date: Fri, 29 Aug 2025 15:49:53 -0700 Subject: [PATCH 02/12] Fix typos --- src/mars_patcher/minimap_tile_creator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mars_patcher/minimap_tile_creator.py b/src/mars_patcher/minimap_tile_creator.py index f41f416..d9c302d 100644 --- a/src/mars_patcher/minimap_tile_creator.py +++ b/src/mars_patcher/minimap_tile_creator.py @@ -410,8 +410,8 @@ def draw_tunnel(gfx: bytearray, edges: TileEdges) -> None: draw_colored_pixel_art(gfx, PIXELS_TUNNEL) edge = edges.left x = 0 - elif (isinstance(edges.right, Edge) and edges.left == Edge.DOOR) or isinstance( - edges.left, ColoredDoor + elif (isinstance(edges.right, Edge) and edges.right == Edge.DOOR) or isinstance( + edges.right, ColoredDoor ): # Door on right, arrow faces left flipped = [reversed(row) for row in PIXELS_TUNNEL] From 6b12796d7e20ca98d12144e6e55bc13da5d55a2a Mon Sep 17 00:00:00 2001 From: biosp4rk <37962487+biosp4rk@users.noreply.github.com> Date: Fri, 29 Aug 2025 16:03:57 -0700 Subject: [PATCH 03/12] Fix reversing strings --- src/mars_patcher/minimap_tile_creator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mars_patcher/minimap_tile_creator.py b/src/mars_patcher/minimap_tile_creator.py index d9c302d..220d557 100644 --- a/src/mars_patcher/minimap_tile_creator.py +++ b/src/mars_patcher/minimap_tile_creator.py @@ -414,7 +414,7 @@ def draw_tunnel(gfx: bytearray, edges: TileEdges) -> None: edges.right, ColoredDoor ): # Door on right, arrow faces left - flipped = [reversed(row) for row in PIXELS_TUNNEL] + flipped = [row[::-1] for row in PIXELS_TUNNEL] draw_colored_pixel_art(gfx, flipped) edge = edges.right x = 7 From c9d93bf1b879e82270421918484fdb2f306764d7 Mon Sep 17 00:00:00 2001 From: biosp4rk <37962487+biosp4rk@users.noreply.github.com> Date: Fri, 29 Aug 2025 16:37:10 -0700 Subject: [PATCH 04/12] Fix missing corner on minimap tile --- src/mars_patcher/constants/minimap_tiles.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mars_patcher/constants/minimap_tiles.py b/src/mars_patcher/constants/minimap_tiles.py index f3cd910..ae7d575 100644 --- a/src/mars_patcher/constants/minimap_tiles.py +++ b/src/mars_patcher/constants/minimap_tiles.py @@ -490,7 +490,6 @@ def v_flip(self) -> MapTile: 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-TL-D"), 0x109: MapTile.from_str("WDxW_xxxx_B-R-D"), 0x10D: MapTile.from_str("xxDx_xxxx_B-L-E"), 0x10F: MapTile.from_str("xDxW_xxxx_B-TR-B"), @@ -562,6 +561,7 @@ def v_flip(self) -> MapTile: 0x1AB: MapTile.from_str("DDWW_xxxx_O"), # New Tiles 0x0A4: MapTile.from_str("WDSW_xxxx_T"), + 0x108: MapTile.from_str("xxDW_Cxxx_B-TL-D"), 0x10A: MapTile.from_str("WxDx_xxxx_B-BL-E"), 0x10C: MapTile.from_str("xDxW_xCxx_B-TR-E"), 0x170: MapTile.from_str("WWDx_xxxx_w"), From 1ab4f8f4ab48c9e83e0ea043f3ff05a181dab2cb Mon Sep 17 00:00:00 2001 From: biosp4rk <37962487+biosp4rk@users.noreply.github.com> Date: Fri, 29 Aug 2025 16:53:37 -0700 Subject: [PATCH 05/12] Update both Main Deck maps --- src/mars_patcher/door_locks.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/mars_patcher/door_locks.py b/src/mars_patcher/door_locks.py index ac80e4e..2c99cde 100644 --- a/src/mars_patcher/door_locks.py +++ b/src/mars_patcher/door_locks.py @@ -200,11 +200,15 @@ def factory() -> dict: minimap_x = room_entry.map_x + screen_offset_x minimap_y = room_entry.map_y + screen_offset_y - map_tile = minimap_changes[area][minimap_x, minimap_y, room] - if facing_right: - map_tile["left"] = lock - else: - map_tile["right"] = lock + minimap_areas = [area] + if area == 0: + minimap_areas = [0, 9] # Main Deck 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: From 11775ce465ba799e84ae71d47ae81b0ce9530065 Mon Sep 17 00:00:00 2001 From: biosp4rk <37962487+biosp4rk@users.noreply.github.com> Date: Fri, 29 Aug 2025 17:15:08 -0700 Subject: [PATCH 06/12] Don't mark map tile 0x107 as blank --- src/mars_patcher/constants/minimap_tiles.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/mars_patcher/constants/minimap_tiles.py b/src/mars_patcher/constants/minimap_tiles.py index ae7d575..d18913e 100644 --- a/src/mars_patcher/constants/minimap_tiles.py +++ b/src/mars_patcher/constants/minimap_tiles.py @@ -612,7 +612,6 @@ def v_flip(self) -> MapTile: 0x0FD, 0x0FE, 0x0FF, - 0x107, 0x110, 0x111, 0x112, From d9f52d5cde6f83ee41389c47083b951904536d21 Mon Sep 17 00:00:00 2001 From: biosp4rk <37962487+biosp4rk@users.noreply.github.com> Date: Mon, 1 Sep 2025 15:26:22 -0700 Subject: [PATCH 07/12] Address comments for tile builder --- src/mars_patcher/constants/minimap_tiles.py | 1 + src/mars_patcher/door_locks.py | 8 +- src/mars_patcher/minimap_tile_creator.py | 134 +++++++++++--------- 3 files changed, 82 insertions(+), 61 deletions(-) diff --git a/src/mars_patcher/constants/minimap_tiles.py b/src/mars_patcher/constants/minimap_tiles.py index d18913e..36f78b7 100644 --- a/src/mars_patcher/constants/minimap_tiles.py +++ b/src/mars_patcher/constants/minimap_tiles.py @@ -574,6 +574,7 @@ def v_flip(self) -> MapTile: ALL_DOOR_TILES = COLORED_DOOR_TILES | NORMAL_DOOR_TILES ALL_DOOR_TILE_IDS = COLORED_DOOR_TILE_IDS | NORMAL_DOOR_TILE_IDS +# IDs of blank minimap tiles that can be used for creating new tiles BLANK_TILE_IDS = [ 0x067, 0x071, diff --git a/src/mars_patcher/door_locks.py b/src/mars_patcher/door_locks.py index 2c99cde..81af17f 100644 --- a/src/mars_patcher/door_locks.py +++ b/src/mars_patcher/door_locks.py @@ -339,7 +339,7 @@ def tile_exists() -> bool: if not tile_exists(): logging.debug( - "Could not edit map tile door icons for " + "Could not reuse existing map tile for " f"area {area} room {room:X}. ({x:X}, {y:X})." ) logging.debug(f" Desired tile: {orig_new_tile_data.as_str}") @@ -357,7 +357,7 @@ def tile_exists() -> bool: next_blank_tile += 1 else: # No blank tiles remaining, try replacing with open doors - logging.debug(" No blank tiles available, trying open doors.") + logging.warning(" No blank tiles available, trying 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: @@ -365,9 +365,9 @@ def tile_exists() -> bool: new_tile_data = orig_new_tile_data._replace(edges=edges) if not tile_exists(): - logging.debug(" Still no luck. Using vanilla tile.") + logging.warning(" Still no luck. Using vanilla tile.") - logging.debug("") + logging.warning("") if tile_exists(): minimap.set_tile_value( diff --git a/src/mars_patcher/minimap_tile_creator.py b/src/mars_patcher/minimap_tile_creator.py index 220d557..a27dbd2 100644 --- a/src/mars_patcher/minimap_tile_creator.py +++ b/src/mars_patcher/minimap_tile_creator.py @@ -36,6 +36,12 @@ COLOR_RED_HIDDEN = COLOR_RED_EDGE + COLOR_COUNT COLOR_YELLOW_HIDDEN = COLOR_YELLOW_EDGE + COLOR_COUNT +# The following string lists represent pixels in a minimap tile +# - Pixels with "." are left unchanged +# - Pixels with "-" are set to the background color (COLOR_BG) +# - Pixels with "#" are set to a provided color (see draw_pixel_art below) +# - Spaces are ignored + PIXELS_ITEM = [ ". . . . . . . .", ". . . . . . . .", @@ -126,18 +132,25 @@ ". # . . # .", ] -PIXELS_TUNNEL = [ +# The following string lists represent pixels in a minimap tile +# - Pixels with "." are left unchanged +# - Pixels with "-" are set to the background color (COLOR_BG) +# - Pixels with a hex character (0-9 and A-F) are set to that color +# (see draw_colored_pixel_art below) +# - Spaces are ignored + +COLORED_PIXELS_TUNNEL = [ "4 4 4 4 4 4 4 4", "4 5 5 5 1 5 5 4", "1 1 1 5 1 1 5 4", - "4 5 5 5 1 1 1 4", - "4 5 5 5 1 1 5 4", + "5 5 5 5 1 1 1 4", + "5 5 5 5 1 1 5 4", "1 1 1 5 1 5 5 4", "4 5 5 5 5 5 5 4", "4 4 4 4 4 4 4 4", ] -PIXELS_MAJOR = [ +COLORED_PIXELS_MAJOR = [ ". . . . . . . .", ". . . 6 6 . . .", ". - 6 C C 6 - .", @@ -147,7 +160,7 @@ ". . . 6 6 . . .", ". . . . . . . .", ] -PIXELS_CHOZO = [ +COLORED_PIXELS_CHOZO = [ ". . . . . . . .", ". . 6 6 6 . . .", ". 6 E E E 6 - .", @@ -213,45 +226,47 @@ def create_tile(tile: MapTile) -> bytearray: # TODO: ZM hatch types # Content - # The following don't have doors near them so they aren't needed: - # Gunship, Animals, Boiler Pad if tile.content in HAS_RED_OUTLINE: draw_red_outline(gfx, tile.edges) - if tile.content == Content.NAVIGATION: - draw_pixel_art(gfx, COLOR_YELLOW_LETTER, PIXELS_NAVIGATION) - elif tile.content == Content.SAVE: - draw_pixel_art(gfx, COLOR_YELLOW_LETTER, PIXELS_SAVE) - elif tile.content == Content.RECHARGE: - draw_pixel_art(gfx, COLOR_YELLOW_LETTER, PIXELS_RECHARGE) - elif tile.content == Content.HIDDEN_RECHARGE: - draw_pixel_art(gfx, COLOR_YELLOW_HIDDEN, PIXELS_RECHARGE) - elif tile.content == Content.DATA: - draw_pixel_art(gfx, COLOR_YELLOW_LETTER, PIXELS_DATA) - elif tile.content == Content.ITEM: - draw_pixel_art(gfx, COLOR_WHITE_ITEM, PIXELS_ITEM) - elif tile.content == Content.OBTAINED_ITEM: - draw_obtained_tank(gfx) - elif tile.content == Content.BOSS_RIGHT_DOWNLOADED: - draw_boss_room(gfx, COLOR_CONNECTION_BG, 2, 1) - elif tile.content == Content.BOSS_BOTTOM_LEFT_EXPLORED: - draw_boss_room(gfx, COLOR_WHITE_ITEM, 0, 2) - elif tile.content == Content.BOSS_TOP_LEFT_DOWNLOADED: - draw_boss_room(gfx, COLOR_CONNECTION_BG, 0, 0) - elif tile.content == Content.BOSS_LEFT_EXPLORED: - draw_boss_room(gfx, COLOR_WHITE_ITEM, 0, 1) - elif tile.content == Content.BOSS_TOP_RIGHT_BOTH: - draw_boss_room(gfx, COLOR_WHITE_OUTLINE, 2, 0) - elif tile.content == Content.BOSS_TOP_RIGHT_EXPLORED: - draw_boss_room(gfx, COLOR_WHITE_ITEM, 2, 0) - elif tile.content == Content.GUNSHIP_EDGE: - draw_gunship_edge(gfx) - elif tile.content == Content.SECURITY: - draw_pixel_art(gfx, COLOR_YELLOW_LETTER, PIXELS_SECURITY) - elif tile.content == Content.AUXILLARY_POWER: - draw_pixel_art(gfx, COLOR_YELLOW_LETTER, PIXELS_AUXILIARY) - elif tile.content != Content.EMPTY and tile.content != Content.EMPTY_RED_WALLS: - raise ValueError(f"No implementation to create tile content {tile.content}") + # The following don't have doors near them so they aren't needed: + # Gunship, Animals, Boiler Pad + match tile.content: + case Content.NAVIGATION: + draw_pixel_art(gfx, COLOR_YELLOW_LETTER, PIXELS_NAVIGATION) + case Content.SAVE: + draw_pixel_art(gfx, COLOR_YELLOW_LETTER, PIXELS_SAVE) + case Content.RECHARGE: + draw_pixel_art(gfx, COLOR_YELLOW_LETTER, PIXELS_RECHARGE) + case Content.HIDDEN_RECHARGE: + draw_pixel_art(gfx, COLOR_YELLOW_HIDDEN, PIXELS_RECHARGE) + case Content.DATA: + draw_pixel_art(gfx, COLOR_YELLOW_LETTER, PIXELS_DATA) + case Content.ITEM: + draw_pixel_art(gfx, COLOR_WHITE_ITEM, PIXELS_ITEM) + case Content.OBTAINED_ITEM: + draw_obtained_tank(gfx) + case Content.BOSS_RIGHT_DOWNLOADED: + draw_boss_room(gfx, COLOR_CONNECTION_BG, 2, 1) + case Content.BOSS_BOTTOM_LEFT_EXPLORED: + draw_boss_room(gfx, COLOR_WHITE_ITEM, 0, 2) + case Content.BOSS_TOP_LEFT_DOWNLOADED: + draw_boss_room(gfx, COLOR_CONNECTION_BG, 0, 0) + case Content.BOSS_LEFT_EXPLORED: + draw_boss_room(gfx, COLOR_WHITE_ITEM, 0, 1) + case Content.BOSS_TOP_RIGHT_BOTH: + draw_boss_room(gfx, COLOR_WHITE_OUTLINE, 2, 0) + case Content.BOSS_TOP_RIGHT_EXPLORED: + draw_boss_room(gfx, COLOR_WHITE_ITEM, 2, 0) + case Content.GUNSHIP_EDGE: + draw_gunship_edge(gfx) + case Content.SECURITY: + draw_pixel_art(gfx, COLOR_YELLOW_LETTER, PIXELS_SECURITY) + case Content.AUXILLARY_POWER: + draw_pixel_art(gfx, COLOR_YELLOW_LETTER, PIXELS_AUXILIARY) + case _: + if tile.content != Content.EMPTY and tile.content != Content.EMPTY_RED_WALLS: + raise ValueError(f"No implementation to create tile content {tile.content}") return gfx @@ -266,28 +281,28 @@ def set_pixel(gfx: bytearray, color: int, x: int, y: int) -> None: def draw_pixel_art(gfx: bytearray, color: int, art: list[str]) -> None: for y, row in enumerate(art): - for x, c in enumerate(row.replace(" ", "")): - if c != ".": - p = COLOR_BG if c == "-" else color - set_pixel(gfx, p, x, y) + for x, character in enumerate(row.replace(" ", "")): + if character != ".": + actual_color = COLOR_BG if character == "-" else color + set_pixel(gfx, actual_color, x, y) def draw_colored_pixel_art(gfx: bytearray, art: list[str]) -> None: for y, row in enumerate(art): - for x, c in enumerate(row.replace(" ", "")): - if c != ".": - color = COLOR_BG if c == "-" else int(c, 16) + for x, character in enumerate(row.replace(" ", "")): + if character != ".": + color = COLOR_BG if character == "-" else int(character, 16) set_pixel(gfx, color, x, y) def draw_wall(gfx: bytearray, side: TileSide) -> None: - n = 0 if side == TileSide.TOP or side == TileSide.LEFT else 7 + additional_coordinate = 0 if side == TileSide.TOP or side == TileSide.LEFT else 7 if side == TileSide.TOP or side == TileSide.BOTTOM: for x in range(8): - set_pixel(gfx, COLOR_WHITE_OUTLINE, x, n) + set_pixel(gfx, COLOR_WHITE_OUTLINE, x, additional_coordinate) elif side == TileSide.LEFT or side == TileSide.RIGHT: for y in range(8): - set_pixel(gfx, COLOR_WHITE_OUTLINE, n, y) + set_pixel(gfx, COLOR_WHITE_OUTLINE, additional_coordinate, y) def draw_connection(gfx: bytearray, side: TileSide) -> None: @@ -355,7 +370,13 @@ def draw_obtained_tank(gfx: bytearray) -> None: def draw_red_outline(gfx: bytearray, edges: TileEdges) -> None: - # NOTE: This code assumes the left and right sides are always walls or hatches + if (isinstance(edges.left, Edge) and edges.left == Edge.EMPTY) or ( + isinstance(edges.right, Edge) and edges.right == Edge.EMPTY + ): + raise ValueError( + "The current implementation for red outline minimap tiles only supports " + "left and right edges being walls or doors" + ) # Check for top and bottom walls if edges.top == Edge.WALL: @@ -392,8 +413,8 @@ def draw_red_outline(gfx: bytearray, edges: TileEdges) -> None: def draw_boss_room(gfx: bytearray, color: int, x_offset: int, y_offset: int) -> None: # Offsets are relative to the icon being in the top left corner for y, row in enumerate(PIXELS_BOSS): - for x, c in enumerate(row.replace(" ", "")): - if c == "#": + for x, character in enumerate(row.replace(" ", "")): + if character == "#": set_pixel(gfx, color, x + x_offset, y + y_offset) @@ -402,19 +423,18 @@ def draw_gunship_edge(gfx: bytearray) -> None: def draw_tunnel(gfx: bytearray, edges: TileEdges) -> None: - # NOTE: Assumes one side has a door if (isinstance(edges.left, Edge) and edges.left == Edge.DOOR) or isinstance( edges.left, ColoredDoor ): # Door on left, arrow faces right - draw_colored_pixel_art(gfx, PIXELS_TUNNEL) + draw_colored_pixel_art(gfx, COLORED_PIXELS_TUNNEL) edge = edges.left x = 0 elif (isinstance(edges.right, Edge) and edges.right == Edge.DOOR) or isinstance( edges.right, ColoredDoor ): # Door on right, arrow faces left - flipped = [row[::-1] for row in PIXELS_TUNNEL] + flipped = [row[::-1] for row in COLORED_PIXELS_TUNNEL] draw_colored_pixel_art(gfx, flipped) edge = edges.right x = 7 From 53135d694b8938bced8832d5eb3d812d89fded22 Mon Sep 17 00:00:00 2001 From: biosp4rk <37962487+biosp4rk@users.noreply.github.com> Date: Fri, 5 Sep 2025 12:18:01 -0700 Subject: [PATCH 08/12] Support obtained item and transparent tiles --- src/mars_patcher/constants/minimap_tiles.py | 22 +++---- src/mars_patcher/door_locks.py | 71 ++++++++++++++++++--- src/mars_patcher/minimap_tile_creator.py | 21 ++++++ 3 files changed, 92 insertions(+), 22 deletions(-) diff --git a/src/mars_patcher/constants/minimap_tiles.py b/src/mars_patcher/constants/minimap_tiles.py index 36f78b7..7575be9 100644 --- a/src/mars_patcher/constants/minimap_tiles.py +++ b/src/mars_patcher/constants/minimap_tiles.py @@ -211,6 +211,7 @@ class MapTile(NamedTuple): edges: TileEdges = TileEdges() corners: TileCorners = TileCorners() content: Content = Content.EMPTY + transparent: bool = False @property def as_str(self) -> str: @@ -223,6 +224,7 @@ def from_str(cls, value: str) -> Self: edges=TileEdges.from_str(edges), corners=TileCorners.from_str(corners), content=Content(content), + transparent=False, ) def h_flip(self) -> MapTile: @@ -233,6 +235,7 @@ def h_flip(self) -> MapTile: edges=self.edges.h_flip(), corners=self.corners.h_flip(), content=self.content, + transparent=self.transparent, ) def v_flip(self) -> MapTile: @@ -243,6 +246,7 @@ def v_flip(self) -> MapTile: edges=self.edges.v_flip(), corners=self.corners.v_flip(), content=self.content, + transparent=self.transparent, ) @@ -575,23 +579,13 @@ def v_flip(self) -> MapTile: ALL_DOOR_TILE_IDS = COLORED_DOOR_TILE_IDS | NORMAL_DOOR_TILE_IDS # IDs of blank minimap tiles that can be used for creating new tiles -BLANK_TILE_IDS = [ +BLANK_TILE_IDS = { 0x067, 0x071, 0x076, 0x087, 0x08C, 0x091, - 0x0A1, - 0x0B2, - 0x0B3, - 0x0B8, - 0x0B9, - 0x0BA, - 0x0BB, - 0x0BC, - 0x0BD, - 0x0BE, 0x0C0, 0x0C1, 0x0D2, @@ -667,4 +661,8 @@ def v_flip(self) -> MapTile: 0x1BD, 0x1BE, 0x1BF, -] +} + +# IDs of blank minimap tiles that can be used for creating new tiles +# that require an additional transparent version +BLANK_TRANSPARENT_TILE_IDS = {0x0A1, 0x0B2, 0x0B3, 0x0B8, 0x0B9, 0x0BA, 0x0BB, 0x0BC, 0x0BD, 0x0BE} diff --git a/src/mars_patcher/door_locks.py b/src/mars_patcher/door_locks.py index 81af17f..ba6e88f 100644 --- a/src/mars_patcher/door_locks.py +++ b/src/mars_patcher/door_locks.py @@ -15,7 +15,9 @@ ALL_DOOR_TILE_IDS, ALL_DOOR_TILES, BLANK_TILE_IDS, + BLANK_TRANSPARENT_TILE_IDS, ColoredDoor, + Content, Edge, ) from mars_patcher.minimap import Minimap @@ -286,8 +288,6 @@ def change_minimap_tiles( # HatchLock.LOCKED: Edge.WALL, } - next_blank_tile = 0 - for area, area_map in minimap_changes.items(): with Minimap(rom, area) as minimap: for (x, y, room), tile_changes in area_map.items(): @@ -344,17 +344,43 @@ def tile_exists() -> bool: ) logging.debug(f" Desired tile: {orig_new_tile_data.as_str}") - if next_blank_tile < len(BLANK_TILE_IDS): - # Create a new tile and replace the graphics + # Try getting a blank tile ID + requires_transparent_tile = orig_new_tile_data.content == Content.TUNNEL or any( + isinstance(e, Edge) and e == Edge.SHORTCUT for e in orig_new_tile_data.edges + ) + blank_tile_ids = ( + BLANK_TRANSPARENT_TILE_IDS if requires_transparent_tile else BLANK_TILE_IDS + ) + is_item = orig_new_tile_data.content == Content.ITEM + new_tile_id = get_blank_minimap_tile_id(blank_tile_ids, is_item) + + if new_tile_id is not None: + # Create new graphics for the tile gfx = create_tile(orig_new_tile_data) - tile_id = BLANK_TILE_IDS[next_blank_tile] - addr = minimap_graphics(rom) + tile_id * 32 - rom.write_bytes(addr, gfx) + new_tiles = [(new_tile_id, gfx, orig_new_tile_data)] + + # If the tile has an item, add another tile for the obtained item + if is_item: + data = orig_new_tile_data._replace(content=Content.OBTAINED_ITEM) + gfx = create_tile(data) + new_tiles.append((new_tile_id + 1, gfx, data)) + + # If the tile doesn't fill the whole square, add another tile with + # transparency + if requires_transparent_tile: + for tile_id, gfx, data in list(new_tiles): + data = data._replace(transparent=True) + gfx = create_tile(data) + new_tiles.append((tile_id + 0x20, gfx, data)) + + for tile_id, gfx, data in new_tiles: + addr = minimap_graphics(rom) + tile_id * 32 + rom.write_bytes(addr, gfx) + + ALL_DOOR_TILE_IDS[data] = tile_id + logging.debug(f" Created new tile: 0x{tile_id:X}.") new_tile_data = orig_new_tile_data - ALL_DOOR_TILE_IDS[new_tile_data] = tile_id - logging.debug(f" Created new tile: 0x{tile_id:X}.") - next_blank_tile += 1 else: # No blank tiles remaining, try replacing with open doors logging.warning(" No blank tiles available, trying open doors.") @@ -378,3 +404,28 @@ def tile_exists() -> bool: h_flip, v_flip, ) + + +def get_blank_minimap_tile_id(blank_tile_ids: set[int], is_item: bool) -> int | None: + """Finds a usable tile from the provided set of blank tile IDs. Item tiles require a blank + tile next to them. Non-item tiles can use any blank tile, but solitary tiles are preferred + to save more tiles for item tiles.""" + valid_tile_id: int | None = None + for tile_id in blank_tile_ids: + if is_item: + # Item tiles require a blank tile next to them for the obtained item tile + if tile_id + 1 in blank_tile_ids: + valid_tile_id = tile_id + break + else: + # Prefer solitary blank tiles for non-item tiles + if tile_id + 1 not in blank_tile_ids: + valid_tile_id = tile_id + break + elif valid_tile_id is None: + valid_tile_id = tile_id + if valid_tile_id is not None: + blank_tile_ids.remove(valid_tile_id) + if is_item: + blank_tile_ids.remove(valid_tile_id + 1) + return valid_tile_id diff --git a/src/mars_patcher/minimap_tile_creator.py b/src/mars_patcher/minimap_tile_creator.py index a27dbd2..006be23 100644 --- a/src/mars_patcher/minimap_tile_creator.py +++ b/src/mars_patcher/minimap_tile_creator.py @@ -195,6 +195,8 @@ def create_tile(tile: MapTile) -> bytearray: # Handle tunnels separately if tile.content == Content.TUNNEL: draw_tunnel(gfx, tile.edges) + if tile.transparent: + make_transparent(gfx) return gfx # Corners @@ -214,6 +216,7 @@ def create_tile(tile: MapTile) -> bytearray: draw_wall(gfx, side) elif edge == Edge.DOOR: draw_connection(gfx, side) + # TODO: Support Edge.SHORTCUT elif isinstance(edge, ColoredDoor): if edge == ColoredDoor.BLUE: draw_hatch_mf(gfx, COLOR_BLUE_EDGE, side) @@ -268,9 +271,27 @@ def create_tile(tile: MapTile) -> bytearray: if tile.content != Content.EMPTY and tile.content != Content.EMPTY_RED_WALLS: raise ValueError(f"No implementation to create tile content {tile.content}") + if tile.transparent: + make_transparent(gfx) return gfx +def make_transparent(gfx: bytearray) -> None: + for y in range(8): + for x in range(8): + color = get_pixel(gfx, x, y) + if color == COLOR_BLANK_OUTLINE or color == COLOR_BLANK_BG: + set_pixel(gfx, 0, x, y) + + +def get_pixel(gfx: bytearray, x: int, y: int) -> None: + index = (y * 8 + x) // 2 + if x % 2 == 0: + return gfx[index] & 0xF + else: + return gfx[index] >> 4 + + def set_pixel(gfx: bytearray, color: int, x: int, y: int) -> None: index = (y * 8 + x) // 2 if x % 2 == 0: From b5c2b0429218373cf576c04b0c156f57667e345c Mon Sep 17 00:00:00 2001 From: biosp4rk <37962487+biosp4rk@users.noreply.github.com> Date: Fri, 5 Sep 2025 12:19:55 -0700 Subject: [PATCH 09/12] Fix return type hint --- src/mars_patcher/minimap_tile_creator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mars_patcher/minimap_tile_creator.py b/src/mars_patcher/minimap_tile_creator.py index 006be23..165f8da 100644 --- a/src/mars_patcher/minimap_tile_creator.py +++ b/src/mars_patcher/minimap_tile_creator.py @@ -284,7 +284,7 @@ def make_transparent(gfx: bytearray) -> None: set_pixel(gfx, 0, x, y) -def get_pixel(gfx: bytearray, x: int, y: int) -> None: +def get_pixel(gfx: bytearray, x: int, y: int) -> int: index = (y * 8 + x) // 2 if x % 2 == 0: return gfx[index] & 0xF From 757c736a0a8e871e2c6f8797514927ccb0a80733 Mon Sep 17 00:00:00 2001 From: biosp4rk <37962487+biosp4rk@users.noreply.github.com> Date: Fri, 5 Sep 2025 14:12:09 -0700 Subject: [PATCH 10/12] Use copies for minimap ID "constants" --- src/mars_patcher/door_locks.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/mars_patcher/door_locks.py b/src/mars_patcher/door_locks.py index ba6e88f..2c4de74 100644 --- a/src/mars_patcher/door_locks.py +++ b/src/mars_patcher/door_locks.py @@ -288,6 +288,10 @@ def change_minimap_tiles( # HatchLock.LOCKED: Edge.WALL, } + all_door_tile_ids = dict(ALL_DOOR_TILE_IDS) + remaining_blank_tile_ids = set(BLANK_TILE_IDS) + remaining_blank_transparent_tile_ids = set(BLANK_TRANSPARENT_TILE_IDS) + for area, area_map in minimap_changes.items(): with Minimap(rom, area) as minimap: for (x, y, room), tile_changes in area_map.items(): @@ -311,7 +315,7 @@ def change_minimap_tiles( new_tile_data = orig_new_tile_data def tile_exists() -> bool: - return new_tile_data in ALL_DOOR_TILE_IDS + return new_tile_data in all_door_tile_ids if new_tile_data.content.can_h_flip and not tile_exists(): # Try flipping horizontally @@ -349,7 +353,9 @@ def tile_exists() -> bool: isinstance(e, Edge) and e == Edge.SHORTCUT for e in orig_new_tile_data.edges ) blank_tile_ids = ( - BLANK_TRANSPARENT_TILE_IDS if requires_transparent_tile else BLANK_TILE_IDS + remaining_blank_transparent_tile_ids + if requires_transparent_tile + else remaining_blank_tile_ids ) is_item = orig_new_tile_data.content == Content.ITEM new_tile_id = get_blank_minimap_tile_id(blank_tile_ids, is_item) @@ -377,7 +383,7 @@ def tile_exists() -> bool: addr = minimap_graphics(rom) + tile_id * 32 rom.write_bytes(addr, gfx) - ALL_DOOR_TILE_IDS[data] = tile_id + all_door_tile_ids[data] = tile_id logging.debug(f" Created new tile: 0x{tile_id:X}.") new_tile_data = orig_new_tile_data @@ -399,7 +405,7 @@ def tile_exists() -> bool: minimap.set_tile_value( x, y, - ALL_DOOR_TILE_IDS[new_tile_data], + all_door_tile_ids[new_tile_data], palette, h_flip, v_flip, From 256bc46dff15bb336abe9eae6d58f41a12a3bb8c Mon Sep 17 00:00:00 2001 From: biosp4rk <37962487+biosp4rk@users.noreply.github.com> Date: Tue, 7 Oct 2025 08:30:22 -0700 Subject: [PATCH 11/12] Convert blank tile ID sets to lists --- src/mars_patcher/constants/minimap_tiles.py | 6 +++--- src/mars_patcher/door_locks.py | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/mars_patcher/constants/minimap_tiles.py b/src/mars_patcher/constants/minimap_tiles.py index 7575be9..3198df1 100644 --- a/src/mars_patcher/constants/minimap_tiles.py +++ b/src/mars_patcher/constants/minimap_tiles.py @@ -579,7 +579,7 @@ def v_flip(self) -> MapTile: ALL_DOOR_TILE_IDS = COLORED_DOOR_TILE_IDS | NORMAL_DOOR_TILE_IDS # IDs of blank minimap tiles that can be used for creating new tiles -BLANK_TILE_IDS = { +BLANK_TILE_IDS = [ 0x067, 0x071, 0x076, @@ -661,8 +661,8 @@ def v_flip(self) -> MapTile: 0x1BD, 0x1BE, 0x1BF, -} +] # IDs of blank minimap tiles that can be used for creating new tiles # that require an additional transparent version -BLANK_TRANSPARENT_TILE_IDS = {0x0A1, 0x0B2, 0x0B3, 0x0B8, 0x0B9, 0x0BA, 0x0BB, 0x0BC, 0x0BD, 0x0BE} +BLANK_TRANSPARENT_TILE_IDS = [0x0A1, 0x0B2, 0x0B3, 0x0B8, 0x0B9, 0x0BA, 0x0BB, 0x0BC, 0x0BD, 0x0BE] diff --git a/src/mars_patcher/door_locks.py b/src/mars_patcher/door_locks.py index 2c4de74..363fd80 100644 --- a/src/mars_patcher/door_locks.py +++ b/src/mars_patcher/door_locks.py @@ -289,8 +289,8 @@ def change_minimap_tiles( } all_door_tile_ids = dict(ALL_DOOR_TILE_IDS) - remaining_blank_tile_ids = set(BLANK_TILE_IDS) - remaining_blank_transparent_tile_ids = set(BLANK_TRANSPARENT_TILE_IDS) + remaining_blank_tile_ids = list(BLANK_TILE_IDS) + remaining_blank_transparent_tile_ids = list(BLANK_TRANSPARENT_TILE_IDS) for area, area_map in minimap_changes.items(): with Minimap(rom, area) as minimap: @@ -412,8 +412,8 @@ def tile_exists() -> bool: ) -def get_blank_minimap_tile_id(blank_tile_ids: set[int], is_item: bool) -> int | None: - """Finds a usable tile from the provided set of blank tile IDs. Item tiles require a blank +def get_blank_minimap_tile_id(blank_tile_ids: list[int], is_item: bool) -> int | None: + """Finds a usable tile from the provided list of blank tile IDs. Item tiles require a blank tile next to them. Non-item tiles can use any blank tile, but solitary tiles are preferred to save more tiles for item tiles.""" valid_tile_id: int | None = None From 3a286404c789d196b0bb5aa2161583e89b68c8c8 Mon Sep 17 00:00:00 2001 From: biosp4rk <37962487+biosp4rk@users.noreply.github.com> Date: Tue, 7 Oct 2025 08:48:27 -0700 Subject: [PATCH 12/12] Fix minimap tile constants --- src/mars_patcher/constants/minimap_tiles.py | 246 +++++++++++++++++ .../mf/constants/minimap_tiles.py | 253 +----------------- src/mars_patcher/mf/door_locks.py | 4 +- 3 files changed, 248 insertions(+), 255 deletions(-) create mode 100644 src/mars_patcher/constants/minimap_tiles.py diff --git a/src/mars_patcher/constants/minimap_tiles.py b/src/mars_patcher/constants/minimap_tiles.py new file mode 100644 index 0000000..f3034a7 --- /dev/null +++ b/src/mars_patcher/constants/minimap_tiles.py @@ -0,0 +1,246 @@ +from __future__ import annotations + +from enum import Enum +from typing import NamedTuple + +from typing_extensions import Self + +# String Format +# __ + +# Chunk 1 (tile edges) +# - see Edge and ColoredDoor for values + +# Chunk 2 (tile corners) +# - C: corner pixel +# - x: none + +# Chunk 3 (tile content) +# - see Content for values + + +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, + ) + + +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, + ) + + +class Content(Enum): + EMPTY = "x" + EMPTY_RED_WALLS = "w" + NAVIGATION = "N" + SAVE = "S" + RECHARGE = "R" + HIDDEN_RECHARGE = "H" + DATA = "D" + ITEM = "I" + OBTAINED_ITEM = "O" + BOSS_RIGHT_DOWNLOADED = "B-R-D" # Serris skeleton + BOSS_BOTTOM_LEFT_EXPLORED = "B-BL-E" # Serris + BOSS_TOP_LEFT_DOWNLOADED = "B-TL-D" # BOX 1 + BOSS_LEFT_EXPLORED = "B-L-E" # Mega-X + BOSS_TOP_RIGHT_BOTH = "B-TR-B" # Nightmare + BOSS_TOP_RIGHT_EXPLORED = "B-TR-E" # BOX 2 + GUNSHIP = "G" + GUNSHIP_EDGE = "P" + SECURITY = "K" + AUXILLARY_POWER = "Y" + ANIMALS = "A" + TUNNEL = "T" + BOILER_PAD = "L" + + @property + def can_h_flip(self) -> bool: + exclude = { + Content.NAVIGATION, + Content.SAVE, + Content.RECHARGE, + Content.HIDDEN_RECHARGE, + Content.DATA, + Content.GUNSHIP, + Content.GUNSHIP_EDGE, + Content.SECURITY, + Content.AUXILLARY_POWER, + Content.BOILER_PAD, + } + return self not in exclude + + @property + def can_v_flip(self) -> bool: + exclude = { + Content.NAVIGATION, + Content.SAVE, + Content.RECHARGE, + Content.HIDDEN_RECHARGE, + Content.BOSS_RIGHT_DOWNLOADED, + Content.BOSS_BOTTOM_LEFT_EXPLORED, + Content.BOSS_TOP_LEFT_DOWNLOADED, + Content.BOSS_LEFT_EXPLORED, + Content.BOSS_TOP_RIGHT_BOTH, + Content.BOSS_TOP_RIGHT_EXPLORED, + Content.GUNSHIP, + Content.GUNSHIP_EDGE, + Content.SECURITY, + Content.AUXILLARY_POWER, + Content.TUNNEL, + Content.BOILER_PAD, + } + return self not in exclude + + +class MapTile(NamedTuple): + edges: TileEdges = TileEdges() + corners: TileCorners = TileCorners() + content: Content = Content.EMPTY + transparent: bool = False + + @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: + edges, corners, content = value.split("_") + return cls( + edges=TileEdges.from_str(edges), + corners=TileCorners.from_str(corners), + content=Content(content), + transparent=False, + ) + + 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, + transparent=self.transparent, + ) + + 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, + transparent=self.transparent, + ) diff --git a/src/mars_patcher/mf/constants/minimap_tiles.py b/src/mars_patcher/mf/constants/minimap_tiles.py index 3198df1..26410a1 100644 --- a/src/mars_patcher/mf/constants/minimap_tiles.py +++ b/src/mars_patcher/mf/constants/minimap_tiles.py @@ -1,256 +1,5 @@ -from __future__ import annotations +from mars_patcher.constants.minimap_tiles import MapTile -from enum import Enum -from typing import NamedTuple - -from typing_extensions import Self - -# String Format -# __ - -# Chunk 1 (tile edges) -# - see Edge and ColoredDoor for values - -# Chunk 2 (tile corners) -# - C: corner pixel -# - x: none - -# Chunk 3 (tile content) -# - 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" - EMPTY_RED_WALLS = "w" - NAVIGATION = "N" - SAVE = "S" - RECHARGE = "R" - HIDDEN_RECHARGE = "H" - DATA = "D" - ITEM = "I" - OBTAINED_ITEM = "O" - BOSS_RIGHT_DOWNLOADED = "B-R-D" # Serris skeleton - BOSS_BOTTOM_LEFT_EXPLORED = "B-BL-E" # Serris - BOSS_TOP_LEFT_DOWNLOADED = "B-TL-D" # BOX 1 - BOSS_LEFT_EXPLORED = "B-L-E" # Mega-X - BOSS_TOP_RIGHT_BOTH = "B-TR-B" # Nightmare - BOSS_TOP_RIGHT_EXPLORED = "B-TR-E" # BOX 2 - GUNSHIP = "G" - GUNSHIP_EDGE = "P" - SECURITY = "K" - AUXILLARY_POWER = "Y" - ANIMALS = "A" - TUNNEL = "T" - BOILER_PAD = "L" - - @property - def can_h_flip(self) -> bool: - exclude = { - Content.NAVIGATION, - Content.SAVE, - Content.RECHARGE, - Content.HIDDEN_RECHARGE, - Content.DATA, - Content.GUNSHIP, - Content.GUNSHIP_EDGE, - Content.SECURITY, - Content.AUXILLARY_POWER, - Content.BOILER_PAD, - } - return self not in exclude - - @property - def can_v_flip(self) -> bool: - exclude = { - Content.NAVIGATION, - Content.SAVE, - Content.RECHARGE, - Content.HIDDEN_RECHARGE, - Content.BOSS_RIGHT_DOWNLOADED, - Content.BOSS_BOTTOM_LEFT_EXPLORED, - Content.BOSS_TOP_LEFT_DOWNLOADED, - Content.BOSS_LEFT_EXPLORED, - Content.BOSS_TOP_RIGHT_BOTH, - Content.BOSS_TOP_RIGHT_EXPLORED, - Content.GUNSHIP, - Content.GUNSHIP_EDGE, - Content.SECURITY, - Content.AUXILLARY_POWER, - Content.TUNNEL, - Content.BOILER_PAD, - } - return self not in exclude - - -# Tile -class MapTile(NamedTuple): - edges: TileEdges = TileEdges() - corners: TileCorners = TileCorners() - content: Content = Content.EMPTY - transparent: bool = False - - @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: - edges, corners, content = value.split("_") - return cls( - edges=TileEdges.from_str(edges), - corners=TileCorners.from_str(corners), - content=Content(content), - transparent=False, - ) - - 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, - transparent=self.transparent, - ) - - 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, - transparent=self.transparent, - ) - - -# Constants COLORED_DOOR_TILES = { 0x005: MapTile.from_str("WBxx_xxxx_x"), 0x006: MapTile.from_str("DBxx_xxxx_x"), diff --git a/src/mars_patcher/mf/door_locks.py b/src/mars_patcher/mf/door_locks.py index 8d8b08c..37d277e 100644 --- a/src/mars_patcher/mf/door_locks.py +++ b/src/mars_patcher/mf/door_locks.py @@ -5,6 +5,7 @@ from mars_patcher.common_types import AreaId, AreaRoomPair, RoomId from mars_patcher.constants.game_data import area_doors_ptrs, minimap_graphics +from mars_patcher.constants.minimap_tiles import ColoredDoor, Content, Edge from mars_patcher.mf.auto_generated_types import MarsschemamfDoorlocksItem from mars_patcher.mf.constants.game_data import hatch_lock_event_count, hatch_lock_events from mars_patcher.mf.constants.minimap_tiles import ( @@ -12,9 +13,6 @@ ALL_DOOR_TILES, BLANK_TILE_IDS, BLANK_TRANSPARENT_TILE_IDS, - ColoredDoor, - Content, - Edge, ) from mars_patcher.minimap import Minimap from mars_patcher.minimap_tile_creator import create_tile