L2: падение с мечом, разбег-прыжок через 3 тайла, стартовое состояние ворот

BUG-FALL-SWORD-1 — start_fall (seg006:1044) был портирован не целиком:
не хватало трёх веток выбора последовательности и уборки меча в ножны.
Из-за seq_7 с set_fall(1,15) (дрейф 1 px/кадр) Кид с мечом уезжал примерно
на тайл вбок; в оригинале это seq_81_fightfall — падение строго вниз.
Сверено по логу SDLPoP: кадры 102..105 дают x = 155,157,159,160.

BUG-RJUMP-1 — run_jump (seg005:0AA8) не выравнивал Кида по кромке пола
перед толчком, а был заглушкой «полировка K3».  Суммарный dx seq_4 —
62 px при тайле 14, то есть провал ровно в три тайла берётся ТОЛЬКО с
кромки: без выравнивания Кид не перепрыгивал его никогда.  Порт —
pop_run_jump_align() в pop_map (беззнаковое сравнение оригинала = «сдвиг
не попал в [-8,-1]»).  На харнессе: было — толчок с x=165, кадр 44 в
колонке 3 (провал); стало — 5 кадров добега, толчок с x=149, кадр 44 даёт
x=87 col=1 row=1.  Остальные 8 сценариев не изменились.

BUG-GATEMOD-1 — load_alter_mod (seg008:198E) был портирован только для
зелий, поэтому ворота с bg=1 («Open» по спецификации DAT, табл. 8)
стартовали закрытыми.  Добавлены ветки gate (1 -> 188) и loose.  Ветка
wall намеренно НЕ портируется: связи стен наш pop_bg считает по типам
соседей в момент отрисовки.  На уровнях 1-3 таких ворот всего двое
(ур. 1 комн. 5 (0,9); ур. 2 комн. 13 (1,5)) — у остальных bg=2, а 2 и 0
ведут себя одинаково.

Убран временный трассировщик pop_dbg_trace/pop_dbg_draw; pop_dbg_trap()
оставлен как многоразовый инструмент.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Александр Петров
2026-08-05 12:34:26 +03:00
parent 3d8b81c9f6
commit cfc3602375
9 changed files with 574 additions and 17 deletions
+251 -7
View File
@@ -11,6 +11,151 @@
---
## BUG-RJUMP-1. Разбег-прыжок не берёт провал в три тайла — **ЗАКРЫТ 2026-08-04**
**Симптом (пользователь, приёмка уровня 2).** Комната 1: Кид с разбегу
обязан перелететь колодец с (0,5) на (0,1) — у нас он долетает до колодца и
валится вертикально вниз. Комната 9: то же с (1,5) на (1,1). Обобщение
пользователя оказалось точным: **провал ровно в три пустых тайла наш Кид не
перепрыгивал никогда**, а провалы поменьше брал.
**Почему «никогда», а не «иногда».** Суммарный `dx` последовательности
`seq_4_run_jump` (кадры 34..44) — 62 пикселя при ширине тайла 14. Это
ровно 4 колонки и меньше половины тайла запаса. То есть перелёт трёх
пустых тайлов возможен ТОЛЬКО если оттолкнуться почти точно от кромки; из
случайной фазы бегового цикла он не получается никогда.
**Корень.** Оригинал именно поэтому и не даёт прыгать откуда попало:
`run_jump` (seg005:0AA8) перед стартом **выравнивает Кида по кромке**.
```c
short xpos = char_dx_forward(4);
short col = get_tile_div_mod_m7(xpos);
for (short tiles_forward = 0; tiles_forward < 2; ++tiles_forward) {
col += dir_front[Char.direction + 1];
get_tile(Char.room, col, Char.curr_row);
if (curr_tile2 == tiles_2_spike || !tile_is_floor(curr_tile2)) {
pos_adjustment = distance_to_edge(xpos) + TILE_SIZEX * tiles_forward - TILE_SIZEX;
if ((word)pos_adjustment < (word)-8 || pos_adjustment >= 2) {
if (pos_adjustment < 128) return; // ПРЫЖКА НЕТ
pos_adjustment = -3;
}
Char.x = char_dx_forward(pos_adjustment + 4);
break;
}
}
control_up = release_arrows();
seqtbl_offset_char(seq_4_run_jump);
```
**У нас этой половины не было** — стояла заглушка с честным комментарием
«оригинал выравнивает Kid по краю пола (нужны tile-запросы) — это полировка
K3; K2b просто запускает run-jump». Полировкой это не оказалось: без
выравнивания три тайла непроходимы в принципе.
**Две тонкости, которые легко потерять при порте.**
1. **Беззнаковое сравнение.** `(word)pos_adjustment < (word)-8 || pos_adjustment >= 2`
означает ровно «`pos_adjustment` НЕ попал в `[-8,-1]`». Ветка
`pos_adjustment = -3` недостижима: `distance_to_edge ∈ [0,13]`,
`tiles_forward ∈ {0,1}`, значит `pos_adjustment ∈ [-14,13]`, а туда нужно
`>= 128`. В порте она записана как мёртвая, с объяснением.
2. **Отказ НЕ гасит `control_up`.** `return` выходит из `run_jump` до
`release_arrows()`, поэтому Кид бежит дальше с зажатой «вверх» и пробует
снова на следующем кадре. Именно так игрок и ловит фазу — просто
удерживая клавишу. Если погасить, прыжок у кромки станет одноразовым и
почти всегда неудачным.
**Фикс.** Тайловая половина — `pop_run_jump_align()` в `pop_map` (там живут
`get_tile`/`distance_to_edge`), диспетчерская — в `pop_ctrl.run_jump`.
Разделение то же, что у `pop_jump_up_seq`: pop_map правит `Kid.x` напрямую,
и `pop_savekid_state` эту правку намеренно не затирает.
**Проверка — на харнессе, а не в MAME.** Сценарий
`phys_running_jump_over_3tile_gap` в [`tests-host/t_phys.c`](tests-host/t_phys.c)
(комната с провалом в колонках 2–4). Было: прыжок со старта в кадре 34 при
`x=165`, кадр 44 приходится на колонку 3 — провал, падение. Стало: Кид
пробегает лишние 5 кадров, выравниватель ловит фазу, прыжок стартует при
`x=149`, кадр 44 даёт `x=87, col=1, row=1` — приземление на пол и бег
дальше. Остальные 8 сценариев набора не изменились ни на байт: правка
трогает только ветку разбег-прыжка у кромки.
---
## BUG-FALL-SWORD-1. Отход с мечом в провал: не та последовательность падения — **ЗАКРЫТ 2026-08-04**
**Симптом (приёмка уровня 2, комната 4).** Кид с вынутым мечом отступает
от стража к дыре от упавших loose-плит. Наблюдение пользователя: он
«проваливается раньше времени», летит **с клинком в руке**, и падает **по
другим X**, чем в оригинале — «почти на целый тайл левее».
**Разбор.** Сверка `seg006:1044 start_fall` показала, что наш порт
пропустил ТРИ вещи из оригинала, и все три бьют именно по этому сценарию:
```c
void start_fall() {
Char.sword = sword_0_sheathed; // (1) меч В НОЖНЫ
inc_curr_row(); start_chompers();
...
} else if (frame >= 81 && frame < 86) { // (2) срыв при приземлении
seq_id = seq_19_fall; // после прыжка вверх
Char.x = char_dx_forward(5);
load_fram_det_col();
} else if (frame >= 150 && frame < 180) { // (3) кадры С МЕЧОМ
droppedout = 1;
if (Char.direction < dir_0_right && distance_to_edge_weight() <= 7)
Char.x = char_dx_forward(-5);
seq_id = seq_81_kid_pushed_off_ledge;
}
```
У нас все они падали в общий `else``seq_7` (stepfall). Разница между
`seq_7` и `seq_81` в `seqtbl.c` и объясняет ВЕСЬ симптом:
```
seq_7 stepfall : dx(1) dy(3) | 102 | dx(2) dy(6) | dx(-1) dy(9) | dy(12) | dx(-2) set_fall(1,15)
seq_81 fightfall : dy(-1)| 102 | dx(-2) dy(6)| dx(-2) dy(9) | dx(-1) dy(12) | dx(-3) set_fall(0,15)
```
`set_fall(1, 15)` против `set_fall(0, 15)`**горизонтальный дрейф**: в
`seq_7` во время всего свободного падения `Char.x` уходит на 1 в сторону
КАЖДЫЙ кадр. За два этажа падения это и есть тот самый «почти тайл».
Оригинал в бою падает строго вниз.
**Сверка по числам** (лог SDLPoP `DBG shot`, кадры 102..105):
```
SDLPoP: 102 x=155 103 x=157 104 x=159 105 x=160 ← +2 +2 +1 = seq_81
у нас: 102 x=151 ← seq_7
```
Обратный счёт: у SDLPoP в момент решения `x = 150`, дальше
`char_dx_forward(-5)` при `dir=-1` даёт `+5` → 155. У нас решение при
`x = 152` — то есть **по самому правилу срыва расхождения нет**: обе
позиции лежат в колонке 7 (`dx_weight = x + 14`, кадр 157 имеет
`weight_x = 14`; колонка 7 — это `x ∈ [149,163)`). Двухпиксельная разница
— фаза отхода (шаг отступления `dx(-3)+dx(-2)` = 5 пикселей за цикл), а она
зависит от RNG стража и между движками совпасть не обязана. «Раньше
времени» — это не срыв не там, а seq_7 вместо seq_81.
**Что подтвердилось попутно.** Старт уровня 2 у нас **байт в байт** как в
SDLPoP: `frame=15 x=107 y=118 dir=-1 col=3 row=1 room=5`. Таблицы кадров и
`seqtbl` вынуты из оригинального бинарника, так что расходиться могут
только РЕШЕНИЯ движка — искать надо всегда там.
**Связь с [BUG-LAND-SWORD-1](#bug-land-sword-1).** Тот фикс (`land()`
даёт `seq_63` при вынутом мече) остаётся — он есть в оригинале, — но для
Кида он теперь почти недостижим: `start_fall` убирает меч в ножны, и после
приземления кадр 109 разбирает обычный `control_crouched`. То есть
настоящий корень вечного приседа был здесь, а не в `land()`.
**Метод.** Пустышка `pop_dbg_trap()` в резиденте W1 (идея пользователя):
брейкпоинт на банковый код ставить нельзя — 0xC000+ это окно, куда мапятся
все банки, и точка ловит чужие функции. Оставлена в `pop_state.c` как
многоразовый инструмент.
---
## BUG-CTRL-FRAME-1. Геометрия Кида считалась по кадру СТРАЖА — **ЗАКРЫТ 2026-08-04**
Самый неприятный класс: не косметика, а неверные РЕШЕНИЯ движка, причём
@@ -523,13 +668,15 @@ MAME по кромкам.
## НЕ БАГИ (кривая картинка, но совпадает с оригиналом — НЕ чинить)
- **Отход с ВЫНУТЫМ МЕЧОМ роняет Кида в яму «раньше, чем кажется»** (комната 4
уровня 2, кромка ряда 1 у дыры от упавшей loose-плиты). Сверено с SDLPoP
v1.24 пользователем (2026-08-04): оригинал падает **с той же позиции, по той
же траектории и с тем же видом кадра падения** (голова/руки поверх кромки
пола). Наш кадр и кадр SDLPoP совпадают один в один.
- **Кадр падения с мечом: голова/руки поверх кромки пола — САМ РЕНДЕР верен.**
Сверено с SDLPoP v1.24 (2026-08-04): там кадр падения выглядит один в один.
⚠ Речь ТОЛЬКО о виде кадра. Расхождение по X, которое было отмечено здесь
как открытое, разобрано и закрыто: [BUG-FALL-SWORD-1](#bug-fall-sword-1) —
дело не в моменте срыва (он верен), а в том, что мы играли `seq_7` вместо
`seq_81` и получали горизонтальный дрейф `set_fall(1,15)` за всё падение.
Механика, чтобы не разбирать заново. У стоек с мечом ОГРОМНАЯ точка веса:
Механика точки веса, чтобы не разбирать заново. У стоек с мечом она
ОГРОМНАЯ:
```
кадр 158/170/171 (stand_with_sword): dx=0 weight_x=13
@@ -745,8 +892,105 @@ foretable — в той же странице по смещению `0x1000` (с
# Вторая волна прогона 2026-08-03 (вечер)
<a id="bug-kbd-5"></a>
## BUG-KBD-5. Зажатый Shift снимается автоповтором стрелки — **ЗАКРЫТ 2026-08-05**
**Симптом (пользователь).** Прыжок с места с зацепом (уровень 2, комната 9)
не выходит: Кид прыгает, но за кромку не цепляется. Дальше уточнения,
которые и указали на клавиатуру, а не на физику:
- «отпускаю стрелки в полёте и жму Shift один — зацеп есть»;
- «жму Shift не сразу, а когда бо́льшая часть прыжка позади — зацеп есть»;
- «зажимаю Shift, потом стрелки — прыжок есть, зацепа нет»;
- в SDLPoP та же комбинация в том же порядке работает всегда.
Обобщение: **Shift работал в одиночку и не работал вместе со стрелками.**
Ровно то же ломало и осторожный шаг — игрок держит Shift, тапает стрелку, а
Кид на каком-то тапе уходит в бег (это же поведение раньше описывалось как
BUG-KBD-4, см. ниже).
**Замер (MAME, `roomtest`, чтение карты `_kbdraw_down` + breakpoint на выходе
из `in a,($18)` в декодере трамплина).**
1. Зажать LShift → байт 2 карты = `04` (LSh взведён), поток `12 12 12 …`
(Shift автоповторяется сам, пока он последняя нажатая клавиша).
2. Добавить ↑ → байт 46 = `20` (↑ взведена), **байт 2 = `00` — Shift снят,
хотя физически зажат.**
3. Добавить ещё → байт 46 = `30` (обе стрелки видны, 3-key rollover в
порядке), Shift по-прежнему `00`.
4. КОРОТКИЙ тап ↑ при зажатом Shift — Shift выживает. Это и сбивало с
толку: бит сносился, но тут же восстанавливался, потому что после
отпускания стрелки Shift снова становился «последней клавишей» и его
автоповтор `12` взводил бит обратно за ~30 мс.
5. Сам поток при зажатом Shift и зажатой ↑:
```
… E0 E0 E0 75 E0 75 E0 75 ← ни одного F0/12
```
**Причина.** Декодеры (`_irq_tramp.c`, `kbd_raw_poll.c`) делали из «fake
shift» ДВА вывода, и второй был неверен:
- обёртка `E0 F0 12` / `E0 12` есть → Shift зажат → взвести бит ✔ верно;
- расширенный make **без** обёртки → Shift отпущен → снять биты обоих
шифтов ✘ **неверно**.
Обратный вывод опирался на «клавиатура обёртывает КАЖДЫЙ расширенный код».
Замер это опровергает: клавиатура MAME-Sprinter (`pc_kbd ms_naturl`) обёртку
не шлёт вовсе, и уж точно её не бывает на typematic-повторах — а повторы идут
непрерывно, пока стрелка зажата. Значит каждый повтор снимал реально зажатый
Shift, и к кадрам 102…106 (окно `check_grab`) движок видел Shift отпущенным.
Отсюда и «работает, если нажать Shift позже»: бит успевал постоять несколько
кадров до ближайшего повтора стрелки.
**Фикс.** Обратный вывод убран целиком — расширенная клавиша идёт обычным
путём и о состоянии Shift не судит. Прямой вывод оставлен (обёртка, если
клавиатура её всё-таки шлёт, подтверждает «Shift зажат» и стоит дёшево).
Состояние Shift теперь ведут его собственные make/break `12` / `F0 12` —
они приходят всегда. Заодно ушла ставшая ненужной переменная
`_kbdraw_fakesh`, и оба декодера стали короче (трамплину это на пользу: его
клавиатурный блок упирается в диапазон `jr`).
Файлы: `libc/irq/_irq_tramp.c`, `libc/kbd/kbd_raw_poll.c`, `libc/kbd/_kbdraw.h`,
`libc/kbd/_kbdraw_state.c`, `libc/kbd/kbd_raw_sync.c`.
**Чем платим.** Потерянный при Rx-overrun break Shift снять теперь нечем —
модификатор может залипнуть до перенажатия (это старый BUG-KBD-3). Размен
осознанный и решён в ту же сторону, что и раньше в `kbd_raw_sync`: **лучше
залипание, чем отвал** — залипший Shift игрок снимает нажатием Shift, а
сорванный посреди игры Shift в PoP стоит жизни. Вероятность overrun'а сильно
снижена дренажом FIFO опросом из главного цикла (`kbd_raw_poll`, KBD-1).
**Проверено в MAME после фикса** (карта `_kbdraw_down` при `roomtest`):
| действие | LSh (байт 2) | стрелки (байт 46) |
|---|---|---|
| зажать LShift | `04` | `00` |
| + зажать ↑ и → | `04` | `30` |
| держать 5 с (автоповтор идёт) | `04` | `30` |
| отпустить Shift, стрелки держать | `00` | `30` |
| отпустить стрелки | `00` | `00` |
`make size-check`: 70 программ, роста нет.
**Осталось наблюдением, не багом этой задачи.** В карте изредка остаётся
взведённым бит в PLAIN-половине для кода стрелки (напр. байт 14 бит 4 =
`0x74` «Right без E0»). Это потерянный префикс `E0` — след старого рассинхрона
FIFO. Игру не задевает (движок читает `KBD_RIGHT = EXT|0x74`, то есть
расширенную половину), но если всплывёт — искать здесь.
---
<a id="bug-kbd-4"></a>
## BUG-KBD-4 (+ BUG-KBD-3). Shift: сначала залипал, потом стал отваливаться — **ЗАКРЫТ**
## BUG-KBD-4 (+ BUG-KBD-3). Shift: сначала залипал, потом стал отваливаться — **ЗАКРЫТ (с поправкой, см. BUG-KBD-5)**
> **Поправка 2026-08-05.** Вывод «клавиатура обёртывает КАЖДЫЙ расширенный
> код, пока зажат Shift» оказался неверным, и построенный на нём обратный
> вывод («расширенный make без обёртки ⇒ Shift отпущен») убран — он ломал
> Shift вместе со стрелками. Разбор — [BUG-KBD-5](#bug-kbd-5). Проверка
> «пять тапов стрелки подряд» ниже проходила не потому, что вывод был верен,
> а потому что между тапами бит восстанавливал автоповтор самого Shift.
> Актуальное поведение `kbd_raw_sync` — вариант 1 (модификаторы не сбрасываем).
**Симптом (вторая редакция).** Залипание ушло, но появилось обратное: при
зажатом Shift второй-третий-четвёртый тап стрелки отрабатывал уже не