#!/usr/bin/env python3 """ Parse SDLPoP level file (res2001.bin) and render the first room by faithfully replaying the tile-drawing logic of seg008.c (draw_room/draw_tile). Pixel model (from seg008.c): - screen is 320px wide, 10 columns. - col_xh[col] = {0,4,8,12,16,20,24,28,32,36}; x_px = xh*8 -> 0,32,64,...,288 (tile column is 32px; the 4px offset handles overlap of floor/wall edges) - rows drawn 2,1,0 (bottom..top). For each row: draw_bottom_y = 63*drawn_row + 65 draw_main_y = draw_bottom_y - 3 row2 -> bottom_y=191, row1 -> 128, row0 -> 65 - a tile sprite of height h drawn at ybottom is placed with its BOTTOM at ybottom: y = ybottom - h + 1 We replay draw_tile() which composites (in order): floorright, anim_topright, right, anim_right, bottom, loose, base, anim, fore Dependencies on NEIGHBOURS: - tile_left = tile in the column to the left (same room, same row) - row_below_left_[col] = tile in (room_below, col-1, row_below) -> for topright/floorright - For a single room with no links we treat out-of-range neighbours as walls (level edges), matching SDLPoP's custom->drawn_tile_*_level_edge defaults (left = wall, top = floor). We ignore animated variants (spike frames, chomper, torch flames, gate animation) for this first static render; they use curr_modifier/backtable. """ import os from PIL import Image LEVEL_DIR = "/Users/alex/Projects/DIY/Z80/Sprinter/C-Compiler/applications/PoP/SDLPoP/data/LEVELS" VPALACE_DIR = "/Users/alex/Projects/DIY/Z80/Sprinter/C-Compiler/applications/PoP/SDLPoP/data/VPALACE" VDUNGEON_DIR = "/Users/alex/Projects/DIY/Z80/Sprinter/C-Compiler/applications/PoP/SDLPoP/data/VDUNGEON" # For dungeon levels (tbl_level_type == 0) the environment sprites come from # VDUNGEON where present, otherwise fall back to the shared VPALACE set # (PRINCE.DAT common images). For palace levels use VPALACE only. IS_DUNGEON = True # room 1 (res2001) is the dungeon # tile_table[code] = (base_id, floor_left, base_y, right_id, floor_right, # right_y, stripe_id, topright_id, bottom_id, fore_id, # fore_x, fore_y) TILE_TABLE = [ ( 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), # 0x00 empty ( 41, 1, 0, 42, 1, 2, 145, 0, 43, 0, 0, 0), # 0x01 floor (127, 1, 0, 133, 1, 2, 145, 0, 43, 0, 0, 0), # 0x02 spike ( 92, 1, 0, 93, 1, 2, 0, 94, 43, 95, 1, 0), # 0x03 pillar ( 46, 1, 0, 47, 1, 2, 0, 48, 43, 49, 3, 0), # 0x04 gate ( 41, 1, 1, 35, 1, 3, 145, 0, 36, 0, 0, 0), # 0x05 stuck floor ( 41, 1, 0, 42, 1, 2, 145, 0, 96, 0, 0, 0), # 0x06 close button ( 46, 1, 0, 0, 0, 2, 0, 0, 43, 49, 3, 0), # 0x07 door top with floor ( 86, 1, 0, 87, 1, 2, 0, 0, 43, 88, 1, 0), # 0x08 big pillar bottom ( 0, 0, 0, 89, 0, 3, 0, 90, 0, 91, 1, 3), # 0x09 big pillar top ( 41, 1, 0, 42, 1, 2, 145, 0, 43, 12, 2, -3), # 0x0A potion ( 0, 1, 0, 0, 0, 0, 145, 0, 0, 0, 0, 0), # 0x0B loose floor ( 0, 0, 0, 0, 0, 2, 0, 0, 85, 49, 3, 0), # 0x0C door top ( 75, 1, 0, 42, 1, 2, 0, 0, 43, 77, 0, 0), # 0x0D mirror ( 97, 1, 0, 98, 1, 2, 145, 0, 43, 100, 0, 0), # 0x0E debris (147, 1, 0, 42, 1, 1, 145, 0, 149, 0, 0, 0), # 0x0F open button ( 41, 1, 0, 37, 0, 0, 0, 38, 43, 0, 0, 0), # 0x10 leveldoor left ( 0, 0, 0, 39, 1, 2, 0, 40, 43, 0, 0, 0), # 0x11 leveldoor right ( 0, 0, 0, 42, 1, 2, 145, 0, 43, 0, 0, 0), # 0x12 chomper ( 41, 1, 0, 42, 1, 2, 0, 0, 43, 0, 0, 0), # 0x13 torch ( 0, 0, 0, 1, 1, 2, 0, 2, 0, 0, 0, 0), # 0x14 wall ( 30, 1, 0, 31, 1, 2, 0, 0, 43, 0, 0, 0), # 0x15 skeleton ( 41, 1, 0, 42, 1, 2, 145, 0, 43, 0, 0, 0), # 0x16 sword ( 41, 1, 0, 10, 0, 0, 0, 11, 43, 0, 0, 0), # 0x17 balcony left ( 0, 0, 0, 12, 1, 2, 0, 13, 43, 0, 0, 0), # 0x18 balcony right ( 92, 1, 0, 42, 1, 2, 145, 0, 43, 95, 1, 0), # 0x19 lattice pillar ( 1, 0, 0, 0, 0, 0, 0, 0, 2, 9, 0, -53), # 0x1A lattice down ( 3, 0, -10, 0, 0, 0, 0, 0, 0, 9, 0, -53), # 0x1B lattice small ( 4, 0, -10, 0, 0, 0, 0, 0, 0, 9, 0, -53), # 0x1C lattice left ( 5, 0, -10, 0, 0, 0, 0, 0, 0, 9, 0, -53), # 0x1D lattice right ( 97, 1, 0, 98, 1, 2, 0, 0, 43, 100, 0, 0), # 0x1E debris with torch ] TILE_NAMES = { 0x00: "empty", 0x01: "floor", 0x02: "spike", 0x03: "pillar", 0x04: "gate", 0x05: "stuck_floor", 0x06: "close_button", 0x07: "door_top_floor", 0x08: "big_pillar_bottom", 0x09: "big_pillar_top", 0x0A: "potion", 0x0B: "loose_floor", 0x0C: "door_top", 0x0D: "mirror", 0x0E: "debris", 0x0F: "open_button", 0x10: "leveldoor_left", 0x11: "leveldoor_right", 0x12: "chomper", 0x13: "torch", 0x14: "wall", 0x15: "skeleton", 0x16: "sword", 0x17: "balcony_left", 0x18: "balcony_right", 0x19: "lattice_pillar", 0x1A: "lattice_down", 0x1B: "lattice_small", 0x1C: "lattice_left", 0x1D: "lattice_right", 0x1E: "debris_torch", } COL_XH = [0, 4, 8, 12, 16, 20, 24, 28, 32, 36] # chtab -> directory/subfolder + id offset # id_chtab_6_environment -> VPALACE, res(200+id).png # id_chtab_7_environmentwall -> VPALACE (wall images also here, res(200+id)) # id_chtab_1_flameswordpotion -> flames etc (skipped in static render) CHTAB_ENV = 6 CHTAB_WALL = 7 CHTAB_FSP = 1 # flames/sword/potion (animated, skipped) # ---- POP1 level file structure (see POP-DAT-FormatSpecifications.txt, sec 3.4) ---- # Block layout (for the standard 2305-byte level): # 0 .. 720 pop1 foretable (24 rooms x 30 bytes) # 720 .. 1440 pop1 backtable (24 rooms x 30 bytes) # 1440 .. 1696 door I (256 bytes, one per event line) # 1696 .. 1952 door II (256 bytes) # 1952 .. 2048 links (24 rooms x 4 bytes: L,R,Up,Dn) # 2048 .. 2112 unknown I # 2112 .. 2115 start position (room, location, direction) # 2115 .. 2116 unknown II # 2116 .. 2119 unknown III # 2119 .. 2143 guard location (24) # 2143 .. 2167 guard direction (24) # 2167 .. 2191 unknown IV (a) # 2191 .. 2215 unknown IV (b) # 2215 .. 2239 guard skill (24) # 2239 .. 2263 unknown IV (c) # 2263 .. 2287 guard colour (24) # 2287 .. 2303 unknown IV (d) # 2303 .. 2305 0F 09 # foretable byte format: rrmccccc (2 random bits, 1 modifier bit, 5 code bits) FG_RAND_MASK = 0xC0 FG_MODBIT_MASK = 0x20 FG_CODE_MASK = 0x1F # backtable: a tile modifier that depends on the group of the foretable code. # group ids map foretable code -> "group" used to interpret backtable value. # (see Table 7/8). We only need the group for door/event interpretation. GROUP_OF_CODE = { 0x00: "free", 0x01: "free", 0x02: "spike", 0x03: "none", 0x04: "gate", 0x05: "none", 0x06: "event", 0x07: "tapest", 0x08: "none", 0x09: "none", 0x0A: "potion", 0x0B: "none", 0x0C: "ttop", 0x0D: "none", 0x0E: "none", 0x0F: "event", 0x10: "none", 0x11: "none", 0x12: "chomp", 0x13: "none", 0x14: "wall", 0x15: "none", 0x16: "none", 0x17: "none", 0x18: "none", 0x19: "none", 0x1A: "none", 0x1B: "none", 0x1C: "none", 0x1D: "none", 0x1E: "none", 0x1F: "none", } # backtable modifier meanings per group (subset, from Table 8): GATE_MOD = {0: "closed", 1: "open", 2: "with_lattice", 3: "alt_design"} TTOP_MOD = {0: "normal", 1: "black", 2: "empty", 3: "?3", 4: "?4", 5: "?5", 6: "?6", 7: "?7"} SPIKE_MOD = {0: "normal", 1: "barely_out", 2: "half_out", 3: "fully_out"} CHOMP_MOD = {0: "normal", 1: "barely_out", 2: "half_out", 3: "fully_out", 4: "out?"} POTION_MOD = {0: "blue", 1: "no_blue", 2: "window", 3: "spot?"} TAPEST_MOD = {0: "normal", 1: "alt_design", 2: "black", 3: "empty"} EVENT_MOD_DESC = { # group "event": the backtable byte IS the event id 0: "closer", 1: "opener", } class Level: """Full parsed POP1 level file.""" def __init__(self, data): self.data = data self.rooms_fg = {} # room -> list[30] of (fg_byte, code, modbit) self.rooms_bg = {} # room -> list[30] of backtable byte for room in range(1, 25): fg = []; bg = [] for off in range(30): i = (room - 1) * 30 + off fb = data[i]; bb = data[720 + i] fg.append((fb, fb & FG_CODE_MASK, (fb & FG_MODBIT_MASK) >> 5)) bg.append(bb) self.rooms_fg[room] = fg self.rooms_bg[room] = bg # door I / II (event lines 0..255) self.door_i = data[1440:1696] self.door_ii = data[1696:1952] # links: 24 rooms x (L,R,Up,Dn) self.links = {} for room in range(1, 25): b = data[1952 + (room - 1) * 4: 1952 + (room - 1) * 4 + 4] self.links[room] = tuple(b) # (left, right, up, down) # start position self.start = tuple(data[2112:2115]) # guards self.guard_loc = list(data[2119:2143]) self.guard_dir = list(data[2143:2167]) self.guard_skill = list(data[2215:2239]) self.guard_colour = list(data[2263:2287]) def door_for_event(self, event_id): """Decode door I/II for an event line -> (room, location, trigger_next).""" bi = self.door_i[event_id] bii = self.door_ii[event_id] # Byte I: t1 s4 s5 l1 l2 l3 l4 l5 t1 = (bi >> 7) & 1 s4 = (bi >> 6) & 1 s5 = (bi >> 5) & 1 l1 = (bi >> 4) & 1 l2 = (bi >> 3) & 1 l3 = (bi >> 2) & 1 l4 = (bi >> 1) & 1 l5 = bi & 1 # Byte II: s1 s2 s3 00000 s1 = (bii >> 7) & 1 s2 = (bii >> 6) & 1 s3 = (bii >> 5) & 1 screen = (s1 << 4) | (s2 << 3) | (s3 << 2) | (s4 << 1) | s5 loc = (l1 << 4) | (l2 << 3) | (l3 << 2) | (l4 << 1) | l5 return screen, loc, t1 def backtable_desc(self, code, bb): grp = GROUP_OF_CODE.get(code, "none") tbl = {"gate": GATE_MOD, "ttop": TTOP_MOD, "spike": SPIKE_MOD, "chomp": CHOMP_MOD, "potion": POTION_MOD, "tapest": TAPEST_MOD} if code == 0x06 or code == 0x0F: # event group act = EVENT_MOD_DESC.get(bb & 1, "?") return f"event(id={bb}, {act})" if grp in tbl: return tbl[grp].get(bb, f"?0x{bb:02X}") return f"0x{bb:02X}" class Room: def __init__(self, level, room_num): self.level = level self.room_num = room_num self.fg = level.rooms_fg[room_num] self.bg = level.rooms_bg[room_num] self.left_room = None # соседняя комната слева (link L); load_leftroom def tile(self, row, col): """Return (code, fg_byte, modbit, backtable_byte) for an in-room tile. Out-of-range neighbours follow seg008 defaults: col<0 -> берётся из ЛЕВОЙ комнаты (col 9), как load_leftroom(); если левой комнаты нет — уровневая кромка = стена (20). row<0 -> top level edge = floor (code 1, mod 0) """ if col < 0: if self.left_room is not None and 0 <= row < 3: return self.left_room.tile(row, 9) return 20, 0, 0, 0 if row < 0: return 1, 0, 0, 0 if not (0 <= row < 3 and 0 <= col < 10): return 20, 0, 0, 0 i = row * 10 + col fb, code, mbit = self.fg[i] return code, fb, mbit, self.bg[i] # Log of every sprite loaded during a render, for the per-tile report. SPRITE_LOG = [] def load_sprite(id, chtab=CHTAB_ENV, reason=""): """Load resource PNG для картинки id из набора chtab. База ресурса зависит от chtab (seg000.c:1109/1122): chtab_6 environment -> res(200+id) chtab_7 environmentwall -> res(360+id) chtab_1 flame/sword/pot -> res(150+id) (анимация, статикой не грузим) Dungeon (IS_DUNGEON) cascade: try VDUNGEON first (dungeon-specific overrides), then fall back to VPALACE (shared base set). """ if id <= 0: return None base = {CHTAB_ENV: 200, CHTAB_WALL: 360, CHTAB_FSP: 150}.get(chtab, 200) fname = f"res{base + id}.png" if IS_DUNGEON: vd = os.path.join(VDUNGEON_DIR, fname) if os.path.exists(vd): try: img = Image.open(vd).convert("RGBA") CURRENT_TILE_LOG.append((id, "VDUNGEON", reason)) return img except Exception: pass vp = os.path.join(VPALACE_DIR, fname) try: img = Image.open(vp).convert("RGBA") CURRENT_TILE_LOG.append((id, "VPALACE", reason)) return img except FileNotFoundError: CURRENT_TILE_LOG.append((id, "MISSING", reason)) return None def blit(canvas, img, x, y): """Composite img onto canvas at (x,y) using its alpha as mask (draw_main style).""" if img is None: return w, h = img.size if x + w <= 0 or y + h <= 0 or x >= canvas.width or y >= canvas.height: return canvas.alpha_composite(img, (x, y)) def blit_black(canvas, img, x, y): """blitters_9_black (seg008.c:1054): рисует МАСКУ спрайта ЧЁРНЫМ — непрозрачная область формы заливается чёрным (тень-силуэт), а не пикселями картинки. Используется для floor-right в draw_tile_floorright (тень уступа тайла снизу-слева).""" if img is None: return w, h = img.size if x + w <= 0 or y + h <= 0 or x >= canvas.width or y >= canvas.height: return alpha = img.split()[3] solid = Image.new("RGBA", img.size, (0, 0, 0, 255)) canvas.alpha_composite(Image.composite(solid, Image.new("RGBA", img.size, (0, 0, 0, 0)), alpha), (x, y)) # ---- Dungeon wall_pattern() (seg008.c:1928) ---- # PRNG: seg009.c:321 random_seed = random_seed*214013 + 2531011; return (random_seed>>16) % (max+1) _random_seed = 0 def prandom(maxval): global _random_seed _random_seed = (_random_seed * 214013 + 2531011) & 0xFFFFFFFF return (_random_seed >> 16) % (maxval + 1) # wall sprite ids (chtab_7 / RSET_WALL = 7) res(200+id).png RES_WALL_FACE_MAIN, RES_WALL_FACE_TOP, RES_WALL_CENTRE_BASE, RES_WALL_CENTRE_MAIN = 1, 2, 3, 4 RES_WALL_RIGHT_BASE, RES_WALL_RIGHT_MAIN, RES_WALL_SINGLE_BASE, RES_WALL_SINGLE_MAIN = 5, 6, 7, 8 RES_WALL_LEFT_BASE, RES_WALL_LEFT_MAIN, RES_WALL_DIVIDER1, RES_WALL_DIVIDER2 = 9, 10, 11, 12 RES_WALL_RNDBLOCK = 13 RES_WALL_MARK_TL, RES_WALL_MARK_BL, RES_WALL_MARK_TR, RES_WALL_MARK_BR = 14, 15, 16, 17 WALL_MODIFIER_SWS, WALL_MODIFIER_SWW, WALL_MODIFIER_WWS, WALL_MODIFIER_WWW = 0, 1, 2, 3 # Сплошная грань стены (chtab_7), индекс = wall_modifier (seg008.c:567/687): WALL_FRAM_BOTTOM = [7, 9, 5, 3] # нижняя часть (draw_tile_bottom) WALL_FRAM_MAIN = [8, 10, 6, 4] # основная часть (draw_tile_fore) LPOS = [58, 41, 37, 20, 16] # draw_left_mark vertical offsets RPOS = [52, 42, 31, 21] # draw_right_mark vertical offsets TBL_LINE = [0, 10, 20] # смещение строки в 30-тайловом массиве комнаты def wall_modifier(room, drawn_row, col): """Модификатор стены (SWS/SWW/WWS/WWW = 0..3) по соседям слева/справа. seg008.c:1255-1305 (без USE_FAKE_TILES): сосед считается стеной, если его тип == wall(20). На кромке комнаты без линка сосед по умолчанию считается стеной (wall_to_left/right = 1).""" left_code = room.tile(drawn_row, col - 1)[0] if col > 0 else 20 right_code = room.tile(drawn_row, col + 1)[0] if col < 9 else 20 wall_left = (left_code == 20) wall_right = (right_code == 20) if wall_left and wall_right: return WALL_MODIFIER_WWW # 3 if wall_left: return WALL_MODIFIER_WWS # 2 if wall_right: return WALL_MODIFIER_SWW # 1 return WALL_MODIFIER_SWS # 0 def _wall_add(canvas, which_table, id, xh, xl, ybottom, reason): """Emulate ptr_add_table(RSET_WALL, id, xh, xl, ybottom, blit, 0). which_table 0 -> backtable (blit transparent), 1 -> foretable. RSET_WALL id -> res(200+id).png, cascade VDUNGEON->VPALACE.""" img = load_sprite(id, CHTAB_WALL, reason=reason) if img is None: return y = ybottom - img.height + 1 x = xh * 8 + xl blit(canvas, img, x, y) def draw_left_mark(canvas, decal_variant, arg2, arg1, drawn_row, col): image_id = RES_WALL_MARK_TL if decal_variant % 2: image_id = RES_WALL_MARK_BL lv2 = 0 if decal_variant > 3: lv2 = arg1 + 6 elif decal_variant > 1: lv2 = arg2 + 6 xh = COL_XH[col] draw_bottom_y = 63 * drawn_row + 65 _wall_add(canvas, 1, image_id, xh + (1 if decal_variant in (2, 3) else 0), lv2, draw_bottom_y - LPOS[decal_variant], reason=f"wall left-mark var{decal_variant}") def draw_right_mark(canvas, decal_variant, arg1, drawn_row, col): image_id = RES_WALL_MARK_TR if decal_variant % 2: image_id = RES_WALL_MARK_BR a = 24 if decal_variant < 2 else arg1 - 3 xh = COL_XH[col] draw_bottom_y = 63 * drawn_row + 65 _wall_add(canvas, 1, image_id, xh + (1 if decal_variant > 1 else 0), a, draw_bottom_y - RPOS[decal_variant], reason=f"wall right-mark var{decal_variant}") def wall_pattern(canvas, room, drawn_row, col, which_part, which_table): """Replay seg008.c wall_pattern() for a dungeon wall tile. which_part: 0 = bottom part (draw_tile_bottom), 1 = fore part (draw_tile_fore). which_table: 0 = backtable, 1 = foretable.""" global _random_seed xh = COL_XH[col] draw_bottom_y = 63 * drawn_row + 65 draw_main_y = draw_bottom_y - 3 saved_seed = _random_seed # seg008.c:1941 — зерно зависит от НОМЕРА КОМНАТЫ (не строки!): # random_seed = drawn_room + tbl_line[drawn_row] + drawn_col room_num = getattr(room, "room_num", 1) _random_seed = room_num + TBL_LINE[drawn_row] + col prandom(1) # discard is_dungeon = IS_DUNGEON middle_divider = prandom(1) middle_divider_offset = prandom(4) bottom_divider = prandom(1) bottom_divider_offset = prandom(4) # Модификатор стены (какие соседи — стены) — тот же, что для сплошной грани. bg = wall_modifier(room, drawn_row, col) if bg == WALL_MODIFIER_WWW: if which_part != 0: if prandom(4) == 0: _wall_add(canvas, which_table, RES_WALL_RNDBLOCK, xh, 0, draw_bottom_y - 42, reason="wall rndblock") _wall_add(canvas, which_table, RES_WALL_DIVIDER1 + middle_divider, xh + 1, middle_divider_offset, draw_bottom_y - 21, reason="wall middle-divider") _wall_add(canvas, which_table, RES_WALL_DIVIDER1 + bottom_divider, xh, bottom_divider_offset, draw_bottom_y, reason="wall bottom-divider") if which_part != 0 and is_dungeon: if prandom(4) == 0: draw_right_mark(canvas, prandom(3), middle_divider_offset, drawn_row, col) if prandom(4) == 0: draw_left_mark(canvas, prandom(4), middle_divider_offset - middle_divider, bottom_divider_offset - bottom_divider, drawn_row, col) elif bg == WALL_MODIFIER_SWS: if is_dungeon and which_part != 0: if prandom(6) == 0: draw_left_mark(canvas, prandom(1), middle_divider_offset - middle_divider, bottom_divider_offset - bottom_divider, drawn_row, col) elif bg == WALL_MODIFIER_SWW: if which_part != 0: if prandom(4) == 0: _wall_add(canvas, which_table, RES_WALL_RNDBLOCK, xh, 0, draw_bottom_y - 42, reason="wall rndblock") _wall_add(canvas, which_table, RES_WALL_DIVIDER1 + middle_divider, xh + 1, middle_divider_offset, draw_bottom_y - 21, reason="wall middle-divider") if is_dungeon: if prandom(4) == 0: draw_right_mark(canvas, prandom(3), middle_divider_offset, drawn_row, col) if prandom(4) == 0: draw_left_mark(canvas, prandom(3), middle_divider_offset - middle_divider, bottom_divider_offset - bottom_divider, drawn_row, col) elif bg == WALL_MODIFIER_WWS: if which_part != 0: _wall_add(canvas, which_table, RES_WALL_DIVIDER1 + middle_divider, xh + 1, middle_divider_offset, draw_bottom_y - 21, reason="wall middle-divider") _wall_add(canvas, which_table, RES_WALL_DIVIDER1 + bottom_divider, xh, bottom_divider_offset, draw_bottom_y, reason="wall bottom-divider") if which_part != 0 and is_dungeon: if prandom(4) == 0: draw_right_mark(canvas, prandom(1) + 2, middle_divider_offset, drawn_row, col) if prandom(4) == 0: draw_left_mark(canvas, prandom(4), middle_divider_offset - middle_divider, bottom_divider_offset - bottom_divider, drawn_row, col) _random_seed = saved_seed # Per-tile sprite log: draw_tile resets CURRENT_TILE_LOG, load_sprite appends to it. CURRENT_TILE_LOG = [] TILE_LOGS = {} # (row, col) -> list of (id, source, reason) # seg008.c:1108 — вертикальные слайсы решётки ворот (portcullis). DOOR_FRAM_SLICE = [67, 59, 58, 57, 56, 55, 54, 53, 52] # seg008.c:517 — правый край шипов по кадру get_spike_frame. SPIKES_FRAM_RIGHT = [0, 134, 135, 136, 137, 138, 137, 135, 134, 0] # seg008.c:450 — «blueline»-декали на ЗАДНЕЙ стене (силуэты кладки/окна), # рисуются правым соседом по МОДИФИКАТОРУ соседа слева: # tile_left == empty -> blueline_fram1[mod] (окно/решётка-фрагменты 124..126) # на высоте blueline_fram_y[mod] + main_y; # tile_left == floor -> blueline_fram3[mod] (кирпичные силуэты 44/45) # на main_y - 20 (если mod != !!level_type). BLUELINE_FRAM1 = [0, 124, 125, 126] BLUELINE_FRAM_Y = [0, -20, -20, 0] BLUELINE_FRAM3 = [44, 44, 45, 45] def draw_gate_back(canvas, modifier_left, draw_xh, draw_bottom_y, draw_main_y): """seg008.c:1110 draw_gate_back — портикулис (решётка) ворот. Рисуется на тайле СПРАВА от ворот (draw_tile_anim_right, tile_left==gate). У нас это col 0 room1, где сосед слева — ворота из room 5. modifier_left — модификатор тайла-ворот (степень поднятия решётки).""" x = draw_xh * 8 gate_top_y = draw_bottom_y - 62 gate_openness = (min(modifier_left, 188) >> 2) + 1 gate_bottom_y = draw_main_y - gate_openness if gate_bottom_y + 12 < draw_main_y: img = load_sprite(50, reason="gate bottom+B") # gate поднята высоко if img: blit(canvas, img, x, gate_bottom_y - img.height + 1) else: # id 47 (правая грань ворот) уже рисует draw_tile_right — не дублируем. img = load_sprite(51, reason="gate bottom") if img: blit(canvas, img, x, (gate_bottom_y - 2) - img.height + 1) # Стек 8px-слайсов решётки снизу вверх. ybottom = gate_bottom_y - 12 if ybottom < 192: while ybottom >= 0 and ybottom > 7 and (ybottom - 7) > gate_top_y: img = load_sprite(52, reason="gate slice 8px") if img: blit(canvas, img, x, ybottom - img.height + 1) ybottom -= 8 gate_frame = ybottom - gate_top_y + 1 if 0 < gate_frame < 9: img = load_sprite(DOOR_FRAM_SLICE[gate_frame], reason="gate top slice") if img: blit(canvas, img, x, ybottom - img.height + 1) def draw_tile(canvas, room, drawn_row, col, left_edge_code=20): global CURRENT_TILE_LOG CURRENT_TILE_LOG = [] code, fg_byte, modbit, bg_mod = room.tile(drawn_row, col) # tile_left: колонка слева. col-1 == -1 -> Room.tile берёт из левой # комнаты (или уровневая кромка = стена), см. Room.tile / load_leftroom. left_code, _, _, left_mod = room.tile(drawn_row, col - 1) draw_xh = COL_XH[col] x = draw_xh * 8 draw_bottom_y = 63 * drawn_row + 65 draw_main_y = draw_bottom_y - 3 # For topright/floorright we need the tile below-left. # Room below = same room (no room links) -> row_below = drawn_row+1 if <3 else treat as empty/floor top edge. row_below = drawn_row + 1 if row_below > 2: # below the bottom row of the level: floor top edge rbl_code, rbl_mod = 1, 0 else: rbl_code, _, _, rbl_mod = room.tile(row_below, col - 1) row_below_left_code = rbl_code t = TILE_TABLE[code] lt = TILE_TABLE[left_code] # ---- draw_tile_floorright (seg008.c:392) ---- # Рисуется ТОЛЬКО когда can_see_bottomleft(): текущий тайл — # empty/bigpillar_top/doortop/lattice_down. Иначе topright соседа # снизу-слева перекрыт и рисовать его нельзя (иначе «арки» повсюду — # это была главная причина замусоренного рендера). if code in (0, 9, 12, 26): # can_see_bottomleft _draw_topright(canvas, row_below_left_code, rbl_mod, x, draw_bottom_y, reason=f"topright(below-left tile {row_below_left_code:02X})") if lt[4]: # tile_table[tile_left].floor_right # blitters_9_black: ЧЁРНЫЙ силуэт (тень уступа), не светлый спрайт. img = load_sprite(42, reason=f"floor-right shadow(left tile {left_code:02X})") blit_black(canvas, img, x, draw_main_y + TILE_TABLE[1][5] - (img.height if img else 0) + 1) # ---- draw_tile_anim_topright ---- (gate top mask) - skipped: no anim # ---- draw_tile_right (seg008.c:456) ---- if code != 20: # у самой стены правый край не рисуется level_type = 0 if IS_DUNGEON else 1 if left_code == 20: # сосед слева — стена: её правая грань в текущую колонку (chtab_7 id 1) img = load_sprite(1, CHTAB_WALL, reason="wall right-face(left tile 14)") ry = TILE_TABLE[20][5] # right_y стены = 2 blit(canvas, img, x, ry + draw_main_y - (img.height if img else 0) + 1) elif left_code == 0: # сосед слева — empty: фрагмент окна/решётки по его модификатору if left_mod <= 3: bid = BLUELINE_FRAM1[left_mod] if bid: img = load_sprite(bid, reason=f"blueline-window(left empty mod{left_mod})") blit(canvas, img, x, BLUELINE_FRAM_Y[left_mod] + draw_main_y - (img.height if img else 0) + 1) elif left_code == 1: # сосед слева — floor: правый треугольник (42) + СИЛУЭТ КЛАДКИ (44/45) # на задней стене, если модификатор пола != !!level_type. img = load_sprite(42, reason="floor-right(part of left tile 01)") ry = TILE_TABLE[1][5] # right_y пола = 2 blit(canvas, img, x, ry + draw_main_y - (img.height if img else 0) + 1) num = left_mod if left_mod <= 3 else 0 if num != level_type: bid = BLUELINE_FRAM3[num] img = load_sprite(bid, reason=f"brick-silhouette(left floor mod{num})") blit(canvas, img, x, draw_main_y - 20 - (img.height if img else 0) + 1) else: rid = lt[3] # tile_left.right_id if rid: ry = lt[5] img = load_sprite(rid, reason=f"right(part of left tile {left_code:02X})") blit(canvas, img, x, ry + draw_main_y - (img.height if img else 0) + 1) # stripe: only drawn when tbl_level_type != 0 (palace). Dungeon: skip. sid = lt[6] if sid and not IS_DUNGEON: img = load_sprite(sid, reason=f"stripe(left tile {left_code:02X})") blit(canvas, img, x, draw_main_y - 27 - (img.height if img else 0) + 1) # ---- draw_tile_anim_right (seg008.c:530) ---- # «Анимированные» части соседа слева, проецируемые в текущий тайл. # Статикой (кадр покоя) рисуем те, что формируют ГЕОМЕТРИЮ фона: # gate -> решётка (portcullis) на границе с левой комнатой; # loose -> правый треугольный край шаткой плиты (иначе провал в полу); # spike -> статичный кадр шипов по модификатору. # torch-flame/chomper — чистая анимация, пропускаем (отдельные sprite_t). if left_code == 4: # tiles_4_gate draw_gate_back(canvas, left_mod, draw_xh, draw_bottom_y, draw_main_y) elif left_code == 11: # tiles_11_loose — правый край плиты (loose_fram_right[0]=42) img = load_sprite(42, reason="loose right (part of left tile 0B)") if img: blit(canvas, img, x, (draw_bottom_y - 1) - img.height + 1) elif left_code == 2: # tiles_2_spike — статичный кадр (spikes_fram_right) sf = 5 if (left_mod & 0x80) else left_mod sid = SPIKES_FRAM_RIGHT[sf] if 0 <= sf < len(SPIKES_FRAM_RIGHT) else 0 if sid: img = load_sprite(sid, reason="spike right (part of left tile 02)") if img: blit(canvas, img, x, (draw_main_y - 7) - img.height + 1) # ---- draw_tile_bottom (seg008.c:570) ---- if code == 20: # Сплошная НИЖНЯЯ грань стены (chtab_7 wall_fram_bottom) + декали. wmod = wall_modifier(room, drawn_row, col) img = load_sprite(WALL_FRAM_BOTTOM[wmod], CHTAB_WALL, reason="wall bottom-face") if img: blit(canvas, img, x, draw_bottom_y - img.height + 1) wall_pattern(canvas, room, drawn_row, col, 0, 0) else: bid = t[8] img = load_sprite(bid, reason=f"bottom({TILE_NAMES.get(code,'?')})") if img: blit(canvas, img, x, draw_bottom_y - img.height + 1) # ---- draw_loose (seg008.c:600) — статический кадр loose floor ---- # Нижняя часть шаткой плиты (loose_fram_bottom[0]=43) на draw_bottom_y. if code == 11: # 0x0B loose floor img = load_sprite(43, reason="loose bottom (draw_loose)") if img: blit(canvas, img, x, draw_bottom_y - img.height + 1) # ---- draw_tile_base (seg008.c:611) ---- base_id = t[0] base_y = t[2] if code == 11: # loose floor: базовая (верхняя) часть = loose_fram_left[0] = 41 base_id = 41 img = load_sprite(base_id, reason=f"base({TILE_NAMES.get(code,'?')})") if img: blit(canvas, img, x, base_y + draw_main_y - img.height + 1) # ---- draw_tile_anim ---- (spike/potion/chomper animated) skipped # ---- draw_tile_fore (seg008.c:690) ---- if code == 20: # Основная (средняя) грань стены (chtab_7 wall_fram_main) + декали/марки. wmod = wall_modifier(room, drawn_row, col) img = load_sprite(WALL_FRAM_MAIN[wmod], CHTAB_WALL, reason="wall main-face") if img: blit(canvas, img, x, draw_main_y - img.height + 1) wall_pattern(canvas, room, drawn_row, col, 1, 1) fid = t[9] if fid: # fore_x — в единицах xh (×8 пикс!), не в пикселях: seg008.c:743 # xh = fore_x + draw_xh, затем x = xh*8. fore_y — в пикселях. fx = t[10] * 8 + x fy = t[11] + draw_main_y img = load_sprite(fid, reason=f"fore({TILE_NAMES.get(code,'?')})") if img: blit(canvas, img, fx, fy - img.height + 1) # Save per-tile log (keyed by logical room position: row=drawn_row, col) TILE_LOGS[(drawn_row, col)] = list(CURRENT_TILE_LOG) def _draw_topright(canvas, tiletype, modifier, x, draw_bottom_y, reason=""): if tiletype in (7, 12): # doortop_with_floor / doortop -> palace uses frame return elif tiletype == 20: # wall img = load_sprite(2, CHTAB_WALL, reason=reason) else: tid = TILE_TABLE[tiletype][7] if tid == 0: return img = load_sprite(tid, reason=reason) if img: blit(canvas, img, x, draw_bottom_y - img.height + 1) def render_room(room_num): with open(os.path.join(LEVEL_DIR, f"res{2000 + room_num}.bin"), "rb") as f: data = f.read() level = Level(data) room = Room(level, room_num) # load_leftroom(): левая комната по линку L — её col 9 виден на границе # (col 0) текущей комнаты (напр. решётка ворот из соседней комнаты). left_link = level.links[room_num][0] if left_link: room.left_room = Room(level, left_link) canvas = Image.new("RGBA", (320, 192), (0, 0, 0, 0)) # rows drawn 2,1,0 (bottom first) for drawn_row in (2, 1, 0): for col in range(10): draw_tile(canvas, room, drawn_row, col) return canvas, room, level def render_single_tile(code, modifier=0, left_code=20, below_left_code=1): """Render one tile in isolation on a 40x64 canvas (matching SDLPoP tile box).""" TILE_W, TILE_H = 40, 64 canvas = Image.new("RGBA", (TILE_W, TILE_H), (0, 0, 0, 0)) # Place tile as if it were at drawn_row=2 (bottom), col=0 drawn_row = 2 col = 0 # Build a fake Room backed by single values class FakeRoom: def tile(self, r, c): if c < 0: return left_code, 0, 0, 0 if r < 0: return 1, 0, 0, 0 if r == drawn_row and c == 0: return code, 0, 0, modifier return below_left_code, 0, 0, 0 room = FakeRoom() draw_tile(canvas, room, drawn_row, col, left_edge_code=left_code) return canvas def main(): import sys room_num = 1 if len(sys.argv) > 1: room_num = int(sys.argv[1]) canvas, room, level = render_room(room_num) out = f"./room{room_num}_render.png" # Прозрачный фон (задняя чёрная стена, которую мы не рисуем) -> ЧЁРНЫМ, # а не белым как показывает вьюер прозрачность. Композитим по альфе. flat = Image.new("RGB", canvas.size, (0, 0, 0)) flat.paste(canvas, (0, 0), canvas) flat.save(out) print(f"Saved {out} ({canvas.width}x{canvas.height})") # debug grid with code labels (на чёрном фоне) from PIL import ImageDraw dbg = flat.copy() d = ImageDraw.Draw(dbg) for r in range(3): by = 63 * r + 65 for c in range(10): x = COL_XH[c] * 8 d.rectangle([x, by - 63, x + 31, by], outline=(255, 0, 0), width=1) code, _, _, _ = room.tile(r, c) d.text((x + 2, by - 60), f"{code:02X}", fill=(255, 255, 0)) dbg.save(f"./room{room_num}_debug.png") print(f"Saved room{room_num}_debug.png") # Per-tile individual renders (the 30 tiles of the room) # For proper neighbour context, render each tile inside its real room, # then crop its 32x64 (40x64) column box. tiles_dir = f"./tiles_room{room_num}" os.makedirs(tiles_dir, exist_ok=True) for r in range(3): by = 63 * r + 65 for c in range(10): x = COL_XH[c] * 8 code, _, _, _ = room.tile(r, c) # crop the column box (40 wide to include left overlap, 64 tall above bottom) box = (x, by - 64, x + 40, by) tile_img = canvas.crop(box) tile_img.save(os.path.join(tiles_dir, f"r{r}c{c}_{code:02X}.png")) print(f"Saved 30 tile crops to {tiles_dir}/") # Per-tile report: which images each tile pulls and why. print() print("=" * 78) print(f"PER-TILE SPRITE REPORT (room {room_num}, {'DUNGEON' if IS_DUNGEON else 'PALACE'})") print("=" * 78) for r in range(3): for c in range(10): code, fg_byte, mbit, bb = room.tile(r, c) name = TILE_NAMES.get(code, f"?0x{code:02X}") btd = level.backtable_desc(code, bb) line = (f"\n[{r},{c}] code=0x{code:02X} ({name}) " f"fg=0x{fg_byte:02X} mbit={mbit} backtable=0x{bb:02X} ({btd})") # If this tile is an event activator (group 'event'), show the door it drives. if code in (0x06, 0x0F): scr, loc, trig = level.door_for_event(bb) line += f"\n -> DOOR EVENT id={bb}: screen={scr} loc={loc} trigger_next={trig}" print(line) logs = TILE_LOGS.get((r, c), []) if not logs: print(" (no sprites drawn)") for sid, source, reason in logs: print(f" res{200+sid:03d}.png <- {source:8s} {reason}") print() print("=" * 78) # Level-level summary: start position, links, door events, guards. print() print("LEVEL STRUCTURE SUMMARY") print("-" * 78) print(f"start position: room={level.start[0]} loc={level.start[1]} dir={level.start[2]}") links = level.links.get(room_num, (0, 0, 0, 0)) print(f"room {room_num} links: L={links[0]} R={links[1]} Up={links[2]} Dn={links[3]}") # guard in this room gi = room_num - 1 gloc = level.guard_loc[gi] if gloc < 30: gr, gc = gloc // 10, gloc % 10 print(f"guard: loc=tile(r{gr},c{gc}) dir={level.guard_dir[gi]} " f"skill={level.guard_skill[gi]} colour={level.guard_colour[gi]}") else: print("guard: none") # door events driven by any tile in this room print("door events referenced by this room's tiles:") seen = set() for off in range(30): fb, code, mbit = room.fg[off] bb = room.bg[off] if code in (0x06, 0x0F): if bb not in seen: seen.add(bb) scr, loc, trig = level.door_for_event(bb) print(f" event id={bb} (tile off {off}): screen={scr} loc={loc} trigger_next={trig}") if not seen: print(" (none)") print() print("Room", room_num, "foreground codes (3 rows x 10 cols):") for r in range(3): print(" ", " ".join(f"{room.tile(r,c)[0]:02X}" for c in range(10))) if __name__ == "__main__": main()