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
+53
View File
@@ -45,6 +45,7 @@
| — | [L1-SPEED](#l1-speed) | игра на ~39 % быстрее оригинала | ощущение от ВСЕХ уровней; берётся в любой момент |
| — | [L1-PASS](#l1-pass) | сквозной прогон ур. 1 + таблица 24 комнат | приёмка ур. 1 |
| — | [DBG-CHEATS](#dbg-cheats) | `[`/`]` — подгонка Кида по X | отладка (BUG-GATE-PASS-1) |
| — | [MEM-BANK5](#mem-bank5) | новый банк кода: W1/W2 осталось 38 Б кучи | берётся по факту нехватки места |
---
@@ -154,6 +155,17 @@ loose-плиту (комната 7, колонка 4, ряд 0). Механик
### KBD-1. Shift + стрелки: нажатия теряются — **ПРИЧИНА НАЙДЕНА, ФИКС ПРОВЕРЕН**
> **ПОПРАВКА К ПОСЫЛКЕ (2026-08-05).** Ниже «fake shift» подан как
> установленный факт («при зажатом Shift PS/2 удваивает трафик»). Прямой
> замер потока байт это не подтвердил: клавиатура MAME-Sprinter
> (`pc_kbd ms_naturl`) обёртку `E0 F0 12` / `E0 12` не шлёт вовсе — при
> зажатом Shift поток на стрелку ровно `E0 75 E0 75 …`. Значит удвоения
> трафика в связке Shift+стрелка нет, и мотивировка «поэтому FIFO
> переполняется» отпадает; сам ФИКС (плотный опрос `kbd_raw_poll`) остаётся
> верным и нужным — переполнение вызывает не Shift, а короткая жизнь
> импульса IRQ (пункт 3 гипотезы) плюс DI-окна графики. Разбор и следствия
> — [BUG-KBD-5](bug_closed.md#bug-kbd-5).
>
> **Итог (2026-08-01).** Причина — не наш код и не DI-окна графики: при
> зажатом Shift PS/2 удваивает трафик («fake shift»), а импульс запроса
> прерывания здесь теряется примерно в 44 % случаев, и трёхбайтовый FIFO
@@ -659,6 +671,47 @@ Shift+L → уровень 3 (комната 9). То есть цепочка
---
### <a id="mem-bank5"></a>MEM-BANK5. Разгрузка W1/W2 новым банком кода
Вопрос 2026-08-04: «надо делать новый банк?». **Да, и он лечит именно то,
что жмёт.** В нашей раскладке (`MEMORY=huge`, small-вариант) CODE и DATA
живут в ОДНОМ 32-КБ пространстве W1+W2 — карта текущей сборки:
```
_CODE 0x4100..0xAA00 26880 Б
_DATA 0xAAD0..0xB9B0 3808 Б
_BSS 0xB9B8..0xBADA 290 Б
куча 0xBADA..0xBB00 38 Б ← упёрлись сюда, добавляя отладку
стек 0xBB00..0xC000 1280 Б
```
Поэтому **каждый килобайт кода, уехавший в банк, становится килобайтом,
доступным данным**. Отдельного «дефицита W2» у нас нет — дефицит один.
(38 байт кучи не опасны сами по себе: malloc'ом мы не пользуемся, страницы
берутся через `mem_alloc_block`. Опасно то, что следующая структура
данных упрётся в стек молча.)
Резидентный код по модулям (из `.sprinter-cc-roomtest/*.rel`):
| модуль | _CODE | как часто зовётся | в банк? |
|--------|------:|-------------------|---------|
| `pop_kid.c` | 6287 | `load_frame`/`play_seq` — 2×/кадр (Кид + страж) | частично: холодная половина (загрузка страниц спрайтов, `pop_kid_load`) — да; движок кадров — нет |
| `roomtest.c` | 3893 | main-loop | нет (точка входа, зовёт всех) |
| `pop_trob.c` | 2526 | `do_trobs` 1×/кадр, но `pop_trob_modif` — горячий аксессор | **кандидат №1**, если вынести аксессор в резидент |
| `pop_level.c`| 2292 | `pop_level_tile` — из `pop_bg` (банк 2) на каждый тайл | **нет**: банк→банк на каждый тайл убьёт отрисовку |
| `pop_ctrl.c` | 2189 | `user_control` 1×/кадр | **кандидат №2** — дёшево и безопасно |
| `pop_guard.c`| 923 | 1×/кадр | нет смысла |
Порядок действий, когда упрёмся: `pop_ctrl.c` → банк 5 (2.2 КБ, один
banked-вызов за кадр), затем расщепление `pop_kid.c` на горячее ядро и
холодную загрузку. Критерий кандидата — **не размер, а частота вызова и
отсутствие горячих банк→банк переходов**; `pop_level` показывает, что
большой холодный на вид модуль может быть горячим аксессором.
Брать по факту нехватки места, не заранее.
---
## Отложено осознанно (не брать, пока не появится причина)
- **Звук** (CBL-эффекты, Фаза 5 `PORT_PLAN.md`) — геймплей не блокирует.
+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 второй-третий-четвёртый тап стрелки отрабатывал уже не
+141 -3
View File
@@ -27,8 +27,10 @@ BUG-GATE-PASS-1 — однократное наблюдение прохода
| ID | что | тип | статус |
|----|-----|-----|--------|
| [Уровень 2](#уровень-2) | баги отрисовки с приёмки | | **ждут списка от пользователя** |
| [BUG-GATE-PASS-1](#bug-gate-pass-1) | проход сквозь закрывшуюся решётку (0,9) комнаты 5 | Major | **ждёт сценария воспроизведения** |
| [BUG-GRAB-1](#bug-grab-1) | ур. 2 комн. 9: прыжок с места через 3 тайла — нет зацепа за кромку | Major | **причина найдена (клавиатура, BUG-KBD-5), фикс есть, ждёт игровой проверки** |
| [BUG-GATEMOD-1](#bug-gatemod-1) | ворота стартуют закрытыми, хотя в уровне открыты | Major | **фикс есть, ждёт проверки** |
| [Уровень 2](#уровень-2) | остальные баги с приёмки | — | принимаются по ходу |
| [BUG-GATE-PASS-1](#bug-gate-pass-1) | проход сквозь закрывшуюся решётку (0,9) комнаты 5 | Major | **перепроверить после [BUG-GATEMOD-1](#bug-gatemod-1)** — та же решётка стартовала не в том состоянии |
| [T-1](#t-1) | пики перерисовываются безусловно | оптимизация | открыт |
| [T-2](#t-2) | Кид перерисовывается в покое | оптимизация | открыт |
| [обход 24 комнат](#обход-всех-24-комнат-уровня-1) | таблица заполнена на 5 строк из 24 | ревизия | открыт |
@@ -54,10 +56,146 @@ BUG-GATE-PASS-1 — однократное наблюдение прохода
вводит ровно три новых фоновых тайла — **большая колонна (низ 8 / верх 9)
и верх двери (12)**; если артефакт рядом с ними, это первый подозреваемый.
*(записей пока нет)*
<a id="bug-grab-1"></a>
## BUG-GRAB-1. Прыжок с места через провал в 3 тайла: зацепа нет — **ПРИЧИНА НАЙДЕНА, ФИКС ЕСТЬ, ЖДЁТ ИГРОВОЙ ПРОВЕРКИ**
> **Итог 2026-08-05.** Физика тут ни при чём — виновата клавиатура.
> Зажатый Shift снимался автоповтором зажатой стрелки, поэтому к кадрам
> 102…106 (окно зацепа) движок видел Shift отпущенным. Полный разбор и
> фикс — [BUG-KBD-5](bug_closed.md#bug-kbd-5); поведение Shift в MAME
> проверено замером карты `_kbdraw_down`. Осталось подтвердить сам зацеп
> живой игрой; версии 2 и 3 ниже проверять только если он всё ещё не выйдет.
**Симптом.** Уровень 2, комната 9. Перепрыгнув на (1,1), Кид должен
вернуться обратно: разбегаться негде, поэтому он встаёт на самый край
плиты, прыгает с места и **цепляется руками за (1,5)**, после чего
подтягивается. У нас Кид с зажатым Shift всё равно срывается.
**Что уже точно известно (и не надо перепроверять).**
1. **Физика прыжка у нас совпадает с оригиналом кадр в кадр.** Сверено по
логу SDLPoP против трассы харнесса при одинаковом старте `x=95`:
```
кадр 16 18 22 23 24 25 102 103 104 105
SDLPoP 95 97 105 112 121 126 128 130 131 133
наш 95 97 105 112 121 126 128 130 131 133
```
Совпадает и по `y`, и по колонке/ряду, и по приземлению на 107–108.
2. **В оригинале зацеп срабатывает на кадре 106, а не 102..105.**
`check_grab` зовётся из ДВУХ мест: ветка «в воздухе» в `check_action`
(кадры 102..105) и `do_fall` (seg005) для `actions_4_in_freefall`.
Успешная попытка из лога:
```
GRAB try f=106 x=135 y=166 col=4 row=2 fall_y=18
GRAB probe x=127 col=4 through=0 front_above=3 modif=0
GRAB can_grab=1
GRAB OK dist=9
-> f=91 x=136 y=181 col=5 row=2 act=2 (повис)
```
Наш `do_fall` (`pop_map.c`) `check_grab()` из этой ветки тоже зовёт —
то есть структура на месте, расходится что-то внутри.
3. **По харнессу зацеп у нас РАБОТАЕТ**: окно стартовых `x = 91…95`, и
короткий шаг ставит Кида ровно туда (91 после первого нажатия, 95 после
второго). Зафиксировано тестом `t_grab`.
**Отсюда главный вопрос был: почему харнесс говорит «работает», а живая
машина — «нет».** Расхождение между ними и оказалось уликой; версии
выдвигались по убыванию правдоподобия, и сработала первая:
- **Shift не доезжает до движка — ПОДТВЕРЖДЕНО, это и была причина.**
Харнесс подменяет клавиатуру и потому этот путь не проверяет вовсе, а у
нас есть история проблем ровно с «Shift + стрелки» (KBD-1, BUG-KBD-3/4).
Замер в MAME: при зажатом Shift и зажатой стрелке бит `LSh` в
`_kbdraw_down` стоял в нуле. Разбор — [BUG-KBD-5](bug_closed.md#bug-kbd-5).
- **Сцена харнесса не равна комнате 9.** Там изолированная комната
(соседи — стена), а в игре слева комната 8; кромки шва участвуют в
`get_tile`. Проверять чтением `Kid.x` в момент прыжка: попал ли он в
окно 91…95 вообще.
- **Расхождение в `check_grab`.** Наш вариант зовёт `determine_col()`
там, где оригинал зовёт `load_fram_det_col()` (перезагрузка кадра +
колонка). Для Кида это обычно одно и то же (`cur_frame` в фазе физики
принадлежит ему), но проверить стоит.
**Инструменты готовы.** В SDLPoP включена отладка (пометка `DBG-GRAB`):
`JMP` — покадровая трасса прыжка/падения/виса, `GRAB try|probe|fail|OK`
вход в `check_grab` и причина отказа. Снимается поиском по `DBG-GRAB`.
**Найдено попутно, отдельным наблюдением.** После касания площадки на
кадрах 107–108 (x=140) оба движка снова падают, но X расходится: SDLPoP
уводит Кида на 134 (колонка 4), мы — на 141 (колонка 5). Похоже на разную
отработку `in_wall()` у стены (2,7). На зацеп не влияет.
---
<a id="bug-gatemod-1"></a>
## BUG-GATEMOD-1. Ворота стартуют закрытыми, хотя в уровне открыты — **ФИКС ЕСТЬ, ЖДЁТ ПРОВЕРКИ**
**Симптом (пользователь, 2026-08-04).** Уровень 2, комната 13: решётка
между (2,5) и (2,6) обязана быть ОТКРЫТА в начале и захлопнуться, когда Кид
нажмёт кнопку (2,4) — после этого назад дороги нет. У нас она закрыта
сразу, кнопка бессмысленна, проход не работает.
**Корень.** Модификатор тайла в ФАЙЛЕ уровня и модификатор в РАНТАЙМЕ —
разные величины; оригинал переводит их при загрузке в `load_alter_mod`
(seg008:198E), которую зовёт `alter_mods_allrm` из `load_level`:
```c
case tiles_4_gate: *modif = (*modif == 1) ? 188 : 0; break;
case tiles_11_loose:*modif = 0; break;
case tiles_10_potion:*modif <<= 3; break;
```
Наш `pop_trob_modif` портировал из неё **только зелье**. Для ворот
`bg = 1` — это «открыты» (Table 8 спецификации DAT), а в рантайме открытость
измеряется высотой подъёма 0..188; мы клали в рантайм-модификатор сырую
единицу, то есть «закрыты на 1/188».
**Фикс.** Ветки ворот и loose дописаны в ленивую инициализацию
`pop_trob_modif` (`pop_trob.c`). Ветка СТЕН не портируется намеренно: у нас
`pop_bg` считает связи кладки по типам соседей прямо при отрисовке
(`wall_modifier`), сохранённый модификатор стены не читается.
**Что это ещё задевает.** Решётка (0,9) комнаты 5 уровня 1 тоже имеет
`bg = 1`, то есть обязана стартовать открытой — Кид сваливается в комнату 1
именно через неё, и она захлопывается у него за спиной. Закрывает её
стартовый триггер `do_startpos` (seg003:167): для уровней с
`tbl_entry_pose == 1` оригинал ВИРТУАЛЬНО ЖМЁТ кнопку комнаты 5 (0,2) —
```c
// Special event: press button + falling entry
get_tile(5, 2, 0); trigger_button(0, 0, -1); seqtbl_offset_char(seq_7_fall);
```
Замер в SDLPoP (лог по кадрам): `gate(5,0,9)` идёт `188 → 148 → 88 → 8 → 0`,
шаги 40/60/80 — это `gate_close_speeds`, то есть быстрое закрытие
(`trigger_gate` вернул тип 3). У нас этот триггер портирован, и закрытие
работает.
Полный список ворот с `bg = 1`: ур. 1 комн. 5 (0,9); ур. 2 комн. 13 (1,5).
Остальные ворота уровней 1–3 имеют `bg = 2` → 0, и для них ничего не
меняется (при модификаторе 2 и 0 и отрисовка, и `can_bump_into_gate` дают
одно и то же).
**Побочная находка: чит обхода комнат отматывал мир.** После фикса
пользователь увидел «ворота снова открылись», пройдя `+` в комнату 2 и `-`
обратно. Причина не в воротах: `ROOMNAV` звал `pop_trob_reset()` перед
`enter_room`, тот обнулял `room_seen[]`, и `pop_trob_modif()` перечитывал
модификаторы из уровня заново — то есть чит откатывал открытые/закрытые
ворота, выдвинутые пики и нажатые кнопки. Пока ворота с `bg=1` ошибочно
стартовали закрытыми, откат был не виден. `pop_trob_reset()` из навигации
убран: она обязана только телепортировать, исходное состояние даёт
перезапуск уровня. Замер, который это показал: `room_modif` комнаты 5
после `+`/`-` = `00 00 0B 00 09 00 08 01 00 BC` — последний байт 0xBC = 188,
файловое значение.
---
<a id="ручная-перепроверка-2026-08-03"></a>
# Ручная перепроверка фиксов (2026-08-03)
+6 -2
View File
@@ -136,11 +136,15 @@ static uint8_t get_item_action(void)
return 0;
}
/* run_jump (seg005:0AA8). Выравнивание по кромке пола живёт в pop_map
* (там тайловые запросы) pop_run_jump_align; здесь только диспетчерская
* часть. Отказ выравнивателя = прыжка в этом кадре НЕТ, и control_up
* гасить нельзя: Кид бежит дальше с зажатой «вверх» и попробует снова на
* следующем кадре. Ровно так игрок и «ловит» фазу перед провалом. */
static void run_jump(void)
{
/* Оригинал выравнивает Kid по краю пола (нужны tile-запросы) — это
* полировка K3; K2b просто запускает run-jump. */
if (Char.frame >= FRAME_7_RUN) {
if (!pop_run_jump_align()) return;
control_up = release_arrows();
seqtbl_offset_char(SEQ_4_RUN_JUMP);
}
+65
View File
@@ -82,6 +82,7 @@
#define SEQ_51_SPIKED 51 /* напороться на пики (смерть) */
#define SEQ_22_CRUSHED 22 /* разбиться при падении (смерть) */
#define SEQ_63_ACTIVE_AFTER_FALL 63 /* стойка С МЕЧОМ после приземления */
#define SEQ_81_FIGHTFALL 81 /* fightfall: падение из боевой стойки */
#define TILE_FLOOR 1
#define FRAME_109_CROUCH 109
@@ -522,11 +523,34 @@ static void land(void)
static void start_fall(void)
{
uint8_t frame = Kid.frame, seq, tile;
/* seg006:1044 первым делом убирает меч в ножны: дальше падением рулит
* не боевой диспетчер, а обычный. Без этого Kid летит «с клинком», и
* control_with_sword разбирает кадры падения как боевые. */
Kid.sword = SWORD_0_SHEATHED;
inc_curr_row();
/* start_chompers() — чомперов ещё нет (L3-CHOMP). */
/* seg006:1044 start_fall: frame 9 -> seq_7, 13 -> seq_19 (лишний dx(1)) */
if (frame == 13) seq = SEQ_19_FALL;
else if (frame == 26) seq = SEQ_18_FALL_STANDJUMP;
else if (frame == 44) seq = SEQ_21_FALL_RUNJUMP;
else if (frame >= 81 && frame < 86) {
/* сорвался, приземляясь после прыжка вверх: сдвиг ВПЕРЁД на 5 */
seq = SEQ_19_FALL;
Kid.x = (uint8_t)char_dx_forward(5);
pop_load_fram_det_col();
}
else if (frame >= 150 && frame < 180) {
/* Кадры С МЕЧОМ (150..179) — отступил в провал. У оригинала это
* ОТДЕЛЬНАЯ последовательность seq_81 (fightfall): падение строго
* вниз (set_fall(0,15)), а не seq_7 с дрейфом fall_x=1 из-за
* дрейфа наш Kid уезжал на ~тайл в сторону за время падения.
* Плюс сдвиг на 5 назад, если стоит у самой кромки лицом влево.
* Ветка стража (seq_82/83) не нужна: start_fall только Kid.
* TODO: droppedout=1 (guard_follows_kid_down, guards.c:296). */
if (Kid.direction < 0 && distance_to_edge_weight() <= 7)
Kid.x = (uint8_t)char_dx_forward(-5);
seq = SEQ_81_FIGHTFALL;
}
else seq = SEQ_7_FALL; /* frame 9 + stand/step/crouch */
kid_set_seq(seq);
pop_kid_play();
@@ -760,6 +784,47 @@ uint8_t pop_jump_up_seq(void) __banked
return jump_up_plain();
}
/* run_jump (seg005:0AA8), часть «выровнять по кромке» ------------------ *
* Оригинал НЕ отталкивается откуда попало: перед разбег-прыжком он смотрит
* на 12 тайла вперёд и, если там провал (или пика), подгоняет X так, чтобы
* толчок пришёлся ровно на кромку пола. Иначе прыжок стартует в случайной
* фазе бегового цикла а запаса у seq_4 почти нет: суммарный dx кадров
* 34..44 равен 62 px при ширине тайла 14, то есть ровно 4 колонки и меньше
* половины тайла сверху. Поэтому без выравнивания провал в ТРИ пустых
* тайла (перелёт с колонки 5 на колонку 1) становится непроходимым
* BUG-RJUMP-1, уровень 2, комнаты 1 и 9.
*
* Возврат: 1 прыгать (Kid.x уже подогнан), 0 прыжок ОТМЕНИТЬ. Отмена
* в оригинале НЕ гасит control_up: Кид бежит дальше с зажатой «вверх», и на
* следующем кадре попытка повторяется так игрок ловит нужную фазу, просто
* удерживая клавишу. */
uint8_t pop_run_jump_align(void) __banked
{
int xpos = char_dx_forward(4);
int8_t col = get_tile_div_mod_m7(xpos);
uint8_t tf;
for (tf = 0; tf < 2; tf++) {
uint8_t t;
col = (int8_t)(col + dir_front[Kid.direction + 1]);
t = get_tile(col, Kid.curr_row);
if (t != TILE_SPIKE && tile_is_floor(t)) continue; /* тут пол — дальше */
{
int adj = distance_to_edge(xpos) + TILE_SIZEX * (int)tf - TILE_SIZEX;
/* Оригинал сравнивает БЕЗЗНАКОВО:
* if ((word)adj < (word)-8 || adj >= 2) { if (adj < 128) return; adj = -3; }
* что означает ровно «adj НЕ попал в [-8,-1] прыжка нет».
* Ветка с adj = -3 недостижима: distance_to_edge [0,13] и
* tf {0,1} дают adj [-14,13], а туда нужно adj >= 128. */
if (adj < -8 || adj > -1) return 0;
Kid.x = (uint8_t)char_dx_forward((int8_t)(adj + 4));
}
break;
}
return 1;
}
/* Стена впереди достаточно близко для стопа бега? Детект (без сдвига —
* позицию держит check_bumped=in_wall). d<=2: передний край почти у грани. */
int pop_wall_ahead(void) __banked
+7
View File
@@ -154,6 +154,13 @@ uint8_t pop_edge_type(void) __banked;
* control_running (чистый стоп у стены). */
int pop_wall_ahead(void) __banked;
/* Разбег-прыжок: выравнивание по кромке пола (run_jump, seg005:0AA8).
* Смотрит на 12 тайла вперёд; если там провал/пика подгоняет Kid.x под
* толчок ровно с кромки. 1 = прыгать, 0 = ОТМЕНИТЬ прыжок (Кид не в той
* фазе относительно кромки; control_up гасить НЕЛЬЗЯ попытка повторится
* на следующем кадре). Без этого провал в три тайла непроходим. */
uint8_t pop_run_jump_align(void) __banked;
/* Прыжок вверх в стойке (↑): seq чистого прыжка (seq_28/seq_14) ЛИБО
* прыжка-с-зацепом за уступ выше (seq_8/24/16) + выравнивание Kid.x.
* check_jump_up/grab_up (K4.4). control_up гасит caller. */
+20
View File
@@ -23,3 +23,23 @@ uint8_t pop_immortal;
* clip_char из pop_map. См. pop_state.h. */
int pop_leveldoor_right;
int pop_leveldoor_ybottom;
/* ---- Отладочная «пустышка» для брейкпоинтов из БАНКОВ ---------------- *
* Зачем. PC-брейкпоинт видит только логический адрес, а 0xC000+ это
* окно, куда мапятся ВСЕ банки: точка на адресе банковой функции ловит
* заодно чужой код, случайно легший по тому же смещению (проверено:
* точка на start_fall из банка 3 срабатывала на каждом кадре попадала
* в pop_bg из банка 2). Условные брейкпоинты этот отладчик MAME не
* поддерживает («error in assignment expression» на `==`).
*
* Приём (идея пользователя, 2026-08-04): позвать ЭТУ функцию ровно из
* того места банкового кода, которое отлаживаем, и поставить брейкпоинт
* на неё она резидентна в W1, её адрес однозначен. По возврату из неё
* читаем что нужно. Условие «когда именно ловить» пишется обычным `if`
* в C это гибче любых выражений отладчика.
*
* Счётчик нужен, чтобы вызов не выкинул оптимизатор; заодно видно, сколько
* раз точка прошла, если ловим не останавливаясь. */
uint8_t pop_dbg_hits;
void pop_dbg_trap(void) { pop_dbg_hits++; }
+6
View File
@@ -29,4 +29,10 @@ extern uint8_t pop_immortal;
extern int pop_leveldoor_right;
extern int pop_leveldoor_ybottom;
/* Отладочная «пустышка» для брейкпоинтов из банков — см. pop_state.c.
* Звать из отлаживаемого места под нужным `if`, брейкпоинт ставить на
* _pop_dbg_trap (резидент W1, адрес однозначен). */
extern uint8_t pop_dbg_hits;
void pop_dbg_trap(void);
#endif
+25 -5
View File
@@ -21,6 +21,7 @@
#define TILE_DEBRIS 0x0E /* button_type для «насовсем открыть» (died_on_button) */
#define TILE_OPENER 0x0F
#define TILE_POTION 0x0A
#define TILE_LOOSE 0x0B /* проваливающийся пол (фаза тряски в modif) */
#define TILE_SWORD 0x16 /* меч-предмет на полу */
#define TILE_LEVELDOOR 0x10 /* левая половина двери уровня (на ней modif) */
#define TILE_TORCH 0x13
@@ -67,12 +68,31 @@ uint8_t *pop_trob_modif(uint8_t room)
if (!room_seen[room - 1]) {
uint8_t i;
pop_level_room_bg(room, room_modif[room - 1]);
/* seg009 load_level: у ЗЕЛЬЯ модификатор сдвигается влево на 3 —
* старшие биты = тип зелья, младшие 3 = фаза пузырька. */
/* load_alter_mod (seg008:198E): модификатор В ФАЙЛЕ уровня и
* модификатор В РАНТАЙМЕ разные величины, оригинал переводит их
* при загрузке. Порт того, что нас касается:
*
* ворота 1 = «открыты» (Table 8 спецификации DAT) -> 188, то есть
* рабочая высота подъёма; ЛЮБОЕ другое значение -> 0
* (закрыты). Без этого перевода ворота, которые в
* уровне открыты, стартуют закрытыми, и кнопка-closer
* рядом с ними теряет смысл (ур. 2, комн. 13).
* loose -> 0: фаза тряски всегда начинается с нуля.
* зелье -> <<3: старшие биты = тип, младшие 3 = фаза пузырька.
*
* Ветка стен (blue-line/связи кладки) НЕ портируется намеренно: у
* нас pop_bg считает связи по типам СОСЕДЕЙ прямо при отрисовке
* (wall_modifier), сохранённый модификатор стены не читается вовсе. */
pop_level_access_begin();
for (i = 0; i < POP_ROOMTILES; i++)
if ((pop_level_tile_raw(room, i) & 0x1F) == TILE_POTION)
room_modif[room - 1][i] = (uint8_t)(room_modif[room - 1][i] << 3);
for (i = 0; i < POP_ROOMTILES; i++) {
uint8_t *m = &room_modif[room - 1][i];
switch (pop_level_tile_raw(room, i) & 0x1F) {
case TILE_GATE: *m = (uint8_t)((*m == 1) ? 188 : 0); break;
case TILE_LOOSE: *m = 0; break;
case TILE_POTION: *m = (uint8_t)(*m << 3); break;
default: break;
}
}
pop_level_access_end();
room_seen[room - 1] = 1;
}