BUG-CTRL-FRAME-1: геометрия Кида считалась по кадру стража

cur_frame — один глобал на всех персонажей (как в оригинале), владелец —
тот, кто последним прошёл load_frame.  Последним в кадре тикает страж,
поэтому к моменту control() Кида там лежал кадр СТРАЖА, а через
kid_cur_dx/dy/flags по нему считается вся геометрия управления:
dx_weight -> determine_col -> distance_to_edge_weight -> get_edge_distance
-> выбор ветки в check_jump_up.

Оригинал зовёт load_fram_det_col() (seg006:0144) сразу после
loadkid/loadshad и ДО control() — play_kid_frame (seg000:1211) и
play_guard_frame (seg000:1248).  У нас этого не было.

Замерено брейкпоинтом на pop_jump_up_seq: Kid x=156 col=6 кадр 15
(dx=0 weight_x=3) при кадре стража image17 (dx=-1 weight_x=8) дал
curr_col=7 и distance=2 вместо 6 и 10 — то есть jump_up_plain (вернулось
A=28, пустой прыжок) вместо «шаг назад на x=160 + зацеп».  Предсказание
по кадру стража совпало с намеренным до единицы.

Отсюда же плавающее поведение: кадр стража меняется каждый тик, distance
Кида скакал через порог 6 — то прыжок, то попытка зацепа с неверной X.
И «голова Кида поверх плиты (1,6)» — не баг отрисовки, а следствие позы,
которой в оригинале в этом месте не бывает.

Фикс: pop_load_fram_det_col() (pop_kid.c) + вызовы в pop_ctrl_tick и
pop_guard_tick.  determine_col — только на ветке Кида: у нас он
существует лишь для него (pop_map работает с Kid, а не с Char).

Разбор с числами — bug_closed.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Александр Петров
2026-08-04 12:55:43 +03:00
parent 5353bdaaec
commit e67117219f
7 changed files with 227 additions and 3 deletions
+149
View File
@@ -11,6 +11,155 @@
---
## BUG-CTRL-FRAME-1. Геометрия Кида считалась по кадру СТРАЖА — **ЗАКРЫТ 2026-08-04**
Самый неприятный класс: не косметика, а неверные РЕШЕНИЯ движка, причём
плавающие — одна и та же поза Кида давала разный результат в зависимости от
того, в какой фазе анимации находится страж.
**Симптом (приёмка уровня 2, комната 4).** Кид под сплошной плитой (1,6),
справа от него дыра от упавшей loose-плиты. По ↑ он то прыгает вверх
впустую (упираясь головой в плиту), то пытается зацепиться — но **не с той
координаты X**, с которой запрыгивает оригинал. Наблюдение пользователя,
оказавшееся точным: «похоже, дело в страже — в комнатах без стража то же
самое рисуется и работает правильно».
**Замер (брейкпоинт на входе `pop_jump_up_seq`, состояние снято в момент
решения).**
```
_Kid frame=15 (стойка) x=156 y=181 dir=влево curr_col=6 curr_row=2
кадр 15 Кида (kid_data.bin): dx=0 weight_x=3
pop_gframe (кадр СТРАЖА, image 17): dx=-1 weight_x=8
```
Считаем `dx_weight()` обоими кадрами:
| | кадр Кида (правильно) | кадр стража (что было) |
|---|---|---|
| `dx_weight()` | 156+3 = **159** | 156+9 = **165** |
| `m7()` | (159758)/14 = 6 ост. **10** | (165758)/14 = 7 ост. **2** |
| `curr_col` | 6 | **7** ← намерено |
| `distance_to_edge_weight()` | **10** | **2** |
| ветка `jump_up_or_grab` | 10 ≥ 6 → шаг назад + зацеп | 2 < 6 → `jump_up_plain` |
| результат | x 156 → **160**, `seq_24/8` | x без изменений, **`seq_28`** ← намерено (A=28 на выходе) |
Обе строки «намерено» совпали с предсказанием по кадру стража до единицы —
диагноз подтверждён, а не выведен.
**Корень.** `cur_frame` (`pop_kid.c:200`) — ОДИН глобал на всех персонажей
(так и в оригинале), его владелец — тот, кто последним прошёл `load_frame`.
В нашем кадре последним тикает страж (`pop_guard_tick`), поэтому к моменту
управления Кидом там лежит кадр стража. А `kid_cur_dx/dy/flags` читают этот
глобал напрямую, и через него считается ВСЯ геометрия управления:
`dx_weight``determine_col``distance_to_edge_weight`
`get_edge_distance` → выбор ветки в `check_jump_up`.
Оригинал страхуется явно и симметрично: `play_kid_frame` (seg000:1209) и
`play_guard_frame` (seg000:1246) сразу после `loadkid`/`loadshad` зовут
**`load_fram_det_col()`** (= `load_frame` + `determine_col`, seg006:0144) —
ДО `control()`. У нас этого вызова не было; в `pop_ctrl_tick` стоял
комментарий «кадр не перезагружаем — play_seq в kid_tick сделает это
следующим шагом», и он был неверен: `control()` читает `cur_frame` РАНЬШЕ,
чем `play_seq` его обновит.
**Фикс.** `pop_load_fram_det_col()` (`pop_kid.c`) — порт связки; зовётся
после `pop_loadkid()` в `pop_ctrl_tick` и после `pop_loadshad_and_opp()` в
`pop_guard_tick`. Вторая половина связки (`determine_col`) выполняется
только на ветке Кида: `determine_col` у нас существует лишь для него —
`pop_map` работает с `Kid` напрямую, а не с абстрактным `Char`.
**Что это объясняет задним числом.** Плавающее поведение прыжка — кадр
стража меняется каждый тик, вместе с ним `dx`/`weight_x`, и `distance`
Кида скакал через порог 6. А «голова Кида поверх плиты (1,6)», с которой
начался разбор, — не баг отрисовки: Кид просто оказывался в позе, которой
в оригинале в этом месте не бывает.
**Урок.** Любой глобал, который в оригинале «принадлежит активному
`Char`», у нас обязан перезагружаться на КАЖДОМ входе в окно `Char` — иначе
между персонажами течёт состояние, и баг проявляется только когда в комнате
есть второй персонаж. Здесь такой глобал уже ловили однажды: см. запись
про кэш кадра отрисовки в `load_frame` (`pop_kid.c:342`).
---
## BUG-LAND-SWORD-1. Кид навсегда застревает в приседе после падения с мечом — **ЗАКРЫТ 2026-08-04**
**Симптом (приёмка уровня 2).** Страж ударил Кида, Кид потерял HP,
провалился через loose-плиту на этаж ниже — и **сел в присед, из которого
не выходит**: клавиши не действуют вообще.
**Что показало ЖИВОЕ состояние в MAME** (мост `mame-z80`, чтение по
адресам из `roomtest.noi` — окно застало багу в момент):
```
_Kid frame=109 x=144 y=181 dir=-1 col=6 row=2 action=1 room=4
sword=2 (ВЫНУТ) alive=-1 curr_seq=0x2037
_holding_sword=1 _can_guard_see_kid=0 control_x/y/shift = 0
```
`curr_seq = 0x2037` — это внутри метки `softland_crouch`
(`SEQTBL_BASE + 1736 = 0x2036`). Смотрим `seqtbl.c:916`:
```c
LABEL(softland) // seq_17_soft_land
act(actions_5_bumped), SEQ_KNOCK_DOWN, dx(1), frame_107_fall_land_1,
dx(2), frame_108_fall_land_2,
act(actions_1_run_jump), LABEL(softland_crouch) frame_109_crouch,
jmp(softland_crouch), // ВЕЧНЫЙ ЦИКЛ на кадре 109
```
То есть `seq_17_soft_land` **не заканчивается сам** — он крутится на кадре
109, и вывести из него может ТОЛЬКО `control_crouched()`.
**Корень.** `control()` (seg005:252) до `control_crouched` при вынутом
мече не доходит — раньше срабатывает ветка
```c
} else if (Char.sword == sword_2_drawn) {
control_with_sword();
```
а `control_with_sword` (seg005:964) в мирной обстановке
(`can_guard_see_kid < 2`, под ногами не loose) умеет ровно одно: если кадр
== 171 (стойка с мечом) — убрать меч. Кадр 109 он не знает. **Замкнутый
круг: последовательность ждёт control_crouched, а диспетчер туда не
пускает.**
Оригинал такой позы просто не допускает — `land()` (seg005:176):
```c
if (Char.charid >= charid_2_guard || Char.sword == sword_2_drawn) {
Char.sword = sword_2_drawn;
seq_id = seq_63_guard_active_after_fall; // боевая стойка
} else {
seq_id = seq_17_soft_land; // присед
}
```
**У нас этой ветки не было**`pop_map.c land()` ставил
`SEQ_17_SOFT_LAND` безусловно. На уровне 1 не всплывало, потому что там
меч подбирается поздно и падать с ним особо негде; на уровне 2 меч у Кида
**с первого кадра** (`have_sword = level >= 2`), а в комнате 4 страж стоит
в одном ряду с двумя loose-плитами — сценарий собирается сам.
**Фикс.** Порт недостающей ветки: при `Kid.sword == SWORD_2_DRAWN`
падение на один этаж даёт `seq_63` (боевая стойка), а не присед. Ветка
`charid >= charid_2_guard` нам не нужна — наш `land()` работает только с
Кидом.
**Почему не задело падение на ДВА этажа** (`seq_20_medium_land`): там
`jmp` в конце нет — 29 кадров приседа и автоматический подъём
(`seqtbl.c:934`), поэтому оно развязывается само. Вечный цикл только у
`softland`.
**Урок на будущее.** Симптом «персонаж не реагирует на управление» стоит
диагностировать не по кадру, а по `curr_seq`: адрес прямо показывает, в
какой метке `seqtbl` он завис, и дальше видно, кто обязан был его оттуда
вывести.
---
## Проверено в MAME 2026-08-01 (ревизия L1-TRIAGE)
Три бага стояли как **Critical** с 2026-07-21 и по исходникам выглядели