From 5e09bd9d5a0801be69d6c4628b021082826dc14e Mon Sep 17 00:00:00 2001 From: Alexander Petrov Date: Tue, 28 Jul 2026 10:47:58 +0300 Subject: [PATCH] =?UTF-8?q?PoP=20roomtest:=20=D0=BD=D0=B5=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D0=B7=D1=80=D0=B0=D1=87=D0=BD=D1=8B=D0=B5=20=D0=BD=D0=B8?= =?UTF-8?q?=D0=B6=D0=BD=D0=B8=D0=B5=20=D0=B3=D1=80=D0=B0=D0=BD=D0=B8=20+?= =?UTF-8?q?=20edge-=D1=82=D1=80=D0=B8=D0=B3=D0=B3=D0=B5=D1=80=20=D0=B1?= =?UTF-8?q?=D0=B0=D0=BC=D0=BF=D0=B0=20+=202=20=D0=BA=D0=BE=D0=BB=D0=BE?= =?UTF-8?q?=D0=BD=D0=BA=D0=B8=20=D1=88=D0=B2=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Три фикса, найденные разбором в 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 --- applications/PoP/poc/res/bg/pop_env2.atl | Bin 10798 -> 10798 bytes applications/PoP/poc/res/bg/pop_env3.atl | Bin 5032 -> 5032 bytes applications/PoP/poc/res/bg/pop_env4.atl | Bin 8982 -> 8982 bytes applications/PoP/roomtest/pop_level.c | 17 ++++-- applications/PoP/roomtest/pop_level.h | 7 ++- applications/PoP/roomtest/pop_map.c | 67 +++++++++++++++++++--- applications/PoP/roomtest/pop_map.h | 5 +- applications/PoP/roomtest/roomtest.c | 5 +- applications/PoP/toolchain/pop_pack_bg.py | 24 ++++++-- 9 files changed, 104 insertions(+), 21 deletions(-) diff --git a/applications/PoP/poc/res/bg/pop_env2.atl b/applications/PoP/poc/res/bg/pop_env2.atl index 2470cbab73acaab2c4b6a1fb6b9848416a8fd9d1..9773976404984193187426e80cbc9c205a5e9efd 100644 GIT binary patch delta 57 ncmZ1%vMyu+Cw~Acn9R?aB8n;m= 1 && left <= 24) { const uint8_t *lf = room_fg_ptr(left); const uint8_t *lb = room_bg_ptr(left); 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]; } } 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: @@ -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 *rb = room_bg_ptr(right); 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]; } } 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) комнаты снизу */ diff --git a/applications/PoP/roomtest/pop_level.h b/applications/PoP/roomtest/pop_level.h index aa0c8ef..ff435ac 100644 --- a/applications/PoP/roomtest/pop_level.h +++ b/applications/PoP/roomtest/pop_level.h @@ -28,8 +28,11 @@ void pop_level_free(void); /* Извлечь комнату room (1..24) в массивы приложения (W2): * fg[30], bg[30] — тайлы комнаты (fg маскирован &0x1F, bg raw); - * lcol_fg[3], lcol_bg[3] — правый столбец (col9) комнаты СЛЕВА (кромка - * col0); нет комнаты → стена/0; + * lcol_fg[6], lcol_bg[3] — [0..2] правый столбец (col9) комнаты СЛЕВА + * (кромка col0), [3..5] предпоследний (col8) — + * для коллизии на колонке −2 (Kid стоит В шве); + * rcol_fg[6] симметрично (col0 / col1); + * нет комнаты → стена/0; * below_fg[10] — верхний ряд (row0) комнаты СНИЗУ («стена * вниз»); нет комнаты → стена. * 0 — OK, -1 — уровень не загружен / room вне диапазона. */ diff --git a/applications/PoP/roomtest/pop_map.c b/applications/PoP/roomtest/pop_map.c index 57750aa..00b7daa 100644 --- a/applications/PoP/roomtest/pop_map.c +++ b/applications/PoP/roomtest/pop_map.c @@ -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_obj_xl; /* позиция внутри тайла (get_tile_div_mod) */ static uint8_t grab_timer; /* после зацепа (12): блокирует climb-up */ -/* Связи комнаты (0 = нет соседа) + кромки швов: g_lcol = col9 левого соседа - * (за col0), g_rcol = col0 правого соседа (за col9). Для коллизии на швах — +/* Состояние edge-триггера бампа (порт prev_coll_flags, seg004): колонка, для + * которой сохранены флаги перекрытия, и сами флаги (бит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), стоял - * бы «в стене» уровня и 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_lcol[3], g_rcol[3]; +static uint8_t g_lcol[6], g_rcol[6]; /* ---- Плита-ПОТОЛОК: loose ряда 2 комнаты СВЕРХУ ------------------- * * В оригинале это тот же 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) { 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 (rcol_fg) { g_rcol[0] = rcol_fg[0]; g_rcol[1] = rcol_fg[1]; g_rcol[2] = rcol_fg[2]; } + if (lcol_fg) { uint8_t i; for (i = 0; i < 6; i++) g_lcol[i] = lcol_fg[i]; } + 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) @@ -165,8 +180,12 @@ static uint8_t get_tile(int8_t col, int8_t row) g_curr_tile = TILE_WALL; else if (col == -1 && g_link_l) /* левый шов: тайл col9 комнаты слева */ 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 комнаты справа */ 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) g_curr_tile = TILE_WALL; /* край уровня / нет соседа = стена */ else @@ -767,7 +786,41 @@ static void check_bumped(void) tcol = get_tile_div_mod_m7(char_front_coll()); t = get_tile(tcol, Kid.curr_row); 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)) return; /* открытые ворота (шов/внутри) — Kid проходит (не бамп) */ d = dist_from_wall_forward(t, tcol); diff --git a/applications/PoP/roomtest/pop_map.h b/applications/PoP/roomtest/pop_map.h index 1b93f9c..5b0a5e2 100644 --- a/applications/PoP/roomtest/pop_map.h +++ b/applications/PoP/roomtest/pop_map.h @@ -20,8 +20,9 @@ void pop_map_set(uint8_t *fg); /* Связи комнаты (0=нет соседа; иначе номер) + кромки швов для коллизии: - * lcol_fg = col9 левого соседа (за col0), rcol_fg = col0 правого соседа - * (за col9). Звать в enter_room. */ + * lcol_fg[6] = col9 левого соседа (за col0) в [0..2] и его col8 (колонка −2) + * в [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, const uint8_t *lcol_fg, const uint8_t *rcol_fg); diff --git a/applications/PoP/roomtest/roomtest.c b/applications/PoP/roomtest/roomtest.c index 14a4233..0cb4435 100644 --- a/applications/PoP/roomtest/roomtest.c +++ b/applications/PoP/roomtest/roomtest.c @@ -36,7 +36,10 @@ __sfr __at (0xFE) io_border; * соседей для кромок. Заполняются pop_room_load из уровня. File-scope, * чтобы enter_room переключал комнату без длинного списка параметров. */ 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 cur_room; /* номер ОТРИСОВАННОЙ комнаты (drawn_room) */ static uint8_t kid_room; /* РЕАЛЬНАЯ комната Kid (S1: == cur_room; расходятся в S3 straddle) */ diff --git a/applications/PoP/toolchain/pop_pack_bg.py b/applications/PoP/toolchain/pop_pack_bg.py index 287c033..e5a101d 100644 --- a/applications/PoP/toolchain/pop_pack_bg.py +++ b/applications/PoP/toolchain/pop_pack_bg.py @@ -143,9 +143,22 @@ def collect_used(): # ---- 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. - Пиксель 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 pal_base = 0x60 if chtab == R.CHTAB_WALL else 0x50 fname = f"res{base_res + id}.png" @@ -161,7 +174,10 @@ def load_indexed(chtab, id): if im.mode != "P": die(f"{fname}: mode {im.mode}, ожидался P (16-цветный)") 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 @@ -330,7 +346,7 @@ def main(): # ENV фон -> страницы по id>>SHIFT pages = {} 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) max_pg = max(pages) if pages else -1 env_page_names = []