Сверено в SDLPoP: Кид, стоящий колонкой правее лежащего трупа, и там рисуется поверх его головы. Значит порт верен, а артефакт врождённый: set_objtile_at_char приписывает персонажа РОВНО ОДНОМУ тайлу, ширина спрайта на выбор тайла не влияет, и всё, что стоит правее, перекрывает выступающую часть тела. Механизма «широкий объект в нескольких тайлах» в оригинале нет — сверено с draw_objtable_items_at_tile, sort_curr_objs и веткой tile_object_redraw == 0xFF (та про оверлеи пола). Закрытая запись перечисляет все четыре корня, которые пришлось снять по очереди: порядок по роли вместо тайла; своя регрессия с общим окном fore-клипа; колонка трупа из тайла вместо X; непортированная ветка actions_1_run_jump. Записано и то, как отличать остаток от этих багов, и цена «починки» остатка (тайл по центру габарита = отход от эталона). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
53 KiB
roomtest — архив закрытых багов
Сюда переезжает всё, что закрыто: подтверждённые фиксы, снятые
диагнозы, осознанные решения «не делать». Открытые баги — в
bug_list.md, текущие задачи — в TASKS.md.
Файл существует не ради истории как таковой: половина записей ниже — это
разбор КОРНЯ (odd-pixel арифметика char_x, подстановка тайла нажатой
кнопки, баг кодогенератора SDCC), и он экономит часы, когда похожий симптом
всплывёт снова. Прежде чем заводить новый баг — грепни здесь по симптому.
Проверено в MAME 2026-08-01 (ревизия L1-TRIAGE)
Три бага стояли как Critical с 2026-07-21 и по исходникам выглядели
закрытыми, но переподтверждены не были. Прогон в MAME (roomtest, ROOMNAV,
чтение _Kid через мост) закрыл все три.
BUG-1. Боковой переход через ворота: Kid проваливается на row 1 — ЗАКРЫТ (не воспроизводится)
Был симптом: при проходе через ОТКРЫТЫЕ ворота в соседнюю комнату (через шов) Kid оказывался на ряду row 1 вместо row 0.
Проверка 2026-08-01, точный сценарий бага. Комната 6, Kid на ряду 0; осторожный шаг на кнопку (0,2) — решётка room8 (0,9) поднимается; удержание ← через открытый шов:
| момент | room |
x |
y |
curr_col |
curr_row |
|---|---|---|---|---|---|
| на кнопке в room6 | 6 | 97 | 55 | 2 | 0 |
| после перехода | 8 | 181 | 55 | 8 | 0 |
Ряд и Y сохранены, Kid стоит на полу ряда 0 (скриншот triage_06.png).
Провала нет.
Заодно снят и сам диагноз записи — он был неверен. В записи стояло:
«Y/curr_row при боковом переходе НЕ репроецируются, в отличие от
check_leave_below». Это не дефект, а точное поведение оригинала:
goto_other_room (SDLPoP/src/seg002.c:390) для направлений left/right
меняет ТОЛЬКО Char.x (±140), а Char.y/curr_row трогает исключительно
для up/down. Наш check_leave (pop_map.c:1402) делает ровно то же.
Реальной причиной симптома была, судя по всему, кромочная коллизия — её
закрыл char_x_forward_edge (см. BUG-SEAM-PINGPONG ниже).
BUG-2. Возврат из комнаты назад: Kid отбрасывается обратно (ping-pong) — ЗАКРЫТ (не воспроизводится)
Был симптом: после перехода в соседнюю комнату попытка сразу вернуться приводила к тому, что Kid снова закидывался в ту же комнату — выйти нельзя.
Проверка 2026-08-01. Шов room2↔room3 (ряд 1, без ворот — чистый горизонтальный переход), с намеренным разворотом СРАЗУ после пересечения:
| действие | room |
x |
curr_col |
|---|---|---|---|
| старт в room2 | 2 | 86 | 1 |
| держим → | 3 | 122 | 3 |
| сразу держим ← | 2 | 195 | 9 |
| сразу держим → | 3 | 85 | 1 |
| сразу держим ← | 2 | 183 | 8 |
Комната меняется РОВНО один раз на пересечение, туда и обратно, без
осцилляции. Механизм на месте: pop_leave_timer (порт exit_room_timer,
pop_map.c:112,1553) + char_x_forward_edge (pop_map.c:365).
BUG-3. Climb-up на тайл-кнопку: неправильная окклюзия — ЗАКРЫТ фиксом от 2026-07-28
Был симптом: при подтягивании на тайл, верх которого — кнопка (opener/closer), Kid рисовался ПОВЕРХ кнопки вместо того, чтобы быть перекрытым её передней гранью.
Почему закрыт. Запись требовала: «трактовать нажатую кнопку как
floor-тайл в climb-overlay, учесть подстановку из get_tile_to_draw».
Ровно это и сделано tile_code_drawn() — climb_overlay_tile
(pop_bg.c:1346) берёт ПОДСТАВЛЕННЫЙ код тайла, а не сырой. То есть
BUG-3 — дубль пункта «спуск с кнопки (room8, кромка (0,6))» из раздела
«Исправлено», заведённый до фикса.
Оговорка, чтобы не выдавать желаемое: покадрово в MAME снимался СПУСК
(кадры 148..138). Подъём идёт через ту же ветку и ту же таблицу
FLOOR_LEFT_OVERLAY[fidx], поэтому отдельного дефекта тут быть не может,
но визуально направление «вверх» не переснималось. Если при сквозном
прохождении (L1-PASS) увидишь Kid поверх кнопки на подъёме — заводи заново.
Косметика окклюзии — закрыта кодом, список отставал (сверено 2026-08-01)
Четыре записи от 2026-07-22 висели как открытые Medium. Каждая описывала недостающий кусок порта; каждый из них с тех пор написан, но записи никто не снял. Ниже — что именно закрывает каждую.
BUG-CEIL-1. Прыжок вверх: руки Kid рисуются ПОВЕРХ потолка — ЗАКРЫТ
Был симптом: при прыжке вверх (SEQ up, кадры 67..79) руки/голова Kid заходили в полосу кладки у потолка (row −1) и рисовались ПОВЕРХ неё.
Чем закрыт: ceil_over_kid_tile() (pop_bg.c:1275) — порт «нижняя грань
потолка и кадр плиты-потолка идут в FOREtable», т.е. поверх персонажа.
Зовётся из pop_fore_over_kid (pop_bg.c:1575) и из fore-прохода стража
(:1607). Комментарий на месте прямо называет причину: «иначе руки
прыгающего Kid лезут на кромку потолка».
BUG-CEIL-2. Тряска/разбитие loose-плиты в потолке (row −1) — ЗАКРЫТ
Был симптом: loose-плита в ряду 2 верхнего соседа (room5 (2,5) → потолок room6 над (0,5)) при прыжках Kid на (0,5) не тряслась и не разбивалась — loose-состояние соседней комнаты не тянулось.
Чем закрыт: отдельное состояние плиты-потолка pop_ceil_modif[10]
(pop_bg.c:547, ведёт pop_map) + пара pop_ceil_shake_draw() /
pop_ceil_bake_empty() (pop_bg.c:815,827): дрожание рисуется на текущей
странице поверх фона, а провал «запекается» в ОЗУ-копию, чтобы heal его
сохранял. В полосе у потолка виден только НИЗ плиты (draw_tile_aboveroom),
всё выше режется клипом POP_YOFF — как в оригинале.
Из этого следует, что и запись «требует персистентного per-room modifier соседей, это Фаза P0 gates_spikes_plan» больше не верна: понадобился не общий механизм, а один массив на 10 байт под конкретный случай.
BUG-CEIL-3. Анимация ворот стирает потолок над ними — ЗАКРЫТ
Был симптом: при анимации решётки шва полоса кладки у потолка НАД
воротами пропадала — чёрный bar по col0 стирал ряд −1, а redraw его не
восстанавливал.
Чем закрыт: pop_room_redraw_seam_left() (pop_bg.c:793) больше не
трогает полосу потолка — bar идёт с POP_YOFF + 3, а не с POP_YOFF:
низ полосы кладки на room-space y=2, бары ворот начинаются с y=3
(gate_top_y = dby − 62). Приятный побочный эффект: отпала необходимость
перерисовывать draw_tile(-1,0) на КАЖДОМ кадре анимации решётки — то есть
фикс не только косметический, но и вдвое дешевле прежнего.
BUG-OCCL-1. Тень дальней колонны перекрывает Kid — ЗАКРЫТ
Был симптом: Kid у (0,4)-(0,5) частично перекрыт тёмной штриховкой — боковой гранью ДАЛЬНЕЙ колонны, которая окклюдить персонажа не должна (over-occlusion fore-слоя, рисовавшего fore футпринт-тайлов без учёта глубины).
Чем закрыт: разделение слоёв по признаку из оригинала, а не по нашим
соображениям о глубине (overlay_mid_tile, pop_bg.c:1367). Ключ,
подтверждённый трассой оригинала: draw_tile_right, draw_tile_anim_right и
draw_loose кладут спрайты через add_backtable НАПРЯМУЮ, минуя
ptr_add_table — значит правая грань левого соседа («шахматка» столба 93,
blueline, грани пик/loose соседа, кадр loose) всегда рисуется ПОД
персонажем. Через ptr_add_table (→ midtable при draw_other_overlay) идут
только «floor B» 42 при левом соседе-поле, draw_tile_base,
draw_tile_anim (свои пики) и draw_tile_bottom.
BUG-DOOR-CLIP. Подъём по лестнице двери уровня: нет обрезки по правому косяку — ИСПРАВЛЕН 2026-08-01
Был симптом (найден пользователем сразу после L1-EXIT): при подъёме по лестнице за дверью уровня (кадры 224..228) силуэт Кида вылезал ПРАВЕЕ правого косяка проёма. По высоте обрезка была корректна.
Причина — недопортированная половина clip_char (seg006:1231). Для
кадров двери оригинал ставит ДВА клипа, у нас был только первый:
if (frame >= frame_224_exit_stairs_8 && frame < 229) {
obj_clip_top = leveldoor_ybottom + 1;
obj_clip_right = leveldoor_right;
}
Отдельная ловушка: комментарий в SDLPoP говорит «frames 217..228», а КОД
проверяет >= frame_224_exit_stairs_8, то есть 224..228. Портировано по
коду.
Почему именно обрезка, а не fore-слой: створка и косяк уходят в оригинале
ЦЕЛИКОМ в backtable (draw_leveldoor, все add_backtable), то есть рисуются
ПОД персонажем и перекрыть его не могут. Единственный способ спрятать
поднимающегося — срезать сам спрайт.
Фикс:
- libbgi:
gfx_blit_cols_part_w(..., uint8_t maxw)— обрезка СПРАВА у колоночного блита. Для column-major это ровно уменьшение числа колонок, то есть внутри ядра механизм уже был (так же клипается край экрана:w = _bgi_maxx + 1 - x), наружу не было выведено. Тело блита переехало туда,gfx_blit_cols_partстал тонкой обёрткой (maxw=0) — тем же приёмом, какимgfx_blit_colsуже обёрнут вокругgfx_blit_cols_part. Работает и при flip: первыеmaxwнарисованных колонок всегда ложатся в ЛЕВУЮ часть футпринта. - PoP:
pop_leveldoor_right/pop_leveldoor_ybottom(порт одноимённых глобалов) пишетdraw_leveldoorвpop_state— их читаетclip_charиз другого банка;pop_clip_char_right()отдаёт границу,kid_drawпревращает её вmaxwи уводит эти кадры с noclip-пути на общий.kid_lw(прямоугольник heal) тоже сужается — стираем ровно нарисованное.
Проверено в MAME: pop_leveldoor_right = 176 — ровно
(draw_xh<<3) + 48 для двери комнаты 9; pop_leveldoor_ybottom = 112 у
закрытой створки и 69 у поднятой (сходится с формулой оригинала).
Отрисовка подтверждена пользователем на живом подъёме.
BUG-SEAM-PINGPONG (#4). Пинг-понг drawn_room у шва с закрытыми воротами — РЕШЁН 2026-07-22
Настоящий корень найден потиковой трассой ЖИВОГО SDLPoP 1.23 (lldb-брейкпоинты на leave_room/bumped/safe_step с логом Char + char_x_left/right; fixes выключены = vanilla). Прежние гипотезы оставлены ниже для истории — они НЕ были причиной.
КОРЕНЬ (подтверждён трассой + исходником)
set_char_collision (seg006:0723): char_x_right = obj_x/2 + 58, где
load_frame_to_obj (seg008:1728) считает obj_x = 2*char_dx_forward(dx) - 116
и добавляет +1 для кадров «чётного пикселя»:
if ((sbyte)(cur_frame.flags ^ obj_direction) >= 0) ++obj_x;
(бит 0x80 флагов кадра XOR направление; вправо: +1 если бит НЕ стоит).
Деление obj_x/2 — C-усечение К НУЛЮ, поэтому при e = x+dx <= 57
(obj_x < 0, зона левого шва) поправка +1 даёт char_x_right = e+1, а при
e >= 58 формула сокращается к чистому e.
Итог: Kid, осевший после отскока от ворот шва на x=57 (frame15, флаги 0x43 —
бит 0x80 не стоит), имеет char_x_right = 58 и порога leave-left (<=57)
НЕ достигает. Наш движок считал передний край как Kid.x + dx без поправки
→ 57 → ложный leave → пинг-понг.
Эталонный цикл SDLPoP (нормализовано по трассе): стойка x=61 → тап вправо → safe_step(d=0) → step, на первом dx(1) x=62 → bump (edge-триггер) → align 61 → seq47 dx(-4) → x=57 (скрыт за кромкой) → кадры 50/51/52 (cxr 61/60/58, у всех бит 0x80 снят, e>57 — без сдвига) → стойка cxr=58 → leave НЕ срабатывает; тап → safe_step d=3 → x=60 (1/3 видно); тап → step1 → x=61 (2/3 видно); тап → bump → 57 … по кругу. Char.room и drawn_room НЕ меняются.
Фикс (pop_map.c)
char_x_forward_edge(): e = char_dx_forward(dx); if (((flags ^ (dir<0 ? 0x80 : 0)) & 0x80) == 0 && e <= 57) e++; — используется в char_front_coll
(коллизия/bump/edge_distance) и в check_leave (порог ухода). Плюс порт
doortop-гарда leave-right из leave_room (тайл (9,row) = doortop → правого
выхода нет). pop_leave_timer (exit_room_timer) оставлен — он реален в
seg002/seg003.
Симптом (как выглядел)
Kid стоит за решёткой закрытых ворот шва (левый сосед room8 виден в кромке room6). При удержании/нажатии ВПРАВО экран пинг-понгует между двумя состояниями:
- A: показывается room6, Kid у левой кромки за решёткой (спрайт на 2/3);
- B: показывается room8, Kid у его правой кромки.
Эталон SDLPoP: drawn_room всегда остаётся room6, Kid осциллирует у кромки (1/3→2/3→отступил→по кругу), в room8 экран НЕ переключается.
Инструментальный диагноз (watchpoint на pop_leave_dir)
В момент лишнего свитча A→B: pop_leave_dir=1 (LEFT), Kid frame=15 (СТОЯ,
не transient!), Kid.x=57 (до репроекции +140). То есть:
char_x_right = char_dx_forward(kid_cur_dx()) = Kid.x + frame15.dx = 57+0 = 57.- Порог leave-left (взгляд вправо):
char_x_right <= 57→ срабатывает РОВНО на 57. - Грань ворот (где их держит коллизия) = 61 (
wall_dist_from_left[1]=10 + coll_tile_left_xpos=51). Между 57 и 61 — зазор 4px: Kid НЕ удержан воротами (d=61−57=4≥0 → check_bumped не бампит), но уже на пороге ухода. - Kid оседает на 57 из-за recoil отскока: seq_47 =
act(bumped), dx(-4), frame_50, 51, 52; SEQ_DX(−4) двигает Char.x на −4 суммарно; frame_50.dx=4 компенсирует ТОЛЬКО точку коллизии НА кадре 50, но при возврате в стойку (frame15, dx=0)char_x_right = Char.x = aligned−4 = 57.
Что было ИСКЛЮЧЕНО (сверено с исходниками SDLPoP, НЕ причина)
- Формула char_x:
char_x_right = obj_x/2+58 = Char.x+frame.dx(seg006 set_char_collision) — совпадает с нашим char_dx_forward. - Позиция грани ворот:
get_left_wall_xpos = wall_dist_from_left[1](10) + xpos_in_drawn_room(x_bump[9+5])+7 = 10+(184−140)+7 = 61— совпадает с нашим (x_bump[−1+5]=44, +7, +10 = 61). - Порог leave: SDLPoP leave_room looking-right
char_x_right<=57— совпадает. - Данные кадров: frame_50 (image=49,dx=4,flags=0x67) и frame_15 (image=14,dx=0,flags=0x43,sword=9) — БАЙТ-В-БАЙТ как в SDLPoP frame_table_kid.
- seq_47 (act bumped, dx(-4), frame 50/51/52) — совпадает.
Почему точечные фиксы НЕ работали
- Гард в check_leave (подавить leave на закрытых воротах) — это ОТСЕБЯТИНА, не SDLPoP (в leave_room такого нет); откачено.
exit_room_timer=2(порт seg002 exit_room — РЕАЛЬНЫЙ механизм, оставлен как pop_leave_timer): блокирует leave 2 кадра после входа в комнату. НЕ спасал: положение Char.x=57 устойчивое (Kid стоит), а не transient.
Задел, который НЕ понадобился: порт coll_room + отложенный drawn_room
Трасса показала, что в эталоне Char.room/drawn_room вообще не меняются,
поэтому план S2/S3 для этого бага не потребовался. Остаётся заготовкой под
стражей/двух персонажей в кадре:
- Раздельные комнаты.
Char.room≠drawn_room. Уже естьkid_room(S1) + рендер-смещениеpop_kid_set_render_dx(∓140). - Коллизия по Char.room через coll_room (seg004, S2). Портировать
check_collisions→get_row_collision_data→get_left_wall_xpos/get_right_wall_xpos→curr_row_coll_room[]/_flags[],bump_col_left_of_wall/bump_col_right_of_wall→check_bumped_look_*→bumped(). Тайлы — из РЕАЛЬНОЙ комнаты колонки, НЕ из снапшота. - Отрисовка по drawn_room, персонаж со сдвигом ±140.
- Отложенная смена drawn_room (seg002/seg000, S3):
leave_room→goto_other_room→exit_room(next_room) →check_the_end.
Реализовано на 2026-07-22: S1 + pop_leave_timer.
Память: pop_seam_room_model, sdlpop_odd_pixel_char_x.
Решено НЕ делать
OPT-1. Хирургический редрой левого шва (ворота соседа) — 2026-07-22
Возможность: pop_room_redraw_seam_left() (pop_bg.c) на каждое изменение
openness рисует bar(BLACK) по всему col0 + полный draw_tile(0,0)
(стены, topright, wall_pattern с prandom() — десятки блитов). Реально
анимируются только бары решётки — draw_gate_back (~9 env_b).
Стоимость: seam-блок (синий io_border) занимает ~30-50% кадрового периода,
но только пока openness меняется — во время открытия и медленного
авто-закрытия (~5 сек после схода с кнопки). В покое — 0%. Замерено в MAME:
брейк на _pop_room_redraw_seam_left (0x5A54) срабатывает ⟺ сегмент дорогой.
Приём (heal НЕ годится: печёные бары устаревшие, поэтому и стоит
bar(BLACK)+redraw): bar(BLACK) только по полосе баров (x=0,
gate_top..gate_bot) + draw_gate_back(lmod,...) + дорисовать статику тайла
(0,0), задетую полосой. Пиксель-чувствительно — обязательна выверка в
MAME по кромкам.
Решение: оставляем как есть — стоимость транзиентная, в бюджет помещаемся. Делать, только если упрёмся в кадровый бюджет на сценах с воротами.
Исправлено
-
Спуск с кнопки (room8, кромка (0,6)): Кид просвечивал в щель, ближняя рука срезана до одного пикселя — ИСПРАВЛЕНО 2026-07-28. Две причины: (а) не был портирован
clip_char()(seg006:1749) — верхняя обрезка спрайта поy_clip[curr_row+1], когда тайл над головой стена/пол; сделано (pop_clip_char_topв pop_map.c +gfx_blit_cols_partв libbgi, heal чистит уже обрезанный прямоугольник); (б)climb_overlay_tileвыбирал веткуdraw_floor_overlay(seg008:1E3A) по СЫРОМУ коду тайла — а нажатая кнопка вget_tile_to_draw(seg008:240) подменяется на floor/stuck. Тайл-кнопка не проходил тест floor, уходил вdraw_other_overlayи закрашивал Kid ЦЕЛЫМ тайлом вместо узкой кромкиfloor_left_overlay[frame-137]. Фикс —tile_code_drawn()(одна подстановка на все слои). Проверено покадрово в MAME (кадры 148..138). Этим же фиксом закрыт BUG-3 (см. выше). -
Шов ворот жёг 50% кадра в покое (закрытая решётка) — ИСПРАВЛЕНО 2026-07-22. Причина — баг кодогенератора SDCC z80 (memory
sdcc_z80_cmp_store_a_bug):if (m[9] != seam_sig) seam_sig = m[9];компилировался вsub (seam_sig)(A ← разность) +ld (seam_sig),a— сохранял РАЗНОСТЬm[9]-seam_sig, неm[9].seam_sigосциллировала (напр. 25↔231),m[9]!=seam_sigистинно каждый кадр →draw_tile(0,0)каждый кадр даже у неподвижной решётки. Фикс: store-до-сравнения (seam_sig = g;из чистогоgДОsub), подтверждён в .asm. Прочёс всех модулей PoP: других случайных compare-then-store нет. -
Пики: 2×полный
draw_tileна кадр анимации → хирургический редрой, 2026-07-22.pop_spike_redraw:heal_offуже возвращает всю печёную статику (база 127, пол, грани); поверх анимируются только два острия (SPIKES_FRAM_LEFTв своей ячейке +SPIKES_FRAM_RIGHTв соседней). Заменили 2×draw_tileна 2×env_b— пиксель-в-пиксель тот же результат, в разы дешевле (шахта из нескольких пик больше не съедает полный кадр). -
Отрисовка нажатой кнопки (0,2)/(0,3) — ИСПРАВЛЕНО. Причина:
fore_tile(pop_bg.c, fore-слой поверх Kid) рисовал переднюю грань КНОПКИ (bottom_id 149) поверх уже нарисованной грани пола (43) — «остаток нажатой кнопки». Фикс:fore_tileприменяет ту же подстановку нажатой кнопки, что иdraw_tile(opener→floor / closer→stuck при таймере связи >1). Плюсpop_button_redraw— wipe своей ячейки + правой грани (дальний угол в 0,3), низ строго yb+64 (не залезать в стену ряда 1).
НЕ БАГИ (кривая картинка, но совпадает с оригиналом — НЕ чинить)
-
Голова стоящего Кида поверх падающей на него loose-плиты. Комната 12: зацеп не удался, Кид остался стоять, сбитая плита падает прямо на него — голова рисуется ПОВЕРХ плиты. Сверено покадрово с SDLPoP v1.24 (2026-07-29): там ровно то же самое. Артефакт оригинального движка (порядок midtable), а не наш баг; «починка» увела бы от эталона. Отличать от соседних случаев, которые БЫЛИ нашими багами и исправлены: вис/подтягивание на кромке плиты и падение вместе с плитой — там плита обязана быть поверх Кида.
-
Ноги стоящего Кида поверх головы лежащего трупа стража. Комната 21: страж убит, комната покинута и открыта заново (у трупа своя запомненная X), Кид СТОИТ на одну колонку правее тела — его ноги рисуются поверх головы. Проверено в SDLPoP (2026-08-03): там ровно то же.
Корень — устройство движка, а не наш порт.
set_objtile_at_char(seg006:13F3) приписывает персонажа РОВНО ОДНОМУ тайлу, и ширина спрайта на выбор тайла не влияет никак; дальшеredraw_needed_tiles(seg008:1B06) обходит тайлы рядами 2,1,0 и колонками 0..9, и кто позже — тот поверх. Стоящий Кид укладывается примерно в колонку, поэтому у него это незаметно, а лежащий труп занимает две-три колонки, но «принадлежит» левой. Всё, что стоит правее, перекрывает выступающую часть тела. Механизма «широкий объект участвует в нескольких тайлах» в оригинале нет — сверено сdraw_objtable_items_at_tile,sort_curr_objsи веткойtile_object_redraw == 0xFF(последняя про оверлеи пола, не про персонажей).Отличать от того, что БЫЛО нашими багами в этом же месте и исправлено (BUG-DRAWORDER-1): колонка трупа считалась из тайла вместо X, ветка
actions_1_run_jumpне была портирована, окно fore-клипа затиралось стражем. Если когда-нибудь захочется «починить» и это — минимальный вариант — брать тайл по ЦЕНТРУ габарита, а не по левой границе; но это осознанный отход от эталона, и в других позах порядок поменяется в обратную сторону. -
Комнаты 13, 18, 24 недостижимы в обычной игре — свойство ДАННЫХ уровня 1, не наш баг. Обход графа от стартовой комнаты (по
res2001.bin, links @1952) показывает: у всех трёх ссылки наружу есть, а на них не ссылается никто (24:L→9, но у 9R=0; 13 и 18 связаны только друг с другом). Признак «комнату выкинули из компоновки, связи не почистили» — несимметричные ссылки ровно у этих трёх, у остальных 21 симметрия полная:13 L→22, у 22 R=16 | 18 L→15, у 15 R=12 | 24 L→9, у 9 R=0 13 R→16, у 16 L=22 | 18 R→12, у 12 L=15 | 18 D→19, у 19 U=12Следствие: в 13/18/24 возможен «мусор в шве» — наш рендер кромки читает крайнюю колонку соседа ПО ССЫЛКЕ, а сосед этих комнат соседом себя не считает. Приоритет багов в этих трёх комнатах — низкий (в игре не видно).
Проверено в MAME 2026-08-03 (прогон уровня 1: 11 наблюдений → 6 корней)
Сырой список наблюдений с обхода всех комнат разобран по корням.
Проверка — MAME + мост mame-z80: чтение _Kid по адресу из
.sprinter-cc-roomtest/roomtest.noi, ROOMNAV для навигации, потиковые
трассы, скриншоты.
Оговорка о полноте проверки. Каждый корень закрыт тем, что его СОБСТВЕННЫЙ
сценарий больше не воспроизводится; сквозного прохождения уровня и поиска
регрессий в соседней механике автоматика не делала. Чек-лист для ручной
перепроверки — в bug_list.md, раздел «Ручная перепроверка
фиксов». Особое внимание — порту check_collisions: он переписал ВСЮ
горизонтальную коллизию.
BUG-LVLSTATE-1. Уровень был немутабельным — ЗАКРЫТ
Симптомы: выпитое зелье / поднятый меч / разбитая плита возвращались при возврате в комнату; иногда кувшина нет, а пузырёк над ним крутится; в комнате 15 раз в 1–2 с мигал контур кладки. «Первое остаётся, остальное возвращается».
Корень. Уровень лежал в EMM-странице только на чтение, enter_room
перезаливал room_fg[30] из неё при каждом входе, а персистентность держала
таблица переопределений на 8 записей (OVR_MAX в roomtest.c) — девятая
и дальше молча терялись. Второй хвост: pop_trob.c брал тип тайла из
СТРАНИЦЫ (pop_level_tile_raw), поэтому заводил trob зелья/меча там, где
предмет уже поднят — отсюда пузырёк без кувшина и мигание от animate_sword.
Фикс. pop_level_set_tile() пишет тайл ПРЯМО в страницу уровня (порт
curr_room_tiles[…] = …: do_pickup seg006:1671, remove_loose seg007:0EB8,
loose_land seg007:11E8). Таблица ovr_* удалена. Эталонная копия
foretable — в той же странице по смещению 0x1000 (страница 16 КБ, данных
2.3 КБ).
Проверено: комната 22, зелье (0,6) выпито → выход в 23 → возврат: кувшина нет, пузырька нет; ROOMNAV ставит Кида уже на (0,6), потому что тайл стал полом.
BUG-RESPAWN-1. Респавн не перезагружал уровень — ЗАКРЫТ
Как в оригинале: цикл play_level (seg003:57) на КАЖДОЙ итерации, в том
числе после смерти, зовёт load_level() — уровень читается заново.
Фикс. pop_level_reset_tiles() (восстановление foretable из эталонной
копии) в pop_start_level рядом с pop_trob_reset().
Проверено: после смерти от стража зелье в комнате 22 снова на месте.
BUG-DEATH-1. Смерть от меча не доводилась до конца — ЗАКРЫТ
Симптом: страж убивает Кида, тот «воскресает» на месте и его убивают снова, по кругу.
Корень. hurt_by_sword ставил seq_85 и обнулял HP, но Kid.alive
оставался −1, а pop_kid_dead (по нему главный цикл делает респавн) взводил
только путь пик/падения. В оригинале это первая строка control_kid
(seg006:0CD1): if (Char.alive < 0 && hitp_curr == 0) Char.alive = 0;.
Фикс. Порт этой ветки в начало pop_ctrl_tick + pop_kid_dead = 1.
Проверено: страж в комнате 21 убивает Кида → респавн в стартовой позиции уровня (комната 1), цикла нет.
BUG-GATE-ANIM-1. Ворота в отрисованной комнате не перерисовывались — ЗАКРЫТ
Корень. pop_process_trobs продвигал модификатор ворот, но пометки
перерисовки не ставил; ворота рисовались только при полной отрисовке комнаты
и в pop_room_redraw_seam_left. В оригинале animate_door (seg007:0522)
заканчивается draw_trob() (seg007:01E6).
Фикс. Вид перерисовки POP_RD_GATE + pop_gate_redraw(row,col) в
pop_bg.c (wipe зоны решётки в ячейке ПРАВОГО соседа + draw_tile), пометка
из pop_process_trobs (текущая страница каждый кадр, обе — на последнем).
На уровне 1 это ровно ОДНА решётка: room5 (0,5). Все остальные стоят в
колонке 9, их бары рисуются уже в соседней комнате — там работает
pop_room_redraw_seam_left.
Проверено: кнопка room5 (0,4) — решётка (0,5) поднимается на экране.
BUG-COLL-1. Bump искал стену только в колонке переднего края — ЗАКРЫТ
Симптомы: пробегание сквозь закрытую решётку (комната 12, room5 (0,9)); влёт внутрь стены на длинном прыжке (комната 6).
Два корня.
check_bumpedбрал ОДНУ колонку — ту, в которой оказался передний край. Для решётки в col9 окно этой колонки всего 4 px (201..204), беговой кадр его перескакивает — бампа нет, аcheck_leaveтут же уводит в соседнюю комнату. Оригинал (check_collisions, seg004:0004) считает флаги перекрытия для ВСЕХ колонок ряда и берёт колонку с переходом флага 0→1.- Наш guard
action == FREEFALL || MIDAIR → returnвcheck_bumped. В оригинале таких гардов НЕТ:bumped_fall(seg004:04E4) специально разбираетactions_4_in_freefall. Отсюда влёт в стену в прыжке.
Фикс. Полный порт check_collisions + get_row_collision_data +
is_obstacle + bumped(delta, push_dir); колонки считаются от −2 до 11,
чтобы решётки СОСЕДНЕЙ комнаты (швы) бампили как свои; гарды в check_bumped
приведены к оригинальным (только вис и подтягивание).
Проверено: комната 5, бег вправо в закрытую решётку (0,9) — Кид упирается (x встаёт на 60 в системе комнаты 1 и дальше не растёт).
Остаток: экран при этом перелистывается на соседнюю комнату, и Кид в шве
не рисуется — отдельный баг BUG-SEAM-DRAW-1 (модель straddle S3), см.
bug_list.md.
BUG-STANDUP-1. Вставание из приседа у стены роняло сквозь пол — ЗАКРЫТ
Симптом: комната 5, падение с кнопки (0,6) на щебень (2,7) с уроном, присед — и при вставании провал в комнату 6.
Корень — лишний guard if (Kid.action == ACT_BUMPED) return; в
bumped_floor (в оригинале, seg004:0520, там if (Char.alive)). Цепочка:
вставание двигает Кида на 1 px в стену → бамп → bumped_floor прижимает y
к полу, отменяя dy(−2) из начала medland → наш guard возвращает
управление вместо seq_47, medland доигрывает свои dy(+1),dy(+1) уже
ОТ пола → Кид НИЖЕ пола → следующий бамп читает беззнаковую разность как
«высоко над полом» → bumped_fall → выпадение вниз. У оригинала seq_47
обрывает medland, лишних dy нет.
Найдено потиковой трассой + временной диагностикой (колонка бампа = 8, тайл
после разрешения = 14 щебень, ветка = bumped_floor).
Заодно убран полу-порт опционального FIX_STAND_ON_THIN_AIR: у нас была
взята только его первая часть (кадры вставания 110..119 требуют пол), без
парной правки seqtbl (dx(1)→dx(0), dx(−4)→dx(−3)), которую применить
нельзя — seqtbl извлечён из данных оригинала. Вернулись к ванильному
seg006:909 (только кадр 109).
Вторая волна прогона 2026-08-03 (вечер)
BUG-KBD-4 (+ BUG-KBD-3). Shift: сначала залипал, потом стал отваливаться — ЗАКРЫТ
Симптом (вторая редакция). Залипание ушло, но появилось обратное: при зажатом Shift второй-третий-четвёртый тап стрелки отрабатывал уже не осторожным шагом, а бегом. Для PoP это ХУЖЕ залипания: игрок рассчитывает на короткий шаг, а Кид убегает в яму или на пики.
Почему обе прежние редакции были неправильны. Это был размен между двумя способами «починить» потерю байта при Rx-overrun SIO (FIFO 3 байта):
- исключать модификаторы из сброса — потерянный break Shift снять нечем, Shift залипает навсегда (BUG-KBD-3);
- сбрасывать всю карту, как DSS (
KBD_Receiver_OverrunвKEYINTER.ASMчистит иKEYCTRL, иKEY_FLG) — залипания нет, но Shift сносится каждым overrun'ом (BUG-KBD-4).
Причина, по которой вариант 2 бил так часто, замерена: при зажатом Shift клавиатура обёртывает КАЖДЫЙ расширенный код служебной парой, поэтому тап стрелки — это не 5 байт, а 10, и overrun почти гарантирован.
Что нашлось (замер в MAME, watchpoint на порт данных SIO + чтение карты
_kbdraw_down). Эта самая обёртка — «fake shift» — и есть решение, а не
помеха. Клавиатура шлёт E0 F0 12 перед расширенным make и E0 12 после
его break, и шлёт ТОЛЬКО пока Shift реально зажат. Проверено обоими
шифтами: правый обёртывается своим кодом 0x59 (bit 1 байта 11 карты), левый
— 0x12 (bit 2 байта 2). Это непрерывное и прямое свидетельство реального
состояния Shift — единственное доступное, потому что опросить состояние у
PS/2 нельзя, а typematic повторяет только ПОСЛЕДНЮЮ нажатую клавишу, то есть
стрелку, а не Shift.
Фикс. Оба декодера (libc/irq/_irq_tramp.c, libc/kbd/kbd_raw_poll.c)
читают обёртку в обе стороны:
- увидели fake shift → реальный Shift ЗАЖАТ → взвести plain-бит (бит кладёт общий писатель: достаточно обнулить префиксы, и он попадёт в plain-половину карты как make; в расширенную половину не пишем — это не клавиша);
- расширенный make БЕЗ предшествующей обёртки → Shift ОТПУЩЕН → снять plain-биты обоих шифтов.
kbd_raw_sync вернулся к исключению модификаторов — теперь это безопасно:
состояние Shift подтверждается независимо от того, что съел overrun, а
залипание снимается первым же нажатием стрелки.
Побочно найдена и исправлена своя ошибка в новом коде kbd_raw_poll:
после bit 1, a в A лежал pending, а не скан-код, — обычные
(нерасширенные) клавиши декодировались бы из мусора. Оба декодера приведены
к одной логике.
Раскладка трамплина. Клавиатурный блок перевалил за 127 байт, а jp
внутри трамплина запрещён (код копируется в W2 побайтно, абсолютные
само-ссылки сломают копию). Поэтому префиксные обработчики переехали вплотную
к своим cp, а посередине тела стоят три ретранслятора (tr_kbd_hub,
tr_hub_notkbd, tr_hub_dss) — до них дотягиваются jr и сверху, и снизу.
В kbd_raw_poll такого ограничения нет, там три перехода стали jp.
Проверено в MAME:
- Shift зажат, пять тапов стрелки подряд →
LShостаётся04во всех пяти,ovr = 0, расширенная половина карты чистая; - штатное отпускание Shift →
LSh = 00; - искусственно залипший Shift (бит записан в карту отладчиком) → снимается ПЕРВЫМ же тапом стрелки;
make size-check: 70 программ, роста нет.
BUG-RESPAWN-2. После respawn стражи остаются мёртвыми — ЗАКРЫТ
Вопрос из отчёта: «после respawn — должны ли оживать стражники?»
Ответ по SDLPoP: да. Цикл play_level (seg003:57) на КАЖДОЙ итерации —
в том числе после смерти Кида — делает load_level(), следом pos_guards()
(seg003:83), а затем Guard.charid = charid_2_guard; Guard.direction = dir_56_none. То есть стражи — такая же часть данных уровня, как тайлы, и
перезагрузка файла возвращает их всех.
Корень у нас. gstate_init() (живая копия таблицы стражей: тайл,
направление, мастерство, поза трупа) вызывался ТОЛЬКО при загрузке уровня.
Рестарт возвращал тайлы (pop_level_reset_tiles), но не стражей: в gstate
оставался сохранённый leave_guard'ом seq_hi != 0, и pop_guard_enter
поднимал стража трупом с guardhp_curr = 0.
Фикс. pop_level_reset_guards() (тот же gstate_init) + pop_guard_reset()
в pop_start_level, рядом с pop_level_reset_tiles(). Туда же уехал
pop_loose_mob_reset() — настоящий mobs_count = 0 из start_level.
Проверено в MAME: комната 21, страж жив (alive = -1, hp 3) → убит читом
K (alive = 0, hp 0) → Кид убит стражем → респавн → возврат в комнату 21:
страж снова alive = -1, hp 3.
BUG-DRAWORDER-1. Кид рисовался поверх тела стража — ЗАКРЫТ
Симптом. В оригинале Кид проходит ЗА телом убитого стража; у нас — перед ним.
Три разных корня, найденные по очереди. Стоит того, чтобы перечислить: два первых захода были неполны, и каждый следующий кадр от тестера вскрывал новый слой.
-
Порядок задавался ролью, а не тайлом. Мы безусловно рисовали
pop_guard_draw()→kid_draw(), то есть Кид был сверху всегда. В оригинале никто не «поверх» по определению: оба персонажа попадают в midtable при обработке СВОЕГО тайла (set_objtile_at_char, seg006:13F3), а тайлы обходятся строго —redraw_needed_tiles(seg008:1B06) идёт рядами 2,1,0 и внутри ряда колонками 0..9; кто позже, тот поверх. Совпали тайлы — решаетsort_curr_objs(seg008:203C): ниже поobj_y= позже. Фикс:guard_over_kid()вroomtest.c. -
Своя же регрессия: Кид полез поверх передних столбов. Окно fore-клипа (
pop_fore_set_clip) одно на всех, и его ставит каждый, кто рисует персонажа. Пока страж рисовался строго ДО Кида, к моментуpop_fore_over_kidв окне оказывался Кид и всё сходилось само. Как только порядок стал переменным, при «страж поверх» окно оставалось СТРАЖЬИМ, и fore-проход Кида отсекался целиком. Фикс:kid_fore_clip_restore()— окно восстанавливается явно, а не «по счастливому порядку вызовов». -
Колонка трупа была нулевой.
leave_guardсохраняет тайл какget_tilepos(0, row), то есть колонку 0 всегда, аenter_guardу нас бралcurr_colоттуда и следом перетирал X запомненным значением. В памяти это было видно прямо:col=0приx=95. Оригинал (seg002:180) выводит колонку ИЗ X:Char.curr_col = get_tile_div_mod_m7(Char.x). У живого стража незаметно (X там сама считается из колонки), у запомненного ТРУПА — ломало порядок. Колонка нужна не только отрисовке: на неё смотрят коллизия иcheck_can_guard_see_kid. -
Ветка
actions_1_run_jumpоказалась не «упрощаемой». Сначала я решил, что её можно не портировать. Кадр в движении показалACT=1: в беге оригинал берёт тайл не изcurr_row/curr_col, а из нижнего ряда и ЛЕВОЙ колонки ГАБАРИТА (char_bottom_row/char_col_leftизset_char_collision, seg006:0723). При беге вправо левая колонка меньшеcurr_colпримерно на тайл — потому бегущий Кид и уходит за объекты справа. Отсюда «некоторые кадры бега рисовали Кида поверх тела». Считается для ОБОИХ персонажей:enter_guardставитaction = 1и стражу, односторонний учёт дал бы перекос в другую сторону. Тонкость:char_col_leftберётся по НЕ утоньшённой границе — поправку THIN оригинал применяет только к*_coll.
Проверено в MAME: после возврата в комнату у трупа col=5 при x=135
(было col=0); Кид слева от тела — и стоя, и в кадре бега — рисуется за телом;
передние столбы снова перекрывают Кида.
Остаток, который НЕ баг. Ноги стоящего Кида поверх головы трупа, когда он стоит колонкой правее тела, — врождённое свойство движка (один спрайт = один тайл при спрайте шире колонки). Сверено в SDLPoP: там так же. Подробности — в разделе «НЕ БАГИ» выше.