safe_step при distance == 0: возвращена ветка оригинала (seg005:0604) —
seq_39 (шаг 11) вместо нашего «шага-1». Для СТЕНЫ результат прежний: первый
же dx(1) даёт бамп, ровно как в трассе живого SDLPoP из BUG-SEAM-PINGPONG.
Для ЧОМПЕРА бампа нет (в разомкнутой фазе он не препятствие), и оригинал
уносит Кида на все 11 px — а мы шагали на один. Это и был «микрошаг вместо
нормального короткого шага» перед челюстями.
ВНИМАНИЕ на приёмке ур.1: это тот самый safe_step из BUG-SEAM-PINGPONG —
проверить комнату 6, осторожный шаг вплотную к воротам шва.
BUG-CHOMP-JUMP-1 (низкий, маловоспроизводим, на пререлиз): прыжок с места
вплотную к чомперу иногда даёт кадр с отступом назад. В запись сложено всё,
что выяснено: seq_3_standing_jump состоит ТОЛЬКО из положительных dx (значит
отступ даёт bumped, а не анимация); is_obstacle для чомпера у нас совпадает
с оригиналом; прямая трасса (UP+RIGHT одновременно) отката не показала —
главная гипотеза в порядке нажатий (↑ раньше → уводит в up_pressed с
выравниванием x). Там же метод ловли.
T-2 (idle-skip) закрыт — сделан шире, чем формулировался, как DRAW-COST
шаг 1 (a25ce58).
29 KiB
roomtest — ОТКРЫТЫЕ баги и незакрытые оптимизации
Только то, что не закрыто. Всё закрытое (и, что важнее, разбор корней)
переехало в bug_closed.md: прежде чем заводить новый баг,
грепни там по симптому.
Приоритеты работ — в TASKS_OPEN.md (закрытые задачи с
протоколами — TASKS_CLOSED.md), а не здесь. Правило
проекта: механику сверять с ../SDLPoP/src/ ДО кодинга.
Ревизия списка: 2026-08-07 — прогон ВСЕХ комнат уровней 1 и 2
(пользователь). Крупных багов нет. Поэтому в bug_closed.md
уехали разом: чек-листы ручной перепроверки фиксов (2026-08-03, обе волны),
таблица обхода 24 комнат уровня 1 и таблица сырых наблюдений того же прогона —
все они закрыты этим проходом. С прогона пришли три новые записи, все по
уровню 2 (сырые формулировки — bugs_level2.md).
| ID | что | тип | статус |
|---|---|---|---|
| BUG-CHEAT-FIGHT-1 | +/− в бою с вынутым мечом → Кид теряет управление |
Major (чит) | открыт |
| BUG-GATE-PASS-1 | проход сквозь закрывшуюся решётку (0,9) комнаты 5 | Major | ждёт сценария: прогоном 2026-08-07 не воспроизведён |
| BUG-SPIKE-1 | пики залипают выдвинутыми рядом с Кидом | низкий | маловоспроизводимый: ни сценарием, ни попиксельной подгонкой X не поднимается |
| BUG-CHOMP-JUMP-1 | прыжок с места вплотную к чомперу: кадр с отступом назад | низкий | маловоспроизводимый: на повторе не поднялся; на пререлиз |
| T-1 | пики перерисовываются безусловно | оптимизация | открыт |
| Кид перерисовывается в покое | оптимизация | ЗАКРЫТ 2026-08-08 — DRAW-COST шаг 1 |
Уровень 2 — баги с приёмки
Приёмка уровня 2 закрыта (L2-PASS): smoke
2026-08-05 + обход всех комнат 2026-08-07. Карта содержимого уровня (что где
стоит по res2002.bin, какие кнопки какие ворота открывают) — там же, по ней
видно, «механика не сработала» это или «так и задумано».
Закрыто с этой волны: BUG-LOOSE-3 — чёрный бар под
упавшей плитой-потолком; BUG-GUARD-DEAF-1 — страж не
оборачивался на вернувшегося Кида;
BUG-GUARD-SPLASH-1 — «брызги» при
попадании по стражу; BUG-SWORD-GHOST-1 —
меч, спрятанный посреди боя после перехода комнаты (корень — мнимая стена за
краем комнаты в get_tile; кэш соседей расширен до полных комнат);
BUG-GUARD-COLOR-1 — цвет стража и его
полосы HP теперь берётся из guards_color уровня (2026-08-07).
Ниже — то, что осталось открытым после прогона 2026-08-07.
BUG-CHEAT-FIGHT-1. Чит +/− в бою: Кид остаётся в режиме боя и теряет управление
Наблюдение (пользователь, 2026-08-07). Если нажать +/− (ROOMNAV,
переход по комнатам) в момент, когда Кид вытащил меч для битвы, — в новой
комнате Кид не управляется: нажатия стрелок игнорируются.
Корень (прочитан по коду, сверен с seg005). Чит ROOMNAV
(roomtest.c:576) телепортирует и сбрасывает позу и HP —
enter_room(), kid_init(SEQ_STAND, …), pop_kid_hp_reset(), — но не
трогает состояние боя: Kid.sword остаётся SWORD_2_DRAWN. Диспетчер
pop_control (pop_ctrl.c:578) при вынутом мече уходит в
control_with_sword, а там единственный выход из режима боя —
if (Char.frame == FRAME_171_STAND_WITH_SWORD) { /* seg005:987 */
Char.sword = SWORD_0_SHEATHED;
seqtbl_offset_char(SEQ_92_PUT_SWORD_AWAY);
}
Стража в новой комнате нет (can_guard_see_kid = 0), кадр после
kid_init(SEQ_STAND) — обычная стойка, а не 171, поэтому не срабатывает ни
swordfight(), ни ветка «убрать меч»: control_with_sword каждый кадр
не делает НИЧЕГО, и ввод не доходит до движения. Заклинивание вечное.
Это баг нашего чита, а не порта: в оригинале телепорта между комнатами нет, и в режим боя без соперника попасть нечем.
Как чинить. В ветке ROOMNAV телепорт трактовать как выход из боя (то же
самое, что делает pop_start_level): Kid.sword = SWORD_0_SHEATHED,
holding_sword = 0, сбросить offguard/guard_refrac и состояние стража
(pop_guard_reset() вызывается по входу в комнату — проверить, что он
обнуляет Opp). Меч в инвентаре (pop_have_sword) при этом НЕ терять.
Второй кандидат на ту же болезнь — чит K (убить стража) в момент, когда
Кид в стойке с мечом, но не в кадре 171: там оригинал сам доводит до 171
через swordfight, так что проверить сценарием, а не менять вслепую.
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 текущего кадра); если разойдутся позиции — это отдельный
баг посадки, а пики — его следствие.
Уровень 3 — не баги, а неначатые задачи
Smoke-прогон уровня 3 (пользователь, 2026-08-05) дал четыре наблюдения. Два
были настоящими багами и закрыты в тот же день (разбор — в
bug_closed.md: BUG-JUMPWALL-1 и BUG-SEAM-WEDGE-1).
Оставшиеся два — не баги, а неначатые задачи:
| наблюдение | что это на самом деле |
|---|---|
| к.22: чомпер (2,6) не анимируется и вообще не рисуется | L3-CHOMP — механики чомперов НЕТ. В таблице тайлов pop_bg.c:211 строка 12 chomper рисует только основание, правую грань и низ; самих челюстей (спрайт из chtab, draw_tile_anim seg008) нет вовсе. Так и должно выглядеть до порта |
| к.10: скелет не оживает | L3-SKEL — check_skel (seg002:0E1F) не портирован. И комната другая: тайл skeleton(21), который оживает, лежит в к.1 (1,5) — это skeleton_room=1, skeleton_column=5, skeleton_row=1. Ещё два скелета уровня (к.17 (2,7), к.19 (2,2)) — просто декорация, они не оживают никогда. Плюс условие: скелет встаёт, только когда дверь уровня уже открыта и Кид стоит в колонке 2 или 3 |
Приёмки уровня 3 (полного обхода комнат) ещё не было — она осмысленна только после L3-CHOMP/L3-SKEL.
Открытые баги уровня 1
BUG-GATE-PASS-1. Проход сквозь закрывшуюся решётку — ЖДЁТ СЦЕНАРИЯ
Статус: наблюдался один раз (2026-08-03), воспроизвести не удалось ни тогда, ни прогоном всех комнат уровня 1 (2026-08-07). Заведён, чтобы наблюдение не потерялось; закрывать нельзя — ни как исправленный, ни как «не баг», пока нет надёжного сценария. Оговорка: BUG-GATEMOD-1, из-за которого решётка стартовала не в том состоянии, с тех пор закрыт, и наблюдение могло быть его следствием.
Что наблюдалось. Комната 5: Кид стоял НА тайле решётки (0,9) и ждал, пока она опустится. После закрытия пошёл вправо — прошёл в комнату 1 и упал на (1,1).
Что уже измерено и в чём загвоздка. Сразу после наблюдения повторить не
получилось: в том же месте Кид стоит на x = 196, col = 9, и решётка его
ДЕРЖИТ — то есть штатно.
Арифметика оригинала объясняет разницу. is_obstacle (seg004) ставит
плоскость блокировки в x_bump[col + FIRST_ONSCREEN_COLUMN] + TILE_MIDX, для
колонки 9 это x = 205. При этом «колонка 9» по get_tile_div_mod_m7 —
это x ∈ [191, 205). Пока curr_col == 9, Кид гарантированно левее
плоскости и обязан блокироваться; чтобы пройти, он должен оказаться правее
205, то есть уже на дальней стороне решётки, — и тогда уход вправо законен:
решётка закрылась у него за спиной, в оригинале она блокирует плоскость, а не
весь тайл.
Отсюда рабочая гипотеза: в момент наблюдения Кид стоял правее 205 (успел зайти
по тайлу дальше, пока решётка была поднята), и поведение штатное. Но повторить
эту позу и снять x пока не удалось, поэтому гипотеза НЕ подтверждена.
Что снять в следующий раз (без этих чисел вопрос не закрыть):
Kid.xиKid.curr_colв момент, когда решётка уже закрылась, а Кид ещё стоит на её тайле — до шага вправо;- модификатор решётки (openness) комнаты 5, тайл 9 —
can_bump_into_gate()считает её препятствием только пока(modif >> 2) + 6 < char_height, то есть пока она опустилась достаточно низко относительно РОСТА кадра; Kid.xпокадрово на самом шаге вправо — где именно перестал блокировать.
Быстрый способ снять первое: отладочный стоп-кадр (1 заморозить, 2
продолжить), затем чтение _Kid из отладчика MAME; подгонка позы по пикселю —
читы [/].
Возможный корень, если гипотеза не подтвердится. Проверка идёт по колонке,
которая на шве уже принадлежит СОСЕДНЕЙ комнате (curr_row_coll_room[] в
оригинале); у нас межкомнатная коллизия на шве — исторически проблемное место
(ср. закрытый BUG-SEAM-PINGPONG). Второй кандидат — char_height в
can_bump_into_gate(): если он берётся не от того кадра, решётка может
перестать считаться препятствием раньше времени.
Оптимизация отрисовки (записано 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, отдельно не окупается.
T-2. Idle-skip — ЗАКРЫТ 2026-08-08
Сделан как шаг 1 задачи DRAW-COST (коммит
a25ce58), и шире, чем формулировался здесь: пропускается не только Кид, а
ЛЮБОЙ персонаж, у которого с прошлой отрисовки этой страницы дабл-буфера не
изменился ни один вход отрисовки, — включая труп стража и ждущего стража.
Условие «обе страницы уже получили это состояние», которого требовала эта
запись, выполнено само собой: снимок входов хранится ПО СТРАНИЦАМ.
Замер: комната 1.3 с трупом стража, Кид стоит — 210 % -> 116 % кадрового
периода, ноль вызовов pop_heal_fast за кадр. Контракт — шапка
pop_cdraw.h, разбор и что делать дальше — TASKS_OPEN.md#draw-cost.
Связь с T-1 сработала как и предсказано: пока Кида не перерисовываем, heal'а нет, стирать пики нечем. Но T-1 остаётся открытым — трогать пики безусловно мы всё ещё продолжаем, когда Кид рядом ДВИЖЕТСЯ.
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 недостижимы в обычной игре — это свойство
данных уровня (разбор — «НЕ БАГИ» в
bug_closed.md); приоритет багов в них низкий. Аналогично 23/24 на уровне 3.