PoP roomtest: непрозрачные нижние грани + edge-триггер бампа + 2 колонки шва

Три фикса, найденные разбором в MAME.

1. Просвечивание Кида сквозь торец кнопки (подъём/спуск у кромки).
   draw_tile_bottom (seg008:570) рисует нижнюю грань блиттером
   blitters_0_no_transp: пиксель индекса 0 заливается ЧЁРНЫМ, а не
   пропускается.  У граней кнопок (env 96/149) и нижних кадров loose (73/74)
   последняя строка целиком нулевая — наш упаковщик переводил 0 в 0xFF для
   ВСЕХ спрайтов, и она становилась дырой, сквозь которую был виден Kid за
   тайлом (на запечённом фоне не видно — там и так чёрное).
   pop_pack_bg.py: NO_TRANSP_ENV_IDS пакуется с сохранением индекса 0 как
   ЦВЕТА (0x50 = чёрный в палитре env).  Ноль тактов в рантайме.

2. Разворот в проёме опущенной решётки выкидывал Кида на другую её сторону.
   check_bumped проверял столкновение УРОВНЕМ («край зашёл за грань»), поэтому
   разворот на месте внутри тайла-стены менял, какую грань мы меряем, и это
   читалось как новое столкновение → Kid.x = char_dx_forward(d) швырял его
   сквозь решётку.  Оригинал (check_collisions + get_row_collision_data,
   seg004:0004) считает флаги перекрытия от ГАБАРИТА персонажа, БЕЗ
   направления, и зовёт bumped() только на переходе 0 -> 1 (вправо смотрит на
   0x0F, влево на 0xF0).  Порт этого edge-триггера: флаги от габарита + память
   прошлого кадра по колонке, сброс при смене комнаты (в оригинале это
   несовпадение prev_coll_room/curr_row_coll_room).  Проверено: теперь
   разворот даёт уход в комнату 8 (leave_room, взгляд влево, char_x_left<=54)
   — то есть туда, где Kid физически и стоит.

3. Снимок шва расширен до ДВУХ колонок с каждой стороны (lcol_fg[6]/
   rcol_fg[6]: col9+col8 и col0+col1).  Kid, стоящий В шве (curr_col=-1),
   смотрит вперёд на колонку -2, где раньше была мнимая стена (get_tile ->
   TILE_WALL).  Оригинал резолвит любую колонку через find_room_of_tile
   (seg006:005D).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-28 10:47:58 +03:00
parent 6582154381
commit 5e09bd9d5a
9 changed files with 104 additions and 21 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
+12 -5
View File
@@ -110,16 +110,22 @@ int pop_room_load(uint8_t room, uint8_t *fg, uint8_t *bg,
right = lnk[1]; /* R */ right = lnk[1]; /* R */
down = lnk[3]; /* D */ down = lnk[3]; /* D */
/* leftcol: правый столбец (col9) комнаты СЛЕВА, строки 0..2 (кромка col0) */ /* leftcol: правый столбец (col9) комнаты СЛЕВА, строки 0..2 (кромка col0),
* а в [3..5] — ПРЕДпоследний (col8). Вторая колонка нужна коллизии: Kid,
* стоящий В шве (curr_col = −1), смотрит вперёд на колонку −2, и без неё
* там оказывалась мнимая стена (get_tile → TILE_WALL), которая выпихивала
* его сквозь решётку. Оригинал резолвит любую колонку через
* find_room_of_tile (seg006:005D). */
if (left >= 1 && left <= 24) { if (left >= 1 && left <= 24) {
const uint8_t *lf = room_fg_ptr(left); const uint8_t *lf = room_fg_ptr(left);
const uint8_t *lb = room_bg_ptr(left); const uint8_t *lb = room_bg_ptr(left);
for (i = 0; i < 3; i++) { for (i = 0; i < 3; i++) {
lcol_fg[i] = (uint8_t)(lf[i * 10 + 9] & 0x1F); lcol_fg[i] = (uint8_t)(lf[i * 10 + 9] & 0x1F);
lcol_fg[3 + i] = (uint8_t)(lf[i * 10 + 8] & 0x1F);
lcol_bg[i] = lb[i * 10 + 9]; lcol_bg[i] = lb[i * 10 + 9];
} }
} else { } else {
for (i = 0; i < 3; i++) { lcol_fg[i] = 20; lcol_bg[i] = 0; } /* нет комнаты = стена */ for (i = 0; i < 3; i++) { lcol_fg[i] = 20; lcol_fg[3 + i] = 20; lcol_bg[i] = 0; }
} }
/* rightcol: левый столбец (col0) комнаты СПРАВА, строки 0..2 (кромка col9: /* rightcol: левый столбец (col0) комнаты СПРАВА, строки 0..2 (кромка col9:
@@ -128,11 +134,12 @@ int pop_room_load(uint8_t room, uint8_t *fg, uint8_t *bg,
const uint8_t *rf = room_fg_ptr(right); const uint8_t *rf = room_fg_ptr(right);
const uint8_t *rb = room_bg_ptr(right); const uint8_t *rb = room_bg_ptr(right);
for (i = 0; i < 3; i++) { for (i = 0; i < 3; i++) {
rcol_fg[i] = (uint8_t)(rf[i * 10 + 0] & 0x1F); rcol_fg[i] = (uint8_t)(rf[i * 10 + 0] & 0x1F);
rcol_fg[3 + i] = (uint8_t)(rf[i * 10 + 1] & 0x1F); /* колонка 11 (см. выше) */
rcol_bg[i] = rb[i * 10 + 0]; rcol_bg[i] = rb[i * 10 + 0];
} }
} else { } else {
for (i = 0; i < 3; i++) { rcol_fg[i] = 20; rcol_bg[i] = 0; } /* нет комнаты = стена */ for (i = 0; i < 3; i++) { rcol_fg[i] = 20; rcol_fg[3 + i] = 20; rcol_bg[i] = 0; }
} }
/* belowrow: верхний ряд (row0) комнаты снизу */ /* belowrow: верхний ряд (row0) комнаты снизу */
+5 -2
View File
@@ -28,8 +28,11 @@ void pop_level_free(void);
/* Извлечь комнату room (1..24) в массивы приложения (W2): /* Извлечь комнату room (1..24) в массивы приложения (W2):
* fg[30], bg[30] — тайлы комнаты (fg маскирован &0x1F, bg raw); * fg[30], bg[30] — тайлы комнаты (fg маскирован &0x1F, bg raw);
* lcol_fg[3], lcol_bg[3] — правый столбец (col9) комнаты СЛЕВА (кромка * lcol_fg[6], lcol_bg[3] — [0..2] правый столбец (col9) комнаты СЛЕВА
* col0); нет комнаты → стена/0; * (кромка col0), [3..5] предпоследний (col8) —
* для коллизии на колонке −2 (Kid стоит В шве);
* rcol_fg[6] симметрично (col0 / col1);
* нет комнаты → стена/0;
* below_fg[10] — верхний ряд (row0) комнаты СНИЗУ («стена * below_fg[10] — верхний ряд (row0) комнаты СНИЗУ («стена
* вниз»); нет комнаты → стена. * вниз»); нет комнаты → стена.
* 0 — OK, -1 — уровень не загружен / room вне диапазона. */ * 0 — OK, -1 — уровень не загружен / room вне диапазона. */
+60 -7
View File
@@ -108,12 +108,23 @@ static uint8_t *g_fg; /* fg[30], row*10+col (МУТАБЕЛЬНАЯ
static uint8_t g_curr_tile; /* = curr_tile2: тайл последнего get_tile */ static uint8_t g_curr_tile; /* = curr_tile2: тайл последнего get_tile */
static uint8_t g_obj_xl; /* позиция внутри тайла (get_tile_div_mod) */ static uint8_t g_obj_xl; /* позиция внутри тайла (get_tile_div_mod) */
static uint8_t grab_timer; /* после зацепа (12): блокирует climb-up */ static uint8_t grab_timer; /* после зацепа (12): блокирует climb-up */
/* Связи комнаты (0 = нет соседа) + кромки швов: g_lcol = col9 левого соседа /* Состояние edge-триггера бампа (порт prev_coll_flags, seg004): колонка, для
* (за col0), g_rcol = col0 правого соседа (за col9). Для коллизии на швах — * которой сохранены флаги перекрытия, и сами флаги (бит0 = левая грань стены,
* бит1 = правая). BUMP_COL_NONE — данных нет (смена комнаты/старт). */
#define BUMP_COL_NONE ((int8_t)-128)
static int8_t bump_prev_col;
static uint8_t bump_prev_flags;
/* Связи комнаты (0 = нет соседа) + кромки швов: g_lcol[0..2] = col9 левого
* соседа (за col0), g_lcol[3..5] = его col8 (колонка −2); g_rcol симметрично
* (col0 / col1 правого соседа, колонки 10 и 11). Для коллизии на швах —
* иначе Kid, войдя в соседнюю комнату у самой кромки (curr_col=-1/10), стоял * иначе Kid, войдя в соседнюю комнату у самой кромки (curr_col=-1/10), стоял
* бы «в стене» уровня и in_wall выбрасывал бы его. Задаются pop_map_set_edges. */ * бы «в стене» уровня и in_wall выбрасывал бы его. ВТОРАЯ колонка нужна,
* когда Kid стоит В шве и смотрит НАРУЖУ: передняя колонка тогда −2 (или 11),
* и мнимая стена там выпихивала его сквозь решётку (баг разворота у ворот).
* Оригинал резолвит любую колонку через find_room_of_tile (seg006:005D).
* Задаются pop_map_set_edges. */
static uint8_t g_link_l, g_link_r, g_link_u, g_link_d; static uint8_t g_link_l, g_link_r, g_link_u, g_link_d;
static uint8_t g_lcol[3], g_rcol[3]; static uint8_t g_lcol[6], g_rcol[6];
/* ---- Плита-ПОТОЛОК: loose ряда 2 комнаты СВЕРХУ ------------------- * /* ---- Плита-ПОТОЛОК: loose ряда 2 комнаты СВЕРХУ ------------------- *
* В оригинале это тот же loose, просто get_tile ушёл в комнату сверху * В оригинале это тот же loose, просто get_tile ушёл в комнату сверху
@@ -150,8 +161,12 @@ void pop_map_set_edges(uint8_t left, uint8_t right, uint8_t up, uint8_t down,
const uint8_t *lcol_fg, const uint8_t *rcol_fg) const uint8_t *lcol_fg, const uint8_t *rcol_fg)
{ {
g_link_l = left; g_link_r = right; g_link_u = up; g_link_d = down; g_link_l = left; g_link_r = right; g_link_u = up; g_link_d = down;
if (lcol_fg) { g_lcol[0] = lcol_fg[0]; g_lcol[1] = lcol_fg[1]; g_lcol[2] = lcol_fg[2]; } if (lcol_fg) { uint8_t i; for (i = 0; i < 6; i++) g_lcol[i] = lcol_fg[i]; }
if (rcol_fg) { g_rcol[0] = rcol_fg[0]; g_rcol[1] = rcol_fg[1]; g_rcol[2] = rcol_fg[2]; } if (rcol_fg) { uint8_t i; for (i = 0; i < 6; i++) g_rcol[i] = rcol_fg[i]; }
/* смена комнаты — колонки перепроецированы, прошлые флаги перекрытия
* недействительны (в оригинале prev_coll_room != curr_row_coll_room и
* бампа на кадре входа нет). */
bump_prev_col = BUMP_COL_NONE;
} }
static uint8_t get_tile(int8_t col, int8_t row) static uint8_t get_tile(int8_t col, int8_t row)
@@ -165,8 +180,12 @@ static uint8_t get_tile(int8_t col, int8_t row)
g_curr_tile = TILE_WALL; g_curr_tile = TILE_WALL;
else if (col == -1 && g_link_l) /* левый шов: тайл col9 комнаты слева */ else if (col == -1 && g_link_l) /* левый шов: тайл col9 комнаты слева */
g_curr_tile = g_lcol[row] & 0x1F; g_curr_tile = g_lcol[row] & 0x1F;
else if (col == -2 && g_link_l) /* на тайл глубже в комнату слева (col8) */
g_curr_tile = g_lcol[3 + row] & 0x1F;
else if (col == 10 && g_link_r) /* правый шов: тайл col0 комнаты справа */ else if (col == 10 && g_link_r) /* правый шов: тайл col0 комнаты справа */
g_curr_tile = g_rcol[row] & 0x1F; g_curr_tile = g_rcol[row] & 0x1F;
else if (col == 11 && g_link_r) /* на тайл глубже в комнату справа (col1) */
g_curr_tile = g_rcol[3 + row] & 0x1F;
else if (col < 0 || col > 9) else if (col < 0 || col > 9)
g_curr_tile = TILE_WALL; /* край уровня / нет соседа = стена */ g_curr_tile = TILE_WALL; /* край уровня / нет соседа = стена */
else else
@@ -767,7 +786,41 @@ static void check_bumped(void)
tcol = get_tile_div_mod_m7(char_front_coll()); tcol = get_tile_div_mod_m7(char_front_coll());
t = get_tile(tcol, Kid.curr_row); t = get_tile(tcol, Kid.curr_row);
wt = wall_type(t); wt = wall_type(t);
if (!wt) return; /* EDGE-триггер (порт check_collisions + get_row_collision_data, seg004:0004).
* Оригинал считает флаги перекрытия от ГАБАРИТА персонажа, БЕЗ направления:
* 0x0F = left_wall_xpos < char_x_right_coll («в левую грань стены»)
* 0xF0 = right_wall_xpos > char_x_left_coll («в правую грань»)
* и зовёт bumped() ТОЛЬКО когда нужный флаг перешёл 0 -> 1 (взгляд вправо
* смотрит на 0x0F, влево — на 0xF0). Без этого проверка «уровнем» ловит
* ЛЮБОЕ перекрытие: Kid, стоящий В проёме опущенной решётки, при развороте
* на месте получал ложный бамп и выпихивался сквозь неё в соседнюю
* комнату. С флагами от габарита разворот их не меняет — бампа нет. */
{
uint8_t f = 0, prev, need;
if (wt) {
int coll_left = x_bump[tcol + FIRST_ONSCREEN_COLUMN] + TILE_MIDX;
int lw = wall_dl[wt] + coll_left; /* get_left_wall_xpos */
int rw = coll_left - wall_dr[wt] + TILE_RIGHTX; /* get_right_wall_xpos */
int e = char_x_forward_edge(), wh = ((int)kid_fp_width() + 1) >> 1;
int xl, xr;
if (Kid.direction < 0) { xl = e; xr = e + wh; } /* габарит персонажа */
else { xr = e; xl = e - wh; }
if (kid_cur_flags() & FRAME_THIN) { xl += 4; xr -= 4; }
if (lw < xr) f |= 1;
if (rw > xl) f |= 2;
}
/* прошлый кадр: та же колонка — её флаги; другая — считаем, что не
* перекрывали; «нет данных» (смена комнаты) — считаем, что перекрывали
* всё, чтобы на кадре входа бампа не было (в оригинале это даёт
* несовпадение prev_coll_room и curr_row_coll_room). */
prev = (bump_prev_col == BUMP_COL_NONE) ? 3u
: (bump_prev_col == tcol ? bump_prev_flags : 0u);
bump_prev_col = tcol;
bump_prev_flags = f;
if (!wt) return;
need = (Kid.direction < 0) ? 2u : 1u;
if (!(f & need) || (prev & need)) return; /* не перекрыл / уже перекрывал */
}
if (t == TILE_GATE && gate_passable(tcol, Kid.curr_row)) if (t == TILE_GATE && gate_passable(tcol, Kid.curr_row))
return; /* открытые ворота (шов/внутри) — Kid проходит (не бамп) */ return; /* открытые ворота (шов/внутри) — Kid проходит (не бамп) */
d = dist_from_wall_forward(t, tcol); d = dist_from_wall_forward(t, tcol);
+3 -2
View File
@@ -20,8 +20,9 @@
void pop_map_set(uint8_t *fg); void pop_map_set(uint8_t *fg);
/* Связи комнаты (0=нет соседа; иначе номер) + кромки швов для коллизии: /* Связи комнаты (0=нет соседа; иначе номер) + кромки швов для коллизии:
* lcol_fg = col9 левого соседа (за col0), rcol_fg = col0 правого соседа * lcol_fg[6] = col9 левого соседа (за col0) в [0..2] и его col8 (колонка −2)
* (за col9). Звать в enter_room. */ * в [3..5]; rcol_fg[6] симметрично (col0 / col1 = колонки 10 и 11). Вторая
* колонка нужна, когда Kid стоит В шве и смотрит наружу. Звать в enter_room. */
void pop_map_set_edges(uint8_t left, uint8_t right, uint8_t up, uint8_t down, void pop_map_set_edges(uint8_t left, uint8_t right, uint8_t up, uint8_t down,
const uint8_t *lcol_fg, const uint8_t *rcol_fg); const uint8_t *lcol_fg, const uint8_t *rcol_fg);
+4 -1
View File
@@ -36,7 +36,10 @@ __sfr __at (0xFE) io_border;
* соседей для кромок. Заполняются pop_room_load из уровня. File-scope, * соседей для кромок. Заполняются pop_room_load из уровня. File-scope,
* чтобы enter_room переключал комнату без длинного списка параметров. */ * чтобы enter_room переключал комнату без длинного списка параметров. */
static uint8_t room_fg[30], room_bg[30]; static uint8_t room_fg[30], room_bg[30];
static uint8_t lcol_fg[3], lcol_bg[3], rcol_fg[3], rcol_bg[3], below_fg[10]; /* lcol_fg/rcol_fg — ШЕСТЬ записей: [0..2] кромочная колонка соседа (col9/col0),
* [3..5] следующая за ней (col8/col1) для коллизии, когда Kid стоит В шве и
* смотрит наружу (колонка −2 / 11). См. pop_level.h. */
static uint8_t lcol_fg[6], lcol_bg[3], rcol_fg[6], rcol_bg[3], below_fg[10];
static uint8_t above_fg[10], above_mod[10]; /* ряд 2 верхнего соседа (полоса кладки у потолка) */ static uint8_t above_fg[10], above_mod[10]; /* ряд 2 верхнего соседа (полоса кладки у потолка) */
static uint8_t cur_room; /* номер ОТРИСОВАННОЙ комнаты (drawn_room) */ static uint8_t cur_room; /* номер ОТРИСОВАННОЙ комнаты (drawn_room) */
static uint8_t kid_room; /* РЕАЛЬНАЯ комната Kid (S1: == cur_room; расходятся в S3 straddle) */ static uint8_t kid_room; /* РЕАЛЬНАЯ комната Kid (S1: == cur_room; расходятся в S3 straddle) */
+20 -4
View File
@@ -143,9 +143,22 @@ def collect_used():
# ---- 2. Загрузка спрайта в 8bpp-блоб --------------------------------- # ---- 2. Загрузка спрайта в 8bpp-блоб ---------------------------------
def load_indexed(chtab, id): # Спрайты chtab_6, которые оригинал рисует БЕЗ прозрачности
# (blitters_0_no_transp): нижние грани тайлов (draw_tile_bottom, seg008:570) и
# нижние кадры loose (draw_loose, seg008:600). У 96/149 (грани кнопок) и 73/74
# (кадры loose) ПОСЛЕДНЯЯ строка — пиксель 0; в оригинале она заливается ЧЁРНЫМ
# поверх того, что сзади, а у нас 0 -> 0xFF делал её прозрачной, и сквозь неё
# просвечивал Kid (спуск/подъём у кромки кнопки). 43/36 полностью непрозрачны —
# для них флаг ничего не меняет, оставлены для полноты списка.
NO_TRANSP_ENV_IDS = {36, 43, 73, 74, 96, 149}
def load_indexed(chtab, id, opaque=False):
"""PNG (mode P, 16 цветов) -> (w, h, bytes 8bpp). Каскад VDUNGEON->VPALACE. """PNG (mode P, 16 цветов) -> (w, h, bytes 8bpp). Каскад VDUNGEON->VPALACE.
Пиксель 0 -> 0xFF (прозрач); i -> base+i (env 0x50 / wall 0x60).""" Пиксель 0 -> 0xFF (прозрач); i -> base+i (env 0x50 / wall 0x60).
opaque=True — пиксель 0 остаётся ЦВЕТОМ (base+0 = чёрный), а не ключом
прозрачности: порт blitters_0_no_transp (см. NO_TRANSP_ENV_IDS)."""
base_res = 360 if chtab == R.CHTAB_WALL else 200 base_res = 360 if chtab == R.CHTAB_WALL else 200
pal_base = 0x60 if chtab == R.CHTAB_WALL else 0x50 pal_base = 0x60 if chtab == R.CHTAB_WALL else 0x50
fname = f"res{base_res + id}.png" fname = f"res{base_res + id}.png"
@@ -161,7 +174,10 @@ def load_indexed(chtab, id):
if im.mode != "P": if im.mode != "P":
die(f"{fname}: mode {im.mode}, ожидался P (16-цветный)") die(f"{fname}: mode {im.mode}, ожидался P (16-цветный)")
src = im.tobytes() src = im.tobytes()
out = bytes((0xFF if v == 0 else pal_base + v) for v in src) if opaque:
out = bytes((pal_base + v) for v in src)
else:
out = bytes((0xFF if v == 0 else pal_base + v) for v in src)
return im.width, im.height, out return im.width, im.height, out
@@ -330,7 +346,7 @@ def main():
# ENV фон -> страницы по id>>SHIFT # ENV фон -> страницы по id>>SHIFT
pages = {} pages = {}
for id in env_bg: for id in env_bg:
w, h, pix = load_indexed(R.CHTAB_ENV, id) w, h, pix = load_indexed(R.CHTAB_ENV, id, id in NO_TRANSP_ENV_IDS)
pages.setdefault(id >> ENV_SHIFT, {})[id & ENV_MASK] = (w, h, pix) pages.setdefault(id >> ENV_SHIFT, {})[id & ENV_MASK] = (w, h, pix)
max_pg = max(pages) if pages else -1 max_pg = max(pages) if pages else -1
env_page_names = [] env_page_names = []