d7d18aef77
L7-FEATHER: тип 3 (уровень 7, комната 1) больше не пустой TODO. - pop_map.c: pop_feather — счётчик кадров эффекта (POP_FEATHER_FRAMES = 225, ванильный порог do_timers seg003:0517; привязку к звуку взять неоткуда). fall_accel — ускорение 1 / потолок 4 (seg006:057C) вместо 3 / 33. proc_get_object case 3: взвести эффект + зелёная вспышка на 3 кадра. - pop_kid.c: опкод JMP_IF_FEATHER (0xF7) больше не пропускает адрес безусловно — под пером прыгает по нему, то есть seqtbl уводит падение и удар в ветки stepfloat/bumpfloat (плавные кадры, без урона). - roomtest.c: pop_flash_red -> pop_flash_color (жёлтый/красный/ЗЕЛЁНЫЙ); сброс pop_feather в pop_start_level (seg003:189). - Цвет пузырька по ТИПУ зелья (seg008:652), чего у нас не было вовсе: 3/4 зелёный, 5/6 СИНИЙ, остальные красный. Mono-блиттера с параметром цвета в libbgi нет, поэтому цвет запекается при упаковке: pop_pack_bg.py кладёт те же 7 кадров ещё дважды (id 30..36 зелёные, 40..46 синие), pop_potion_draw выбирает набор. Атлас 23 -> 37 спрайтов (+423 Б). - Синее зелье «−HP» приведено к оригиналу (seg006:1892): своей вспышки не ставит (красный кадр даёт общий flash_if_hurt — иначе экран красился дважды), а на уровне зелий забирает ПОЛОВИНУ запаса HP. Такое зелье стоит уже на пройденном уровне 2 (комната 13, тайл (1,3)), а также ур.8 комн.2 и весь ур.15 — до сих пор оно было красным. Тесты: phys_feather_fall_is_slow_and_harmless (обычное падение с двух рядов разгоняется и стоит HP, под пером скорость <= 4 и HP целое); tests-host 5/5 (1727 в [phys]), size-check OK. Расхождения с ванилью — в docs/impl_diff.md (перо ловит только Кида; синее зелье без своей вспышки). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
468 lines
36 KiB
Markdown
468 lines
36 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) | Кид в проёме ворот виден поверх решётки | окклюзия | открыт: портирован только шовный случай |
|
||
| [FORE-DUP](#fore-dup) | передний слой тайла рисуется дважды при перекрытии объектов | оптимизация | открыт: **сначала замерить**, потом чинить |
|
||
| [DIED-ON-BUTTON](#died-on-button) | `died_on_button` (seg007:776) не портирован | порт | открыт |
|
||
| [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) | зацеп в падении срабатывает менее стабильно, чем в оригинале | **после всех уровней** | открыт: подозрение на клавиатурный модуль, не на физику |
|
||
|
||
---
|
||
|
||
<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="died-on-button"></a>
|
||
## DIED-ON-BUTTON. `died_on_button` (seg007:776) не портирован
|
||
|
||
Обнаружено при разборе [BUG-LOOSE-BUTTON-1](BUGS_CLOSED.md#bug-loose-button-1).
|
||
`check_press` (seg006:1707) разбирает ЛЮБОГО мёртвого `Char`, не только Кида:
|
||
|
||
```c
|
||
if (curr_tile2 == tiles_15_opener || curr_tile2 == tiles_6_closer) {
|
||
if (Char.alive < 0) trigger_button(1, 0, -1); /* жив */
|
||
else died_on_button(); /* мёртв */
|
||
}
|
||
```
|
||
|
||
`died_on_button` на **opener** делает тайл обычным полом и форсирует
|
||
`button_type = tiles_14_debris` — ворота открываются насовсем; на любой другой
|
||
кнопке ставит `tiles_5_stuck` (заклинена, `link_timer == 0x1F`, связь мертва).
|
||
|
||
У Кида эффект живёт до перезапуска уровня (смерть → `is_restart_level` →
|
||
`play_level` заново зовёт `load_level()`, тайлы перечитываются), а вот когда на
|
||
кнопке умирает СТРАЖ — перезапуска нет, и кнопка заклинена до конца уровня.
|
||
|
||
Что нужно: сам порт `died_on_button`, константа `TILE_STUCK = 5` и тайл
|
||
заклиненной кнопки в атласе фона (сейчас его там нет).
|
||
|
||
---
|
||
|
||
<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-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.
|