diff --git a/applications/PoP/roomtest/TASKS.md b/applications/PoP/roomtest/TASKS.md
index 75f83df..1ccfcfc 100644
--- a/applications/PoP/roomtest/TASKS.md
+++ b/applications/PoP/roomtest/TASKS.md
@@ -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). То есть цепочка
---
+### 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`) — геймплей не блокирует.
diff --git a/applications/PoP/roomtest/bug_closed.md b/applications/PoP/roomtest/bug_closed.md
index ca1e3b2..0763d10 100644
--- a/applications/PoP/roomtest/bug_closed.md
+++ b/applications/PoP/roomtest/bug_closed.md
@@ -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 (вечер)
+
+## 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`, то есть
+расширенную половину), но если всплывёт — искать здесь.
+
+---
+
-## 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 второй-третий-четвёртый тап стрелки отрабатывал уже не
diff --git a/applications/PoP/roomtest/bug_list.md b/applications/PoP/roomtest/bug_list.md
index c41a416..efbbeb2 100644
--- a/applications/PoP/roomtest/bug_list.md
+++ b/applications/PoP/roomtest/bug_list.md
@@ -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)**; если артефакт рядом с ними, это первый подозреваемый.
-*(записей пока нет)*
+
+## 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). На зацеп не влияет.
---
+
+## 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,
+файловое значение.
+
+---
+
+
# Ручная перепроверка фиксов (2026-08-03)
diff --git a/applications/PoP/roomtest/pop_ctrl.c b/applications/PoP/roomtest/pop_ctrl.c
index 0c03e42..ba4208a 100644
--- a/applications/PoP/roomtest/pop_ctrl.c
+++ b/applications/PoP/roomtest/pop_ctrl.c
@@ -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);
}
diff --git a/applications/PoP/roomtest/pop_map.c b/applications/PoP/roomtest/pop_map.c
index b5b361e..0064112 100644
--- a/applications/PoP/roomtest/pop_map.c
+++ b/applications/PoP/roomtest/pop_map.c
@@ -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), часть «выровнять по кромке» ------------------ *
+ * Оригинал НЕ отталкивается откуда попало: перед разбег-прыжком он смотрит
+ * на 1–2 тайла вперёд и, если там провал (или пика), подгоняет 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
diff --git a/applications/PoP/roomtest/pop_map.h b/applications/PoP/roomtest/pop_map.h
index 606cd5b..5e91f3b 100644
--- a/applications/PoP/roomtest/pop_map.h
+++ b/applications/PoP/roomtest/pop_map.h
@@ -154,6 +154,13 @@ uint8_t pop_edge_type(void) __banked;
* control_running (чистый стоп у стены). */
int pop_wall_ahead(void) __banked;
+/* Разбег-прыжок: выравнивание по кромке пола (run_jump, seg005:0AA8).
+ * Смотрит на 1–2 тайла вперёд; если там провал/пика — подгоняет 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. */
diff --git a/applications/PoP/roomtest/pop_state.c b/applications/PoP/roomtest/pop_state.c
index fae7ec3..85c401e 100644
--- a/applications/PoP/roomtest/pop_state.c
+++ b/applications/PoP/roomtest/pop_state.c
@@ -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++; }
diff --git a/applications/PoP/roomtest/pop_state.h b/applications/PoP/roomtest/pop_state.h
index 239cb46..1660c13 100644
--- a/applications/PoP/roomtest/pop_state.h
+++ b/applications/PoP/roomtest/pop_state.h
@@ -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
diff --git a/applications/PoP/roomtest/pop_trob.c b/applications/PoP/roomtest/pop_trob.c
index f741df7..42fda36 100644
--- a/applications/PoP/roomtest/pop_trob.c
+++ b/applications/PoP/roomtest/pop_trob.c
@@ -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;
}