Три Critical'а (BUG-1 провал на row 1 при боковом переходе, BUG-2 ping-pong при возврате, BUG-3 окклюзия climb-up на кнопке) висели непроверенными с 2026-07-21. Прогнал в MAME: - BUG-1 не воспроизводится: room6 → кнопка (0,2) → открытая решётка → переход влево даёт room8, y=55, curr_row=0. Заодно снят и сам диагноз записи — репроекция Y при БОКОВОМ переходе не нужна: goto_other_room (seg002.c:390) меняет только x, наш check_leave делает то же. - BUG-2 не воспроизводится: шов room2↔room3, четыре пересечения с разворотом сразу после входа — комната меняется ровно раз на пересечение. - BUG-3 закрыт фиксом tile_code_drawn от 2026-07-28 (это дубль уже записанного «спуск с кнопки»); оговорка про непереснятый подъём — в bug_closed.md. bug_list.md теперь только открытое (BUG-CEIL-1/2/3, BUG-OCCL-1, T-1, T-2, таблица обхода 24 комнат) + индекс с якорями. bug_closed.md — закрытое вместе с разбором корней (odd-pixel char_x, подстановка тайла кнопки, баг кодогенератора SDCC), он и есть главная ценность архива. TASKS.md: кросслинки на открытые баги в шапке, в L1-TRIAGE, L1-PASS и «Отложено». Указатели в CLAUDE.md/README/room_model_plan/layout_plan_v2 переведены на нужный из двух файлов. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
21 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 поверх кнопки на подъёме — заводи заново.
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), а не наш баг; «починка» увела бы от эталона. Отличать от соседних случаев, которые БЫЛИ нашими багами и исправлены: вис/подтягивание на кромке плиты и падение вместе с плитой — там плита обязана быть поверх Кида.
-
Комнаты 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 возможен «мусор в шве» — наш рендер кромки читает крайнюю колонку соседа ПО ССЫЛКЕ, а сосед этих комнат соседом себя не считает. Приоритет багов в этих трёх комнатах — низкий (в игре не видно).