Files
Sprinter-SDCC/applications/PoP/toolchain/render_room.py
T
snark13 cd8d566d82 applications/PoP: порт Prince of Persia — PoC (roomtest) + пайплайн
Порт PoP на Sprinter.  Текущий PoC — applications/PoP/roomtest/:
комната 1 (фон-композиция тайлов) + Kid с управлением на raw-клавиатуре
и коллизией с картой.

- roomtest — pop_bg (фон), pop_kid (спрайты Kid, column-major флип,
  seqtbl-анимация), pop_ctrl (порт control() PoP на held-state
  kbd_raw), pop_map (коллизия seg004/005: бег/стоп у стены,
  падение/приземление, отскок seq_47, вертикальный прыжок K4.1).
  MEMORY=small (DATA сразу за CODE, ~23КБ кода не лезет в huge).
- toolchain (PoP) — pop_pack_kid/pop_pack_bg/render_room/extract —
  распаковка res-графики MSDOS в атласы + композиция комнат.
- toolchain/ (корень) — make_hdd.sh (быстрый HDD-тест вместо FDD),
  png_strip.py / room_compose.py (ассет-пайплайн).
- docs — PORT_PLAN, KID_PLAN, форматы ресурсов (Apple II / MSDOS / DAT).
- bgtest/coltest/poc — ранние PoC (фон, коллизия, первый прототип).

.gitignore: build-артефакты applications/*/*/*; исключены внешние
референс-репозитории (SDLPoP/mininim/PR/Apple-II — свои git-клоны) и
оригинальные game-данные MSDOS/ (копирайт, только для реверса форматов).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 17:51:08 +03:00

843 lines
39 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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()