Трасса kidobj для сверки с SDLPoP + разбор боя у шва

Инструмент: pop_dbg_kidobj печатает то же, что вывод, добавленный
пользователем в SDLPoP add_kid_to_objtable — tilepos/frame/act/col/cols/rows
(+ наша комната).  Считает по set_char_collision и set_objtile_at_char.
Выключен по умолчанию (DBG_KIDOBJ 0), включается одним define; вывод
забирает брейкпоинт MAME на резидентном pop_dbg_trap.

Сверка подтвердила фикс a498255: окно перехода совпало кадр в кадр, за бой
на уровне 1 комната сменилась один раз, на уровне 8 у шва 24/18 — четыре
раза на 387 кадров боя, и все четыре на РАЗРЕШЁННЫХ кадрах (170/164/170/165).
Смен на запрещённых кадрах во всей трассе нет.

Разбор целиком — BUGS_CLOSED.md#seam-fight-flicker, включая невыясненное
расхождение поля cols.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-18 17:07:01 +03:00
parent a498255471
commit 3f643532c9
5 changed files with 157 additions and 0 deletions
+86
View File
@@ -3152,3 +3152,89 @@ else died_on_button(...); /* мёртв — кнопка С
Вероятнее всего это кадр уже НАЧАВШЕГОСЯ падения (при падении X сносит), но
доказательства нет. Если `(0,-1)` появится на исправленном билде — это
отдельный баг.
---
<a id="seam-fight-flicker"></a>
## SEAM-FIGHT-FLICKER. Бой у шва: комната перерисовывалась то одна, то другая
**Наблюдение (пользователь, 2026-08-18).** Уровень 8, шов комнат 24/18: во
время схватки со стражем экран постоянно переключался между комнатами —
«драться очень неудобно». Отдельно пользователь заметил, что уходит Кид,
даже когда просто держит «вверх» (защита), и предположил верную причину:
стойка защиты не одна, Кид периодически проходит через обычную.
**Корень.** `leave_room` (seg002:0490) запрещает смену комнаты не только на
развороте, подъёме-с-зацепа (135..149) и вставании из приседа (110..119), но
и на ВСЕЙ боевой анимации — кадры **150..162** и **166..168**. У нас были
только первые три условия.
Разрешены при этом 163..165, 169 (`begin_block`) и 170/171 (`stand with
sword`) — то есть уйти можно, но только на «легальном» кадре. Цикл защиты
(seqtbl: `readyblock` = 169 -> `blocking` = 150, петля) как раз проходит
через 169, а между атаками Кид возвращается в 158/170 — отсюда и ощущение
«стоять в защите нельзя».
Побочный эффект известен и в оригинале эксплуатируется — **Trick 35,
«retreat without leaving the room»**: определённым ритмом «назад» кадр 170 не
наступает никогда, и Кид пятится из комнаты, не переключая её. В SDLPoP это
чинит `FIX_RETREAT_WITHOUT_LEAVING_ROOM`, по умолчанию ВЫКЛЮЧЕННЫЙ, — мы
портируем оригинал.
**Сторона стража проверена отдельно, расхождений нет.** `play_guard_frame`
(seg000:0F48) ухода из комнаты не содержит вовсе (ни `leave_room`, ни
`check_leave_below`), физика загейчена окном `Char.x ∈ [44, 211)`. Комнату
страж меняет только через `follow_guard` (у нас `pop_guard_follow`) и
`check_guard_fallout` (у нас `pop_guard_fallout`) — оба портированы.
**Проверка: покадровая сверка с SDLPoP.** Пользователь добавил в SDLPoP
вывод в конце `add_kid_to_objtable` (seg008:1679); мы навесили такой же
(`pop_dbg_kidobj`, см. ниже). Окно перехода совпало КАДР В КАДР:
| | SDLPoP | наша |
|---|---|---|
| | `tilepos=10 frame=161 col=-1` | `tilepos=10 frame=161 col=-1` |
| | `tilepos=10 frame=160 col=-1` | `tilepos=10 frame=160 col=-1` |
| | `tilepos=10 frame=157 col=-2` | `tilepos=10 frame=157 col=-2` |
| | `tilepos=10 frame=158 col=-2` | `tilepos=10 frame=158 col=-2` |
| **смена** | `tilepos=18 frame=169 col=9` | `tilepos=18 frame=169 col=9` |
Прогон уровня 1 комнаты 3 (1779 кадров): за весь бой комната сменилась
ОДИН раз; 34 боевых кадра прошли за краем комнаты (`col<0`), самый длинный
непрерывный прогон — 17 кадров, и комната при этом не переключалась — ровно
как в трассе SDLPoP.
Прогон уровня 8, шов 24/18 (1433 кадра, 360 кадров с мечом): внутри боя
**4 смены комнаты на 387 кадров** — то есть примерно раз в 6 секунд, и это
настоящее выдавливание, а не мерцание. Все четыре пришлись на РАЗРЕШЁННЫЕ
кадры (170, 164, 170, 165); ни одной смены на запрещённом кадре во всей
трассе нет. Кид подолгу держался на `col=-1` и `col=10`, комната не
переключалась.
**Инструмент (оставлен в дереве, выключен).** `pop_dbg_kidobj`
(`roomtest_cold.c`) + резидентный буфер `pop_dbg_obj` (`pop_state.c`),
включается `#define DBG_KIDOBJ 1` в `roomtest.c`. Считает то же, что
`set_char_collision` + `set_objtile_at_char`. Вывод забирает брейкпоинт
MAME на резидентном `pop_dbg_trap`; сборщик — `collect_kidobj.py`.
Грабли, стоившие времени:
- `#define DBG_KIDOBJ` сперва положили внутрь `#ifndef PROF_BORDER`, а
`PROF_BORDER` задаётся ключом сборки — блок не выполнялся, трасса молча
не включалась. Нужен свой `#ifndef`.
- Знаковые значения через выражения отладчика MAME получить не вышло: `b@`
связывается слабее `+`, и `b@ADDR + 128` читает ДРУГОЙ адрес; скобки не
помогают. Зонд печатает сырые байты, перевод — в сборщике.
- Кольцо `clog` не потребляющее, одинаковые строки от повторов не отличить —
в строку добавлен `totalcycles`, по нему и склеиваются чанки.
- Замерено: один зонд с коротким `printf` — **98 % полной скорости**
эмуляции, играть можно (в отличие от четырёх зондов профилировщика).
**Осталось невыясненным.** Поле `cols` (`char_col_left..char_col_right`) у
нас систематически на единицу меньше, чем в SDLPoP, и дважды дало `0..-1`,
чего в оригинале не бывает. Все поля, которые считает ДВИЖОК (`tilepos`,
`frame`, `act`, `col`, точка перехода), совпали полностью — расхождение
только в величине, которую выводит сам отладочный помощник из ширины кадра
`pop_cd[].fpw`. Подозрение на паддинг кадров в атласе
(`png_strip_padding_tradeoff`) либо на мой пересчёт переднего края мимо
боевого `char_x_forward_edge`. Проверять отдельно.