747783c422
Регрессия 4086dde: тот коммит перенёс шаг постраничной подкачки «в паузу
кадра» — внутрь цикла ожидания тика насоса — и одновременно ввёл защиту от
промотки (snd_want подтягивается к snd_done). Вместе это убило подкачку:
паузы в этой сцене почти нет, цикл ожидания выходит сразу, шаг не
вызывается. Реплики Джафара не успевали загрузиться к своему тику,
pop_music_play() молча ничего не делал, и после первой реплики сцена шла
в тишине.
Замер в MAME (адреса статиков pop_music.c — от public _pop_mus_page по
смещениям из .sym): до фикса ld_next стоял на нуле 20 секунд при активной
загрузке; после — 11 страниц из 11, mus_ready=1, и звучат все три реплики
m50 → m53 → m52.
Фикс: один безусловный load_step в кадре сцены; шаг в паузе оставлен, он
бесплатный, когда время есть.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1150 lines
88 KiB
Markdown
1150 lines
88 KiB
Markdown
# roomtest — ОТКРЫТЫЕ баги и незакрытые оптимизации
|
||
|
||
**Здесь ТОЛЬКО незакрытое.** Всё закрытое (и, что важнее, разбор корней)
|
||
живёт в [`BUGS_CLOSED.md`](BUGS_CLOSED.md) — прежде чем заводить новый баг,
|
||
грепни там по симптому. Сырые формулировки пользователя с прогонов —
|
||
[`bugs_level1.md`](bugs_level1.md) / [`bugs_level2.md`](bugs_level2.md).
|
||
|
||
Приоритеты работ — в [`TASKS_OPEN.md`](TASKS_OPEN.md) (закрытые задачи с
|
||
протоколами — [`TASKS_CLOSED.md`](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) | зацеп в падении срабатывает менее стабильно, чем в оригинале | **после всех уровней** | открыт: подозрение на клавиатурный модуль, не на физику |
|
||
| [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-24) |
|
||
| [L10-BUTTON-DEBRIS](#l10-button-debris) | ур.10 к.1: плита упала на кнопку (1,8) — щебень есть, а решётки закрыты | кнопки/ворота | открыт: **не воспроизводится**, проверено многое (см. разбор) |
|
||
| [LOOSE-SEAM-ANIM](#loose-seam-anim) | плита в шве (col −1), уроненная в прошлой комнате, исчезает без анимации падения | **мелкий** | открыт по решению пользователя: «пока пусть будет так» |
|
||
|
||
---
|
||
|
||
<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-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="cutscene-ltr"></a>
|
||
## CUTSCENE-LTR. Переходы story через экран TITLE — ЗАКРЫТ 2026-08-26
|
||
|
||
Разбор и что именно добавилось — [`BUGS_CLOSED.md`](BUGS_CLOSED.md#cutscene-ltr).
|
||
|
||
---
|
||
|
||
<a id="pv-music-stall"></a>
|
||
## PV-MUSIC-STALL. В сцене с принцессой звучала только первая реплика — ЗАКРЫТ 2026-08-26
|
||
|
||
Разбор — [`BUGS_CLOSED.md`](BUGS_CLOSED.md#pv-music-stall).
|
||
|
||
---
|
||
|
||
<a id="grab-kbd-timing"></a>
|
||
## GRAB-KBD-TIMING. Зацеп в падении срабатывает НЕ ТАК СТАБИЛЬНО, как в оригинале — РАЗБОР ПОСЛЕ ВСЕХ УРОВНЕЙ
|
||
|
||
> **Решение пользователя (2026-08-12): отложено до готовности всех уровней.**
|
||
> Механика работает, это вопрос ощущения, а не проходимости.
|
||
|
||
**Наблюдение (пользователь).** Вход на уровень 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`, для roomtest 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`](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-roomtest/roomtest.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` в `roomtest.c`).
|
||
Известный баг этого чита закрыт — [BUG-CHEAT-FIGHT-1](BUGS_CLOSED.md#bug-cheat-fight-1).
|
||
- Комнаты **13, 18, 24 уровня 1 недостижимы** в обычной игре — это свойство
|
||
данных уровня (разбор — «НЕ БАГИ» в [`BUGS_CLOSED.md`](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`](../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` в
|
||
`roomtest.c`, маршрут CUTSCENE → LEVEL_LOAD → PLAYING);
|
||
- Restart Game через меню — уровень 1 с НОРМАЛЬНОЙ палитрой.
|
||
|
||
**Контекст.** Маршрут demo_new_game (`roomtest.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`](../docs/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` (`roomtest.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 плана палитр.
|