# 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), уроненная в прошлой комнате, исчезает без анимации падения | **мелкий** | открыт по решению пользователя: «пока пусть будет так» | --- ## 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`) и модификаторы комнат, то есть даёт ровно то состояние, которое сейчас не удаётся собрать искусственно. --- ## 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 — там уже закрытые случаи залипания того же битмапа. --- ## CUTSCENE-LTR. Переходы story через экран TITLE — ЗАКРЫТ 2026-08-26 Разбор и что именно добавилось — [`BUGS_CLOSED.md`](BUGS_CLOSED.md#cutscene-ltr). --- ## PV-MUSIC-STALL. В сцене с принцессой звучала только первая реплика — ЗАКРЫТ 2026-08-26 Разбор — [`BUGS_CLOSED.md`](BUGS_CLOSED.md#pv-music-stall). --- ## 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)) — тогда «менее стабильно» это не клавиатура, а темп, и чинить надо темп. --- # С приёмки уровня 2 (2026-08-07) Всё найденное тем прогоном закрыто, кроме двух записей ниже (`BUG-SPIKE-1` — уровень 2, комната 6). Карта содержимого уровня 2 (что где стоит по `res2002.bin`, какие кнопки какие ворота открывают) — в [`TASKS_CLOSED.md`](TASKS_CLOSED.md#l2-pass), по ней видно, «механика не сработала» это или «так и задумано». ## 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` текущего кадра); если разойдутся позиции — это отдельный баг посадки, а пики — его следствие. --- --- # Оптимизация отрисовки (записано 2026-07-29) Не баги — план работ. Оба пункта про одно: у оригинала пометка тайла к перерисовке стоит копейки (бит в таблице, которая всё равно чистится каждый кадр), а у нас каждая такая пометка превращается в реальный heal (копию из ОЗУ-копии акселератора) плюс блиты. Поэтому буквальный порт «перерисовываем безусловно» корректен, но дорог. ## 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, отдельно не окупается. --- ## 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. --- ## 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`), а зелье — переставить в порядке обхода так, чтобы оно рисовалось ПОСЛЕ факела. --- ## 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»; сравнить число нарисованных кусков с числом уникальных тайлов. Если дубль мал — оставить как есть и закрыть запись. --- --- ## 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. Симптом редкий (пользователь поймал дважды). ## 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. --- ## 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`, а не умереть; падение на стража сверху по-прежнему безболезненно (это оригинал, не баг). --- ## 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)), плита-потолок, рестарт уровня после смерти на дрожащей плите. --- ## 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; в оригинале плита в шве дрожит и роняет кусок так же, как в своей комнате. --- ## 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; за передней колонной Кид уходит и сейчас — это не должно сломаться. --- ## 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` на кусок за кадр — при шести падающих плитах это шесть отрисовок тайла. --- ## 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) — там та же техническая сердцевина: привести наши слои в соответствие с таблицами оригинала. --- ## 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` живёт в резиденте. --- ## 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) — иначе поймаем только половину. ### 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`). ### 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. **Приоритет низкий:** один раз за сеанс, на играбельность не влияет. --- ## 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 в отдельных маршрутах. Точечно чинить только после стабильного воспроизведения. --- ## 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 плана палитр.