Files
Sprinter-SDCC/applications/PoP/roomtest/bug_list.md
T
snark13 6673279cef PoP: скелет уровня 3, цвета стражей, окклюзия соперника; кэш соседних комнат
Скелет (L3-SKEL, ассеты + механика):
- pop_pack_guard.py получил параметр набора (GUARD/SKEL): атлас скелета
  poc/res/skel/g0..g3.atl (28 кадров), палитра — из его res750.pal (на ур. 3
  curr_guard_color = 0, оригинал палитру не подменяет);
- pop_guard_load выбирает набор по tbl_guard_type и перезагружается ПРИ СМЕНЕ
  УРОВНЯ (load_lev_spr, seg000:1092) — без этого скелет рисовался атласом
  стража и был невидим;
- load_frame: charid_4_skeleton идёт по таблице стража (seg006:529), тень —
  только в кадрах 150..189.  Пока ветка была одна (charid_2_guard), скелет
  получал image из таблицы Кида (180 при 28 спрайтах) и не рисовался;
- check_skel (seg002:1042), ветка charid_4 в enter_guard, возрождение в
  комнате 3 при падении (seg002:252), autocontrol_skeleton;
- leveldoor_open (seg007:456) — новый флаг, сбрасывается стартом уровня.

Цвета стражей (BUG-GUARD-COLOR-1, закрыт):
- все 7 палитр res10.bin -> pop_guard_pal.h, заливка 16 слотов по
  guards_color комнаты перед отрисовкой (set_chtab_palette, seg003:257).
  Проверено в MAME: ур. 2 комн. 11 = цвет 1, комн. 7 = цвет 3, полоса HP
  меняется вместе со стражем.  Грабля: gfx_pal_load отдаёт указатель в BIOS,
  а тот читает только #4000-#BFFF — таблицу из банка копируем в стек.

Кэш соседних комнат (BUG-SWORD-GHOST-1, закрыт):
- pop_map кэширует fg соседей слева/справа ЦЕЛИКОМ и резолвит col -10..19.
  Было -2..11, дальше мнимая стена: луч видимости упирался в неё (страж
  после follow_guard в col 12), Кид прятал меч посреди боя и не мог достать
  обратно.  +48 байт W2.

Окклюзия соперника:
- pop_fore_over_char получил проход other_overlay_tile (порядок midtable,
  seg008:1B06) и расширение перебора объединённым прямоугольником
  «персонаж + клинок + брызги» — падающий скелет больше не рисуется поверх
  кладки и верхней грани пола;
- клип полем 192 строк (reset_obj_clip, seg006:0507) для спрайта, клинка
  (общий pop_sword_draw) и брызг — спрайт не залезает на полосу HP;
- ROOMNAV после смерти Кида делает честный pop_start_level: телепорт
  «оживлял» мёртвого мимо старта уровня, оставляя живого скелета рядом с
  вернувшейся кучей костей.

Ассеты чомпера (под L3-CHOMP): весь набор кадров в атласе явным списком
(101-105 низ, 111-113 верх, 106-110 фронт, 114-123 кровь mono-силуэтом) —
render_room анимированные тайлы пропускает, и в атласе не было ни одного.
Число EMM-страниц не изменилось.

Тесты: tests-host все 5 наборов зелёные, t_char вырос до 65 проверок
(резолв колонок за краем комнаты, возрождение скелета); в testkit добавлен
гард «код наехал на данные» (DATA_LOC).

Доски: TASKS.md разнесён на TASKS_OPEN/TASKS_CLOSED, закрытые баги с
разбором корней — в bug_closed.md; заведены DRAW-CHAR (отрисовка одна на всех
Char, как физика после GUARD-PHYS) и L3-COLOR (зелёная кладка уровня 3:
level_var_palettes = ресурс 20, есть в MSDOS/PRINCE.DAT).

В roomtest.c временно оставлен автостоп на падении соперника (отладка
падений скелета) — помечен ВРЕМЕННО.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 22:11:03 +03:00

322 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# roomtest — ОТКРЫТЫЕ баги и незакрытые оптимизации
Только то, что **не закрыто**. Всё закрытое (и, что важнее, разбор корней)
переехало в [`bug_closed.md`](bug_closed.md): прежде чем заводить новый баг,
грепни там по симптому.
Приоритеты работ — в [`TASKS_OPEN.md`](TASKS_OPEN.md) (закрытые задачи с
протоколами — [`TASKS_CLOSED.md`](TASKS_CLOSED.md)), а не здесь. Правило
проекта: механику сверять с `../SDLPoP/src/` ДО кодинга.
**Ревизия списка: 2026-08-07 — прогон ВСЕХ комнат уровней 1 и 2**
(пользователь). Крупных багов нет. Поэтому в [`bug_closed.md`](bug_closed.md)
уехали разом: чек-листы ручной перепроверки фиксов (2026-08-03, обе волны),
таблица обхода 24 комнат уровня 1 и таблица сырых наблюдений того же прогона —
все они закрыты этим проходом. С прогона пришли **три новые записи**, все по
уровню 2 (сырые формулировки — [`bugs_level2.md`](bugs_level2.md)).
| ID | что | тип | статус |
|----|-----|-----|--------|
| [BUG-CHEAT-FIGHT-1](#bug-cheat-fight-1) | `+`/`` в бою с вынутым мечом → Кид теряет управление | Major (чит) | открыт |
| [BUG-GATE-PASS-1](#bug-gate-pass-1) | проход сквозь закрывшуюся решётку (0,9) комнаты 5 | Major | **ждёт сценария**: прогоном 2026-08-07 не воспроизведён |
| [BUG-SPIKE-1](#bug-spike-1) | пики залипают выдвинутыми рядом с Кидом | **низкий** | маловоспроизводимый: ни сценарием, ни попиксельной подгонкой X не поднимается |
| [T-1](#t-1) | пики перерисовываются безусловно | оптимизация | открыт |
| [T-2](#t-2) | Кид перерисовывается в покое | оптимизация | открыт |
---
<a id="уровень-2"></a>
# Уровень 2 — баги с приёмки
Приёмка уровня 2 закрыта ([L2-PASS](TASKS_CLOSED.md#l2-pass)): smoke
2026-08-05 + обход всех комнат 2026-08-07. Карта содержимого уровня (что где
стоит по `res2002.bin`, какие кнопки какие ворота открывают) — там же, по ней
видно, «механика не сработала» это или «так и задумано».
**Закрыто с этой волны:** [BUG-LOOSE-3](bug_closed.md) — чёрный бар под
упавшей плитой-потолком; [BUG-GUARD-DEAF-1](bug_closed.md) — страж не
оборачивался на вернувшегося Кида;
[BUG-GUARD-SPLASH-1](bug_closed.md#bug-guard-splash-1) — «брызги» при
попадании по стражу; [BUG-SWORD-GHOST-1](bug_closed.md#bug-sword-ghost-1) —
меч, спрятанный посреди боя после перехода комнаты (корень — мнимая стена за
краем комнаты в `get_tile`; кэш соседей расширен до полных комнат);
[BUG-GUARD-COLOR-1](bug_closed.md#bug-guard-color-1) — цвет стража и его
полосы HP теперь берётся из `guards_color` уровня (2026-08-07).
Ниже — то, что осталось открытым после прогона 2026-08-07.
<a id="bug-cheat-fight-1"></a>
## BUG-CHEAT-FIGHT-1. Чит `+`/`−` в бою: Кид остаётся в режиме боя и теряет управление
**Наблюдение (пользователь, 2026-08-07).** Если нажать `+`/`` (ROOMNAV,
переход по комнатам) в момент, когда Кид вытащил меч для битвы, — в новой
комнате Кид не управляется: нажатия стрелок игнорируются.
**Корень (прочитан по коду, сверен с seg005).** Чит ROOMNAV
(`roomtest.c:576`) телепортирует и сбрасывает позу и HP —
`enter_room()`, `kid_init(SEQ_STAND, …)`, `pop_kid_hp_reset()`, — но **не
трогает состояние боя**: `Kid.sword` остаётся `SWORD_2_DRAWN`. Диспетчер
`pop_control` (`pop_ctrl.c:578`) при вынутом мече уходит в
`control_with_sword`, а там единственный выход из режима боя —
```c
if (Char.frame == FRAME_171_STAND_WITH_SWORD) { /* seg005:987 */
Char.sword = SWORD_0_SHEATHED;
seqtbl_offset_char(SEQ_92_PUT_SWORD_AWAY);
}
```
Стража в новой комнате нет (`can_guard_see_kid` = 0), кадр после
`kid_init(SEQ_STAND)` — обычная стойка, а не 171, поэтому не срабатывает ни
`swordfight()`, ни ветка «убрать меч»: `control_with_sword` каждый кадр
не делает НИЧЕГО, и ввод не доходит до движения. Заклинивание вечное.
Это баг **нашего чита**, а не порта: в оригинале телепорта между комнатами
нет, и в режим боя без соперника попасть нечем.
**Как чинить.** В ветке ROOMNAV телепорт трактовать как выход из боя (то же
самое, что делает `pop_start_level`): `Kid.sword = SWORD_0_SHEATHED`,
`holding_sword = 0`, сбросить `offguard`/`guard_refrac` и состояние стража
(`pop_guard_reset()` вызывается по входу в комнату — проверить, что он
обнуляет `Opp`). Меч в инвентаре (`pop_have_sword`) при этом НЕ терять.
Второй кандидат на ту же болезнь — чит `K` (убить стража) в момент, когда
Кид в стойке с мечом, но не в кадре 171: там оригинал сам доводит до 171
через `swordfight`, так что проверить сценарием, а не менять вслепую.
<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>
# Уровень 3 — не баги, а неначатые задачи
Smoke-прогон уровня 3 (пользователь, 2026-08-05) дал четыре наблюдения. Два
были настоящими багами и **закрыты в тот же день** (разбор — в
[`bug_closed.md`](bug_closed.md): **BUG-JUMPWALL-1** и **BUG-SEAM-WEDGE-1**).
Оставшиеся два — не баги, а неначатые задачи:
| наблюдение | что это на самом деле |
|------------|------------------------|
| к.22: чомпер (2,6) не анимируется и **вообще не рисуется** | [L3-CHOMP](TASKS_OPEN.md#l3-chomp) — механики чомперов НЕТ. В таблице тайлов `pop_bg.c:211` строка `12 chomper` рисует только основание, правую грань и низ; самих челюстей (спрайт из `chtab`, `draw_tile_anim` seg008) нет вовсе. Так и должно выглядеть до порта |
| к.10: скелет не оживает | [L3-SKEL](TASKS_OPEN.md#l3-skel) — `check_skel` (seg002:0E1F) не портирован. **И комната другая:** тайл `skeleton(21)`, который оживает, лежит в **к.1 (1,5)** — это `skeleton_room=1, skeleton_column=5, skeleton_row=1`. Ещё два скелета уровня (к.17 (2,7), к.19 (2,2)) — просто декорация, они не оживают никогда. Плюс условие: скелет встаёт, только когда **дверь уровня уже открыта** и Кид стоит в колонке 2 или 3 |
Приёмки уровня 3 (полного обхода комнат) ещё не было — она осмысленна только
после L3-CHOMP/L3-SKEL.
---
# Открытые баги уровня 1
<a id="bug-gate-pass-1"></a>
## BUG-GATE-PASS-1. Проход сквозь закрывшуюся решётку — ЖДЁТ СЦЕНАРИЯ
**Статус: наблюдался один раз (2026-08-03), воспроизвести не удалось ни
тогда, ни прогоном всех комнат уровня 1 (2026-08-07).** Заведён, чтобы
наблюдение не потерялось; закрывать нельзя — ни как исправленный, ни как
«не баг», пока нет надёжного сценария. Оговорка: BUG-GATEMOD-1, из-за
которого решётка стартовала не в том состоянии, с тех пор закрыт, и
наблюдение могло быть его следствием.
**Что наблюдалось.** Комната 5: Кид стоял НА тайле решётки (0,9)
и ждал, пока она опустится. После закрытия пошёл вправо — прошёл в комнату 1
и упал на (1,1).
**Что уже измерено и в чём загвоздка.** Сразу после наблюдения повторить не
получилось: в том же месте Кид стоит на `x = 196`, `col = 9`, и решётка его
ДЕРЖИТ — то есть штатно.
Арифметика оригинала объясняет разницу. `is_obstacle` (seg004) ставит
плоскость блокировки в `x_bump[col + FIRST_ONSCREEN_COLUMN] + TILE_MIDX`, для
колонки 9 это **x = 205**. При этом «колонка 9» по `get_tile_div_mod_m7`
это `x ∈ [191, 205)`. Пока `curr_col == 9`, Кид гарантированно левее
плоскости и обязан блокироваться; чтобы пройти, он должен оказаться **правее
205**, то есть уже на дальней стороне решётки, — и тогда уход вправо законен:
решётка закрылась у него за спиной, в оригинале она блокирует плоскость, а не
весь тайл.
Отсюда рабочая гипотеза: в момент наблюдения Кид стоял правее 205 (успел зайти
по тайлу дальше, пока решётка была поднята), и поведение штатное. Но повторить
эту позу и снять `x` пока не удалось, поэтому гипотеза НЕ подтверждена.
**Что снять в следующий раз** (без этих чисел вопрос не закрыть):
1. `Kid.x` и `Kid.curr_col` в момент, когда решётка уже закрылась, а Кид ещё
стоит на её тайле — до шага вправо;
2. модификатор решётки (openness) комнаты 5, тайл 9 — `can_bump_into_gate()`
считает её препятствием только пока `(modif >> 2) + 6 < char_height`, то
есть пока она опустилась достаточно низко относительно РОСТА кадра;
3. `Kid.x` покадрово на самом шаге вправо — где именно перестал блокировать.
Быстрый способ снять первое: отладочный стоп-кадр (**1** заморозить, **2**
продолжить), затем чтение `_Kid` из отладчика MAME; подгонка позы по пикселю —
читы `[`/`]`.
**Возможный корень, если гипотеза не подтвердится.** Проверка идёт по колонке,
которая на шве уже принадлежит СОСЕДНЕЙ комнате (`curr_row_coll_room[]` в
оригинале); у нас межкомнатная коллизия на шве — исторически проблемное место
(ср. закрытый BUG-SEAM-PINGPONG). Второй кандидат — `char_height` в
`can_bump_into_gate()`: если он берётся не от того кадра, решётка может
перестать считаться препятствием раньше времени.
---
# Оптимизация отрисовки (записано 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>
## T-2. Idle-skip: не перерисовывать Кида, когда ничего не происходит
**Сейчас:** `kid_heal``kid_draw``pop_fore_over_kid` идут каждый кадр,
даже когда Kid стоит и в его тайлах ничего не меняется. Это ровно поведение
оригинала (`draw_game_frame`, seg000:917 — `draw_moving()` + `draw_tables()`
безусловно), но у него это дёшево, а у нас нет.
**Надо:** пропускать heal+draw Кида, когда кадр/поза/координаты не менялись
и в его тайлах нет активной анимации.
**Осторожно (дабл-буфер):** пропускать можно **не раньше второго подряд**
неизменного кадра — иначе одна из двух страниц останется со старым
содержимым. Условие «обе страницы уже получили это состояние».
**Связь с T-1:** после T-2 пики отпадают сами — раз Кида не перерисовываем,
heal'а нет, стирать пики нечем, редрой не нужен.
**Связь с KBD-1:** это ещё и минус DI-окна в самых спокойных кадрах — ровно
там, где тапают Shift+стрелку (остаток KBD-1 отложен, см.
[`TASKS_CLOSED.md`](TASKS_CLOSED.md#kbd-1)).
---
## Заметки (отладка)
- Тестовые клавиши осторожного шага: **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](#bug-cheat-fight-1).
- Комнаты **13, 18, 24 уровня 1 недостижимы** в обычной игре — это свойство
данных уровня (разбор — «НЕ БАГИ» в [`bug_closed.md`](bug_closed.md));
приоритет багов в них низкий. Аналогично 23/24 на уровне 3.