dc8b2b7115
Четыре разбора с прогонов пользователя; три бага закрыты, четвёртый (BUG-SPIKE-1, пики) заведён с замером и гипотезой. BUG-JUMPWALL-1 (Critical) — недолетевший прыжок проходил СКВОЗЬ стену. check_collisions держал флаги перекрытия ОДНОГО ряда, а оригинал (seg004:0004) — трёх, и move_coll_to_prev (seg004:00DF) берёт «прошлые» флаги из нужного. Наш prev=3 («уже перекрывал») подавлял бамп ровно на кадре смены ряда, а в падении ряд меняется почти каждый кадр — переход 0→1 на стене приходился как раз на него. Порт трёх рядов дословно. Воспроизведено и закрыто на харнессе (новый набор t_wall: свип по 20 стартовым X, 4 давали проход сквозь кладку); 1723 трассы t_phys НЕ изменились — правка поведение-сохраняющая. Живьём подтвердил пользователь. BUG-GUARD-DEAF-1 (Major) — is_guard_notice не взводился НИГДЕ, поэтому неактивный страж не оборачивался на Кида за спиной никогда. Портированы все пять мест оригинала: опкод SOUND в play_seq (звуки 0..2), bumped_sound, мягкое/среднее приземление, обрушенная плита, щелчок кнопки. Ждёт игровой проверки боем в комнате 11 уровня 2. BUG-SEAM-WEDGE-1 — клин кладки в пустом (2,0). Сосед угла снизу-слева лежит в комнате по диагонали (room_BL); мы безусловно считали его стеной, оригинал (load_rowbelow, seg008:368) — только когда такой комнаты нет. Ряд «снизу» стал 11-байтным: [10] = тайл (0,9) диагональной комнаты. BUG-LOOSE-3 — чёрный бар под упавшей плитой-потолком. pop_ceil_bake_empty стирал полосу и восстанавливал только два тайла ряда −1, а в полосу лезет графика соседа слева и верхушки ряда 0. Теперь перерисовываются ряды −1 и 0, колонки col−1..col+1. Проверено попиксельной сверкой с эталонной перерисовкой: 0 различий. Плюс карта связности комнат уровней 1–3 (TASKS.md): на ур. 2 недостижимых нет, на ур. 3 это 23 и 24 — те же односторонние ссылки, что дали 13/18/24 на уровне 1, только комнаты полностью пустые. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
599 lines
49 KiB
Markdown
599 lines
49 KiB
Markdown
# roomtest — ОТКРЫТЫЕ баги и незакрытые оптимизации
|
||
|
||
Только то, что **не закрыто**. Всё закрытое (и, что важнее, разбор корней)
|
||
переехало в [`bug_closed.md`](bug_closed.md): прежде чем заводить новый баг,
|
||
грепни там по симптому.
|
||
|
||
Приоритеты работ — в [`TASKS.md`](TASKS.md), а не здесь. Правило проекта:
|
||
механику сверять с `../SDLPoP/src/` ДО кодинга.
|
||
|
||
Ревизия списка: **2026-08-03** (прогон всех комнат уровня 1). Одиннадцать
|
||
наблюдений с прогона свелись к шести корням; **все шесть закрыты и проверены
|
||
в MAME** — разбор в [`bug_closed.md`](bug_closed.md): BUG-LVLSTATE-1 (уровень
|
||
стал мутабельным), BUG-RESPAWN-1 (респавн перезагружает уровень),
|
||
BUG-DEATH-1 (смерть от меча), BUG-GATE-ANIM-1 (анимация ворот), BUG-COLL-1
|
||
(полный порт `check_collisions`), BUG-STANDUP-1 (лишний guard в
|
||
`bumped_floor`). Клавиатурный BUG-KBD-3 доведён до конца во второй волне
|
||
(см. ниже); из первой волны открытым остался косметический BUG-SEAM-DRAW-1.
|
||
|
||
Ревизия **2026-08-03, вечер** (вторая волна с прогона): все четыре наблюдения
|
||
закрыты — BUG-KBD-4 (фантомный отжим Shift), BUG-RESPAWN-2 (стражи не оживают),
|
||
BUG-DRAWORDER-1 (Кид поверх тела стража) и BUG-LOOSE-2 (осколки только от одной
|
||
из двух плит). У BUG-DRAWORDER-1 остался артефакт «ноги поверх головы»,
|
||
сверенный с SDLPoP и записанный в «НЕ БАГИ». Тогда же закрыт как
|
||
невоспроизводящийся BUG-SEAM-DRAW-1 (Кид в шве рисуется) и заведён
|
||
BUG-GATE-PASS-1 — однократное наблюдение прохода сквозь закрывшуюся решётку,
|
||
которое ЖДЁТ надёжного сценария.
|
||
|
||
| ID | что | тип | статус |
|
||
|----|-----|-----|--------|
|
||
| [BUG-SPIKE-1](#bug-spike-1) | пробег по выдвинутым пикам не убивает: модификатор застревает в «полностью вышли» (h=1), а это в оригинале не смертельно для бегущего | Major | **открыт, замер есть, заведён 2026-08-05** |
|
||
| [BUG-GUARD-DEAF-1](#bug-guard-deaf-1) | страж не оборачивается на вернувшегося Кида: `is_guard_notice` не взводился | Major | **фикс есть, ждёт игровой проверки** |
|
||
| [BUG-GRAB-1](#bug-grab-1) | ур. 2 комн. 9: прыжок с места через 3 тайла — нет зацепа за кромку | Major | **причина найдена (клавиатура, BUG-KBD-5), фикс есть, ждёт игровой проверки** |
|
||
| [BUG-GATEMOD-1](#bug-gatemod-1) | ворота стартуют закрытыми, хотя в уровне открыты | Major | **фикс есть, ждёт проверки** |
|
||
| [Уровень 2](#уровень-2) | остальные баги с приёмки | — | принимаются по ходу |
|
||
| [BUG-GATE-PASS-1](#bug-gate-pass-1) | проход сквозь закрывшуюся решётку (0,9) комнаты 5 | Major | **перепроверить после [BUG-GATEMOD-1](#bug-gatemod-1)** — та же решётка стартовала не в том состоянии |
|
||
| [T-1](#t-1) | пики перерисовываются безусловно | оптимизация | открыт |
|
||
| [T-2](#t-2) | Кид перерисовывается в покое | оптимизация | открыт |
|
||
| [обход 24 комнат](#обход-всех-24-комнат-уровня-1) | таблица заполнена на 5 строк из 24 | ревизия | открыт |
|
||
|
||
---
|
||
|
||
<a id="уровень-2"></a>
|
||
# Уровень 2 — баги с приёмки (заведено 2026-08-04)
|
||
|
||
Уровень 2 заиграл 2026-08-04 (см. [`TASKS.md`](TASKS.md) запись **L2**).
|
||
Пользователь при первом прогоне увидел **несколько багов отрисовки** и
|
||
подаст их списком отдельно — раздел заведён под них заранее, чтобы
|
||
наблюдения не растеклись по переписке.
|
||
|
||
**Закрыто с этой волны:** [BUG-LOOSE-3](bug_closed.md#bug-loose-3) — чёрный
|
||
бар под упавшей плитой-потолком (запечка восстанавливала не всё, что стёрла).
|
||
|
||
**Порядок работы по уровню 2 (2026-08-05):** наблюдения СНАЧАЛА копятся
|
||
здесь целиком — фиксы берём, когда уровень пройден до конца. Причина:
|
||
косметика отрисовки почти всегда сводится к общему корню (ср. волну
|
||
BUG-CEIL-1/2/3 + BUG-OCCL-1), и чинить по одному наблюдению — значит
|
||
чинить один и тот же корень трижды.
|
||
|
||
Карта содержимого уровня (что где стоит по данным `res2002.bin`, какие
|
||
кнопки какие ворота открывают) — в [`TASKS.md`](TASKS.md#l2-pass),
|
||
запись **L2-PASS**: по ней видно, «механика не сработала» это или
|
||
«так и задумано».
|
||
|
||
Перед заведением записи — грепнуть [`bug_closed.md`](bug_closed.md) по
|
||
симптому: почти вся косметика окклюзии уровня 1 уже разбиралась
|
||
(BUG-CEIL-1/2/3, BUG-OCCL-1, BUG-DOOR-CLIP, BUG-DRAWORDER-1), и уровень 2
|
||
вводит ровно три новых фоновых тайла — **большая колонна (низ 8 / верх 9)
|
||
и верх двери (12)**; если артефакт рядом с ними, это первый подозреваемый.
|
||
|
||
<a id="уровень-3"></a>
|
||
# Уровень 3 — баги с приёмки (заведено 2026-08-05)
|
||
|
||
Smoke-прогон уровня 3 (пользователь, 2026-08-05) дал четыре наблюдения. Два
|
||
из них — **не баги, а неначатые задачи**, и записаны как таковые:
|
||
|
||
| наблюдение | что это на самом деле |
|
||
|------------|------------------------|
|
||
| к.22: чомпер (2,6) не анимируется и **вообще не рисуется** | [L3-CHOMP](TASKS.md#l3-chomp) — механики чомперов НЕТ. В таблице тайлов `pop_bg.c:211` строка `12 chomper` рисует только основание, правую грань и низ; самих челюстей (спрайт из `chtab`, `draw_tile_anim` seg008) нет вовсе. Так и должно выглядеть до порта |
|
||
| к.10: скелет не оживает | [L3-SKEL](TASKS.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 |
|
||
|
||
Остальные два были настоящими багами и **закрыты в тот же день** — разбор
|
||
корней в [`bug_closed.md`](bug_closed.md): **BUG-JUMPWALL-1** (флаги
|
||
коллизий считались для одного ряда вместо трёх — бамп терялся на кадре
|
||
смены ряда) и **BUG-SEAM-WEDGE-1** (сосед угла (2,0) безусловно считался
|
||
стеной).
|
||
|
||
<a id="bug-spike-1"></a>
|
||
## BUG-SPIKE-1. Пики не убивают бегущего Кида — ОТКРЫТ, ЗАМЕР ЕСТЬ
|
||
|
||
**Наблюдение (пользователь, 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 построчно и совпадают дословно.
|
||
|
||
**Значит расходится ТАЙМИНГ, а не логика:** у нас пики успевают полностью
|
||
выйти ДО того, как Кид на них вбежит, и он проскакивает мимо смертельного
|
||
окна 1..4. Триггер — `check_spike_below` (seg006:1199), и у нас он берёт
|
||
габарит спрайта из `kid_fp` — метрик ПОСЛЕДНЕГО отрисованного кадра, а
|
||
оригинал берёт `char_x_left/right` ТЕКУЩЕГО кадра (`set_char_collision`).
|
||
Это первый подозреваемый; второй — момент вызова `process_trobs`
|
||
относительно `play_frame` (сколько кадров модификатор успевает пройти,
|
||
пока Кид добегает).
|
||
|
||
**ВТОРОЕ НАБЛЮДЕНИЕ (то же место, сравнение с оригиналом) — оно и уточняет
|
||
гипотезу.** Кид уронил плиту-потолок и спрыгнул вниз; пики выдвинулись и
|
||
«спрятались не все — часть артефактов осталась». Скриншоты рядом:
|
||
[наш](bugscreens/l2-r6-spikes-ours.png) и
|
||
[SDLPoP](bugscreens/l2-r6-spikes-sdlpop.png) в той же позе. У нас из-под
|
||
щебня торчат белые острия, у оригинала пик не видно ВООБЩЕ.
|
||
|
||
Это стыкуется с замером: пока Кид стоит рядом, `check_spike_below` каждый
|
||
кадр зовёт `start_anim_spike`, а тот при отрицательном модификаторе
|
||
переставляет его обратно в 0x8F — то есть пики у нас «залипают выдвинутыми»
|
||
и отсчёт до уборки не доходит. В оригинале в той же позе они убраны,
|
||
значит его `check_spike_below` эту колонку УЖЕ не задевает.
|
||
|
||
**Отсюда единая рабочая гипотеза на оба симптома: наш триггер шире/раньше
|
||
оригинального.** Оригинал берёт габарит ТЕКУЩЕГО кадра
|
||
(`char_x_left/right`, их ставит `set_char_collision` в этом же кадре), мы —
|
||
`kid_fp`, метрики ПОСЛЕДНЕГО ОТРИСОВАННОГО кадра. Если наш диапазон
|
||
колонок хоть на пиксель шире, получаем ровно это: пики выходят до подхода
|
||
Кида (он проскакивает окно 1..4 → не гибнет) и не убираются, пока он рядом
|
||
(→ белые остатки на экране).
|
||
|
||
**Что снять при разборе:** покадрово `room_modif[13]` вместе с `Kid.x`,
|
||
`Kid.frame` и вычисленными `c0..c1` на подходе и пробеге через тайл; тот же
|
||
прогон в SDLPoP с печатью `left_checked_col`/`right_checked_col`. Сверять
|
||
надо не результат, а ДИАПАЗОН КОЛОНОК кадр в кадр.
|
||
|
||
**Не регрессия правок 2026-08-05:** ни трёхрядные флаги коллизий, ни
|
||
`is_guard_notice`, ни правки отрисовки в путь пик не входят (`check_spiked`
|
||
сам зовёт `get_tile_at_char`, модификаторы читает из `pop_trob_modif`).
|
||
|
||
---
|
||
|
||
<a id="bug-guard-deaf-1"></a>
|
||
## BUG-GUARD-DEAF-1. Страж не оборачивается на вернувшегося Кида — ФИКС ЕСТЬ, ЖДЁТ ИГРОВОЙ ПРОВЕРКИ
|
||
|
||
**Наблюдение (пользователь, 2026-08-05).** Уровень 2, комната 11, идёт бой
|
||
со стражем. Страж **выталкивает Кида в правую комнату 22**. Кид заходит
|
||
обратно в комнату 11 — страж стоит на (1,1), **повёрнут налево и Кида не
|
||
видит**. Предположение пользователя: это часть большой задачи «полностью
|
||
переделать поведение стража по образцу Кида».
|
||
|
||
**Диагноз: большая переделка тут ни при чём, корень маленький и точный.**
|
||
Само возвращение стража в исходную позу — ПРАВИЛЬНОЕ поведение, порт верен;
|
||
не хватает ровно одного сигнала.
|
||
|
||
Разбор по SDLPoP:
|
||
|
||
1. **Выход Кида из комнаты «усыпляет» стража — так и в оригинале.**
|
||
`leave_guard` (seg002:02F5) складывает стража обратно в данные уровня
|
||
(тайл, `x`, **направление**, skill, HP), а `enter_guard` (seg002:0112)
|
||
при возврате поднимает живого стража **с УБРАННЫМ мечом**
|
||
(`sword_0_sheathed` + `seq_77_guard_stand_inactive`) и обнуляет
|
||
`is_guard_notice`/`guard_refrac`. То есть страж после возврата ВСЕГДА
|
||
неактивен и смотрит в запомненную сторону — у нас так же
|
||
(`pop_guard_enter`, `pop_guard.c:127`).
|
||
2. **Дальше решает `autocontrol_guard_inactive` (seg002:0876), и он Кида за
|
||
спиной ИГНОРИРУЕТ.** Кид вернулся справа, страж смотрит влево →
|
||
`char_opp_dist()` отрицательна → ветка `else if (distance < 0) return;`.
|
||
Единственный выход из неё — флаг **`is_guard_notice`**: «Кид нашумел».
|
||
При нём страж оборачивается (`move_4_down`). Направление в
|
||
`check_can_guard_see_kid` НЕ участвует вовсе, так что «не видит» — это
|
||
не про луч видимости, а именно про этот флаг.
|
||
3. **У нас `is_guard_notice` не взводится НИГДЕ.** `grep` по всем исходникам
|
||
roomtest: объявление (`pop_guard.c:20`), сброс (`pop_guard.c:47`) и одно
|
||
чтение (`guards.c:171`). Присваивания `= 1` нет ни одного — флаг мёртв,
|
||
поэтому неактивный страж не обернётся НИКОГДА, что бы Кид ни делал.
|
||
|
||
**Где его взводит оригинал** (это и есть объём фикса):
|
||
|
||
| место | событие |
|
||
|-------|---------|
|
||
| `seg006:633..641` — `play_seq`, опкод SOUND | звук `SND_SILENT`(0), `SND_FOOTSTEP`(1), `SND_BUMP`(2). `SND_DRINK`(3)/`SND_LEVEL`(4) — НЕ шум. Главный источник: шаги бега/приземления |
|
||
| `seg004:05F1` `bumped_sound` | удар в стену |
|
||
| `seg005:185,195` | мягкое и среднее приземление (**только `charid_0_kid`**) |
|
||
| `seg006:1294`, `seg006:1734` | Кид обрушил loose-плиту (зацепом и наступив) |
|
||
| `seg007:766` | нажата кнопка |
|
||
|
||
У нас опкод SOUND в `play_seq` (`pop_kid.c:426`) просто съедает байт
|
||
аргумента — звука нет, и флаг вместе с ним потерялся. Именно эта строка —
|
||
90 % фикса: `SND_SILENT` называется «silent» потому, что звука не издаёт,
|
||
**но стражи его всё равно замечают**, и в `seqtbl` он стоит, например, в
|
||
`ready` (доставание меча).
|
||
|
||
**Ожидаемое поведение после фикса.** Кид возвращается в комнату 11 бегом →
|
||
первый же `SND_FOOTSTEP` взводит флаг → страж оборачивается и достаёт меч.
|
||
Стоя на месте, Кид может подкрасться к стражу со спины — это НЕ баг, а
|
||
механика оригинала.
|
||
|
||
**Оговорка (не проверено вживую).** Разбор построен на том, что страж после
|
||
возврата неактивен (меч убран, кадр 166). Это следует из кода
|
||
`pop_guard_enter`, но в MAME не снималось; если окажется, что меч у него
|
||
ОБНАЖЁН, то работает другая ветка (`autocontrol_guard_active`, где
|
||
`can_guard_see_kid == 2` направления не спрашивает) — и тогда корень другой.
|
||
Снять при фиксе: `Guard.sword`, `Guard.frame`, `can_guard_see_kid` сразу
|
||
после входа в комнату.
|
||
|
||
**Фикс (2026-08-05): все пять мест портированы.** Опкод SOUND в `play_seq`
|
||
(`pop_kid.c`) взводит флаг для звуков 0..2; `bumped_fall`/`bumped_floor`,
|
||
мягкое и среднее приземление, обрушенная плита (все три пути `check_press`)
|
||
— в `pop_map.c`; щелчок кнопки — в `pop_trob.c`. Заглушка `is_guard_notice`
|
||
добавлена в `tests-host/stubs.c` (автопилота стража в наборах нет).
|
||
|
||
**Как проверить в игре:** комната 11 уровня 2, дать стражу вытолкнуть Кида
|
||
вправо в комнату 22 и вернуться бегом — на первом же шаге страж обязан
|
||
обернуться и достать меч. Подкрасться к нему СТОЯ (не двигаясь) по-прежнему
|
||
можно — это механика оригинала, а не баг.
|
||
|
||
---
|
||
|
||
<a id="bug-grab-1"></a>
|
||
## BUG-GRAB-1. Прыжок с места через провал в 3 тайла: зацепа нет — **ПРИЧИНА НАЙДЕНА, ФИКС ЕСТЬ, ЖДЁТ ИГРОВОЙ ПРОВЕРКИ**
|
||
|
||
> **Итог 2026-08-05.** Физика тут ни при чём — виновата клавиатура.
|
||
> Зажатый Shift снимался автоповтором зажатой стрелки, поэтому к кадрам
|
||
> 102…106 (окно зацепа) движок видел Shift отпущенным. Полный разбор и
|
||
> фикс — [BUG-KBD-5](bug_closed.md#bug-kbd-5); поведение Shift в MAME
|
||
> проверено замером карты `_kbdraw_down`. Осталось подтвердить сам зацеп
|
||
> живой игрой; версии 2 и 3 ниже проверять только если он всё ещё не выйдет.
|
||
|
||
|
||
**Симптом.** Уровень 2, комната 9. Перепрыгнув на (1,1), Кид должен
|
||
вернуться обратно: разбегаться негде, поэтому он встаёт на самый край
|
||
плиты, прыгает с места и **цепляется руками за (1,5)**, после чего
|
||
подтягивается. У нас Кид с зажатым Shift всё равно срывается.
|
||
|
||
**Что уже точно известно (и не надо перепроверять).**
|
||
|
||
1. **Физика прыжка у нас совпадает с оригиналом кадр в кадр.** Сверено по
|
||
логу SDLPoP против трассы харнесса при одинаковом старте `x=95`:
|
||
|
||
```
|
||
кадр 16 18 22 23 24 25 102 103 104 105
|
||
SDLPoP 95 97 105 112 121 126 128 130 131 133
|
||
наш 95 97 105 112 121 126 128 130 131 133
|
||
```
|
||
Совпадает и по `y`, и по колонке/ряду, и по приземлению на 107–108.
|
||
|
||
2. **В оригинале зацеп срабатывает на кадре 106, а не 102..105.**
|
||
`check_grab` зовётся из ДВУХ мест: ветка «в воздухе» в `check_action`
|
||
(кадры 102..105) и `do_fall` (seg005) для `actions_4_in_freefall`.
|
||
Успешная попытка из лога:
|
||
|
||
```
|
||
GRAB try f=106 x=135 y=166 col=4 row=2 fall_y=18
|
||
GRAB probe x=127 col=4 through=0 front_above=3 modif=0
|
||
GRAB can_grab=1
|
||
GRAB OK dist=9
|
||
-> f=91 x=136 y=181 col=5 row=2 act=2 (повис)
|
||
```
|
||
Наш `do_fall` (`pop_map.c`) `check_grab()` из этой ветки тоже зовёт —
|
||
то есть структура на месте, расходится что-то внутри.
|
||
|
||
3. **По харнессу зацеп у нас РАБОТАЕТ**: окно стартовых `x = 91…95`, и
|
||
короткий шаг ставит Кида ровно туда (91 после первого нажатия, 95 после
|
||
второго). Зафиксировано тестом `t_grab`.
|
||
|
||
**Отсюда главный вопрос был: почему харнесс говорит «работает», а живая
|
||
машина — «нет».** Расхождение между ними и оказалось уликой; версии
|
||
выдвигались по убыванию правдоподобия, и сработала первая:
|
||
|
||
- **Shift не доезжает до движка — ПОДТВЕРЖДЕНО, это и была причина.**
|
||
Харнесс подменяет клавиатуру и потому этот путь не проверяет вовсе, а у
|
||
нас есть история проблем ровно с «Shift + стрелки» (KBD-1, BUG-KBD-3/4).
|
||
Замер в MAME: при зажатом Shift и зажатой стрелке бит `LSh` в
|
||
`_kbdraw_down` стоял в нуле. Разбор — [BUG-KBD-5](bug_closed.md#bug-kbd-5).
|
||
- **Сцена харнесса не равна комнате 9.** Там изолированная комната
|
||
(соседи — стена), а в игре слева комната 8; кромки шва участвуют в
|
||
`get_tile`. Проверять чтением `Kid.x` в момент прыжка: попал ли он в
|
||
окно 91…95 вообще.
|
||
- **Расхождение в `check_grab`.** Наш вариант зовёт `determine_col()`
|
||
там, где оригинал зовёт `load_fram_det_col()` (перезагрузка кадра +
|
||
колонка). Для Кида это обычно одно и то же (`cur_frame` в фазе физики
|
||
принадлежит ему), но проверить стоит.
|
||
|
||
**Инструменты готовы.** В SDLPoP включена отладка (пометка `DBG-GRAB`):
|
||
`JMP` — покадровая трасса прыжка/падения/виса, `GRAB try|probe|fail|OK` —
|
||
вход в `check_grab` и причина отказа. Снимается поиском по `DBG-GRAB`.
|
||
|
||
**Найдено попутно, отдельным наблюдением.** После касания площадки на
|
||
кадрах 107–108 (x=140) оба движка снова падают, но X расходится: SDLPoP
|
||
уводит Кида на 134 (колонка 4), мы — на 141 (колонка 5). Похоже на разную
|
||
отработку `in_wall()` у стены (2,7). На зацеп не влияет.
|
||
|
||
---
|
||
|
||
<a id="bug-gatemod-1"></a>
|
||
## BUG-GATEMOD-1. Ворота стартуют закрытыми, хотя в уровне открыты — **ФИКС ЕСТЬ, ЖДЁТ ПРОВЕРКИ**
|
||
|
||
**Симптом (пользователь, 2026-08-04).** Уровень 2, комната 13: решётка
|
||
между (2,5) и (2,6) обязана быть ОТКРЫТА в начале и захлопнуться, когда Кид
|
||
нажмёт кнопку (2,4) — после этого назад дороги нет. У нас она закрыта
|
||
сразу, кнопка бессмысленна, проход не работает.
|
||
|
||
**Корень.** Модификатор тайла в ФАЙЛЕ уровня и модификатор в РАНТАЙМЕ —
|
||
разные величины; оригинал переводит их при загрузке в `load_alter_mod`
|
||
(seg008:198E), которую зовёт `alter_mods_allrm` из `load_level`:
|
||
|
||
```c
|
||
case tiles_4_gate: *modif = (*modif == 1) ? 188 : 0; break;
|
||
case tiles_11_loose:*modif = 0; break;
|
||
case tiles_10_potion:*modif <<= 3; break;
|
||
```
|
||
|
||
Наш `pop_trob_modif` портировал из неё **только зелье**. Для ворот
|
||
`bg = 1` — это «открыты» (Table 8 спецификации DAT), а в рантайме открытость
|
||
измеряется высотой подъёма 0..188; мы клали в рантайм-модификатор сырую
|
||
единицу, то есть «закрыты на 1/188».
|
||
|
||
**Фикс.** Ветки ворот и loose дописаны в ленивую инициализацию
|
||
`pop_trob_modif` (`pop_trob.c`). Ветка СТЕН не портируется намеренно: у нас
|
||
`pop_bg` считает связи кладки по типам соседей прямо при отрисовке
|
||
(`wall_modifier`), сохранённый модификатор стены не читается.
|
||
|
||
**Что это ещё задевает.** Решётка (0,9) комнаты 5 уровня 1 тоже имеет
|
||
`bg = 1`, то есть обязана стартовать открытой — Кид сваливается в комнату 1
|
||
именно через неё, и она захлопывается у него за спиной. Закрывает её
|
||
стартовый триггер `do_startpos` (seg003:167): для уровней с
|
||
`tbl_entry_pose == 1` оригинал ВИРТУАЛЬНО ЖМЁТ кнопку комнаты 5 (0,2) —
|
||
|
||
```c
|
||
// Special event: press button + falling entry
|
||
get_tile(5, 2, 0); trigger_button(0, 0, -1); seqtbl_offset_char(seq_7_fall);
|
||
```
|
||
|
||
Замер в SDLPoP (лог по кадрам): `gate(5,0,9)` идёт `188 → 148 → 88 → 8 → 0`,
|
||
шаги 40/60/80 — это `gate_close_speeds`, то есть быстрое закрытие
|
||
(`trigger_gate` вернул тип 3). У нас этот триггер портирован, и закрытие
|
||
работает.
|
||
|
||
Полный список ворот с `bg = 1`: ур. 1 комн. 5 (0,9); ур. 2 комн. 13 (1,5).
|
||
Остальные ворота уровней 1–3 имеют `bg = 2` → 0, и для них ничего не
|
||
меняется (при модификаторе 2 и 0 и отрисовка, и `can_bump_into_gate` дают
|
||
одно и то же).
|
||
|
||
**Побочная находка: чит обхода комнат отматывал мир.** После фикса
|
||
пользователь увидел «ворота снова открылись», пройдя `+` в комнату 2 и `-`
|
||
обратно. Причина не в воротах: `ROOMNAV` звал `pop_trob_reset()` перед
|
||
`enter_room`, тот обнулял `room_seen[]`, и `pop_trob_modif()` перечитывал
|
||
модификаторы из уровня заново — то есть чит откатывал открытые/закрытые
|
||
ворота, выдвинутые пики и нажатые кнопки. Пока ворота с `bg=1` ошибочно
|
||
стартовали закрытыми, откат был не виден. `pop_trob_reset()` из навигации
|
||
убран: она обязана только телепортировать, исходное состояние даёт
|
||
перезапуск уровня. Замер, который это показал: `room_modif` комнаты 5
|
||
после `+`/`-` = `00 00 0B 00 09 00 08 01 00 BC` — последний байт 0xBC = 188,
|
||
файловое значение.
|
||
|
||
---
|
||
|
||
|
||
<a id="ручная-перепроверка-2026-08-03"></a>
|
||
# Ручная перепроверка фиксов (2026-08-03)
|
||
|
||
Шесть корней закрыты автоматической проверкой в MAME (мост `mame-z80`:
|
||
чтение `_Kid`, потиковые трассы, скриншоты) — этого достаточно, чтобы
|
||
показать, что конкретный сценарий больше не воспроизводится, но НЕ
|
||
достаточно, чтобы поймать регрессии в соседней механике. Ниже — сценарии
|
||
ровно в тех формулировках, в которых баги были заведены.
|
||
|
||
Собрано и лежит в образе (`make` + `make hdd` уже прогнаны; libc пересобран,
|
||
`make size-check` без роста).
|
||
|
||
| # | что проверить | ожидаемо |
|
||
|---|---------------|----------|
|
||
| 1 | комната 22: выпить зелье (0,6), выйти в 16/23 и вернуться | кувшина нет, пузырька над пустым местом нет |
|
||
| 2 | то же для комнат 14 (0,5) и 17 (2,3) | так же |
|
||
| 3 | комната 15: подобрать меч (2,2), выйти и вернуться | меча нет; кладка на дальней стене НЕ мигает |
|
||
| 4 | комната 12: разбить плиты (0,1)/(0,2) и потолок в 16, выйти-вернуться | остаются разбитыми, проём не закрывается |
|
||
| 5 | комната 17 из 23: разбить (1,5)/(1,6), выпить зелье (2,3), вернуться | всё остаётся |
|
||
| 6 | **после смерти** зайти в те же комнаты | ВСЁ восстановлено (в оригинале смерть = `load_level`) |
|
||
| 7 | комната 12: разбег в закрытую решётку (0,9) с полушага | не проходит насквозь; перелистывание экрана штатно (см. BUG-SEAM-DRAW-1 в `bug_closed.md`) |
|
||
| 8 | комната 6: бег справа налево от (0,9), длинный прыжок (0,6)→(0,7) | не влетает внутрь стены |
|
||
| 9 | комната 5: с кнопки (0,6) падение на (2,7), присед, вставание | остаётся в комнате 5 (проверено трассой: `fr=111 x=177` → `seq_47` → `fr=15 x=173`) |
|
||
| 10 | комната 5: нажать кнопку (0,4) | поднимаются ОБЕ решётки — (0,5) видно на экране, (0,9) проверять из комнаты 1 |
|
||
| 11 | страж (комнаты 3, 21) убивает Кида | смерть доигрывается, респавн в стартовой позиции уровня; цикла «убил-воскрес» нет |
|
||
| 12 | клавиатура: долгая игра с Shift+стрелка | ↑ и Shift не залипают; осторожный шаг не превращается в бег (доработано во второй волне — п.17) |
|
||
|
||
## Вторая волна (2026-08-03, вечер)
|
||
|
||
| # | что проверить | ожидаемо |
|
||
|---|---------------|----------|
|
||
| 17 | **[BUG-KBD-4]** долго играть Shift+стрелка, много тапов подряд | Shift не «отваливается»; если однажды залипнет — снимается первым же нажатием стрелки |
|
||
| 18 | **[BUG-RESPAWN-2]** убить стража (комнаты 3/21), умереть, вернуться в ту же комнату | страж снова жив, стоит на исходном тайле, HP полные |
|
||
|
||
Отдельно стоит посмотреть **на регрессии от порта `check_collisions`** — он
|
||
трогает всю горизонтальную коллизию: бамп в стену на бегу и в прыжке,
|
||
осторожный шаг у стены, проход через ОТКРЫТЫЕ ворота, разворот в проёме
|
||
решётки, переходы через швы (старый BUG-SEAM-PINGPONG).
|
||
|
||
---
|
||
|
||
# Открытые баги уровня 1
|
||
|
||
<a id="bug-gate-pass-1"></a>
|
||
## BUG-GATE-PASS-1. Проход сквозь закрывшуюся решётку — ЖДЁТ СЦЕНАРИЯ
|
||
|
||
**Статус: наблюдался один раз, воспроизвести повторно НЕ удалось.** Заведён,
|
||
чтобы наблюдение не потерялось; закрывать нельзя — ни как исправленный, ни как
|
||
«не баг», пока нет надёжного сценария.
|
||
|
||
**Что наблюдалось (2026-08-03).** Комната 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.md`](TASKS.md)).
|
||
|
||
---
|
||
|
||
# Обход всех 24 комнат уровня 1 — поиск багов отрисовки
|
||
|
||
Инструмент: `#define ROOMNAV` в `roomtest.c` — `+`/`-` (цифровой блок либо
|
||
`=`/`-` основного ряда) переключают комнату по номеру (1..24, с обёрткой),
|
||
Kid ставится на первый пол, `pop_trob_reset()` возвращает пики/ворота в
|
||
исходное. Номер комнаты — полосками в верхнем борте: слева десятки, справа
|
||
единицы (`||` `||||` = 24). Убирается закомментированием одной строки.
|
||
|
||
Комнаты **13, 18, 24 недостижимы** в обычной игре — это свойство данных
|
||
уровня, разбор в [`bug_closed.md`](bug_closed.md) («НЕ БАГИ»). Приоритет
|
||
багов в них низкий.
|
||
|
||
Таблица заполнена на 5 строк из 24 — доделать в рамках L1-TRIAGE
|
||
([`TASKS.md`](TASKS.md)).
|
||
|
||
| комната | статус | что не так |
|
||
|---------|--------|------------|
|
||
| 1 | пофикшено | падающая плита (2,6): правая грань видна через пол (2,7) и перекрывает его переднюю грань — `mob_render` брал ряд соседа из `m->row` (счётчик, уже ушедший на ряд вперёд), а не из координаты |
|
||
| 2 | | |
|
||
| 3 | | |
|
||
| 4 | | |
|
||
| 5 | пофикшено | прыжок в решётку: Kid оставался стоять на 6 px ВЫШЕ пола и без приземления-приседания — от `bumped()` (seg004) был портирован только хвост (`seq_47`), не хватало `bumped_floor` (прижатие Y к полу + `seq_46_hardbump` на кадрах прыжка 24/25/40..42/102..106) и `bumped_fall` |
|
||
| 6 | | |
|
||
| 7 | | |
|
||
| 8 | | |
|
||
| 9 | сделано | дверь уровня (1,3)-(1,4) рисовалась чёрным проёмом: не был портирован `draw_leveldoor` (seg008:1D29) — створка (слайсы 33 + верх 34), лестница за ней (99/144) и анимация подъёма по кнопке (`animate_leveldoor`, seg007:05F1, modif 0→43). Спрайты 33/34/99/144 добавлены в атлас явным набором (render_room.py дверь не рисует) |
|
||
| 10 | | |
|
||
| 11 | | |
|
||
| 12 | пофикшено | вис/подтягивание на кромке loose-плиты: плита рисовалась ПОД Кидом. Не хватало двух кусков `draw_tile`: (а) `draw_loose` кладёт нижнюю грань плиты И в foretable (поверх персонажа), (б) `draw_tile_base` подставляет верх плиты из `loose_fram_left`, а в нашем midtable-оверлее стоял голый `base_id` (у loose он 0). Голова Кида поверх падающей на него плиты — см. «НЕ БАГИ» в `bug_closed.md` |
|
||
| 13 | недостижима в игре | |
|
||
| 14 | | |
|
||
| 15 | сделано | меч (2,2) не рисовался: тайл 22 в draw_tile_anim не был портирован. Добавлены отрисовка предмета (chtab_1 id 10/11 на draw_main_y−3), подъём по Shift (check_get_item/get_item/do_pickup: присед → seq_91 pickupsword → меч исчезает с пола) и статус `pop_have_sword` |
|
||
| 16 | | |
|
||
| 17 | | |
|
||
| 18 | недостижима в игре | |
|
||
| 19 | | |
|
||
| 20 | | |
|
||
| 21 | | |
|
||
| 22 | | |
|
||
| 23 | | |
|
||
| 24 | недостижима в игре | |
|
||
|
||
---
|
||
|
||
## Заметки (отладка)
|
||
|
||
- Тестовые клавиши осторожного шага: **J** = шаг влево, **L** = шаг вправо
|
||
(эмуляция Shift+стрелка), см. `pop_ctrl.c` `KBD_DBG_STEP*`. Первый шаг в
|
||
сторону = разворот (как в оригинале safe_step), движение со второго.
|
||
- Читы (`pop_cheat.h`): **K** — убить стража, **I** — бессмертие (toggle),
|
||
**S** — выдать меч.
|
||
- Респавн после смерти — по **↑** (или авто через `RESPAWN_DELAY`); ставит
|
||
Kid в стартовую позицию УРОВНЯ (`pop_start_level`, порт do_startpos).
|
||
- **ROOMNAV (`=`/`-`) — тоже наш чит**, которого в оригинале не было, как и
|
||
`S`. Все они со временем съедутся в общий блок читов, разрешаемый в
|
||
настройках; пока просто включены (`pop_cheats = 1` в `roomtest.c`).
|
||
|
||
|
||
---
|
||
|
||
<a id="сырые-наблюдения-прогон-2026-08-03"></a>
|
||
## Сырые наблюдения (прогон 2026-08-03) → корень
|
||
|
||
Формулировки с прогона, как записаны. Закрытые корни — в
|
||
[`bug_closed.md`](bug_closed.md).
|
||
|
||
| # | наблюдение (кратко) | корень | статус |
|
||
|---|---------------------|--------|--------|
|
||
| 1 | кувшин выпит, а пузырёк рисуется / кувшин возвращается (14, 22, 17) | BUG-LVLSTATE-1 | закрыт |
|
||
| 2 | меч возвращается в 15 / мигает контур кладки | BUG-LVLSTATE-1 | закрыт |
|
||
| 3 | комната 12: пробегает сквозь закрытую решётку (0,9) | BUG-COLL-1 | закрыт |
|
||
| 4 | после respawn плиты остаются разбитыми, зелья выпитыми | BUG-RESPAWN-1 | закрыт |
|
||
| 5 | плита/зелье возвращаются и БЕЗ respawn (12→16, 17, 22) | BUG-LVLSTATE-1 | закрыт |
|
||
| 6 | комната 6: длинный прыжок (0,6)→(0,7) — влёт в стену, респавн | BUG-COLL-1 | закрыт |
|
||
| 7 | залипает ↑ | BUG-KBD-3 | закрыт (см. `bug_closed.md`) |
|
||
| 8 | залипает Shift | BUG-KBD-3 / BUG-KBD-4 | закрыт (см. `bug_closed.md`) |
|
||
| 9 | комната 5: кнопка (0,4) не открывает решётку (0,5) | BUG-GATE-ANIM-1 | закрыт |
|
||
| 10 | комната 5: с кнопки (0,6) на (2,7), присед — провал в комнату 6 | BUG-STANDUP-1 | закрыт |
|
||
| 11 | страж убил Кида → Кид воскресает на месте и его убивают снова | BUG-DEATH-1 | закрыт |
|