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; }