Files
Sprinter-SDCC/applications/PoP/roomtest/BUGS_OPEN.md
T
snark13 36a60f54ba Кнопка в ШВЕ срабатывала как чужая связь (нашёл пользователь на ур. 5)
Симптом: Кид встаёт на плиту, которая физически в СОСЕДНЕЙ комнате (стоя в
шве, curr_col = −1), и вместо одних ворот открываются двое — на уровне 5
плита комнаты 11 открывала и нижние ворота комнаты 24, и верхние, хотя
связана только с нижними.

Причина.  check_press читал тайл через get_tile (тот резолвит шов в
соседнюю комнату), а комнату и tilepos для trigger_button брал из
координат персонажа: (своя комната, row*10 + curr_col).  При curr_col = −1
это tilepos 9 СВОЕЙ комнаты — там стена с bg = 0, то есть индекс цепочки
LINKLOC 0.  Цепочка от нуля в данных уровня 5 ведёт на ДВА тайла: нижние
ворота (link[0], next=1) и верхние (link[1]) — ровно то, что наблюдалось.
Настоящая кнопка имеет индекс 9 и одну цель.

В оригинале этого нет по построению: get_tile зовёт find_room_of_tile
(seg006:005D) и ПЕРЕСТАВЛЯЕТ curr_room/curr_tilepos, а trigger_button
работает уже с ними.  У нас резолв комнаты жил только внутри get_tile, а
наружу не отдавался.

Фикс: tile_room_of(col,row) — комната и tilepos клетки с учётом швов (по
образцу gate_modif, который так делал давно), check_press зовёт
trigger_button с резолвнутыми room/tilepos.  Ветка loose там же оставлена
на координатах персонажа: make_loose_fall работает с g_fg своей комнаты.

Два других вызова trigger_button (зацеп за кромку, севшая на кнопку плита)
правки не требуют — оба ограничены колонками 0..9 своей комнаты.

Заведён GATE-FORE-KID (BUGS_OPEN.md): Кид, стоящий В ПРОЁМЕ ворот, виден
поверх решётки — у нас портирован только шовный случай окклюзии, а
draw_tile_fore (seg008:0D15) рисует решётку поверх персонажа и внутри
комнаты.  Чинить в fore-слое отдельно, он горячий.

tests-host 5/5, make size-check OK.  Банк 3: 10648 -> 10828 Б.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 17:32:11 +03:00

28 KiB
Raw Blame History

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 что тип статус
GATE-FORE-KID Кид в проёме ворот виден поверх решётки окклюзия открыт: портирован только шовный случай
FORE-DUP передний слой тайла рисуется дважды при перекрытии объектов оптимизация открыт: сначала замерить, потом чинить
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). Это корректно, но лишнее для пик, до которых Киду дела нет.

Надо: перерисовывать тайл пики, только если

  1. сменился её видимый кадр (шаг выдвижения/уборки), ЛИБО
  2. её кто-то стёр — а стереть у нас может только 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: Кид стоит вплотную СЛЕВА от чомпера лицом вправо, прыжок с места — в анимации проскакивает кадр, где он «чуть отступил назад», и только потом идёт прыжок. В оригинале прыжок идёт прямо с места. На повторе в тот же заход не воспроизвёлся — отложено до пререлиза.

Что уже установлено (чтобы не разбирать заново).

  1. Это НЕ анимация. seq_3_standing_jump (SDLPoP seqtbl.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 назад.

  2. is_obstacle для чомпера у нас совпадает с оригиналом (seg004:037E): препятствие только при modif == 2, причём именно == 2, БЕЗ маски 0x7F — то есть окровавленный чомпер (0x82) в ванили не бампит вовсе. Проверено, расхождения нет.

  3. Прямая трасса прыжка отката НЕ показала. Ввод «UP и RIGHT одновременно» (mame bridge, key UP 24 + key RIGHT 24) даёт чистое движение вперёд: x 148 -> 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.c KBD_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_levelplay_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»; сравнить число нарисованных кусков с числом уникальных тайлов. Если дубль мал — оставить как есть и закрыть запись.



GATE-FORE-KID. Кид, стоящий В ПРОЁМЕ ворот, виден ПОВЕРХ решётки

Нашёл пользователь 2026-08-11 (уровень 5, комната 24, верхние ворота): Кид стоит на тайле ворот, а решётка рисуется ПОД ним — он виден целиком, хотя должен быть за прутьями.

Как в оригинале. draw_tile_fore (seg008:0D15) первой же строкой:

if (tile_left == tiles_4_gate && Kid.curr_row == drawn_row &&
    Kid.curr_col == drawn_col - 1 && Kid.room != room_R) {
    draw_gate_fore();
}

то есть когда Кид стоит ИМЕННО на тайле ворот, их решётка дорисовывается в foretable — поверх персонажа: draw_gate_fore (seg008) кладёт спрайт 51 (низ решётки) и дальше вверх кусками по 8 px спрайтом 52, до gate_top_y, прозрачным блитом.

Что есть у нас. Портирован только ШОВНЫЙ случай (pop_bg.c, pop_fore_over_char): если футпринт зашёл за левый шов и у соседа слева в этом ряду ворота — их бары перерисовываются поверх персонажа через draw_gate_back. Случая «ворота ВНУТРИ комнаты, персонаж на их тайле» нет вовсе, отсюда симптом.

Почему не чинится одной строкой. Правка идёт в pop_fore_over_char — самый горячий путь кадра (memory pop_fore_layer_cost: когда-то 78 % кадра, окно клипа дало 3.2×). Добавлять проверку надо так, чтобы она не стоила ничего на каждом тайле футпринта: условие дешёвое (тайл под персонажем == ворота), но рисование — это ещё один блит на кадр, пока Кид стоит в проёме. Плюс нужен свой расчёт полосы (gate_top_y по живому openness), а не готовый draw_gate_back, который рисует ЗА персонажем.

Проверять: уровень 5, комната 24 — встать в проём верхних ворот (колонка 1 ряда 0) при частично поднятой решётке; прутья должны перекрывать Кида. Тот же случай — любые ворота на уровнях 1-3.