Доска отставала от кода на три задачи — планировать по ней было нельзя. Сверка проведена грепом по исходникам, а не по записям: - L4-MIRROR ЗАКРЫТА: шаги 1-5 сделаны и проверены пользователем в MAME (зеркало в атласе, постановка тайла, отражение, прыжок сквозь зеркало с рождением тени, левый клип тени). Протокол с разбором решений — в архиве; - L3-CHOMP и L3-SKEL закрыты ещё 2026-08-08/07 (коммитыdc0bd47,4d4323f,db4106a,1461ed5), на доске значились как предстоящие; - тайлсет palace (шаг 2 levels_plan) в коде есть целиком — pop_bg_load(type), pal_*.atl, дворцовый wall_pattern, решётки 25-29 и в tile_table, и в коллизии (tile_is_floor совпадает с seg006:0628); - в GUARD-PHYS остаток пересобран по факту: check_chomped_guard сделан, скелет в check_guard_fallout сделан, ветки ТЕНИ нет — она уехала в L5-SHADOW. Приёмки: по решению пользователя уровни 1-4 приняты SMOKE-тестами, полные обходы всех комнат делаются по готовности ВСЕХ уровней — L3-PASS/L4-PASS как отдельные задачи отменены, вместо них политика приёмок в архиве. Новая цель — уровень 5. Инвентарь res2005.bin: НИ ОДНОГО нового тайла, всё портировано на уровнях 1-4. Единственная новая механика — спецсобытие «тень крадёт зелье» (комната 24): заведена задача L5-SHADOW с портом по SDLPoP (check_shadow / do_init_shad / do_auto_moves + shad_drink_move / autocontrol_shadow_level5 + ветка тени в check_guard_fallout), включая готовые константы и то, что у нас уже есть под это. Заведён MIRROR-FG-STALE (низкий): place_mirror пишет тайл в данные уровня, но не в снимок room_fg, по которому работает коллизия — если зеркало поставлено, пока игрок В комнате 4, оно невидимо для коллизии (тень не родится). В обычном прохождении недостижимо: дверь выхода в другой комнате. Записан точный сценарий воспроизведения читом ROOMNAV и фикс на несколько строк. Правило «в _OPEN только незакрытое» теперь выполняется буквально: - bug_list.md → BUGS_OPEN.md, bug_closed.md → BUGS_CLOSED.md (ссылки обновлены во всех документах и в комментарии pop_trob.c); - из TASKS_OPEN убраны блоки закрытых задач (L3-CHOMP, L3-SKEL, L3-PASS, L4-MIRROR, DRAW-CHAR), справка по связности комнат уехала в архив; - из BUGS_OPEN убраны 8 строк таблицы закрытых багов, закрытый T-2 (уехал в BUGS_CLOSED) и раздел «уровень 3 — неначатые задачи» (обе записи закрыты); сводная таблица пересобрана по реально открытым записям. Все внутренние ссылки проверены скриптом: битых якорей 0. make size-check OK (65 программ), tests-host 5/5. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
28 KiB
roomtest — ОТКРЫТЫЕ баги и незакрытые оптимизации
Здесь ТОЛЬКО незакрытое. Всё закрытое (и, что важнее, разбор корней)
живёт в BUGS_CLOSED.md — прежде чем заводить новый баг,
грепни там по симптому. Сырые формулировки пользователя с прогонов —
bugs_level1.md / bugs_level2.md.
Приоритеты работ — в TASKS_OPEN.md (закрытые задачи с
протоколами — TASKS_CLOSED.md), а не здесь. Правило
проекта: механику сверять с ../SDLPoP/src/ ДО кодинга.
Ревизия 2026-08-11: файл вычищен от закрытых записей (правило «в _OPEN
только открытое»). Уровни 1-4 приняты smoke-тестами; крупных багов нет.
| ID | что | тип | статус |
|---|---|---|---|
| FORE-DUP | передний слой тайла рисуется дважды при перекрытии объектов | оптимизация | открыт: сначала замерить, потом чинить |
| MIRROR-FG-STALE | зеркало поставлено, пока игрок В комнате 4 → коллизия его не видит | низкий (в обычном прохождении недостижимо) | открыт, фикс на несколько строк |
| DIED-ON-BUTTON | died_on_button (seg007:776) не портирован |
порт | открыт |
| TORCH-ANIM-RIGHT | под запечённым пламенем застывают не только челюсти чомпера | низкий | открыт: на уровнях 1-4 такого соседства нет |
| T-1 | пики перерисовываются безусловно | оптимизация | открыт |
| BUG-SPIKE-1 | пики залипают выдвинутыми рядом с Кидом | низкий | маловоспроизводим: ни сценарием, ни попиксельной подгонкой X не поднимается |
| BUG-CHOMP-JUMP-1 | прыжок с места вплотную к чомперу: кадр с отступом назад | низкий | маловоспроизводим: на повторе не поднялся; на пререлиз |
С приёмки уровня 2 (2026-08-07)
Всё найденное тем прогоном закрыто, кроме двух записей ниже
(BUG-SPIKE-1 — уровень 2, комната 6). Карта содержимого уровня 2 (что где
стоит по res2002.bin, какие кнопки какие ворота открывают) — в
TASKS_CLOSED.md, по ней видно, «механика не
сработала» это или «так и задумано».
BUG-SPIKE-1. Пики залипают выдвинутыми — НИЗКИЙ ПРИОРИТЕТ, МАЛОВОСПРОИЗВОДИМ
Статус 2026-08-05, вечер. Пользователь повторить не смог, а замер (ниже) показал, что чистый пробег по убранным пикам убивает штатно. То есть в смертельности бага, похоже, нет вовсе: наблюдался частный случай — пики, УЖЕ выдвинутые полностью (h = 1), для бегущего безвредны и в оригинале. Запись оставлена открытой ровно из-за визуального расхождения со скриншотом SDLPoP (у нас острия торчат, у него убраны) — см. конец.
Как получить состояние нарочно: подойти к пикам вплотную (выдвинутся), отступить на полшага, НЕ выходя из зоны срабатывания, и пробежать по ним. Пользователь пробовал и это, и попиксельную подгонку X читом
[/]— не поднялось. Вывод для будущего разбора: состояние не чисто позиционное, одной шириной габарита его не объяснить; следующий подозреваемый — момент, в которыйprocess_trobsзастаёт модификатор относительно кадра Кида.
Наблюдение (пользователь, 2026-08-05). Уровень 2, комната 6, пики (1,3):
| действие | что происходит |
|---|---|
| длинный прыжок с ряда 0 на пики | смерть — правильно (путь fell_on_spikes) |
| пробег по ряду 1 прямо по пикам | урона нет |
| после уборки пик | на экране остаются белые остатки остриёв (в оригинале чисто) |
| прыжок на месте, стоя на пиках | урона нет |
| просто стоять на выдвинутых пиках | можно сколько угодно |
Замер (MAME, чтение room_modif комнаты 6). Пока Кид стоит на тайле,
модификатор пики (индекс 13) = 0x8E, то есть «пики ПОЛНОСТЬЮ вышли и
идёт обратный отсчёт». Дальше вся арифметика сходится с оригиналом:
is_spike_harmful (seg007:1178): 0/-1 → 0; <0 → 1; 1..4 → 2; >=5 → 0
check_spiked (seg006:0658): убивает при h>=2 на кадрах бега 7..14
и при h!=0 на кадрах приземления 43/26
То есть при h = 1 (пики уже вышли) бегущий не гибнет и в оригинале —
смертельно только окно ВЫДВИЖЕНИЯ (модификатор 1..4, h = 2). Наши
animate_spike, start_anim_spike, is_spike_harmful, check_spiked
сверены с seg006/seg007 построчно и совпадают дословно.
ВТОРОЕ НАБЛЮДЕНИЕ (то же место, сравнение с оригиналом). Кид уронил плиту-потолок и спрыгнул вниз; пики выдвинулись и «спрятались не все — часть артефактов осталась». Скриншоты рядом: наш и SDLPoP в той же позе. У нас из-под щебня торчат белые острия, у оригинала пик не видно ВООБЩЕ.
ГИПОТЕЗА «ТРИГГЕР СРАБАТЫВАЕТ РАНО» ПРОВЕРЕНА И ОПРОВЕРГНУТА (замер
2026-08-05, MAME, watchpoint на room_modif[13] комнаты 6). Чистый
пробег по УБРАННЫМ пикам убивает штатно:
запись modif=1 : кадр 11 (беговой), x=112, curr_col=2 ← пики пошли вверх
запись modif=2 : кадр 12 (беговой), x=117, curr_col=3 ← Кид уже НА тайле, h=2
запись modif=3 : кадр 177 (frame_177_spiked) ← напоролся
То есть check_spike_below, check_spiked, is_spike_harmful и тайминг
выдвижения работают правильно, и «раннего» триггера нет.
Настоящий корень — пики ЗАЛИПАЮТ выдвинутыми. Пока габарит Кида
накрывает колонку пики, check_spike_below каждый кадр зовёт
start_anim_spike, а тот при отрицательном модификаторе переставляет его
обратно в 0x8F — отсчёт до уборки не доходит. А выдвинутые пики (h = 1)
для бегущего БЕЗВРЕДНЫ по правилам оригинала. Отсюда обе жалобы: пробег
по уже вышедшим пикам не убивает, и они же остаются торчать на экране.
Что осталось выяснить (и это единственный открытый вопрос). Код
start_anim_spike у нас с оригиналом совпадает дословно, значит оригинал
тоже удерживал бы пики, стой Кид там же. На скриншоте SDLPoP в похожей
позе пики УБРАНЫ — то есть его Кид стоит чуть левее и его габарит колонку
пики уже не задевает. Разница в 2–3 пикселя посадки, а у нас такие
расхождения по X уже ловились (см. заметку в BUG-GRAB-1: после касания
площадки SDLPoP уводит Кида на 134, мы — на 141).
Как закрывать: инструментировать SDLPoP (печать char_x_left/right,
left/right_checked_col и модификатора пики каждый кадр), проиграть ту же
сцену — падение плиты-потолка в комнате 6 и остановку на щебне — и сверить
с нашей трассой ПОЗИЦИЮ КИДА после приземления. Если позиции совпадут, а
диапазоны колонок разойдутся — виноват габарит (kid_fp против
set_char_collision текущего кадра); если разойдутся позиции — это отдельный
баг посадки, а пики — его следствие.
Оптимизация отрисовки (записано 2026-07-29)
Не баги — план работ. Оба пункта про одно: у оригинала пометка тайла к перерисовке стоит копейки (бит в таблице, которая всё равно чистится каждый кадр), а у нас каждая такая пометка превращается в реальный heal (копию из ОЗУ-копии акселератора) плюс блиты. Поэтому буквальный порт «перерисовываем безусловно» корректен, но дорог.
T-1. Пики: перерисовывать по причине, а не безусловно
Сейчас: pop_process_trobs зовёт pop_spike_redraw каждый кадр для
каждой живой пики в комнате (порт redraw_21h, который animate_spike
вызывает вне всяких if). Это корректно, но лишнее для пик, до которых
Киду дела нет.
Надо: перерисовывать тайл пики, только если
- сменился её видимый кадр (шаг выдвижения/уборки), ЛИБО
- её кто-то стёр — а стереть у нас может только heal, то есть тайл
попал в прямоугольник
kid_healэтого кадра.
Это и есть модель оригинала, просто выраженная флагами: redraw_at_char
(seg003:0576) каждый кадр помечает set_redraw_fore тайлы персонажа, причём
объединение текущего и предыдущего прямоугольника
(MIN(char_top_row, prev_char_top_row) и т.д.), а animate_spike помечает
свой тайл. Итог = {тайл сменил кадр} ∪ {тайлы Кида}.
Как: слой Кида и так считает cL..cR/rT..rB в pop_fore_over_kid —
пусть публикует их (плюс предыдущие, как в оригинале), а цикл trob'ов
сравнивает tilepos с диапазоном целочисленно. Никаких пересечений
прямоугольников (см. память manual_hints_over_auto_detect).
Приоритет: отдаётся почти бесплатно ПОСЛЕ T-2, отдельно не окупается.
BUG-CHOMP-JUMP-1. Прыжок с места вплотную к чомперу: кадр с отступом назад — НИЗКИЙ ПРИОРИТЕТ, МАЛОВОСПРОИЗВОДИМ
Статус 2026-08-08. Наблюдение пользователя на приёмке L3-CHOMP: Кид стоит вплотную СЛЕВА от чомпера лицом вправо, прыжок с места — в анимации проскакивает кадр, где он «чуть отступил назад», и только потом идёт прыжок. В оригинале прыжок идёт прямо с места. На повторе в тот же заход не воспроизвёлся — отложено до пререлиза.
Что уже установлено (чтобы не разбирать заново).
-
Это НЕ анимация.
seq_3_standing_jump(SDLPoPseqtbl.c:382) состоит только из положительных смещений:act(run_jump), f16, f17, dx(2) f18…f22, dx(7) f23, dx(9) f24, dx(5) dy(-6) f25. Ни одного отрицательногоdx— отката в последовательности нет вовсе. Значит отступ даётbumped(), то есть физика посчитала въезд в препятствие и выровнялаChar.xназад. -
is_obstacleдля чомпера у нас совпадает с оригиналом (seg004:037E): препятствие только приmodif == 2, причём именно== 2, БЕЗ маски0x7F— то есть окровавленный чомпер (0x82) в ванили не бампит вовсе. Проверено, расхождения нет. -
Прямая трасса прыжка отката НЕ показала. Ввод «UP и RIGHT одновременно» (mame bridge,
key UP 24+key RIGHT 24) даёт чистое движение вперёд:x148 -> 155, кадр становится 178 (перемололо). Ни одного кадра с уменьшениемx.
Главная гипотеза — ПОРЯДОК НАЖАТИЙ. Если ↑ приходит на кадр раньше →,
то control_standing уходит не в standing_jump(), а в up_pressed() —
вертикальный прыжок с зацепом, а он выравнивает Char.x. Отсюда и
«отступил, потом прыгнул». Проверять надо check_jump_up /
jump_up_or_grab / grab_up_no_floor_behind (seg005:0836 и далее), а не
прыжок с места. Внимание на can_climb_up (seg005:0828): там у чомпера
ЕСТЬ спецкейс (seq_73_climb_up_to_closed_gate при взгляде ВПРАВО) — он у
нас портирован (pop_map.c, ветка TILE_MIRROR || TILE_CHOMPER), но именно
вокруг него и стоит копать.
Как ловить. Брейкпоинт на резидентном _kid_tick с
{printf "f=%d x=%d act=%d col=%d",b@Kid+0,b@Kid+1,b@Kid+6,b@Kid+4; g} —
одна строка на кадр, адрес _Kid из .sprinter-cc-roomtest/roomtest.map
(после КАЖДОЙ пересборки другой). Плюс стоп-кадр 1 в момент отступа и
чтение Kid из памяти. Метод — memory z80_profiling_method.
Заметки (отладка)
- Тестовые клавиши осторожного шага: J = шаг влево, L = шаг вправо
(эмуляция Shift+стрелка), см.
pop_ctrl.cKBD_DBG_STEP*. Первый шаг в сторону = разворот (как в оригинале safe_step), движение со второго. - Читы (
pop_cheat.h): K — убить стража, I — бессмертие (toggle), S — выдать меч, Shift+L — следующий уровень,[/]— сдвиг Кида на пиксель по X. - Респавн после смерти — по ↑ (или авто через
RESPAWN_DELAY); ставит Kid в стартовую позицию УРОВНЯ (pop_start_level, порт do_startpos). - ROOMNAV (
=/-) — тоже наш чит, которого в оригинале не было, как иS. Все они со временем съедутся в общий блок читов, разрешаемый в настройках; пока просто включены (pop_cheats = 1вroomtest.c). Известный баг этого чита закрыт — BUG-CHEAT-FIGHT-1. - Комнаты 13, 18, 24 уровня 1 недостижимы в обычной игре — это свойство
данных уровня (разбор — «НЕ БАГИ» в
BUGS_CLOSED.md); приоритет багов в них низкий. Аналогично 23/24 на уровне 3.
DIED-ON-BUTTON. died_on_button (seg007:776) не портирован
Обнаружено при разборе BUG-LOOSE-BUTTON-1.
check_press (seg006:1707) разбирает ЛЮБОГО мёртвого Char, не только Кида:
if (curr_tile2 == tiles_15_opener || curr_tile2 == tiles_6_closer) {
if (Char.alive < 0) trigger_button(1, 0, -1); /* жив */
else died_on_button(); /* мёртв */
}
died_on_button на opener делает тайл обычным полом и форсирует
button_type = tiles_14_debris — ворота открываются насовсем; на любой другой
кнопке ставит tiles_5_stuck (заклинена, link_timer == 0x1F, связь мертва).
У Кида эффект живёт до перезапуска уровня (смерть → is_restart_level →
play_level заново зовёт load_level(), тайлы перечитываются), а вот когда на
кнопке умирает СТРАЖ — перезапуска нет, и кнопка заклинена до конца уровня.
Что нужно: сам порт died_on_button, константа TILE_STUCK = 5 и тайл
заклиненной кнопки в атласе фона (сейчас его там нет).
TORCH-ANIM-RIGHT. Под пламенем могут застыть не только челюсти чомпера
Открыто 2026-08-10 при закрытии BUG-TORCH-CHOMP-2.
Пламя факела запекается в фон и рисуется в ячейке ПРАВОГО СОСЕДА. Оригинал
после каждого кадра факела метит этого соседа (set_redraw_anim_right,
seg007:0101) и перерисовывает весь его слой anim поверх огня; мы метим
только когда сосед — чомпер. Слой draw_tile_anim (seg008:0644) рисует
также пики, зелье и меч — если такой тайл окажется справа от факела и
будет в статике, пламя накроет и его.
На уровнях 1–4 такого соседства не встретилось, поэтому расширять пометку (она стоит перерисовки тайла каждый кадр на каждый факел) заранее не стали.
Как проверять: поставить факел слева от пики/меча/зелья в тестовой комнате и дождаться статики; либо пройти уровни 5+ и смотреть на клетку справа от каждого факела.
Как чинить, если встретится: там же, в ветке факела pop_process_trobs,
добавить в trob_rcode-проверку нужные коды и соответствующий вид пометки
(POP_RD_SPIKE / POP_RD_FLOOR), а зелье — переставить в порядке обхода
так, чтобы оно рисовалось ПОСЛЕ факела.
FORE-DUP. Передний слой тайла рисуется ДВАЖДЫ при перекрытии объектов
Найдено пользователем 2026-08-10 (вопросом, а не по симптому — картинка верная, страдают только такты).
У нас. Fore-проход зовёт КАЖДЫЙ рисующий по своему прямоугольнику:
pop_char_fore для слотов Кида и соперника, pop_mirror_draw для отражения.
Окно клипа (pop_fore_set_clip) у каждого своё. Если футпринты двух
объектов накрывают один тайл, его передний кусок рисуется два раза.
Когда случается:
- Кид и отражение — ВСЕГДА (стоят на одном тайле зеркала); этот случай создан фиксом fore-прохода над отражением 2026-08-10;
- Кид и соперник в одном тайле — то есть весь ближний бой;
- Кид и падающий кусок плиты.
Картинку не портит: куски переднего слоя идут прозрачным блитом-копией, операция идемпотентная. Портило бы при XOR/mono с накоплением — таких в fore-слое нет.
Такты тратит, и в самом дорогом месте: fore-проход исторически самая
тяжёлая часть кадра (memory pop_fore_layer_cost — было 78 % кадра, окно
клипа и кэш кладки дали 3.2×).
Как устроено в оригинале — дубль НЕВОЗМОЖЕН по построению.
redraw_at_char (seg003:0427) ничего не рисует, а только помечает тайлы
своего прямоугольника:
for (tile_row = x_top_row; tile_row <= char_bottom_row; ++tile_row)
for (tile_col = x_col_left; tile_col <= x_col_right; ++tile_col)
set_redraw_fore(get_tilepos(tile_col, tile_row), 1);
а set_redraw_fore (seg007:0550) — это redraw_frames_fore[tilepos] = frames,
ПРИСВАИВАНИЕ. Два персонажа на одном тайле оставят там ту же единицу, и
единственный обход тайлов нарисует передний кусок один раз.
Что делать. Механизм пометок у нас уже есть и прямо назван портом этой
архитектуры — pop_redraw.h. Fore-слой остался единственным местом на
прямом вызове. Приведение к оригиналу: pop_char_fore/pop_mirror_draw
вместо прохода ставят пометки, а один проход в конце кадра их разбирает.
СНАЧАЛА ЗАМЕРИТЬ, потом чинить. Это самый горячий путь, и окно клипа (вместо перебора тайлов) в своё время дало 3.2× — переход на пометки может часть этого вернуть назад. Замер: брейкпоинт на листьях fore-слоя со счётчиком, сцена «бой в комнате 3 уровня 1» и «Кид на тайле зеркала, ур. 4»; сравнить число нарисованных кусков с числом уникальных тайлов. Если дубль мал — оставить как есть и закрыть запись.
MIRROR-FG-STALE. Зеркало, поставленное при игроке В комнате 4, не попадает в коллизию
Заведено 2026-08-11 как краевой случай, оставшийся открытым от L4-MIRROR (шаг 2). Низкий приоритет: в обычном прохождении недостижимо — дверь выхода уровня 4 стоит не в той комнате, где зеркало, поэтому игрок физически не может быть в комнате 4 в момент её открытия.
Причина (по коду, не по симптому). place_mirror (pop_trob.c) пишет
тайл зеркала в ДАННЫЕ УРОВНЯ (pop_level_set_tile — страница уровня в W0) и,
если комната зеркала на экране, помечает тайл на перерисовку. А коллизия
работает не с данными уровня, а со СНИМКОМ комнаты room_fg, который
pop_room_load делает один раз при входе в комнату (pop_map_set(room_fg)
в roomtest.c). Снимок в этот момент не обновляется.
Что будет: зеркало нарисуется (перерисовка тайла отработает), но для
коллизии его как бы нет — wall_type не вернёт «стена слева», а ветка
is_obstacle для прыжка сквозь зеркало не сработает. То есть Кид пробежит
сквозь зеркало насквозь и тень не родится, пока комнату не перезайти.
Сценарий проверки (нужен чит +/− ROOMNAV, иначе не собрать):
- уровень 4, нажать кнопку, открывающую дверь выхода;
- пока дверь анимируется (43 тика ≈ 3.5 с) уйти читом в комнату 4;
- дождаться, когда дверь дорисует открытие — зеркало появится на экране;
- разбежаться справа налево и прыгнуть в зеркало.
- Ожидание при баге: Кид пролетает насквозь как через пустоту, тени нет.
- После выхода из комнаты и возврата в неё всё работает нормально.
Фикс — несколько строк: в place_mirror, в ветке
cur_room == MIRROR_ROOM, обновить и живую карту, а не только данные уровня
(в pop_map.c уже есть внутренние точки записи g_fg[tilepos] = ...,
нужна публичная «поставить тайл в текущей комнате»). Делать вместе с любой
следующей правкой pop_trob.c — отдельного захода не стоит.