Закрыты BUG-GUARD-IX-1 и BUG-GATE-SEAM-ROW1 (уровень 4)

BUG-GUARD-IX-1 — «зависание» в бою со стражем.  Не зависание: главный цикл
крутился, а отладочная локаль frozen сама становилась ненулевой.  Корень —
у main затирался IX (0xBFFA -> 0xBF00), и все его локали адресовали живой
стековый мусор.  Затирал check_chomped_guard: coll_row() пишет по
flags + scan_off, длину берёт из win_lo/win_hi, а scan_off выставляла
только coll_scan_prepare() из пути Кида.  Ряд стража писался по смещению
Кида длиной стража и при Киде у правого края комнаты вылезал за flags[13]
— прямо в сохранённый IX.  Фикс: coll_scan_prepare() в начале
get_row_collision_data(); заодно чинится расчёт (scan_left0 задаёт x
колонок, чомпер-коллизия стража считалась по координатам Кида).
На уровне 1 не проявлялось: бой идёт левее середины, запись оставалась
внутри массива — данные были неверны молча.

BUG-GATE-SEAM-ROW1 — решётка в шве не анимировалась.  Плита комнаты 1
открывает ворота комнаты 8 в (1,9), видимые через левый шов, а
change-driven редрой смотрел только m[9] (ряд 0) и перерисовывал жёстко
draw_tile(0,0).  На уровне 1 та же связка работала лишь потому, что
решётка соседа стояла в (0,9).  Фикс: сигнатура по всем трём рядам,
маска изменившихся рядов в seam_rows (переживает оба кадра дабл-буфера),
pop_room_redraw_seam_left(rows) перерисовывает только помеченные.  Плюс
ряд выше (changed | changed>>1): верх решётки (draw_tile_anim_topright,
seg008:0568) рисует тайл над-справа от ворот, без этого чёрный
треугольник над ними оставался статичным.

Проверено пользователем в MAME (бой в комнате 18; анимация решётки в
стартовой комнате), детекторы IX висели без починки и не сработали;
make -C tests-host — все 5 наборов.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-10 16:32:14 +03:00
parent 93aa51db45
commit 6a824dd7ab
6 changed files with 172 additions and 22 deletions
+100
View File
@@ -13,6 +13,106 @@
---
<a id="bug-guard-ix-1"></a>
## BUG-GUARD-IX-1. «Зависание» при бое со стражем — затёртый IX главного цикла — **ЗАКРЫТ 2026-08-10**
**Симптом (пользователь, уровень 4).** Дважды подряд при ручном входе в
комнату 18 со стражем игра «повисала»: картинка стоит, управление не
отвечает. Нажатие «2» ненадолго возвращало движение, потом всё повторялось.
**Диагноз — НЕ зависание.** Главный цикл всё это время крутился (маркер
верха цикла тикал раз в кадр). Стоял отладочный стоп-кадр: локаль `frozen`
в `main` сама собой становилась ненулевой.
**Корень — однобайтовый выход за границу локального массива.** У `main` все
локали лежат по `IX-1..IX-19`, и IX затирался: вместо `0xBFFA` в нём
оказывался `0xBF00`. Признак железный — в теле цикла обязано выполняться
`SP == IX-19`, а по факту было `SP=0xBFE7, IX=0xBF00`. Дальше `main` читал
`frozen`, `dbuf`, `front/back`, `dead_frames` и edge-флаги читов из живого
стекового мусора.
Виновник — `check_chomped_guard` (pop_map.c):
```c
uint8_t flags[COLL_N]; /* 14 байт в кадре функции */
calc_coll_window(); /* окно СТРАЖА -> win_lo/win_hi */
get_row_collision_data(Char.curr_row, flags);
```
`coll_row()` пишет по `flags + scan_off`, а число байт берёт из
`win_lo/win_hi`. `scan_off`/`scan_left0` выставляет `coll_scan_prepare()`,
которую звал ТОЛЬКО `check_collisions` — путь Кида. То есть ряд стража
писался **по смещению Кида, длиной стража**. Диапазон записи
`[kid_win_lo+2 … kid_win_lo+2 + ширина_окна_стража − 1]`; при обычной ширине
4 он уезжает за `flags[13]`, когда `kid_win_lo > 8` — то есть **когда Кид
стоит у правого края комнаты** (взаимное расположение Кида и стража ни при
чём, стража даёт только длину). Сразу за массивом лежит сохранённый IX;
затёртый младший байт и превращал `0xBFFA` в `0xBF00`.
Перелёт в отрицательные индексы был невозможен: `scan_off = kid_win_lo + 2`,
а `kid_win_lo >= COLL_C0 = 2` из-за клампа в `calc_coll_window`.
**Почему не ловилось раньше.** На уровне 1 (комната 3) бой идёт левее
середины — запись оставалась внутри массива. Ничего не портилось, но флаги
стража всё равно читались по чужим смещениям и с иксами Кида: тихо неверные
данные, без последствий (чомперов там нет).
**Фикс.** `coll_scan_prepare()` в начале `get_row_collision_data()` — теперь
запись всегда ложится в `[win_lo+2 … win_hi+2]`, а `calc_coll_window` клампит
окно в `[2 … 11]`, то есть индексы гарантированно `0…13`. Заодно чинится
сам расчёт: `scan_left0` задаёт x колонок, и чомпер-коллизия стража считалась
по координатам Кида.
**Как ловилось (приём на будущее).** Детекторы `ix != 0xBFFA` на границах
фаз `PROF()` с починкой IX в верху цикла: игра остаётся живой, а первый
сработавший детектор называет фазу. Дальше — инструкционная трасса MAME
(`trace file,0`), включаемая на подозрительном вызове и выключаемая сразу
после, со стопом только на плохом кадре: в файле остаётся ровно тот вызов,
где IX испортился. В хвосте трассы видно `ld sp,ix / pop ix`, достающий
`BF00` вместо `BFFA`. См. memory `z80_profiling_method`.
**Проверка.** Бой со стражем в комнате 18 уровня 4 (пользователь) —
детекторы IX висели без починки и не сработали ни разу; `make -C tests-host`
— все 5 наборов.
---
<a id="bug-gate-seam-row1"></a>
## BUG-GATE-SEAM-ROW1. Решётка в шве не анимируется, если ворота НЕ в ряду 0 — **ЗАКРЫТ 2026-08-10**
**Симптом (пользователь, уровень 4, стартовая комната).** Плита нажимается,
ворота физически открываются (Кид проходит), но визуально с решёткой ничего
не происходит. Отдельно: чёрный треугольник над воротами (верх решётки под
ковром) статичен, тогда как в оригинале он ездит вместе с барами.
**Корень.** Зонд на `pop_add_trob` показал `room=8 tp=19 type=1`: плита
комнаты 1 открывает ворота **комнаты 8 в (1,9)** — соседней комнаты, видимые
через ЛЕВЫЙ шов (бары ворот в col9 рисует col0 соседа справа). Наш
change-driven редрой шва смотрел только один байт `m[9]` (ряд 0), а
`pop_room_redraw_seam_left()` перерисовывал жёстко `draw_tile(0, 0)`.
Изменение приходило в `m[19]` — сигнатура его не видела.
На уровне 1 та же связка комнат 6/8 работала только потому, что решётка
соседа стояла в **(0,9)**. Тайлсет ни при чём: в подземелье просто не
попадалось ворот в ряду 1 у шва (и треугольника не было — над решётками
всегда потолок).
**Фикс.**
1. `seam_sig` — массив на три ряда, сравниваются все три; накопленная маска
изменившихся рядов живёт в `seam_rows` и переживает оба кадра
дабл-буфера.
2. `pop_room_redraw_seam_left(uint8_t rows)` берёт маску и перерисовывает
только помеченные ряды — не все три, чтобы не платить каждый кадр
анимации.
3. В маску добавляется ряд ВЫШЕ (`changed | (changed >> 1)`): верх решётки
(`draw_tile_anim_topright`, seg008:0568 — маска 68 + кадр
`DOOR_FRAM_TOP`) рисует не сам тайл ворот, а тайл над-справа от него, у
нас `(r1, 0)`. Без этого бары ездили, а треугольник над ними стоял.
**Проверка.** Уровень 4, стартовая комната: нажатие плиты — решётка шва
поднимается/опускается вместе с верхом (пользователь).
<a id="bug-guard-color-1"></a>
## BUG-GUARD-COLOR-1. Страж и его полоса HP — всегда одного цвета — **ЗАКРЫТ 2026-08-07**