400f5cba63
В записи стояло, будто оригинал играет звук 11 «сразу на кадре укола, ДО проверки расстояния», то есть при ЛЮБОМ уколе, попал тот или нет. Это было описание НАШЕГО кода, а не SDLPoP: ровно так звук у нас и стоял — внутри ветки «не парировано» и до присвоения Opp.action = 99_hurt (снято предыдущим коммитом). Классическое нарушение правила «источник истины — SDLPoP»: за оригинал приняли собственную реализацию, и вывод из неё уехал в доску как факт. На деле звук 11 в оригинале защищён условием Opp.action != 99_hurt, то есть звучит ТОЛЬКО на промахе: на попадании играет боль, на парировании Opp.frame уже 161. Что это меняет для самого бага: - УЦЕЛЕЛО наблюдение «в оригинале 11, у нас 8» — это данные. Арифметика приоритетов их подкрепляет: snd_prio[11] = 0x12 против snd_prio[8] = 0x4B (меньше значит важнее), взмах не мог быть заглушён упором в стену. - ОТПАЛ вывод «оригинал ведёт стража атакой с промахом, а мы — столкновением»: он опирался на неверную посылку. - УСИЛИЛАСЬ версия про ПОРОГ ДИСТАНЦИИ: раз в оригинале слышен именно 11, укол у стража СОСТОЯЛСЯ и ПРОМАЗАЛ. Прежняя «версия про столкновение» вытеснила дистанцию зря — возвращаем её в главные подозреваемые. Отдельно предупреждение тому, кто вернётся к багу: диагностика по звуку теперь значит другое, старые заметки прогона будут вводить в заблуждение, сцену надо переснимать. Положена таблица соответствий (11 = промах, 13 = попадание, 8 = столкновение, тишина = ранний выход из check_hurting). На механику урона правки не влияют — расхождение «удар убивает» остаётся открытым как было. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018T2k4VZrSyERwk6H97Sfi1
1485 lines
117 KiB
Markdown
1485 lines
117 KiB
Markdown
# SprPoP — ОТКРЫТЫЕ баги и незакрытые оптимизации
|
||
|
||
**Здесь ТОЛЬКО незакрытое.** Всё закрытое (и, что важнее, разбор корней)
|
||
живёт в [`BUGS_CLOSED.md`](../../PoP/roomtest/BUGS_CLOSED.md) — прежде чем заводить новый баг,
|
||
грепни там по симптому. Сырые формулировки пользователя с прогонов —
|
||
[`bugs_level1.md`](bugs_level1.md) / [`bugs_level2.md`](bugs_level2.md).
|
||
|
||
Приоритеты работ — в [`TASKS_OPEN.md`](TASKS_OPEN.md) (закрытые задачи с
|
||
протоколами — [`TASKS_CLOSED.md`](../../PoP/roomtest/TASKS_CLOSED.md)), а не здесь. Правило
|
||
проекта: механику сверять с `../SDLPoP/src/` ДО кодинга.
|
||
|
||
**Ревизия 2026-08-11:** файл вычищен от закрытых записей (правило «в `_OPEN`
|
||
только открытое»). Уровни 1-4 приняты smoke-тестами; крупных багов нет.
|
||
|
||
| ID | что | тип | статус |
|
||
|----|-----|-----|--------|
|
||
| [SPRITE-ZERO-W0](#sprite-zero-w0) | кадр персонажа изредка отдаёт спрайт 0x0 (залипание уже вылечено) | **редкий** | открыт: нужна трасса маппинга W0 |
|
||
| [GATE-FORE-KID](#gate-fore-kid) | Кид в проёме ворот виден поверх решётки | окклюзия | **фикс есть, ждёт проверки в MAME** (2026-08-13) |
|
||
| [FORE-DUP](#fore-dup) | передний слой тайла рисуется дважды при перекрытии объектов | оптимизация | открыт: **сначала замерить**, потом чинить |
|
||
| [TORCH-ANIM-RIGHT](#torch-anim-right) | под запечённым пламенем застывают не только челюсти чомпера | **низкий** | открыт: на уровнях 1-4 такого соседства нет |
|
||
| [T-1](#t-1) | пики перерисовываются безусловно | оптимизация | открыт |
|
||
| [BUG-SPIKE-1](#bug-spike-1) | пики залипают выдвинутыми рядом с Кидом | **низкий** | маловоспроизводим: ни сценарием, ни попиксельной подгонкой X не поднимается |
|
||
| [BUG-CHOMP-JUMP-1](#bug-chomp-jump-1) | прыжок с места вплотную к чомперу: кадр с отступом назад | **низкий** | маловоспроизводим: на повторе не поднялся; на пререлиз |
|
||
| [GRAB-KBD-TIMING](#grab-kbd-timing) | зацеп в падении срабатывает менее стабильно, чем в оригинале | **после всех уровней** | подозрение на клавиатуру ПОДТВЕРДИЛОСЬ частично: кража байт DSS устранена 2026-08-28, перепроверить |
|
||
| [GUARD-ENTRY-DEATH](#guard-entry-death) | вход в комнату вплотную к стражу = мгновенная смерть (не портирован `bump_into_opponent`) | порт | **фикс есть, ждёт проверки в MAME** |
|
||
| [BUG-SHADOW-SET](#bug-shadow-set) | Тень на 12-м уровне дерётся спрайтами СТРАЖА: SHADOW.DAT у нас нет вовсе | порт | открыт, разбор готов |
|
||
| [PAL-L1-AFTER-INTRO](#pal-l1-after-intro) | вход в игру на уровень 1 после интро — игровая палитра не установлена (чёрный экран) | палитра/fade | открыт: воспроизведение нестабильно, корень не установлен |
|
||
| [PAL-DUNGEON-STALE](#pal-dungeon-stale) | переход 3→4 (подземелье→дворец): остаётся палитра подземелья | палитра/fade | открыт: КОРЕНЬ ЯСЕН — fade_in восстанавливает из протухшего снимка |
|
||
| [BUG-CORPSE-AIR](#bug-corpse-air) | труп Кида висит в воздухе, когда убившая его плита улетела вниз | физика | открыт |
|
||
| [LOOSE-ROOM-CHANGE](#loose-room-change) | расшатанная плита не доваливается, если Кид ушёл в соседнюю комнату | порт | закрыт 2026-08-13, кроме [LOOSE-SEAM-ANIM](#loose-seam-anim) |
|
||
| [JUMP-FLOOR-LEGS](#jump-floor-legs) | в прыжке с пола ноги Кида рисуются ПОВЕРХ кромки пола, а должны за ней | окклюзия | **корень найден и починен, ждёт проверки в MAME** |
|
||
| [MOB-CLIP-RIGHT](#mob-clip-right) | окна и узор кладки рисуются ПОВЕРХ падающих плит | окклюзия | открыт: корень найден 2026-08-13 |
|
||
| [MID-OVERLAY-LAYER](#mid-overlay-layer) | оверлей кромки всегда поверх плит и персонажей (у оригинала — midtable с сортировкой) | слои | открыт: нужен разбор архитектуры |
|
||
| [GUARD-FALLOUT-VICTORY](#guard-fallout-victory) | страж, выпавший из комнаты, не засчитывается убитым | порт | открыт: две строки |
|
||
| [KBD-STUCK-WAIT](#kbd-stuck-wait) | потерянный break-код клавиши вешает игру НАСМЕРТЬ в межуровневой заставке | клавиатура/клин | **корень найден и устранён** 2026-08-28 (байты крал KEYSCAN DSS); ждёт полевой проверки |
|
||
| [L10-BUTTON-DEBRIS](#l10-button-debris) | ур.10 к.1: плита упала на кнопку (1,8) — щебень есть, а решётки закрыты | кнопки/ворота | открыт: **не воспроизводится**, проверено многое (см. разбор) |
|
||
| [LOOSE-SEAM-ANIM](#loose-seam-anim) | плита в шве (col −1), уроненная в прошлой комнате, исчезает без анимации падения | **мелкий** | открыт по решению пользователя: «пока пусть будет так» |
|
||
| [SND-PACE-DEAD](#snd-pace-dead) | пейсинг сцен по насосу CBL не включается: признак «часы идут» вычисляется двумя чтениями подряд | **тайминг/звук** | **снят 2026-08-28**: ветка удалена, сцена на единых часах по лучу |
|
||
| [PV-RENDER-BOUND](#pv-render-bound) | сцена с принцессой рисуется дороже бюджета: ~49 тиков/с вместо 60, музыка уезжает от картинки | производительность | **исправлено 2026-08-28**: кадр разложен на блоки по интервалу, удешевлять не понадобилось |
|
||
| [MUS-LEFT-TEAR](#mus-left-tear) | `pop_mus_left` (16 бит, пишет прерывание) читается из главного цикла неатомарно — возможен ложный «трек кончился» | **потенциальный** | открыт: хазард показан рассуждением, в прогоне не проявился |
|
||
| [CLIMB-VS-GUARD](#climb-vs-guard) | Кид подтягивается к стражу этажом выше: у нас удар засчитывается и убивает, в оригинале Кид просто срывается без урона; страж при этом способен провалиться сквозь пол вслед за Кидом | бой/физика | открыт: цепочка удара сверена — совпадает, расходятся входные данные; толкование звука ИСПРАВЛЕНО 2026-09-02 (звук 11 = промах, а не «любой укол») — главный подозреваемый снова ПОРОГ ДИСТАНЦИИ |
|
||
| [HP-BAR-RESTART](#hp-bar-restart) | после гибели и Ctrl+A на ОДНОЙ из двух страниц остаётся полоса HP по результатам боя | дабл-буфер/UI | КОРЕНЬ НАЙДЕН, фикс есть, ждёт проверки (2026-08-31) |
|
||
|
||
---
|
||
|
||
<a id="l10-button-debris"></a>
|
||
## L10-BUTTON-DEBRIS. Плита, упавшая на кнопку (ур.10, к.1) — щебень есть, решётки закрыты
|
||
|
||
**Наблюдение (пользователь, 2026-08-24).** Уровень 10, комната 1: Кид стоит
|
||
на кнопке `(1,8)` и роняет на неё плиту `(0,8)`. Кнопка превращается в
|
||
щебень, но решётки, которые она открывает, остаются закрытыми. Важная деталь
|
||
из того же прогона: на 10-й уровень пришли **телепортом Shift+L**, а не
|
||
проходом.
|
||
|
||
**Ожидаемое поведение (оно же оригинал).** Плита, севшая на opener, жмёт его
|
||
ОДИН раз с `button_type = tiles_14_debris`; у `trigger_gate` это отдельная
|
||
ветка «открыть насовсем»: `modifier >= 188` -> сразу `0xFF`, иначе возврат 2 и
|
||
`animate_door` доводит створку до `0xFF`. Ворота с `0xFF` заморожены
|
||
открытыми, обычный opener их больше не трогает. Сама кнопка съедается в
|
||
щебень. Разобрано в [BUG-LOOSE-BUTTON-1](BUGS_CLOSED.md#bug-loose-button-1).
|
||
|
||
**НЕ ВОСПРОИЗВЕЛОСЬ** (2026-08-24, отладочный старт `make LEVEL=10 ROOM=1
|
||
POS=18`, MAME): Кид стоит на кнопке -> решётка `(2,7)` открылась; удар головой
|
||
роняет плиту `(0,8)`; кнопка стала щебнем; **решётка осталась открытой** и
|
||
после ухода Кида с тайла. Пользователь тоже не смог повторить в этой сборке.
|
||
|
||
**Что проверено и совпадает с оригиналом:**
|
||
|
||
* цепочка кнопки — ровно две цели, обе в комнате 1: `(2,9)` и `(2,7)`
|
||
(`bg = 20`, терминатор цепочки — бит 0x80 в LINKLOC, как в seg007:0C09);
|
||
* `trigger_gate` построчно равен оригиналу, включая обе ветки debris;
|
||
* `add_trob` дедуплицирует по (room, tilepos) — как `find_trob` оригинала,
|
||
двойных записей на одни ворота не бывает;
|
||
* `animate_door` гасит анимацию при `modifier == 0xFF` и доводит тип 2 до
|
||
`0xFF`;
|
||
* таймеры связей в файле уровня 10 нулевые (не «jammed» 0x1F);
|
||
* `pop_dl1/pop_dl2` перечитываются из файла при каждой загрузке уровня, а
|
||
`pop_level_reset_tiles` восстанавливает LINKMAP из эталонной страницы.
|
||
|
||
**Живые гипотезы (по убыванию правдоподобия):**
|
||
|
||
1. **Ветка `return 2` (створка ещё не «полностью» открыта).** Визуально
|
||
решётка выглядит открытой уже при `modifier ~ 140` (проходимость считается
|
||
как `(modifier >> 2) + 6 >= высота кадра`), а мгновенный `0xFF` требует
|
||
`modifier >= 188`. То есть глазами «открыто», а по коду — ветка с
|
||
анимацией; её в MAME поймать пока не удалось.
|
||
2. **Накопленное состояние после телепорта Shift+L** через несколько уровней
|
||
(у нас уже был такой класс: [GUARD-DROPPEDOUT-CARRY](BUGS_CLOSED.md#guard-droppedout-carry)).
|
||
Отладочный старт сразу на 10-м даёт чистое состояние — и баг не всплывает.
|
||
3. Кнопка была нажата/отпущена ранее в этой же сессии, и падение пришлось на
|
||
момент, когда trob кнопки уже умер, а таймер связи ещё не истёк.
|
||
|
||
**Как ловить в следующий раз:** как только симптом повторится — сразу
|
||
**QuickSave (F6)**, не выходя из сцены. Сохранение везёт с собой LINKMAP
|
||
(`pop_level_qsave` пишет `pop_dl1/pop_dl2`) и модификаторы комнат, то есть даёт
|
||
ровно то состояние, которое сейчас не удаётся собрать искусственно.
|
||
|
||
---
|
||
|
||
<a id="kbd-stuck-wait"></a>
|
||
## KBD-STUCK-WAIT. Потерянный break-код вешает игру насмерть в межуровневой заставке
|
||
|
||
> **КОРЕНЬ НАЙДЕН И УСТРАНЁН 2026-08-28.** Байты у нас крал сам DSS — см.
|
||
> раздел «Корень» ниже. Правка в `libc/irq/_irq_tramp.c`. Запись остаётся
|
||
> открытой до полевой проверки и решения по страховке-таймауту.
|
||
|
||
## Корень (2026-08-28): скан-коды воровал KEYSCAN DSS
|
||
|
||
Пришёл из чтения исходников DSS (`docs/sources/Estex-DSS/DSS/`, подсказка
|
||
пользователя). Обработчик прерывания DSS живёт в IM1 по `0x0038`
|
||
(`DSS-MAIN.ASM`), и его `INTx38_Handler` ПЕРВЫМ ДЕЛОМ делает `CALL KEYSCAN`,
|
||
а `KEYSCAN.RESCAN` (`KEYINTER.ASM`) вычерпывает FIFO SIO досуха.
|
||
|
||
Наш трамплин проверял «есть ли клавиатурный байт» ОДИН раз, на входе в
|
||
прерывание, а хвост кадрового пути уходил в DSS (`jp 0x0038`). Значит
|
||
скан-код, прилетевший ПОЗЖЕ этой проверки, доставался DSS и уезжал в его
|
||
буфер — при этом **Rx-overrun не взводится**, потому что байт не потерян
|
||
железом, он просто прочитан не тем владельцем. Отсюда и загадка
|
||
исходного диагноза: бит залип, а `_kbdraw_overrun == 0`.
|
||
|
||
**Измерено в MAME (сессия 2026-08-28, брейки + `totalcycles`):**
|
||
|
||
| Что | Значение |
|
||
|---|---|
|
||
| окно от входа трамплина до чтения байта DSS | **738 тактов ≈ 34 мкс**, на КАЖДОМ кадровом прерывании (50/с) |
|
||
| из них пролог самого DSS (`jp 0x38` → `.Handler` → `call INTx38_Handler` → 11 `push` → `call KEYSCAN`) | 481 такт ≈ 22 мкс |
|
||
| цепочка кадровых хендлеров у SprPoP | пуста (`_irq_chain_n = 0`) — окно уже минимальное |
|
||
| расчётная частота на железе при удержании клавиши (typematic 30 Гц = 60 байт/с) | потеря байта раз в ~10 с |
|
||
|
||
Последняя строка сходится с полевым наблюдением пользователя: залипание
|
||
стрелки ловилось на ур. 14, когда Кид долго бежит влево через несколько
|
||
комнат.
|
||
|
||
**Пойманный случай (MAME, старая сборка).** Стоп на `IN A,($18)` внутри
|
||
`KEYSCAN` при `_kbdraw_active = 1`: DSS прочитал `A = 0x74` — make стрелки
|
||
«вправо», при том что `_kbdraw_pending = 2` (EXT), то есть префикс `E0` уже
|
||
разобрали МЫ. Посылка `E0 74` разорвана пополам между двумя владельцами
|
||
канала. Злейший подвид — кража `E0` у break'а `E0 F0 6B`: тогда мы гасим
|
||
бит в PLAIN-половине, а взведённый бит в EXT остаётся навсегда, и это ровно
|
||
залипшая стрелка `0x6B`/`0x72` из исходного разбора.
|
||
|
||
**Правка.** Пока raw-канал открыт, кадровый путь трамплина НЕ вызывает
|
||
обработчик DSS вообще (приватный `RETI` вместо `jp 0x0038`) — окно ровно
|
||
ноль. Промежуточный вариант «проверить FIFO перед chain'ом» пробовали и
|
||
отвергли замером: он снимает лишь свою треть окна (738 → 481 такт), кражи
|
||
продолжались. Цена принятого варианта: пока открыт raw, у DSS замирает
|
||
кадровое обслуживание — опрос мыши и мигание текстового курсора (PoP не
|
||
нужно ни то, ни другое; зафиксировано в `<kbd_raw.h>`).
|
||
|
||
**Проверка (MAME 2026-08-28).** До правки брейк на входе `KEYSCAN` с
|
||
условием «страница точно DSS + наш raw открыт» срабатывал мгновенно (50/с);
|
||
после правки молчит, а положительный контроль — брейк на нашем приватном
|
||
`RETI` — срабатывает сразу. ВАЖНО про методику: первый брейк без проверки
|
||
сигнатуры байтов дал ЛОЖНОЕ срабатывание — в W0 периодически лежит
|
||
драйверная страница DSS, и по адресу `0x0570` там своя команда.
|
||
|
||
**Что осталось открытым.** Кража устранена, но потерять break всё ещё
|
||
может настоящий Rx-overrun SIO (редкий: FIFO вычерпывает `kbd_raw_poll` из
|
||
главного цикла). Значит вариант A из таблицы ниже — таймаут в циклах
|
||
«ждём отпускания всех клавиш» — остаётся разумной страховкой от КЛАССА
|
||
отказа «вечный клин», и его стоит сделать отдельно.
|
||
|
||
## Исходный разбор (2026-08-24)
|
||
|
||
**Наблюдение (пользователь, 2026-08-24).** Нажат Shift+L (чит «следующий
|
||
уровень») — игра встала намертво: картинка прежнего уровня, реакции нет.
|
||
|
||
**Диагноз (снят в живой сессии MAME).** Программа НЕ рухнула и не потеряла
|
||
кадровые прерывания — IRQ приходили штатно (вход по вектору виден в
|
||
`history`). Стояли в `pop_pre_cutscene_show` (pop_intro.c:949), в его первом
|
||
цикле:
|
||
|
||
```c
|
||
while (kbd_raw_any_down()) { kbd_raw_sync(); gfx_wait_vsync(); }
|
||
```
|
||
|
||
Цикл намеренный: клавиша, которой закончили уровень (тот самый Shift+L), не
|
||
должна тут же сойти за «пропустить заставку». Внутри — `gfx_wait_vsync`
|
||
(опрос бита 5 порта #FE), он отрабатывал нормально; не выходил ВНЕШНИЙ цикл.
|
||
|
||
Причина — залипший бит в `_kbdraw_down[]`: код `0x72` в PLAIN-половине карты
|
||
(то есть **keypad 2**; стрелка Down — это `E0 72` и живёт в EXT-половине,
|
||
байт 46). Состояние на момент разбора: `_kbdraw_active = 1`,
|
||
`_kbdraw_pending = 0`, `_kbdraw_overrun = 0` — то есть штатная страховка по
|
||
Rx-overrun не срабатывала. Гашение бита отладчиком (`_kbdraw_down[14] = 0`)
|
||
сразу вернуло игру к жизни: заставка прошла, уровень 2 загрузился.
|
||
|
||
**Чем бит может залипнуть** (разбор `libc/irq/_irq_tramp.c`):
|
||
|
||
1. **Потерянный break при переполнении FIFO SIO.** Приёмник глубиной 3
|
||
байта; когда прерывания запрещены надолго (загрузка уровня через ESTEX —
|
||
ровно момент Shift+L), пачка `F0 code` не помещается. Защита есть
|
||
(чтение RR1 → `_kbdraw_overrun` → `kbd_raw_sync()` снимает held), но она
|
||
срабатывает, только если SIO успел выставить сам бит overrun.
|
||
2. **Разъехавшийся префиксный автомат.** `F0` (break) и `E0` (ext) — отдельные
|
||
байты, состояние копится в `_kbdraw_pending`; любой сброс между префиксом
|
||
и кодом переворачивает смысл, и break декодируется как MAKE. Ветка
|
||
«fake shift» (`E0 12` / `E0 59`) делает это по построению — она гасит
|
||
`pending` и пишет код как make даже у break-формы.
|
||
3. **Make и break у разных владельцев канала.** Пока `_kbdraw_active == 0`,
|
||
байты уходят DSS. Клавиша, нажатая до `kbd_raw_open()` (или в момент
|
||
close/open), даёт make одному владельцу, а break — другому: снимать
|
||
нечего, бит остаётся.
|
||
4. **Держащий вход эмулятора.** `set_input ... frames=0` из MCP-моста держит
|
||
клавишу нажатой, typematic шлёт make повторно (наблюдалось: погашенный
|
||
отладчиком бит возвращался). Это артефакт отладки, а не игры, но именно
|
||
он мог создать исходное состояние в этой сессии.
|
||
|
||
**Варианты решения.**
|
||
|
||
| Вариант | За | Против |
|
||
|---------|----|--------|
|
||
| **A. Таймаут в циклах ожидания** (ждать отпускания не дольше ~1 с, дальше идти) | Дёшево, локально; снимает КЛАСС отказа «вечный клин» независимо от причины; заставка от лишней секунды не страдает | Лечит симптом, а не причину: залипшая клавиша дальше продолжит «нажиматься» в gameplay |
|
||
| **B. Анти-залипание в `kbd_raw`** (гасить биты, которые давно не подтверждались typematic-повтором) | Чинит класс причин целиком, помогает и в gameplay | **Модификаторы typematic НЕ повторяют** (обжигались: `kbd_overrun_wipe_modifiers`) — значит Shift/Ctrl гасить нельзя, нужен список исключений; плюс хранение «свежести» на 512 кодов дорого, придётся огрублять |
|
||
| **C. Ужесточить страховку по overrun** (чистить held не только по биту RR1, но и по «подозрительным» ситуациям: длинный DI, возврат из загрузки) | Бьёт по самой вероятной причине (1); дёшево по памяти | Эвристика: можно снять РЕАЛЬНО зажатую клавишу (Кид перестанет бежать посреди разбега) |
|
||
| **D. Ничего, считать артефактом отладки** | Ноль работы | Если это не мост, а SIO, то на железе получаем намертво зависшую игру — худший класс отказа |
|
||
|
||
**Если возвращаться — начинать с точной причины:** лог байтов SIO в
|
||
трамплине (кольцо на 32-64 байта + дамп по клавише) и повтор сценария
|
||
«Shift+L во время дисковой загрузки». Без этого выбор между B и C — гадание.
|
||
|
||
**Смежное:** `kbd_raw_fifo_drain`, `kbd_overrun_wipe_modifiers` в memory —
|
||
там уже закрытые случаи залипания того же битмапа.
|
||
|
||
---
|
||
|
||
<a id="grab-kbd-timing"></a>
|
||
## GRAB-KBD-TIMING. Зацеп в падении срабатывает НЕ ТАК СТАБИЛЬНО, как в оригинале — РАЗБОР ПОСЛЕ ВСЕХ УРОВНЕЙ
|
||
|
||
> **Решение пользователя (2026-08-12): отложено до готовности всех уровней.**
|
||
> Механика работает, это вопрос ощущения, а не проходимости.
|
||
|
||
> **Обновление 2026-08-28.** Гипотеза «дело в клавиатурном модуле, а не в
|
||
> физике» подтвердилась ЧАСТИЧНО: найдена и устранена кража скан-кодов
|
||
> обработчиком DSS (разбор — [KBD-STUCK-WAIT](#kbd-stuck-wait)). Украденный
|
||
> make Shift'а или стрелки — это ровно «нажал, а не сработало», и на
|
||
> железе при удержании клавиши такое случалось раз в ~10 с. Перепроверить
|
||
> ощущение зацепа на новой сборке ДО того, как лезть в физику.
|
||
|
||
**Наблюдение (пользователь).** Вход на уровень 7: Кид влетает в комнату сверху
|
||
и, пролетая мимо края пола, МОЖЕТ за него зацепиться — но получается заметно
|
||
реже, чем в оригинале. «Похоже, это проблемы нашего клавиатурного модуля».
|
||
|
||
**Что уже сделано и почему это НЕ закрывает вопрос.** В ходе разбора
|
||
[GRAB-BELOW-ROOM](BUGS_CLOSED.md#grab-below-room) восстановлена ветка
|
||
`ACT_MIDAIR` в `check_action` (кадры 102..105 — начало падения, где `fall_y` ещё
|
||
не разогнан): раньше на этих четырёх кадрах `check_grab` не звался вовсе, то
|
||
есть окно зацепа было короче оригинального. Это должно было добавить
|
||
стабильности, но саму гипотезу про клавиатуру не проверяет.
|
||
|
||
**Куда смотреть, когда дойдут руки.** Порядок именно такой — сначала
|
||
измерить, потом чинить:
|
||
|
||
1. Снять, ЧТО видит движок в момент падения: `pop_ctrl_shift_held()` покадрово
|
||
(Shift зажат заранее — читается ли он каждый кадр, или FIFO SIO отдаёт
|
||
состояние с пропусками; см. memory `kbd_raw_fifo_drain`,
|
||
`kbd_overrun_wipe_modifiers` — оба прошлых бага были именно про модификаторы).
|
||
2. Сверить с оригиналом ширину окна по кадрам: у нас `check_grab` доступен на
|
||
102..105 (midair) + весь `ACT_FREEFALL`, как в seg006 — расхождений в коде
|
||
быть не должно, значит расхождение либо во вводе, либо в темпе игры
|
||
([L1-SPEED](TASKS_OPEN.md#l1-speed): игра идёт на ~39 % быстрее оригинала,
|
||
а окно зацепа отмеряется В КАДРАХ — то есть по времени оно у нас короче).
|
||
3. Хост-тест `t_grab.c` уже умеет мерить ширину окна (карта исходов по фазе X и
|
||
задержке нажатия) — на нём и проверять «стало стабильнее».
|
||
|
||
### Протокол замера (написан 2026-08-12, чтобы следующий заход начался с фактов)
|
||
|
||
Пользователь связывает с этим же и «двойные срабатывания не вовремя». Прежде
|
||
чем трогать драйвер — доказать, что он вообще виноват: **сегодняшний случай
|
||
«Кид ходит как с зажатым Shift» оказался артефактом MCP-моста, а не драйвера**
|
||
(`pause` освободил 6 незакрытых входов, которые плагин переустанавливал каждый
|
||
кадр; как только они отпустились, драйвер снял бит сам). То есть break-коды
|
||
драйвер обрабатывает верно, и ложная тревога здесь стоила часа.
|
||
|
||
Что мерить:
|
||
|
||
1. **Поток скан-кодов**: кольцевой лог в трамплине прерывания
|
||
(`libc/irq/_irq_tramp.c` / `kbd_raw_poll.c`) — байт кода, флаг make/break,
|
||
номер кадра. Читать из MAME по адресу буфера (`mem`), как читаем `_Kid`.
|
||
2. **Что увидел движок**: `pop_ctrl_shift_held()` и битмап `_kbdraw_down`
|
||
(адрес — из `.map`, для SprPoP 0xAEF9) на тех же кадрах. Расхождение
|
||
«код пришёл, а движок не увидел» = потеря в драйвере; «кода не было, а бит
|
||
стоит» = потеря break (то, что подозревает пользователь).
|
||
3. **Сцены**: бег с отпусканием, разворот, вис + отпускание Shift, падение с
|
||
зажатым заранее Shift (вход на уровень 7), быстрое повторное нажатие
|
||
(проверка «двойного срабатывания» — typematic, см. memory
|
||
`kbd_overrun_wipe_modifiers`).
|
||
|
||
Что считать нормой: каждому make ровно один break; между make и видимым
|
||
`kbd_raw_down()` — не больше кадра; при удержании typematic-повторы НЕ должны
|
||
доходить до `pop_ctrl` как новые нажатия (диспетчер ждёт фронт).
|
||
|
||
Отдельная гипотеза, которую замер тоже закроет: окно зацепа отмеряется В
|
||
КАДРАХ, а игра идёт на ~39 % быстрее оригинала ([L1-SPEED](TASKS_OPEN.md#l1-speed))
|
||
— тогда «менее стабильно» это не клавиатура, а темп, и чинить надо темп.
|
||
|
||
---
|
||
|
||
<a id="уровень-2"></a>
|
||
# С приёмки уровня 2 (2026-08-07)
|
||
|
||
Всё найденное тем прогоном закрыто, кроме двух записей ниже
|
||
(`BUG-SPIKE-1` — уровень 2, комната 6). Карта содержимого уровня 2 (что где
|
||
стоит по `res2002.bin`, какие кнопки какие ворота открывают) — в
|
||
[`TASKS_CLOSED.md`](../../PoP/roomtest/TASKS_CLOSED.md#l2-pass), по ней видно, «механика не
|
||
сработала» это или «так и задумано».
|
||
|
||
<a id="bug-spike-1"></a>
|
||
## BUG-SPIKE-1. Пики залипают выдвинутыми — НИЗКИЙ ПРИОРИТЕТ, МАЛОВОСПРОИЗВОДИМ
|
||
|
||
> **Статус 2026-08-05, вечер.** Пользователь **повторить не смог**, а замер
|
||
> (ниже) показал, что чистый пробег по убранным пикам убивает штатно. То
|
||
> есть в смертельности бага, похоже, нет вовсе: наблюдался частный случай —
|
||
> пики, УЖЕ выдвинутые полностью (h = 1), для бегущего безвредны и в
|
||
> оригинале. Запись оставлена открытой ровно из-за визуального расхождения
|
||
> со скриншотом SDLPoP (у нас острия торчат, у него убраны) — см. конец.
|
||
>
|
||
> **Как получить состояние нарочно:** подойти к пикам вплотную (выдвинутся),
|
||
> отступить на полшага, НЕ выходя из зоны срабатывания, и пробежать по ним.
|
||
> Пользователь пробовал и это, и попиксельную подгонку X читом `[`/`]` —
|
||
> не поднялось. Вывод для будущего разбора: состояние **не чисто
|
||
> позиционное**, одной шириной габарита его не объяснить; следующий
|
||
> подозреваемый — момент, в который `process_trobs` застаёт модификатор
|
||
> относительно кадра Кида.
|
||
|
||
**Наблюдение (пользователь, 2026-08-05).** Уровень 2, комната 6, пики (1,3):
|
||
|
||
| действие | что происходит |
|
||
|----------|----------------|
|
||
| длинный прыжок с ряда 0 на пики | **смерть — правильно** (путь `fell_on_spikes`) |
|
||
| пробег по ряду 1 прямо по пикам | **урона нет** |
|
||
| после уборки пик | **на экране остаются белые остатки остриёв** (в оригинале чисто) |
|
||
| прыжок на месте, стоя на пиках | урона нет |
|
||
| просто стоять на выдвинутых пиках | можно сколько угодно |
|
||
|
||
**Замер (MAME, чтение `room_modif` комнаты 6).** Пока Кид стоит на тайле,
|
||
модификатор пики (индекс 13) = **0x8E**, то есть «пики ПОЛНОСТЬЮ вышли и
|
||
идёт обратный отсчёт». Дальше вся арифметика сходится с оригиналом:
|
||
|
||
```
|
||
is_spike_harmful (seg007:1178): 0/-1 → 0; <0 → 1; 1..4 → 2; >=5 → 0
|
||
check_spiked (seg006:0658): убивает при h>=2 на кадрах бега 7..14
|
||
и при h!=0 на кадрах приземления 43/26
|
||
```
|
||
|
||
То есть **при h = 1 (пики уже вышли) бегущий не гибнет и в оригинале** —
|
||
смертельно только окно ВЫДВИЖЕНИЯ (модификатор 1..4, h = 2). Наши
|
||
`animate_spike`, `start_anim_spike`, `is_spike_harmful`, `check_spiked`
|
||
сверены с seg006/seg007 построчно и совпадают дословно.
|
||
|
||
**ВТОРОЕ НАБЛЮДЕНИЕ (то же место, сравнение с оригиналом).** Кид уронил
|
||
плиту-потолок и спрыгнул вниз; пики выдвинулись и «спрятались не все — часть
|
||
артефактов осталась». Скриншоты рядом:
|
||
[наш](bugscreens/l2-r6-spikes-ours.png) и
|
||
[SDLPoP](bugscreens/l2-r6-spikes-sdlpop.png) в той же позе. У нас из-под
|
||
щебня торчат белые острия, у оригинала пик не видно ВООБЩЕ.
|
||
|
||
**ГИПОТЕЗА «ТРИГГЕР СРАБАТЫВАЕТ РАНО» ПРОВЕРЕНА И ОПРОВЕРГНУТА (замер
|
||
2026-08-05, MAME, watchpoint на `room_modif[13]` комнаты 6).** Чистый
|
||
пробег по УБРАННЫМ пикам убивает штатно:
|
||
|
||
```
|
||
запись modif=1 : кадр 11 (беговой), x=112, curr_col=2 ← пики пошли вверх
|
||
запись modif=2 : кадр 12 (беговой), x=117, curr_col=3 ← Кид уже НА тайле, h=2
|
||
запись modif=3 : кадр 177 (frame_177_spiked) ← напоролся
|
||
```
|
||
|
||
То есть `check_spike_below`, `check_spiked`, `is_spike_harmful` и тайминг
|
||
выдвижения работают правильно, и «раннего» триггера нет.
|
||
|
||
**Настоящий корень — пики ЗАЛИПАЮТ выдвинутыми.** Пока габарит Кида
|
||
накрывает колонку пики, `check_spike_below` каждый кадр зовёт
|
||
`start_anim_spike`, а тот при отрицательном модификаторе переставляет его
|
||
обратно в 0x8F — отсчёт до уборки не доходит. А выдвинутые пики (h = 1)
|
||
для бегущего БЕЗВРЕДНЫ по правилам оригинала. Отсюда обе жалобы: пробег
|
||
по уже вышедшим пикам не убивает, и они же остаются торчать на экране.
|
||
|
||
**Что осталось выяснить (и это единственный открытый вопрос).** Код
|
||
`start_anim_spike` у нас с оригиналом совпадает дословно, значит оригинал
|
||
тоже удерживал бы пики, стой Кид там же. На скриншоте SDLPoP в похожей
|
||
позе пики УБРАНЫ — то есть его Кид стоит чуть левее и его габарит колонку
|
||
пики уже не задевает. Разница в 2–3 пикселя посадки, а у нас такие
|
||
расхождения по X уже ловились (см. заметку в BUG-GRAB-1: после касания
|
||
площадки SDLPoP уводит Кида на 134, мы — на 141).
|
||
|
||
**Как закрывать:** инструментировать SDLPoP (печать `char_x_left/right`,
|
||
`left/right_checked_col` и модификатора пики каждый кадр), проиграть ту же
|
||
сцену — падение плиты-потолка в комнате 6 и остановку на щебне — и сверить
|
||
с нашей трассой ПОЗИЦИЮ КИДА после приземления. Если позиции совпадут, а
|
||
диапазоны колонок разойдутся — виноват габарит (`kid_fp` против
|
||
`set_char_collision` текущего кадра); если разойдутся позиции — это отдельный
|
||
баг посадки, а пики — его следствие.
|
||
|
||
---
|
||
|
||
<a id="уровень-3"></a>
|
||
|
||
---
|
||
|
||
# Оптимизация отрисовки (записано 2026-07-29)
|
||
|
||
Не баги — план работ. Оба пункта про одно: у оригинала пометка тайла к
|
||
перерисовке стоит копейки (бит в таблице, которая всё равно чистится каждый
|
||
кадр), а у нас каждая такая пометка превращается в реальный heal (копию из
|
||
ОЗУ-копии акселератора) плюс блиты. Поэтому буквальный порт «перерисовываем
|
||
безусловно» корректен, но дорог.
|
||
|
||
<a id="t-1"></a>
|
||
## T-1. Пики: перерисовывать по причине, а не безусловно
|
||
|
||
**Сейчас:** `pop_process_trobs` зовёт `pop_spike_redraw` каждый кадр для
|
||
каждой живой пики в комнате (порт `redraw_21h`, который `animate_spike`
|
||
вызывает вне всяких `if`). Это корректно, но лишнее для пик, до которых
|
||
Киду дела нет.
|
||
|
||
**Надо:** перерисовывать тайл пики, только если
|
||
1. **сменился её видимый кадр** (шаг выдвижения/уборки), ЛИБО
|
||
2. **её кто-то стёр** — а стереть у нас может только heal, то есть тайл
|
||
попал в прямоугольник `kid_heal` этого кадра.
|
||
|
||
Это и есть модель оригинала, просто выраженная флагами: `redraw_at_char`
|
||
(seg003:0576) каждый кадр помечает `set_redraw_fore` тайлы персонажа, причём
|
||
**объединение текущего и предыдущего** прямоугольника
|
||
(`MIN(char_top_row, prev_char_top_row)` и т.д.), а `animate_spike` помечает
|
||
свой тайл. Итог = {тайл сменил кадр} ∪ {тайлы Кида}.
|
||
|
||
**Как:** слой Кида и так считает `cL..cR`/`rT..rB` в `pop_fore_over_kid` —
|
||
пусть публикует их (плюс предыдущие, как в оригинале), а цикл trob'ов
|
||
сравнивает `tilepos` с диапазоном целочисленно. Никаких пересечений
|
||
прямоугольников (см. память `manual_hints_over_auto_detect`).
|
||
|
||
**Приоритет:** отдаётся почти бесплатно ПОСЛЕ T-2, отдельно не окупается.
|
||
|
||
<a id="t-2"></a>
|
||
|
||
---
|
||
|
||
<a id="bug-chomp-jump-1"></a>
|
||
|
||
## BUG-CHOMP-JUMP-1. Прыжок с места вплотную к чомперу: кадр с отступом назад — НИЗКИЙ ПРИОРИТЕТ, МАЛОВОСПРОИЗВОДИМ
|
||
|
||
> **Статус 2026-08-08.** Наблюдение пользователя на приёмке L3-CHOMP: Кид
|
||
> стоит вплотную СЛЕВА от чомпера лицом вправо, прыжок с места — в анимации
|
||
> проскакивает кадр, где он «чуть отступил назад», и только потом идёт
|
||
> прыжок. В оригинале прыжок идёт прямо с места. **На повторе в тот же
|
||
> заход не воспроизвёлся** — отложено до пререлиза.
|
||
|
||
**Что уже установлено (чтобы не разбирать заново).**
|
||
|
||
1. **Это НЕ анимация.** `seq_3_standing_jump` (SDLPoP `seqtbl.c:382`)
|
||
состоит только из положительных смещений:
|
||
`act(run_jump), f16, f17, dx(2) f18…f22, dx(7) f23, dx(9) f24, dx(5) dy(-6) f25`.
|
||
Ни одного отрицательного `dx` — отката в последовательности нет вовсе.
|
||
Значит отступ даёт `bumped()`, то есть физика посчитала въезд в
|
||
препятствие и выровняла `Char.x` назад.
|
||
|
||
2. **`is_obstacle` для чомпера у нас совпадает с оригиналом** (seg004:037E):
|
||
препятствие только при `modif == 2`, причём именно `== 2`, БЕЗ маски
|
||
`0x7F` — то есть окровавленный чомпер (`0x82`) в ванили не бампит вовсе.
|
||
Проверено, расхождения нет.
|
||
|
||
3. **Прямая трасса прыжка отката НЕ показала.** Ввод «UP и RIGHT
|
||
одновременно» (mame bridge, `key UP 24` + `key RIGHT 24`) даёт чистое
|
||
движение вперёд: `x` 148 -> 155, кадр становится 178 (перемололо). Ни
|
||
одного кадра с уменьшением `x`.
|
||
|
||
**Главная гипотеза — ПОРЯДОК НАЖАТИЙ.** Если ↑ приходит на кадр раньше →,
|
||
то `control_standing` уходит не в `standing_jump()`, а в `up_pressed()` —
|
||
вертикальный прыжок с зацепом, а он **выравнивает `Char.x`**. Отсюда и
|
||
«отступил, потом прыгнул». Проверять надо `check_jump_up` /
|
||
`jump_up_or_grab` / `grab_up_no_floor_behind` (seg005:0836 и далее), а не
|
||
прыжок с места. Внимание на `can_climb_up` (seg005:0828): там у чомпера
|
||
ЕСТЬ спецкейс (`seq_73_climb_up_to_closed_gate` при взгляде ВПРАВО) — он у
|
||
нас портирован (`pop_map.c`, ветка `TILE_MIRROR || TILE_CHOMPER`), но именно
|
||
вокруг него и стоит копать.
|
||
|
||
**Как ловить.** Брейкпоинт на резидентном `_kid_tick` с
|
||
`{printf "f=%d x=%d act=%d col=%d",b@Kid+0,b@Kid+1,b@Kid+6,b@Kid+4; g}` —
|
||
одна строка на кадр, адрес `_Kid` из `.sprinter-cc-build/.sprinter-cc-sprpop/sprpop.map`
|
||
(после КАЖДОЙ пересборки другой). Плюс стоп-кадр `1` в момент отступа и
|
||
чтение `Kid` из памяти. Метод — memory `z80_profiling_method`.
|
||
|
||
---
|
||
|
||
## Заметки (отладка)
|
||
|
||
- Тестовые клавиши осторожного шага: **J** = шаг влево, **L** = шаг вправо
|
||
(эмуляция Shift+стрелка), см. `pop_ctrl.c` `KBD_DBG_STEP*`. Первый шаг в
|
||
сторону = разворот (как в оригинале safe_step), движение со второго.
|
||
- Читы (`pop_cheat.h`): **K** — убить стража, **I** — бессмертие (toggle),
|
||
**S** — выдать меч, **Shift+L** — следующий уровень, **`[`/`]`** — сдвиг
|
||
Кида на пиксель по X.
|
||
- Респавн после смерти — по **↑** (или авто через `RESPAWN_DELAY`); ставит
|
||
Kid в стартовую позицию УРОВНЯ (`pop_start_level`, порт do_startpos).
|
||
- **ROOMNAV (`=`/`-`) — тоже наш чит**, которого в оригинале не было, как и
|
||
`S`. Все они со временем съедутся в общий блок читов, разрешаемый в
|
||
настройках; пока просто включены (`pop_cheats = 1` в `sprpop.c`).
|
||
Известный баг этого чита закрыт — [BUG-CHEAT-FIGHT-1](BUGS_CLOSED.md#bug-cheat-fight-1).
|
||
- Комнаты **13, 18, 24 уровня 1 недостижимы** в обычной игре — это свойство
|
||
данных уровня (разбор — «НЕ БАГИ» в [`BUGS_CLOSED.md`](../../PoP/roomtest/BUGS_CLOSED.md));
|
||
приоритет багов в них низкий. Аналогично 23/24 на уровне 3.
|
||
|
||
|
||
---
|
||
|
||
<a id="torch-anim-right"></a>
|
||
## TORCH-ANIM-RIGHT. Под пламенем могут застыть не только челюсти чомпера
|
||
|
||
Открыто 2026-08-10 при закрытии
|
||
[BUG-TORCH-CHOMP-2](BUGS_CLOSED.md).
|
||
|
||
Пламя факела запекается в фон и рисуется в ячейке ПРАВОГО СОСЕДА. Оригинал
|
||
после каждого кадра факела метит этого соседа (`set_redraw_anim_right`,
|
||
seg007:0101) и перерисовывает весь его слой `anim` поверх огня; мы метим
|
||
только когда сосед — чомпер. Слой `draw_tile_anim` (seg008:0644) рисует
|
||
также **пики, зелье и меч** — если такой тайл окажется справа от факела и
|
||
будет в статике, пламя накроет и его.
|
||
|
||
На уровнях 1–4 такого соседства не встретилось, поэтому расширять пометку
|
||
(она стоит перерисовки тайла каждый кадр на каждый факел) заранее не стали.
|
||
|
||
**Как проверять:** поставить факел слева от пики/меча/зелья в тестовой
|
||
комнате и дождаться статики; либо пройти уровни 5+ и смотреть на клетку
|
||
справа от каждого факела.
|
||
|
||
**Как чинить, если встретится:** там же, в ветке факела `pop_process_trobs`,
|
||
добавить в `trob_rcode`-проверку нужные коды и соответствующий вид пометки
|
||
(`POP_RD_SPIKE` / `POP_RD_FLOOR`), а зелье — переставить в порядке обхода
|
||
так, чтобы оно рисовалось ПОСЛЕ факела.
|
||
|
||
---
|
||
|
||
<a id="fore-dup"></a>
|
||
## FORE-DUP. Передний слой тайла рисуется ДВАЖДЫ при перекрытии объектов
|
||
|
||
Найдено пользователем 2026-08-10 (вопросом, а не по симптому — картинка
|
||
верная, страдают только такты).
|
||
|
||
**У нас.** Fore-проход зовёт КАЖДЫЙ рисующий по своему прямоугольнику:
|
||
`pop_char_fore` для слотов Кида и соперника, `pop_mirror_draw` для отражения.
|
||
Окно клипа (`pop_fore_set_clip`) у каждого своё. Если футпринты двух
|
||
объектов накрывают один тайл, его передний кусок рисуется два раза.
|
||
|
||
Когда случается:
|
||
- **Кид и отражение** — ВСЕГДА (стоят на одном тайле зеркала); этот случай
|
||
создан фиксом fore-прохода над отражением 2026-08-10;
|
||
- **Кид и соперник в одном тайле** — то есть весь ближний бой;
|
||
- Кид и падающий кусок плиты.
|
||
|
||
**Картинку не портит:** куски переднего слоя идут прозрачным блитом-копией,
|
||
операция идемпотентная. Портило бы при XOR/mono с накоплением — таких в
|
||
fore-слое нет.
|
||
|
||
**Такты тратит,** и в самом дорогом месте: fore-проход исторически самая
|
||
тяжёлая часть кадра (memory `pop_fore_layer_cost` — было 78 % кадра, окно
|
||
клипа и кэш кладки дали 3.2×).
|
||
|
||
**Как устроено в оригинале — дубль НЕВОЗМОЖЕН по построению.**
|
||
`redraw_at_char` (seg003:0427) ничего не рисует, а только помечает тайлы
|
||
своего прямоугольника:
|
||
|
||
```c
|
||
for (tile_row = x_top_row; tile_row <= char_bottom_row; ++tile_row)
|
||
for (tile_col = x_col_left; tile_col <= x_col_right; ++tile_col)
|
||
set_redraw_fore(get_tilepos(tile_col, tile_row), 1);
|
||
```
|
||
|
||
а `set_redraw_fore` (seg007:0550) — это `redraw_frames_fore[tilepos] = frames`,
|
||
ПРИСВАИВАНИЕ. Два персонажа на одном тайле оставят там ту же единицу, и
|
||
единственный обход тайлов нарисует передний кусок один раз.
|
||
|
||
**Что делать.** Механизм пометок у нас уже есть и прямо назван портом этой
|
||
архитектуры — `pop_redraw.h`. Fore-слой остался единственным местом на
|
||
прямом вызове. Приведение к оригиналу: `pop_char_fore`/`pop_mirror_draw`
|
||
вместо прохода ставят пометки, а один проход в конце кадра их разбирает.
|
||
|
||
**СНАЧАЛА ЗАМЕРИТЬ, потом чинить.** Это самый горячий путь, и окно клипа
|
||
(вместо перебора тайлов) в своё время дало 3.2× — переход на пометки может
|
||
часть этого вернуть назад. Замер: брейкпоинт на листьях fore-слоя со
|
||
счётчиком, сцена «бой в комнате 3 уровня 1» и «Кид на тайле зеркала,
|
||
ур. 4»; сравнить число нарисованных кусков с числом уникальных тайлов.
|
||
Если дубль мал — оставить как есть и закрыть запись.
|
||
|
||
---
|
||
|
||
---
|
||
|
||
<a id="sprite-zero-w0"></a>
|
||
## SPRITE-ZERO-W0. Кадр персонажа изредка отдаёт спрайт 0x0 — РЕДКИЙ
|
||
|
||
Всплыло при разборе [BUG-CHAR-STALE-PAGE](BUGS_CLOSED.md#bug-char-stale-page)
|
||
(тень застыла на двух страницах в разных позах). Залипание слота вылечено —
|
||
`cd_sig_make` больше не берёт снимок, если кадр не рисовали, — но САМА
|
||
причина нулевого спрайта не найдена: `atlas_image` для валидного кадра
|
||
изредка отдаёт запись 0x0.
|
||
|
||
Подозрение: конкуренция за окно W0. `pop_char_draw` маппит страницу атласа
|
||
(`gfx_w0_map(pages[page].page)`) и держит её до `gfx_w0_unmap()` в конце, а
|
||
внутри этого окна успевают отработать `cd_splash` и `pop_sword_draw`; рядом
|
||
в кадре W0 маппят чтение данных уровня (`pop_level_tile`, `pop_level_set_tile`)
|
||
и mob-тик. Если какой-то путь размапливает окно раньше времени, чтение
|
||
заголовка спрайта даст нули.
|
||
|
||
Как ловить: watchpoint на порт окна 0 либо счётчик «w==0 при непустом кадре»
|
||
в `pop_char_draw` с записью кадра/страницы/idx в отладочную ячейку, дальше
|
||
читать её из MAME. Симптом редкий (пользователь поймал дважды).
|
||
|
||
<a id="gate-fore-kid"></a>
|
||
## GATE-FORE-KID. Кид, стоящий В ПРОЁМЕ ворот, виден ПОВЕРХ решётки — ФИКС ЕСТЬ, ЖДЁТ ПРОВЕРКИ
|
||
|
||
> **Портировано 2026-08-13** (повтор нашёлся на уровне 10, комната 7, тайл
|
||
> (2,6) — подтверждено чтением памяти MAME: `Kid.room=7, curr_row=2,
|
||
> curr_col=6`, код тайла `0x04`). Сделано так:
|
||
>
|
||
> * **решение** — `pop_gate_over_char(ch)` в `pop_map.c`: персонаж стоит на
|
||
> тайле ворот текущей комнаты (колонки 0..8; девятая отсекается, там бары
|
||
> легли бы в соседнюю комнату — у оригинала это `Kid.room != room_R`);
|
||
> * **отрисовка** — `draw_gate_fore` в `pop_bg.c`, точный порт seg008:1153:
|
||
> только прозрачные куски 51 + ломти 52, без непрозрачного 50 и без
|
||
> хвостового `DOOR_FRAM_SLICE` (фон под барами уже нарисован). Зовётся
|
||
> ОДИН раз на персонажа за кадр, не в тайловом цикле;
|
||
> * **тесты** — `tests-host/t_char.c`: `char_gate_covers_char_standing_in_it`,
|
||
> `char_gate_ignores_neighbour_tiles`, `char_gate_ignores_other_room`,
|
||
> `char_gate_never_in_last_column`. Отрисовка в харнесс не линкуется,
|
||
> поэтому проверяется именно решение — ради этого оно и вынесено в карту.
|
||
>
|
||
> Расхождение с оригиналом (осознанное, в `docs/impl_diff.md`): оригинал
|
||
> спрашивает жёстко про `Kid`, потому что foretable у него одна на проход
|
||
> тайлов; у нас fore-проход идёт по персонажу, поэтому спрашиваем про того,
|
||
> кого рисуем — страж в проёме тоже уходит за решётку.
|
||
>
|
||
> Шовный случай (ворота у СОСЕДА слева) работал и раньше — он ниже.
|
||
> Банк 2 +601 Б, банк 3 +92 Б, резидент не изменился.
|
||
|
||
Нашёл пользователь 2026-08-11 (уровень 5, комната 24, верхние ворота):
|
||
Кид стоит на тайле ворот, а решётка рисуется ПОД ним — он виден целиком,
|
||
хотя должен быть за прутьями.
|
||
|
||
**Как в оригинале.** `draw_tile_fore` (seg008:0D15) первой же строкой:
|
||
|
||
```c
|
||
if (tile_left == tiles_4_gate && Kid.curr_row == drawn_row &&
|
||
Kid.curr_col == drawn_col - 1 && Kid.room != room_R) {
|
||
draw_gate_fore();
|
||
}
|
||
```
|
||
|
||
то есть когда Кид стоит ИМЕННО на тайле ворот, их решётка дорисовывается
|
||
в **foretable** — поверх персонажа: `draw_gate_fore` (seg008) кладёт спрайт
|
||
51 (низ решётки) и дальше вверх кусками по 8 px спрайтом 52, до
|
||
`gate_top_y`, прозрачным блитом.
|
||
|
||
**Что есть у нас.** Портирован только ШОВНЫЙ случай (`pop_bg.c`,
|
||
`pop_fore_over_char`): если футпринт зашёл за левый шов и у соседа слева в
|
||
этом ряду ворота — их бары перерисовываются поверх персонажа через
|
||
`draw_gate_back`. Случая «ворота ВНУТРИ комнаты, персонаж на их тайле»
|
||
нет вовсе, отсюда симптом.
|
||
|
||
**Почему не чинится одной строкой.** Правка идёт в `pop_fore_over_char` —
|
||
самый горячий путь кадра (memory `pop_fore_layer_cost`: когда-то 78 %
|
||
кадра, окно клипа дало 3.2×). Добавлять проверку надо так, чтобы она не
|
||
стоила ничего на каждом тайле футпринта: условие дешёвое (`тайл под
|
||
персонажем == ворота`), но рисование — это ещё один блит на кадр, пока
|
||
Кид стоит в проёме. Плюс нужен свой расчёт полосы (`gate_top_y` по
|
||
живому openness), а не готовый `draw_gate_back`, который рисует ЗА
|
||
персонажем.
|
||
|
||
**Проверять:** уровень 5, комната 24 — встать в проём верхних ворот
|
||
(колонка 1 ряда 0) при частично поднятой решётке; прутья должны
|
||
перекрывать Кида. Тот же случай — любые ворота на уровнях 1-3.
|
||
|
||
---
|
||
|
||
<a id="guard-entry-death"></a>
|
||
## GUARD-ENTRY-DEATH. Вход в комнату со стражем вплотную = мгновенная смерть — ФИКС ЕСТЬ, ЖДЁТ ПРОВЕРКИ
|
||
|
||
**Наблюдение (пользователь, 2026-08-13).** В некоторых ситуациях (особенно
|
||
после телепортов) Кид, входя в комнату, сразу оказывается чуть ли не в одном
|
||
тайле со стражем и немедленно погибает: меч ещё в ножнах, а безоружному любой
|
||
укол смертелен.
|
||
|
||
**Что в оригинале.** Стража при входе НЕ отбрасывает — проверено по SDLPoP:
|
||
`enter_guard` (seg002:0112) ставит его ровно в тайл из данных уровня
|
||
(`Char.x = level.guards_x[room-1]`), никаких проверок дистанции до Кида нет.
|
||
Ситуация «вошёл вплотную» возможна и там. Защит четыре:
|
||
|
||
1. **`bump_into_opponent` (seg003:08AA)** — главная. Безоружный Кид
|
||
(`Char.sword == sword_0_sheathed`) при вооружённом сопернике
|
||
(`Opp.sword != sheathed`, `Opp.action < 2`), лицом к лицу
|
||
(`Char.direction != Opp.direction`) и `can_guard_see_kid >= 2`, на
|
||
`ABS(char_opp_dist()) <= 15` не получает укол, а ОТСКАКИВАЕТ: `Char.y`
|
||
прижимается к полу, `fall_y = 0`, `seq_47_bump`.
|
||
2. Кид сам достаёт меч — `control_standing` (seg005:0351).
|
||
3. Страж поднимается в стойке покоя с мечом в ножнах — `seq_77`, ему нужно
|
||
сперва заметить Кида (`autocontrol_guard_inactive` → `move_down_forw`).
|
||
4. Вплотную страж не колет: `autocontrol_guard_active` при `distance < 12`
|
||
(или `< 8` с убранным мечом) разворачивается/шагает, а `check_hurting`
|
||
требует `min_hurt_range = 8` для безоружного противника.
|
||
|
||
**Корень у нас.** Пункты 2-4 были портированы верно (`pop_ctrl.c:288`,
|
||
`pop_guard_cold.c:107`, `guards.c:553`), а **пункт 1 не портирован вовсе**:
|
||
в `kid_phys` (`pop_map.c`) между `determine_col()` и `check_collisions()`
|
||
было пусто, тогда как `play_kid_frame` (seg000:1238) зовёт там
|
||
`bump_into_opponent`. Без отскока дистанция успевает вырасти в рабочий
|
||
диапазон удара 8..29 — и первый же `move_6_shift` стража даёт
|
||
`hurt_by_sword` с `take_hp(100)`.
|
||
|
||
**Сделано.** `bump_into_opponent` портирован в `pop_map.c` (рядом с
|
||
`bumped_floor`) и вызывается из `kid_phys` на штатном месте. Резидент не
|
||
вырос, банк 3: 11066 → 11156 Б. Опциональные фиксы SDLPoP
|
||
(`fix_painless_fall_on_guard`, `fix_jumping_over_guard`) намеренно НЕ
|
||
портированы — оставлено базовое поведение движка 1989 года.
|
||
|
||
**Проверять:** войти в комнату со стражем вплотную (телепортом `+`/`−` или
|
||
`Shift+L`) без меча в руке — Кид должен отскочить с `seq_47`, а не умереть;
|
||
падение на стража сверху по-прежнему безболезненно (это оригинал, не баг).
|
||
|
||
---
|
||
|
||
<a id="loose-room-change"></a>
|
||
## LOOSE-ROOM-CHANGE. Расшатанная плита не доваливается, если Кид ушёл в соседнюю комнату — ФИКС ЕСТЬ, ЖДЁТ ПРОВЕРКИ
|
||
|
||
**Наблюдение (пользователь, 2026-08-13).** Уровень 10, комната 2: в (2,9)
|
||
стоит плита, которая под бегом Кида должна разрушаться. Если Кид пробегает
|
||
по ней и уходит в правую комнату, плита не успевает разрушиться и упасть —
|
||
переключение комнаты обрывает алгоритм.
|
||
|
||
**Что в оригинале.** loose-пол — обычный **trob** (`make_loose_fall`,
|
||
seg007:0EF6 → `add_trob(curr_room, curr_tilepos, 0)`), а `process_trobs`
|
||
(seg007:0000) крутит ВЕСЬ список каждый кадр, независимо от `drawn_room`:
|
||
`animate_tile` делает `get_room_address(trob.room)` и работает с таблицей
|
||
той комнаты, где trob живёт. Фаза лежит в `curr_room_modif`, то есть прямо
|
||
в таблице уровня. Поэтому брошенная плита досчитывает до
|
||
`loose_floor_delay`, `remove_loose` убирает тайл, `add_mob` рождает кусок —
|
||
и всё это без Кида в комнате. Ровно так же ведут себя `mobs[]`
|
||
(`do_mobs`, seg007:1063) — их список чистится только в `start_level`.
|
||
|
||
**Корень у нас.** Падающие КУСКИ (mob) чужую комнату умели давно
|
||
(`pop_loose_mob_room_changed`, [BUG-LOOSE-2](BUGS_CLOSED.md)), а вот ФАЗА
|
||
тряски/отсчёта жила в `pop_loose_modif[30]` — массиве ТЕКУЩЕЙ комнаты, —
|
||
и `pop_loose_reset()` при смене комнаты обнулял его целиком. То есть
|
||
плита, брошенная на полпути, возвращалась в покой. То же для плит-потолков
|
||
(`pop_ceil_modif[10]`).
|
||
|
||
**Сделано** (двухуровневое хранение вместо переноса всей механики в trob —
|
||
`pop_loose_modif` остаётся быстрым массивом отрисовки текущей комнаты):
|
||
|
||
1. `pop_loose_leave_room()` (`pop_map.c`, зовётся первым делом в
|
||
`enter_room_side`, пока карта ещё описывает СТАРУЮ комнату) — сдаёт фазу
|
||
всех loose-тайлов в персистентный `room_modif` (это и есть
|
||
`curr_room_modif` оригинала) и заводит trob на незаконченные.
|
||
2. `animate_loose()` (`pop_trob.c`, case `TILE_LOOSE` в `pop_process_trobs`)
|
||
— досчитывает фазу заочно; на провале ставит тайл в EMPTY
|
||
(`pop_level_set_tile`), кладёт в модификатор тип уровня (`remove_loose`)
|
||
и рождает кусок через новый `pop_loose_mob_spawn_at(room, row, col)`.
|
||
Пока комната ОТРИСОВАНА, trob снимается сразу — хозяин фазы
|
||
`pop_loose_tick`.
|
||
3. `pop_loose_reset()` теперь не обнуляет, а ЗАБИРАЕТ фазу новой комнаты из
|
||
`room_modif`: вернувшийся Кид застаёт плиту на той же фазе.
|
||
4. `pop_loose_forget()` — старт/рестарт уровня: забыть фазы БЕЗ сдачи
|
||
(у оригинала всё сносит `load_level`). Без него первый же `enter_room`
|
||
после `pop_trob_reset()` занёс бы в свежий уровень дрожь из прошлой жизни.
|
||
|
||
Резидент не изменился (20569 Б); банк 3 +451 Б, банк 6 +181 Б, банк 7 +62 Б.
|
||
Хост-тест `phys_loose_survives_room_change` (`tests-host/t_phys.c`) закрывает
|
||
передачу фазы.
|
||
|
||
**Проверять:** уровень 10, комната 2 — пробежать по плите (2,9) в правую
|
||
комнату и вернуться: дыра должна быть на месте. Регресс: комната 7
|
||
(две соседние плиты подряд), комната 12 плита (0,1) → уход в 15
|
||
([BUG-LOOSE-2](BUGS_CLOSED.md)), плита-потолок, рестарт уровня после смерти
|
||
на дрожащей плите.
|
||
|
||
|
||
---
|
||
|
||
<a id="loose-seam-anim"></a>
|
||
## LOOSE-SEAM-ANIM. Плита в шве исчезает без анимации падения — МЕЛКИЙ, ОТЛОЖЕН
|
||
|
||
> **Решение пользователя (2026-08-13):** «пока пусть будет так».
|
||
|
||
**Наблюдение.** Уровень 10: Кид расшатывает плиту (2,9) комнаты 2 и убегает
|
||
вправо в комнату 7, где та же плита видна в шве как (2,−1). Плита
|
||
доваливается правильно (см. [LOOSE-ROOM-CHANGE](#loose-room-change)) и с
|
||
экрана исчезает, но **мгновенно** — без дрожания и без падающего куска.
|
||
|
||
**Почему так.** Заочный досчёт (`animate_loose`, `pop_trob.c`) сознательно
|
||
не рисует: комната не отрисована, а фаза живёт в `room_modif`, мимо
|
||
`pop_loose_modif`, на который смотрит отрисовка дрожащего кадра. Исчезновение
|
||
видно только потому, что провал меняет ТАЙЛ, а по этому событию
|
||
(`pop_neigh_dirty`) главный цикл перечитывает кромку и перерисовывает шов.
|
||
Падающий кусок (mob) рождается в комнате 2 и честно летит, но `mob_tick_one`
|
||
рисует только `here` (своя комната), а комната 2 — не отрисованная.
|
||
|
||
**Что надо, если браться.** Шов — это НЕ обычный тайл: чужая колонка 9
|
||
рисуется внутри нашей колонки 0 (`draw_tile`, ветка `lcode == 11`), а
|
||
дрожащий кадр берётся из `pop_loose_modif[row*10 + (col-1)]`, то есть при
|
||
`col == 0` индекс уезжает за границу массива (для ряда 0 — в −1). Значит
|
||
анимация шва потребует не «включить рисование», а дать чужой колонке
|
||
собственный источник фазы. Заодно там же чинить и этот выход за границу.
|
||
|
||
**Проверять:** тот же сценарий уровня 10; в оригинале плита в шве дрожит и
|
||
роняет кусок так же, как в своей комнате.
|
||
|
||
|
||
---
|
||
|
||
<a id="jump-floor-legs"></a>
|
||
## JUMP-FLOOR-LEGS. Прыжок с пола: ноги Кида поверх кромки пола — ПОЧИНЕН, ЖДЁТ ПРОВЕРКИ
|
||
|
||
> **Корень (найден трассой 2026-08-13).** Проход оверлеев в
|
||
> `pop_fore_over_char` обходил **только два крайних ряда** футпринта:
|
||
>
|
||
> ```c
|
||
> if (action != 2) other_overlay_tile(rB, c); /* опорный */
|
||
> if (rT != rB) other_overlay_tile(rT, c); /* верхний */
|
||
> ```
|
||
>
|
||
> Это буквальный порт `redraw_at_char2` (seg003:0645), и в оригинале он
|
||
> верен: там `char_top_row` и `char_bottom_row` ВСЕГДА соседние. У нас между
|
||
> ними появляется дыра — `char_footprint` форсит `rT = rB − 1` (голова торчит
|
||
> в ряд выше), а затем окно fore-клипа расширяет `rB` ещё на ряд вниз.
|
||
>
|
||
> На спорном кадре трасса из MAME дала `rT=0 rB=2 cL=0 cR=1 do_ov=1` и **ноль**
|
||
> вызовов `overlay_mid_tile`: ряды 0 и 2 обошли, ряд 1 — тот самый, где стоит
|
||
> плита пола, — пропустили. Отсюда и подсказка пользователя «глубже в падении
|
||
> плита рисуется правильно»: там `y_to_row` даёт уже 2, ряд 1 попадает в пару
|
||
> крайних, и всё сходится.
|
||
>
|
||
> **Фикс:** обходить ВЕСЬ диапазон `rB..rT`, сохранив правило оригинала «при
|
||
> `action == 2` опорный ряд не трогаем». После фикса трасса на том же кадре:
|
||
> один вызов `overlay_mid_tile`, исход для (1,1) = «нарисовали», коды тайлов
|
||
> `curr=3 left=0` — байт в байт как в отладочном выводе SDLPoP.
|
||
>
|
||
> Пилларный оверлей, который «работал и раньше», к этому отношения не имел:
|
||
> это `fore_id = 95` из отдельного fore-прохода, а не midtable-оверлей.
|
||
>
|
||
> Банк 2 +28 Б. Хост-тесты (6 наборов) проходят.
|
||
|
||
### Разбор, по которому искали (оставлен: метод пригодится)
|
||
|
||
**Наблюдение (пользователь, 2026-08-13).** Уровень 10, комната 1, прыжок с
|
||
пола. В оригинале нижняя часть Кида скрыта ЗА передней гранью пола, у нас
|
||
рисуется весь Кид поверх неё (за передней колонной он при этом уходит
|
||
правильно). Скриншоты — в переписке сессии.
|
||
|
||
**Состояние кадра (снято из памяти MAME на замороженном кадре, сборка
|
||
2026-08-13):**
|
||
|
||
| поле | значение |
|
||
|------|----------|
|
||
| `Kid.frame` | 103 (`frame_103_start_fall_2`) |
|
||
| `Kid.action` | 3 (`actions_3_in_midair`) |
|
||
| `Kid.x` / `Kid.y` | 69 / 127 |
|
||
| `Kid.direction` | −1 (влево) |
|
||
| `Kid.curr_col` / `curr_row` | 0 / 2 |
|
||
| `Kid.room` / `cur_room` | 1 / 1 |
|
||
|
||
`pop_y_land[2] = 118` — то есть ноги на 9 px НИЖЕ уровня пола ряда 1, ровно в
|
||
полосе его передней грани (`draw_tile_bottom` рисует её на `63*row + 65 = 128`).
|
||
|
||
Тайлы комнаты 1 (`room_fg`, прочитаны там же):
|
||
|
||
```
|
||
ряд 0: 01 01 00 00 00 00 00 00 04 0B
|
||
ряд 1: 04 00 03 13 10 11 13 01 07 0F
|
||
ряд 2: 0C 01 03 01 01 0F 01 01 04 03
|
||
```
|
||
|
||
Кромочная колонка левого соседа (`lcol_fg[0..2]`): `04 0C 04`.
|
||
|
||
**Что УЖЕ проверено и исключено.**
|
||
|
||
1. **Гейт `redraw_at_char2` (seg003:0645) портирован верно.** Сверено строка
|
||
в строку: наш `do_ov` в `pop_fore_over_char` даёт то же множество поз
|
||
(захват 78-79, `action` 2/3/4/6, `bumped` 102-106, начало подъёма 135-136),
|
||
и climb-ветка 137..144 тоже совпадает. При `action == 3` оверлей
|
||
ВКЛЮЧАЕТСЯ — дело не в гейте.
|
||
2. **`draw_other_overlay` (seg008:1492) на колонке 0 не сработал бы и в
|
||
оригинале.** Обе его ветки требуют пустого соседа: `tile_left == empty`
|
||
(у нас `lcol_fg[1] = 0x0C`, doortop — не пусто) либо `drawn_col > 0` и
|
||
пустой тайл ЧЕРЕЗ ОДИН слева. Значит окклюдер — НЕ он, и наш порт этой
|
||
функции здесь ни при чём.
|
||
3. **`pop_tile_code(row, −1)` кромку читает правильно** — из `pop_t_lfg`, а не
|
||
«стена всегда».
|
||
|
||
**Две живые гипотезы, проверять в этом порядке.**
|
||
|
||
* **Это вообще не отрисовка, а физика.** У нас `Kid.y = 127` при уровне пола
|
||
118 — Кид «утоплен» на 9 px, и ноги торчат ниже кромки просто потому, что
|
||
они там и есть. Проверяется дёшево: снять `Char.y` у SDLPoP на том же
|
||
кадре 103 той же последовательности. Если там 118..120 — искать надо в
|
||
seqtbl/`fall_speed`, а не в слое фона.
|
||
* **Окклюдер — другой тайл/другой механизм.** Тогда нужна очная ставка:
|
||
прогнать SDLPoP с трассой (`dbg_shots`-fprintf в `draw_other_overlay` и
|
||
`add_kid_to_objtable` уже стоят в нашей копии, см. memory
|
||
`pop_pixel_diff_vs_sdlpop`) и сравнить СПИСОК тайлов, попавших в midtable
|
||
на этом кадре, с тем, что рисуем мы. Гадать дальше по коду смысла нет —
|
||
два прохода по seg008 уже не дали ответа.
|
||
|
||
**Проверять после починки:** тот же прыжок в комнате 1 уровня 10; за передней
|
||
колонной Кид уходит и сейчас — это не должно сломаться.
|
||
|
||
|
||
---
|
||
|
||
<a id="mob-clip-right"></a>
|
||
## MOB-CLIP-RIGHT. Окна и узор кладки рисуются поверх падающих плит
|
||
|
||
**Найдено прогонами 2026-08-13 (уровень 13, комнаты 16 и 23).**
|
||
|
||
**Симптом.** На падающей плите проступают элементы ЗАДНЕЙ стены — узор
|
||
кладки, а на ряде 0 и целое окно. Плита при этом цела: элементы ложатся
|
||
поверх неё, не стирая.
|
||
|
||
**Корень.** `mob_render` (`pop_room.c`) каждый кадр вызывает
|
||
`draw_tile(r, m->col + 1)` — перерисовывает ЦЕЛЫЙ соседний тайл поверх уже
|
||
нарисованного куска, чтобы спрятать его правую часть (спрайт 42 лежит в
|
||
столбце `col+1`). `draw_tile` кладёт всё содержимое тайла, включая декор
|
||
пустого тайла — окно и узор. Ряд берётся из координаты куска, поэтому на
|
||
ряде 0 перерисовывается тайл с окном.
|
||
|
||
Правка [#6](#) (клип перерисовки окном коридора heal) сделала артефакт
|
||
ЗАМЕТНЕЕ: раньше перерисовка размазывалась вокруг, теперь попадает точно в
|
||
прямоугольник плиты.
|
||
|
||
**Как в оригинале.** Такой перерисовки нет вовсе. `redraw_at_cur_mob`
|
||
(seg007:1094) зовётся ТОЛЬКО из `loose_fall`, один раз в момент сбития, и
|
||
лишь ставит пометки на тайл и соседа. Правая часть куска прячется
|
||
собственным клипом объекта: `add_mob_to_objtable` (seg007:1161) задаёт
|
||
`clip.right = 40`.
|
||
|
||
**Что делать.** Портировать клип вместо подпорки:
|
||
|
||
1. выяснить, во что превращается `clip.right = 40` в наших координатах
|
||
(у оригинала `obj_x` в удвоенных единицах: `char_x_left = obj_x / 2 + 58`);
|
||
2. блитить правую часть (env 42) с ограничением ширины — примитив с шириной
|
||
есть для персонажей (`gfx_blit_cols_part_w`), для env надо посмотреть;
|
||
3. убрать `draw_tile(r, col+1)` и окно fore-клипа вокруг него;
|
||
4. вернуть коридор heal к 64x32.
|
||
|
||
**Побочная выгода:** уходит по одному полному `draw_tile` на кусок за кадр —
|
||
при шести падающих плитах это шесть отрисовок тайла.
|
||
|
||
---
|
||
|
||
<a id="mid-overlay-layer"></a>
|
||
## MID-OVERLAY-LAYER. Оверлей кромки всегда поверх, а должен сортироваться
|
||
|
||
**Найдено разбором 2026-08-13.**
|
||
|
||
`overlay_mid_tile` (`pop_bg.c`) выполняется в проходе ПОСЛЕ персонажей, то
|
||
есть безусловно поверх всего нарисованного. В оригинале это `draw_other_overlay`
|
||
(seg008:1499), и он переключает таблицу на **midtable** —
|
||
тот же слой, где живут персонажи и падающие куски (`add_mob_to_objtable`,
|
||
тип `0x80`), а внутри слоя всё сортируется по `y` (`compare_curr_objs`,
|
||
seg008:1572).
|
||
|
||
Следствие: узор из оверлея выигрывает у плиты, даже когда плита ближе к
|
||
зрителю. Пока у нас нет сортируемого midtable, точечными правками это не
|
||
лечится.
|
||
|
||
Связано с [BG-ONCE](TASKS_OPEN.md#bg-once) — там та же техническая
|
||
сердцевина: привести наши слои в соответствие с таблицами оригинала.
|
||
|
||
---
|
||
|
||
<a id="guard-fallout-victory"></a>
|
||
## GUARD-FALLOUT-VICTORY. Выпавший из комнаты страж не засчитывается убитым
|
||
|
||
**Найдено разбором 2026-08-13** (при проверке чита `K` на Джафаре).
|
||
|
||
У `on_guard_killed` в оригинале ДВА места вызова:
|
||
|
||
| место | что |
|
||
|---|---|
|
||
| `play_guard`, seg006:1494 | HP кончились — **портировано** |
|
||
| `check_guard_fallout`, seg002:264 | страж выпал из комнаты — **НЕ портировано** |
|
||
|
||
Наш `pop_guard_fallout` (`pop_guard.c`) просто гасит слот. Значит столкнуть
|
||
Джафара в пропасть на 13-м уровне можно, а выход на 14-й от этого не
|
||
откроется — расхождение с оригиналом.
|
||
|
||
Правка: позвать `on_guard_killed()` в ветке «не скелет». Мешает только то,
|
||
что функция сейчас `static` в `guards.c` (банк 1), а `pop_guard_fallout`
|
||
живёт в резиденте.
|
||
|
||
---
|
||
|
||
<a id="leveldoor-startroom-wipe"></a>
|
||
## LEVELDOOR-STARTROOM-WIPE. В стартовой комнате за входной дверью рисуется лестница вместо черноты
|
||
|
||
**Найдено разбором 2026-08-17** (попутно к фиксу
|
||
[LEVELDOOR-PALACE-CLIP](BUGS_CLOSED.md#leveldoor-palace-clip) — то же
|
||
`draw_leveldoor`).
|
||
|
||
`draw_leveldoor` (seg008:1D29) в приподнятой створке различает две комнаты:
|
||
|
||
```c
|
||
if (modifier_left) {
|
||
if (level.start_room != drawn_room) {
|
||
add_backtable(..., 144 /*level door stairs*/, ...);
|
||
} else {
|
||
short leveldoor_width = (tbl_level_type[current_level] == 0) ? 39 : 48;
|
||
sbyte x_low = (tbl_level_type[current_level] == 0) ? 2 : 0;
|
||
add_wipetable(0, 8*(draw_xh + 1) + x_low, ybottom - 4, 45, leveldoor_width, 0);
|
||
}
|
||
}
|
||
```
|
||
|
||
Смысл: за дверью, **через которую вошли на уровень**, лестницы нет — там
|
||
чернота, и оригинал её ЗАТИРАЕТ прямоугольником, а не рисует марш 144.
|
||
Ширина затирки дворцовая/подземная (48 против 39) и подземная сдвинута на
|
||
2 px вправо — то есть здесь ЕЩЁ одно расхождение подземелья и дворца, помимо
|
||
уже починенного `leveldoor_right += 8`.
|
||
|
||
У нас (`pop_room.c`, `draw_leveldoor`) стоит безусловно:
|
||
|
||
```c
|
||
if (modif)
|
||
pop_env_b(144, x, ybottom - 4); /* лестница (комната ≠ стартовой) */
|
||
```
|
||
|
||
— условие названо в комментарии, но не реализовано. Различие стартовой
|
||
комнаты у нас есть только в логике входа (`pop_leveldoor_enter`,
|
||
`pop_map.c:1166`), не в отрисовке.
|
||
|
||
**Когда видно:** только пока створка входной двери приподнята
|
||
(`modif != 0`) в стартовой комнате — то есть в анимации входа на уровень и
|
||
если дверь снова открыть. При закрытой двери `modif == 0`, и обе ветки
|
||
ничего не рисуют, поэтому баг и не попался на обходах уровней 1-4.
|
||
|
||
**Что мешает сделать прямо:** у нас нет понятия wipetable — нужен чёрный
|
||
прямоугольник в проходе backtable, то есть либо прямой `gfx`-fill в
|
||
`draw_leveldoor`, либо запечка. Проверять на уровне 1 (подземелье, ширина
|
||
39, сдвиг 2) И на уровне 4 (дворец, 48, сдвиг 0) — иначе поймаем только
|
||
половину.
|
||
|
||
### <a id="bug-shadow-set"></a>BUG-SHADOW-SET. Тень в бою рисуется спрайтами стража
|
||
|
||
**Симптом** (найдено пользователем 2026-08-20): в бою Тень выглядит не
|
||
Кидом. В SDLPoP она Кид всегда.
|
||
|
||
**Корень.** Набор спрайтов выбирает не charid, а поле `sword` САМОГО
|
||
КАДРА: `load_frame_to_obj` (`seg008.c:1752`) держит `chtab_base` жёстко
|
||
равным `id_chtab_2_kid` и прибавляет `cur_frame.sword >> 6`. У всех 241
|
||
кадра `frame_table_kid` эти биты нулевые, у всех кадров `frame_tbl_guard`
|
||
— `0xC0`. Тень берёт таблицу стража для кадров 150..189 (`seg006.c:533`),
|
||
значит идёт через chtab_5.
|
||
|
||
Дальше и кроется наша ошибка: **chtab_5 — это не «страж», а «соперник
|
||
уровня»**. Он грузится из `tbl_guard_dat[tbl_guard_type[уровень]]`
|
||
(`seg000.c:1117`), а `tbl_guard_type[12] == 4` → **SHADOW.DAT**, и это
|
||
графика КИДА в боевых позах (палитра SHADOW.DAT побайтно равна палитре
|
||
Кида; у GUARD.DAT там серая рампа под перекраску).
|
||
|
||
**У нас** `pop_guard_load` (`pop_cdraw.c:98`) разбирает только типы 2
|
||
(скелет) и 3 (визирь), а всё остальное — включая тип 4 — уводит в
|
||
`guard_names`. SHADOW.DAT в ассетах отсутствует, каталога нет, в
|
||
Makefile правила нет.
|
||
|
||
**Починка** — часть работы по атласу Тени, см.
|
||
[`../docs/shadow_atlas_plan.md`](../../PoP/docs/shadow_atlas_plan.md) Ш0/Ш1:
|
||
довезти SHADOW.DAT и запечь его во вторую половину атласа Тени. Отдельно
|
||
чинить смысла нет: как только Тень поедет своим атласом, ветка выбора
|
||
набора для типа 4 исчезнет вместе с проблемой.
|
||
|
||
**Оговорка.** `pop_guard_set_palette` для типа 4 звать НЕЛЬЗЯ — она
|
||
затрёт палитру Тени палитрой стража (`curr_guard_color` у не-стражей = 0,
|
||
`seg002:183`).
|
||
|
||
### <a id="bug-corpse-air"></a>BUG-CORPSE-AIR. Труп висит в воздухе после падения плиты
|
||
|
||
**Симптом** (найдено пользователем 2026-08-20, ур. 12 к. 15, старт с
|
||
`POS=15`): Кид встал на проваливающийся пол (0,6), плита обрушилась и
|
||
убила его, но ТЕЛО осталось лежать на уровне исчезнувшего пола, а плита
|
||
улетела дальше вниз. Труп висит в пустоте.
|
||
|
||
**Где смотреть.** Кадр смерти — `actions_5_bumped` (см. seq в
|
||
`kid_data`), а гравитация в `check_action` (seg006:05CD) для действия 5
|
||
включается только на кадрах приседа: у оригинала есть отдельные фиксы
|
||
`FIX_DEAD_FLOATING_IN_AIR` (кадры 177..185) именно про этот случай —
|
||
значит **в оригинале 1989 года труп в воздухе тоже висит**, и SDLPoP
|
||
чинит это опциональным фиксом, по умолчанию выключенным.
|
||
|
||
**Отсюда вопрос к решению**, а не сразу правка: чинить ли вообще. Если
|
||
чинить — портировать `fix_dead_floating_in_air` и записать как
|
||
осознанное расхождение в `../docs/impl_diff.md` (мы уже отказались от
|
||
двух других опциональных фиксов SDLPoP в `bump_into_opponent`).
|
||
|
||
**Приоритет низкий:** ситуация возникает только когда труп остаётся на
|
||
проваливающемся полу, играбельности не мешает.
|
||
|
||
---
|
||
|
||
## BUG-SND-FIRSTRUN — при ПЕРВОМ запуске первый эффект слегка искажён
|
||
|
||
**Симптом** (пользователь, 2026-08-20): после загрузки системы, при первом
|
||
запуске программы, самый первый звуковой эффект идёт с искажением; при
|
||
повторном запуске программы искажения нет. Наблюдалось ещё на тестовых
|
||
примерах CBL, до PoP.
|
||
|
||
**Причина, скорее всего, уже устранена.** Аппаратный буфер CBL (256
|
||
слотов) железо не чистит, а запись в порт управления сразу пускает
|
||
воспроизведение с нулевого слота — то есть первые 23,4 мс играет то, что
|
||
лежало в буфере раньше. При первом запуске это остатки после загрузки
|
||
системы, при втором — наша же тишина, поэтому и слышно только один раз.
|
||
Разбор — `../docs/sound_plan.md` §10.2.
|
||
|
||
Лечение сделано в libc: `_cbl_prime` заливает буфер тишиной сразу после
|
||
включения (`cbl_open`). **В MAME проверить нельзя**: эмулируемый буфер
|
||
стартует нулями при двухдополнительном ЦАП, то есть там этот дефект нем
|
||
изначально — записи до и после фикса совпали побитово по огибающей.
|
||
|
||
**Что нужно:** проверка на РЕАЛЬНОМ железе, первый запуск после холодной
|
||
загрузки. Если искажение осталось — значит мусор приходит не из буфера
|
||
CBL, и копать надо в системных буферах DSS.
|
||
|
||
**Приоритет низкий:** один раз за сеанс, на играбельность не влияет.
|
||
|
||
---
|
||
|
||
<a id="pal-l1-after-intro"></a>
|
||
## PAL-L1-AFTER-INTRO. Вход в игру на уровень 1 после интро — палитры нет (чёрный экран)
|
||
|
||
**Наблюдение (пользователь, 2026-08-23).** После заставок (title → intro →
|
||
demo) нажатие клавиши в attract-demo начинает новую игру на уровне 1 —
|
||
экран ЧЁРНЫЙ при живом геймплее. При этом:
|
||
|
||
- Shift+L с уровня 1 на уровень 2 (тот же маршрут через pre-cutscene) —
|
||
палитра появляется (закрыто быстрым фиксом `fade_in_pending` в
|
||
`sprpop.c`, маршрут CUTSCENE → LEVEL_LOAD → PLAYING);
|
||
- Restart Game через меню — уровень 1 с НОРМАЛЬНОЙ палитрой.
|
||
|
||
**Контекст.** Маршрут demo_new_game (`sprpop.c` ~996): dispatch(NEW_GAME)
|
||
→ `pop_level_switch()` → dispatch(LEVEL_READY) → PLAYING. По коду палитру
|
||
там никто не трогает, а демо-ветка перед этим делает
|
||
snapshot+dim(4,0)+fade_in(4) — то есть к моменту новой игры палитра должна
|
||
быть яркой. Контрольный прогон в MAME (сборка с фиксом) демо и уровень 1
|
||
за настройками показал ЯРКУЮ картинку — воспроизведение нестабильно.
|
||
|
||
**Гипотезы для проверки (порядок дешевизны):**
|
||
|
||
1. **Тайминг нажатия.** Клавиша, СОСКИПНУВШАЯ intro (intro_run абортится по
|
||
`kbd_raw_any_down`), остаётся в held-state и первым же кадром демо даёт
|
||
`demo_new_game` — но fade_in демо-ветки блокирующий и завершается ДО
|
||
входа в игровой цикл, так что в эту гипотезу код не верит. Проверить
|
||
трассой: снять обе экранные палитры (`gfx_pal_get` 0 и 1) на первом кадре
|
||
PLAYING.
|
||
2. **Снимок демо-ветки снят с чёрной палитры.** Если нажатие клавиши
|
||
приходится МЕЖДУ `intro_load_palette` (fload+sync+snapshot+dim(4,0)) и
|
||
`intro_restore_game_palette` — аборт оставляет экран чёрным, но restore
|
||
всё равно выполняется после... сверить фактический порядок вызовов
|
||
трассой.
|
||
3. **`font_ready`=0 в момент fade_in** — dim/fade молча выходят (guard в
|
||
`pop_ui.c`). Маловероятно (шрифт грузится в `pop_boot` →
|
||
`pop_menu_init`), но дёшево исключить.
|
||
|
||
**Правильное решение, скорее всего, — этап B из
|
||
[`../docs/palette_plan.md`](palette_plan.md):** явный
|
||
«load → apply/fade» контракт на КАЖДОМ входе в PLAYING, а не точечные
|
||
fade_in в отдельных маршрутах. Точечно чинить только после стабильного
|
||
воспроизведения.
|
||
|
||
---
|
||
|
||
<a id="pal-dungeon-stale"></a>
|
||
## PAL-DUNGEON-STALE. Переход 3→4 (подземелье→дворец): остаётся палитра подземелья
|
||
|
||
**Наблюдение (пользователь, 2026-08-23).** Shift+L с уровня 3 на уровень 4:
|
||
pre-cutscene отыгрывает, уровень 4 начинается — но env/wall-цвета (слоты
|
||
0x50–0x6F) остаются ПОДЗЕМНЫМИ. На переходе 1→2 (оба подземелье) всё
|
||
правильно.
|
||
|
||
**Корень ясен.** Быстрый фикс `fade_in_pending` (`sprpop.c`, маршрут
|
||
CUTSCENE → LEVEL_LOAD → PLAYING) восстанавливает яркость из СНИМКА, а снимок
|
||
делает `intro_restore_game_palette()` ДО `pop_level_switch()`:
|
||
|
||
```
|
||
pop_pre_cutscene_show(pre) → kid.pal + bg(СТАРЫЙ набор) + snapshot + dim(4,0)
|
||
pop_level_switch() → pop_bg_load(дворец): в палитры записаны
|
||
32 записи ДВОРЦА (pal_tile.pal)
|
||
pop_ui_fade_in(4) → восстанавливает ОБЕ палитры ИЗ СНИМКА —
|
||
то есть ЗАЛИВАЕТ НАЗАД подземные 0x50..0x6F
|
||
```
|
||
|
||
fade_in по построению пишет все 256 записей из снимка — он затирает и только
|
||
что загруженный тайлсет. На 1→2 не видно, потому что наборы совпадают.
|
||
|
||
**Как чинить (в порядке правильности):**
|
||
|
||
1. **По плану (этап B `../docs/palette_plan.md`):** разделить «логическую
|
||
палитру» и «яркость» (pop_pal: load_* обновляет снимок ПОСЛЕ bg_load,
|
||
apply/fade только показывает). Тогда снимок на момент fade_in всегда
|
||
актуален.
|
||
2. Точечно: переставить обновление снимка ПОСЛЕ `pop_level_switch()`
|
||
(снять snapshot заново перед fade_in). Работает, но это ещё один пластырь
|
||
поверх модели, которая и так уже в трёх местах по-разному управляет
|
||
яркостью.
|
||
|
||
**Проверять:** Shift+L 3→4 И 5→7 (дворец→подземелье, обратное направление),
|
||
плюс штатный проход уровня 3 через дверь (там же смена набора); после починки
|
||
сверить цвета кладки дворца с `pal_tile.pal` (таблица в palette_plan.md §1.3).
|
||
|
||
**Решение пользователя (2026-08-23): пока не фиксить** — закрыть вместе с
|
||
этапом B плана палитр.
|
||
|
||
---
|
||
|
||
<a id="snd-pace-dead"></a>
|
||
## SND-PACE-DEAD. Пейсинг сцен по насосу CBL не включается — СНЯТ 2026-08-28
|
||
|
||
> **Закрыт удалением самой ветки.** Сцена с Джафаром переведена на ЕДИНЫЕ
|
||
> часы — кадры луча (`intro_pv_animated`): вторая шкала по насосу и гонка,
|
||
> которая её выбирала, убраны целиком. Насос как часы был нужен потому,
|
||
> что кадр сцены рисовался дольше своего интервала; теперь отрисовка
|
||
> разложена по интервалам (`pv_restore_bg`), и счёт кадров честен.
|
||
> Побочный эффект гонки — «кода реплики каждый прогон в другом месте» —
|
||
> исчез; проверено пользователем. Ниже — исходный разбор.
|
||
|
||
**Замер (MAME, 2026-08-27).** В сцене `intro_pv_scene()` зонды показали
|
||
`snd_done == 0` на всём прогоне — то есть ветка `if (snd_pace)`, ради
|
||
которой и заводилась вся арифметика 57/40, **не исполняется ни разу**.
|
||
Сцена идёт по запасной кадровой ветке (`POP_T60` + `gfx_wait_vsync`).
|
||
|
||
**Корень.** `pop_intro.c`:
|
||
|
||
```c
|
||
snd_prev = pop_snd_tick;
|
||
...
|
||
snd_pace = (uint8_t)(pop_snd_tick != snd_prev); /* часы реально идут? */
|
||
```
|
||
|
||
`pop_snd_tick` — счётчик порций насоса, он растёт раз в 11,7 мс. Между
|
||
двумя этими чтениями проходят единицы микросекунд, поэтому значения
|
||
совпадают почти всегда и признак выходит НУЛЕВЫМ, даже когда звук
|
||
исправно играет.
|
||
|
||
**Чем плохо.** Кадровая ветка не имеет догона вообще, и вся работа по
|
||
привязке сцены к часам звука пропадает впустую. Симптом — [PV-RENDER-BOUND](#pv-render-bound).
|
||
|
||
**Как чинить.** Проверять «часы идут» с реальным зазором: снять
|
||
`pop_snd_tick`, подождать кадр (`gfx_wait_vsync()`), снять снова. Одна
|
||
строка, но включение ветки меняет темп ВСЕХ сцен — трогать только вместе
|
||
с прогоном каждой.
|
||
|
||
---
|
||
|
||
<a id="pv-render-bound"></a>
|
||
## PV-RENDER-BOUND. Сцена с принцессой не укладывается в свой бюджет кадра — ИСПРАВЛЕНО 2026-08-28
|
||
|
||
> **Причина оказалась не в цене кадра, а в том, ГДЕ отсчитывался интервал.**
|
||
> Кадр рисовался целиком, и только потом ждали vsync и ещё четыре — то есть
|
||
> отрисовка ПРИБАВЛЯЛАСЬ к делителю. Теперь она разложена на блоки, каждый
|
||
> из которых влезает в кадровый интервал, и после каждого ждём vsync
|
||
> (`pv_restore_bg` + раскладка кадра в `cut_run` / `pre_room_animated` /
|
||
> `intro_pv_draw_frame`); фон восстанавливаем только по картинке, 200 строк
|
||
> вместо 256. Удешевлять сцену не понадобилось: отрисовка ~2 кадра при
|
||
> бюджете 5. Подгонка `PV_MAGIC_LEAD` снята (0) — проверено пользователем:
|
||
> кода попадает в нужный кадр. Ниже — исходный замер.
|
||
|
||
**Замер (MAME, 2026-08-27).** Реплика Джафара `m53` — 1403 порции насоса,
|
||
то есть 16,42 с реального времени. За это время шкала сцены прошла
|
||
**811 тиков вместо 985**: анимация идёт со скоростью ~49 тиков/с при
|
||
номинале 60. Отставание пропорционально времени, поэтому одинаково на
|
||
любом наборе записей (flac / mt32).
|
||
|
||
Зонды исключили другие версии: длина трека отдана насосу полная
|
||
(`pop_mus_left` на пуске = 1403), курсор дошёл до последней страницы
|
||
(10 из 11) — **трек доигрывает целиком, отстаёт именно картинка**.
|
||
|
||
**Следствие, которое важно понимать.** Звук, выданный САМОЙ анимацией
|
||
(створка ворот, дверь покоев), разъехаться с ней не может: у них общая
|
||
шкала. Уезжает только внешний по времени звук — музыка. Поэтому
|
||
эффекты дверей совпадают, а кода реплики приходит раньше молнии.
|
||
|
||
**Обход (сделан 2026-08-27).** `PV_MAGIC_LEAD` в `pop_intro.c` двигает
|
||
жест заклинания целиком (замах, шаг назад, вспышка) на подобранную на
|
||
слух величину. Это подгонка, а не лечение.
|
||
|
||
**Как чинить по-настоящему.** Удешевить кадр сцены: сейчас он
|
||
перерисовывается целиком (`gfx_copy_page` на весь экран + актёры +
|
||
факелы + звёзды). Кандидат — перерисовывать только изменившиеся куски,
|
||
как в игровом слое.
|
||
|
||
---
|
||
|
||
<a id="mus-left-tear"></a>
|
||
## MUS-LEFT-TEAR. Неатомарное чтение `pop_mus_left`
|
||
|
||
**Хазард.** `pop_mus_left` — `volatile uint16_t`, который УМЕНЬШАЕТ
|
||
прерывание насоса (`pop_sfx.c`), а читают его из главного цикла обычным
|
||
чтением: `pop_music_busy()` (`pop_music.c`), `pop_ctrl.c`, `pop_music.c`
|
||
(освобождение трека). На Z80 это две загрузки по байту. Если насос
|
||
успеет между ними при заёме из старшего байта (`0x0100` → `0x00FF`),
|
||
сложится **ложный ноль** — «трек кончился» за 256 порций, то есть за три
|
||
секунды до настоящего конца.
|
||
|
||
**Где выстрелит.** `pop_music_busy()` ждут `hof_wait_music()` (финальная
|
||
тема) и титры («держим логотип до конца темы»): ложный ноль оборвёт
|
||
ожидание на три секунды раньше.
|
||
|
||
**Статус.** Показан рассуждением; в прогоне 2026-08-27 не проявился
|
||
(проверено зондом с устойчивым чтением — результат не изменился). Окно
|
||
узкое: несколько тактов из 245 000 между прерываниями.
|
||
|
||
**Как чинить.** Геттер, читающий до совпадения двух подряд:
|
||
|
||
```c
|
||
uint16_t pop_music_left(void) { uint16_t a, b;
|
||
do { a = pop_mus_left; b = pop_mus_left; } while (a != b); return a; }
|
||
```
|
||
|
||
Вариант с `di`/`ei` тоже годится и стоит те же копейки (десяток тактов
|
||
раз в кадр, задержка прерывания ничтожна против 11,7 мс периода насоса),
|
||
но двойное чтение не трогает состояние прерываний вовсе — на фоне
|
||
[cbl_w0_bios_conflict](../../PoP/roomtest/BUGS_CLOSED.md) это плюс.
|
||
|
||
|
||
## CLIMB-VS-GUARD
|
||
|
||
Кид стоит рядом 2,6, страж — этажом выше на 1,6. Кид тянется подтянуться
|
||
на 1,6.
|
||
|
||
**Оригинал (SDLPoP).** Страж машет мечом, Кид срывается обратно на 2,6
|
||
**без потери HP**.
|
||
|
||
**У нас.** Тот же замах убивает Кида на месте. Если Кид безоружен, это
|
||
мгновенная смерть (ветка «заколот без меча», HP разом в ноль) — и дальше
|
||
тело падает, что до 2026-08-31 давало отдельный симптом «мёртвый
|
||
вприсядку» (закрыт развилкой в `land()`, см. коммит того же дня).
|
||
|
||
**Наблюдение сверх того (пользователь, 2026-08-31).** В этой же связке
|
||
поведение расходится сильнее: страж провалился с ряда 1 на ряд 2 ВМЕСТЕ с
|
||
Кидом — причём Кид ушёл в провал в колонке 7 (там дыра), а страж между
|
||
колонками 5 и 6, где на ряду 1 пол ЕСТЬ. То есть страж проваливается
|
||
сквозь целый пол. Это может быть тем же корнем, что и удар: обоим нужен
|
||
корректный ряд/колонка персонажа в момент подтягивания.
|
||
|
||
**Что уже проверено статически (2026-08-31).** Вся цепочка засчитывания
|
||
удара сверена с оригиналом и совпадает ДОСЛОВНО:
|
||
|
||
- гейт по кадру Кида (нулевой кадр и кадры выхода по лестнице пропускают
|
||
разбор целиком) — `pop_check_sword_hurting`;
|
||
- приоритет стража при встречном попадании (отметка «ранен» у Кида
|
||
снимается) — `pop_check_sword_hurt`;
|
||
- требование ОДНОГО ряда у бьющего и жертвы — `check_hurting`;
|
||
- кадры удара (укол/третий удар), диапазон дистанции (8 для безоружной
|
||
жертвы, 12 для вооружённой, верхняя граница 29);
|
||
- формула расстояния `pop_char_opp_dist` — совпадает с `char_opp_dist`.
|
||
|
||
Значит расходятся не правила, а ВХОДНЫЕ данные: ряд, колонка или X
|
||
персонажа во время виса/подтягивания. Наиболее вероятный кандидат —
|
||
дистанция: висящий вплотную под кромкой в оригинале не дотягивает до
|
||
нижнего порога 8, а у нас пара пикселей переводит его через порог.
|
||
|
||
**Как чинить.** Не гадать — замерить на живой сцене: в момент, когда
|
||
`Opp.action` становится «ранен», снять у обоих `curr_row`, `curr_col`, `x`,
|
||
`direction` и сравнить с теми же величинами в SDLPoP на том же кадре
|
||
(метод — lldb-трасса живого SDLPoP, как в
|
||
[[sdlpop_odd_pixel_char_x]]). Отдельно снять, почему страж теряет опору:
|
||
`get_tile_at_char` под ним в кадре провала.
|
||
|
||
**Уточнение пользователя (2026-08-31, важное).** Оригинальное поведение —
|
||
Кид сорвался без потери HP — ВОСПРОИЗВОДИТСЯ и у нас: исход сильно зависит
|
||
от того, где именно стоит страж. То есть расхождение не абсолютное, а
|
||
пороговое, и это прямо подкрепляет версию про ДИСТАНЦИЮ: пара пикселей
|
||
переводит расстояние через нижний порог засчитывания удара (8 для
|
||
безоружной жертвы), и удар из «мимо» становится смертельным. Значит
|
||
искать надо не потерянную ветку, а сдвиг координаты/порога — сравнивать
|
||
`pop_char_opp_dist` в момент замаха при ОДИНАКОВОЙ расстановке.
|
||
|
||
**ЗВУК КАК ИНДИКАТОР ПУТИ (пользователь, 2026-08-31) — лучшая зацепка.**
|
||
В той же сцене оригинал играет ВЗМАХ (звук 11, «клинок движется»), а у нас
|
||
слышен звук, похожий на упор Кида в стену (звук 8, `bumped`).
|
||
|
||
Набор звуков проверен и НЕ виноват: в `assets/packed/SND/snd.idx` слот 11
|
||
на месте и содержит собственный короткий сэмпл (1280 Б), слот 8 — другой
|
||
(1664 Б). Раскладка не сдвинута.
|
||
|
||
**ПОПРАВКА 2026-09-02 — прежнее толкование звука было ОШИБОЧНЫМ.**
|
||
|
||
Здесь стояло, будто оригинал играет звук 11 «сразу на кадре укола, ДО
|
||
проверки расстояния», то есть при ЛЮБОМ уколе, попал тот или нет. Это
|
||
описание НАШЕГО кода, а не оригинала: ровно так звук стоял у нас, внутри
|
||
ветки «не парировано» и до присвоения `Opp.action = 99_hurt`. В SDLPoP
|
||
(seg002:1039..1044) он стоит в ХВОСТЕ `check_hurting` и защищён условием
|
||
`Opp.action != actions_99_hurt`:
|
||
|
||
if (Char.direction == dir_56_none) return; // Fix looping "sword moving" sound.
|
||
if (Char.frame == frame_154_poking && Opp.frame != frame_161_parry &&
|
||
Opp.action != actions_99_hurt)
|
||
play_sound(sound_11_sword_moving);
|
||
|
||
То есть звук 11 в оригинале — это индикатор **ПРОМАХА**, а не укола: на
|
||
попадании его нет (играет боль), на парировании нет (`Opp.frame` уже 161).
|
||
Расхождение найдено и исправлено 2026-09-02 по симптому «непрерывный взмах
|
||
клинка у безоружного Кида на подъёме» (звук опознан в живой сессии по
|
||
`snd_curr == 11`; указатели насоса при этом сошлись с `snd.idx` байт в
|
||
байт, то есть тракт звука был исправен). Заодно приехал и пропущенный
|
||
ранний выход по `dir_56_none` — тот самый, что автор SDLPoP подписал
|
||
«Fix looping sword moving sound».
|
||
|
||
**Что из прежних выводов уцелело, а что нет.**
|
||
|
||
* УЦЕЛЕЛО: наблюдение «в оригинале слышен 11, у нас 8» — это данные, и они
|
||
остаются. Арифметика приоритетов их подкрепляет: `snd_prio[11] = 0x12`
|
||
(18) против `snd_prio[8] = 0x4B` (75), меньше значит важнее, поэтому
|
||
взмах не мог быть заглушён упором в стену. При ТОГДАШНЕМ коде звук 11
|
||
звучал на любом уколе, значит его отсутствие действительно означало, что
|
||
до кадра укола дело не дошло.
|
||
* ОТПАЛО: вывод «оригинал ведёт стража атакой с промахом, а мы —
|
||
столкновением» опирался на неверную посылку и больше не следует из звука
|
||
сам по себе.
|
||
* УСИЛИЛОСЬ: то, что в оригинале в этой сцене слышен ИМЕННО 11, теперь
|
||
доказывает, что укол у стража СОСТОЯЛСЯ и ПРОМАЗАЛ. А это ровно версия
|
||
про ПОРОГ ДИСТАНЦИИ (см. выше про пару пикселей и нижний порог 8), а не
|
||
про потерянную ветку. Прежняя «версия про столкновение» её вытеснила
|
||
зря — возвращаем дистанцию в главные подозреваемые.
|
||
|
||
**ВНИМАНИЕ тому, кто вернётся к этому багу: диагностика по звуку с
|
||
2026-09-02 ЗНАЧИТ ДРУГОЕ.** Старые заметки прогона будут вводить в
|
||
заблуждение — переснимать сцену заново. Новая таблица:
|
||
|
||
| слышно | что это значит |
|
||
|---|---|
|
||
| 11 (взмах) | укол состоялся и ПРОМАЗАЛ — поведение оригинала в этой сцене |
|
||
| 13 (боль Кида) | укол ПОПАЛ, `Opp.action = 99_hurt` |
|
||
| 8 (упор в стену) | укола не было вовсе, сработало столкновение |
|
||
| тишина | `check_hurting` вышел раньше: меч не вынут / ряды не совпали / кадр не 153-154 |
|
||
|
||
На саму механику урона правка 2026-09-02 НЕ влияет (тронут только звук),
|
||
поэтому расхождение «удар убивает» остаётся открытым как было.
|
||
|
||
**Решение пользователя:** отложено на будущее (2026-08-31) — поведение в
|
||
этой связке расходится широко, чинить нужно целиком, а не по одному
|
||
симптому.
|
||
|
||
|
||
## HP-BAR-RESTART
|
||
|
||
**Симптом (пользователь, 2026-08-31).** Гибель на 2-м уровне, возврат к
|
||
началу по Ctrl+A: строка HP на одном из двух экранов остаётся отрисованной
|
||
по результатам боя. Замечание там же: «в начале уровня она отрисовывается
|
||
только в один экран».
|
||
|
||
**Что уже известно.**
|
||
|
||
Ctrl+A у нас — РЕСТАРТ УРОВНЯ (`sprpop_cold.c`, тот же путь, что пункт меню
|
||
`POP_MENU_RESTART_LEVEL`), а не возврат в заставку.
|
||
|
||
Перерисовка полосы устроена счётчиком страниц, а не флагом:
|
||
`pop_hp_invalidate` ставит `hp_todo = 2`, и `pop_hp_draw` тратит по одной
|
||
странице за кадр (`src/pop_cdraw.c`). Все четыре холодных пути
|
||
(старт уровня, вход в комнату, быстрая загрузка, возврат из меню)
|
||
инвалидацию зовут — механизм на месте.
|
||
|
||
**Главный подозреваемый — зона статус-строки.** Полоса HP делит строку со
|
||
статус-текстом, и пока текст висит (`pop_status_ticks != 0`), стирание
|
||
чистит ТОЛЬКО края — левее `POP_STATUS_L` и правее `POP_STATUS_R`, —
|
||
а середину не трогает, чтобы текст не мигал. При старте уровня текст как
|
||
раз висит («LEVEL 2»), поэтому всё, что от прошлой полосы попало в
|
||
середину, там и остаётся. Полоса стража рисуется справа налево от 314 и
|
||
при большом запасе HP заходит именно в эту незачищаемую зону.
|
||
|
||
**КОРЕНЬ (2026-08-31).** Счётчик считает СТРАНИЦЫ, но тратился по КАДРАМ,
|
||
а это не одно и то же: между двумя вызовами отрисовки переворота может не
|
||
быть, и оба прохода уходили в одну страницу — вторая оставалась с
|
||
делениями прошлого боя. Фикс: проход тратится только когда
|
||
`gfx_get_draw_page()` отличается от страницы прошлого прохода (первый
|
||
проход идёт всегда). Полная чистка всей ширины при инвалидации добавлена
|
||
там же — старая полоса могла заходить под статус-текст, где щадящая чистка
|
||
её не трогала; текст сразу перезапрашивается, чтобы не пропал.
|
||
|
||
**Что проверить при взятии в работу.**
|
||
|
||
1. Значения `POP_STATUS_L`/`POP_STATUS_R` против реальной ширины полос:
|
||
при скольких делениях полоса стража (или Кида) заходит под текст.
|
||
2. Уходят ли оба прохода `hp_todo` в РАЗНЫЕ страницы на старте уровня —
|
||
если между ними нет переворота, обе перерисовки лягут в одну.
|
||
3. Возможное решение: на старте уровня чистить полосу во всю ширину
|
||
независимо от статус-текста (текст всё равно перерисовывается заново
|
||
через `pop_status_invalidate`), либо запоминать максимальную ширину
|
||
прошлой полосы и стирать по ней.
|