4db60c750f
Порт правила оригинала, разобранного в 272cf8f. y_to_row_mod4 даёт −1 и
для куска выше потолка, и для ушедшего ниже комнаты; get_tilepos_nominus
сводит оба в тайл 30, а объекты тайла 30 рисуются в redraw_needed_tiles
ПЕРВЫМИ, до всего обхода тайлов.
Что сделано:
- defer = 0 для таких кусков: они под всем, включая Кида. Раньше
сравнение рядов читало −1 как «обходится последним» = «поверх всего»;
- оверлею отдаётся ориентир 3 («раньше любого ряда 2,1,0») вместо сырого
−1 — гейт other_overlay_tile перестал отбрасывать возврат соседа, из-за
чего тело плиты не возвращалось и оставался только её торец из
переднего слоя;
- в набор перекрываемых тайлов добавлена СВОЯ клетка (только для этого
случая: у куска в обычном ряду объект вливается в midtable после частей
своего тайла, и перерисовывать её нельзя).
Отладочная обвязка разбора (журнал решений оверлея, маска перекрывающих
тайлов) снята; счётчик перерисовок за кадр в pop_redraw_needed оставлен —
он дешёвый и пригодится для HEAL-WIDTH.
ЗАМЕР 13/23, 3032 кадра, против тега mob-order-B-start:
работа 888 984 -> 913 848 (+24 864)
синяя 159 804 -> 159 810
зелёная 427 242 -> 440 418 (+13 176)
циан 378 864 -> 393 000 (+14 136)
Период кадра не изменился: 4 растра в 23 кадрах, 5 в двух.
Зелёная вышла за растровый кадр (440 418 против 430 000). Детализация:
pop_loose_tick 185 826, из них pop_loose_mob_tick 168 180; тробы +
redraw_needed 337 800. Разбор и план возврата тактов — HEAL-WIDTH.
Визуальная проверка комнаты 14 за пользователем: поймать кадр с куском
снимками мне не удалось.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3383 lines
253 KiB
Markdown
3383 lines
253 KiB
Markdown
# roomtest — архив закрытых багов
|
||
|
||
Сюда переезжает всё, что **закрыто**: подтверждённые фиксы, снятые
|
||
диагнозы, осознанные решения «не делать». Открытые баги — в
|
||
[`BUGS_OPEN.md`](BUGS_OPEN.md), текущие задачи — в
|
||
[`TASKS_OPEN.md`](TASKS_OPEN.md), закрытые задачи с протоколами — в
|
||
[`TASKS_CLOSED.md`](TASKS_CLOSED.md).
|
||
|
||
Файл существует не ради истории как таковой: половина записей ниже — это
|
||
разбор КОРНЯ (odd-pixel арифметика `char_x`, подстановка тайла нажатой
|
||
кнопки, баг кодогенератора SDCC), и он экономит часы, когда похожий симптом
|
||
всплывёт снова. Прежде чем заводить новый баг — грепни здесь по симптому.
|
||
|
||
---
|
||
|
||
<a id="grab-below-room"></a>
|
||
## GRAB-BELOW-ROOM. Зацеп за кромку НИЖНЕГО ряда при пролёте вниз не работал вовсе
|
||
|
||
**Наблюдение (пользователь, 2026-08-12).** Уровень 7, комната 14: по сценарию
|
||
уровня Кид спускается с ряда 0 на ряд 2 через зацеп — виснет на кромке кнопки
|
||
`(0,2)`, отпускает и в падении цепляется за кромку `(2,2)`. «Пытался много
|
||
раз — Кид летит и не цепляется ни за `(2,2)` комнаты 14, ни за `(1,2)` комнаты
|
||
15». При этом вис на `(2,2)` с последующим перелётом на `(1,2)` **работает
|
||
стабильно** — то есть сам зацеп в падении жив.
|
||
|
||
**Корень.** `do_fall` (порт seg005:0030) не давал ряду персонажа выйти за
|
||
нижнюю границу комнаты:
|
||
|
||
```c
|
||
else if (Char.curr_row < 2)
|
||
inc_curr_row(); /* «следующий ряд ВНУТРИ комнаты» */
|
||
```
|
||
|
||
В оригинале `inc_curr_row()` безусловный, и `curr_row` доходит до **3** — ряда
|
||
«за нижней кромкой»; `get_tile` для него уходит по `links.down` в комнату снизу
|
||
(`find_room_of_tile`, seg006:005D). Ряд 3 нужен не сам по себе, а потому, что
|
||
`check_grab` целится в тайл **спереди-сверху**, то есть в ряд `curr_row − 1`:
|
||
только при `curr_row == 3` целью становится ряд 2 своей комнаты.
|
||
|
||
С залипшим рядом 2 происходило вот что: пока Кид летел вдоль нижнего ряда,
|
||
`do_fall` каждый кадр уходил в ветку «достиг `y_land`», где `check_grab` не
|
||
вызывается вовсе. К моменту, когда переход в комнату снизу открывал окно
|
||
зацепа заново (там цель — «ряд −1», уже реализованный `g_above`), скорость
|
||
падения успевала дойти до `fall_y = 33`, а `check_grab` требует `< 32`. Окно
|
||
закрывалось **по скорости** — отсюда «летит и не цепляется никак».
|
||
|
||
Второй дефект, найденный там же: ветка `action == ACT_MIDAIR` в `check_action`
|
||
стояла ПУСТОЙ заглушкой с комментарием «frames 102..105: check_grab — K4». В
|
||
оригинале (seg006:0619) это первые четыре кадра падения, где `fall_y` ещё не
|
||
разгоняется (`fall_accel` работает только в `ACT_FREEFALL`), — самое широкое
|
||
окно зацепа. У нас его не было.
|
||
|
||
**Фикс** (`pop_map.c`): `inc_curr_row()` теперь безусловный внутри ветки
|
||
`curr_row <= 2` (то есть 2 → 3 разрешён), а сама ветка «достиг `y_land`»
|
||
гейтится по `curr_row <= 2` — при ряде 3 ни `in_wall`, ни `land` звать нельзя
|
||
(тайлов своей комнаты там нет, `get_tile` отдаёт `WALL`-сентинел; до
|
||
`y_land[4] = 244` дело не доходит, `check_leave_below` переводит в комнату
|
||
снизу на `y >= 211`). Плюс восстановлена ветка `ACT_MIDAIR` с кадрами 102..105.
|
||
|
||
**Подтверждение.**
|
||
- Хост-тест `tests-host/t_grab.c` → `grab_below_room_edge_window_exists`:
|
||
сцена комнаты 14 + переход в 15 по `pop_fell_out` (как в главном цикле).
|
||
Карта исходов по фазе X и задержке Shift **до** фикса — сплошные точки (ни
|
||
одного зацепа), **после** — окно из 8 фаз X при задержках 0..5 кадров.
|
||
Тест проверяет и играбельность: зацеп обязан удаваться при самом
|
||
естественном вводе (отпустил вис и сразу зажал Shift).
|
||
- Живьём в MAME (мост, 2026-08-12): Кид повис на кромке `(2,2)` — в памяти
|
||
`frame=91 y=55 row=0 room=15 act=6`, на экране держится за правый край плиты.
|
||
|
||
**Грабли теста, стоившие часа.** Первая версия сцены засчитывала как успех
|
||
ЛЮБОЙ вис — а Кид на первых кадрах падения цепляется обратно за ту же верхнюю
|
||
кромку, и тест проходил даже на сломанном коде. Признак цели пришлось делать
|
||
позиционным (`went_below` + `curr_row == 0` после перехода). Вторая ловушка:
|
||
кадры спуска `seq_68` идут с `action == 3`, то есть «дождаться падения» по
|
||
одному лишь `action` нельзя — ждать надо ВИСА, и только потом отпускать Shift.
|
||
|
||
---
|
||
|
||
<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`, так что проверить сценарием, а не менять вслепую.
|
||
|
||
**ЗАКРЫТ 2026-08-10.** Сделано ровно по плану выше: ветка ROOMNAV после
|
||
`kid_init`/`pop_kid_hp_reset` гасит состояние схватки —
|
||
`Kid.sword = SWORD_0_SHEATHED`, `holding_sword = 0`, `offguard = 0`,
|
||
`guard_refrac = 0`. Меч в инвентаре (`pop_have_sword`) не теряется, только
|
||
режим боя.
|
||
|
||
|
||
<a id="bug-loose-button-1"></a>
|
||
## BUG-LOOSE-BUTTON-1. Упавшая плита не нажимает кнопку — **ЗАКРЫТ 2026-08-10**
|
||
|
||
**Симптом (пользователь, уровень 4).** Плита `16(1,1)` падает вниз, в комнате
|
||
17 ничего не меняется: кнопка цела, щебня нет, ворота не открылись.
|
||
|
||
**Корень — три слоя.**
|
||
|
||
1. Порт `loose_land` (seg007:11E8) у нас вообще не звал `trigger_button`: и в
|
||
своей комнате (`pop_map.c`), и в комнате снизу (`roomtest.c`) он только
|
||
клал щебень.
|
||
2. `pop_room_col_landing` (pop_level.c) считала площадкой **только чистый
|
||
пол** (`t == 1`). У оригинала площадок семь — `tiles_1_floor`,
|
||
`tiles_2_spike`, `tiles_6_closer`, `tiles_10_potion`, `tiles_15_opener`,
|
||
`tiles_19_torch`, `tiles_30_torch_with_debris`; на всём прочем кусок просто
|
||
исчезает. Кнопка в набор не входила, поэтому плита растворялась.
|
||
3. Сигнал `pop_loose_fell` взводится в момент ОТРЫВА плиты, а по нему главный
|
||
цикл и щебень клал, и кнопку жал — ворота начинали открываться, пока плита
|
||
ещё в воздухе.
|
||
|
||
**Механика оригинала (важно, интуиция обманывает).** Плита не «постоянно
|
||
давит» на кнопку. Нажатие ОДНО, но с `button_type = tiles_14_debris`, а это у
|
||
`trigger_gate` отдельная ветка: возврат 2 → `animate_door` доводит створку и
|
||
ставит `curr_modifier = 0xFF` → ворота заморожены открытыми навсегда, обычный
|
||
opener их больше не трогает (`if (modifier == 0xFF) return -1`). Сама кнопка
|
||
съедается: тайл становится щебнем. На closer — быстрое закрытие и тоже
|
||
щебень. На факеле остаётся отдельный тайл `tiles_30_torch_with_debris`.
|
||
|
||
**Фикс.** Набор площадок расширен до оригинального; `pop_trigger_button`
|
||
зовётся в обоих местах с правильным `button_type`; посадка в комнате снизу
|
||
переехала с `pop_loose_fell` на новый сигнал `pop_loose_exit`, который
|
||
`mob_tick_one` взводит, когда кусок ушёл ниже поля комнаты.
|
||
|
||
**Осознанное расхождение.** Оригинал через `mob_down_a_row` продолжает
|
||
симуляцию куска уже в нижней комнате; у нас он там не летит, а садится сразу.
|
||
Задержка получается «высота комнаты», а не «высота комнаты плюс путь до пола
|
||
внизу».
|
||
|
||
---
|
||
|
||
<a id="bug-gate-ff-1"></a>
|
||
## BUG-GATE-FF-1. Ворота, открытые навсегда, нарисованы открытыми, но непроходимы — **ЗАКРЫТ 2026-08-10**
|
||
|
||
**Симптом.** После BUG-LOOSE-BUTTON-1 ворота `23(0,9)` рисуются поднятыми, но
|
||
Кид в них упирается.
|
||
|
||
**Корень — перегруженное значение.** `gate_modif` (pop_map.c) отдаёт `0xFF`
|
||
как сторожевое «тайла нет за краем уровня», а `trigger_gate` ставит тот же
|
||
`0xFF` как живой модификатор «открыто НАСОВСЕМ». В `gate_passable` стояло
|
||
`if (gm == 0xFF) return 0;`. До сегодняшнего дня значение в игре не
|
||
возникало — ветку по щебню никто не запускал.
|
||
|
||
**Фикс.** Ветка убрана: она мёртвая (нас зовут только когда тайл ТОЧНО
|
||
ворота), а `(0xFF >> 2) + 6 = 69` и так больше любого `fph`. Остальные
|
||
читатели `gate_modif` трактуют 63 как «открыто» и правок не потребовали.
|
||
|
||
---
|
||
|
||
<a id="bug-torch-chomp-1"></a>
|
||
## BUG-TORCH-CHOMP-1. Чомпер стирает пламя соседнего факела — **ЗАКРЫТ 2026-08-10**
|
||
|
||
**Симптом (пользователь).** Комната 23 уровня 4: факел есть, огня нет.
|
||
|
||
**Корень.** Пламя рисуется в ячейке ПРАВОГО соседа (`x = COL_XH[col+1]*8 + 8`
|
||
= 232..248, `y` 33..50), то есть в тайле чомпера (0,7). `pop_chomp_redraw`
|
||
начинается с `pop_heal_off(224, 2, 32, 64)` — восстанавливает запечённый фон в
|
||
`x 224..255, y 30..93`. Пламя в фон не запекалось и рисовалось в
|
||
`GFX_BANK_SPRITE`, поэтому heal его съедал; вдобавок `pop_redraw_needed` идёт
|
||
ПОСЛЕ `pop_process_trobs`, так что стиралось только что нарисованное.
|
||
|
||
**Тупик, в который я сначала свернул.** Вынес отрисовку пламени в отдельный
|
||
проход после `pop_redraw_needed` — стало хуже: пламя легло ПОВЕРХ челюстей
|
||
(правильный z-порядок существует только внутри общего обхода тайлов, где
|
||
`draw_tile_anim_right` идёт до `draw_tile_anim`), плюс проход индексировал
|
||
`trob_code[i]` параллельно `trobs[i]`, а `pop_process_trobs` в конце уплотняет
|
||
массив — индексы разъезжались, и чужой trob рисовался как факел (пламя из
|
||
головы Кида). Проход откачен.
|
||
|
||
**Фикс (подсказан пользователем).** Пламя ЗАПЕКАЕТСЯ: `pop_torch_draw` пишет
|
||
в `GFX_BANK_NORMAL` (видео-ОЗУ + ОЗУ-копия). Это безопасно именно у факела —
|
||
все девять кадров лежат на общем канвасе 16×18 и упакованы `opaque=True`, то
|
||
есть кадр полностью накрывает предыдущий, протухнуть в копии нечему; и факел
|
||
всегда задний план. Тогда heal чомпера возвращает огонь сам, а челюсти
|
||
ложатся поверх — тот же z-порядок, что в оригинале, и без лишнего прохода.
|
||
|
||
Пузырёк ЗЕЛЬЯ так нельзя: он ползёт вверх, себя не накрывает и требует heal —
|
||
`pop_potion_draw` остаётся в `GFX_BANK_SPRITE`. Столкнуться с чомпером в
|
||
одном тайле зелье не может.
|
||
|
||
|
||
<a id="bug-guard-ix-1"></a>
|
||
## BUG-GUARD-IX-1. «Зависание» при бое со стражем — затёртый IX главного цикла — **ЗАКРЫТ 2026-08-10**
|
||
|
||
**Симптом (пользователь, уровень 4).** Дважды подряд при ручном входе в
|
||
комнату 18 со стражем игра «повисала»: картинка стоит, управление не
|
||
отвечает. Нажатие «2» ненадолго возвращало движение, потом всё повторялось.
|
||
|
||
**Диагноз — НЕ зависание.** Главный цикл всё это время крутился (маркер
|
||
верха цикла тикал раз в кадр). Стоял отладочный стоп-кадр: локаль `frozen`
|
||
в `main` сама собой становилась ненулевой.
|
||
|
||
**Корень — однобайтовый выход за границу локального массива.** У `main` все
|
||
локали лежат по `IX-1..IX-19`, и IX затирался: вместо `0xBFFA` в нём
|
||
оказывался `0xBF00`. Признак железный — в теле цикла обязано выполняться
|
||
`SP == IX-19`, а по факту было `SP=0xBFE7, IX=0xBF00`. Дальше `main` читал
|
||
`frozen`, `dbuf`, `front/back`, `dead_frames` и edge-флаги читов из живого
|
||
стекового мусора.
|
||
|
||
Виновник — `check_chomped_guard` (pop_map.c):
|
||
|
||
```c
|
||
uint8_t flags[COLL_N]; /* 14 байт в кадре функции */
|
||
calc_coll_window(); /* окно СТРАЖА -> win_lo/win_hi */
|
||
get_row_collision_data(Char.curr_row, flags);
|
||
```
|
||
|
||
`coll_row()` пишет по `flags + scan_off`, а число байт берёт из
|
||
`win_lo/win_hi`. `scan_off`/`scan_left0` выставляет `coll_scan_prepare()`,
|
||
которую звал ТОЛЬКО `check_collisions` — путь Кида. То есть ряд стража
|
||
писался **по смещению Кида, длиной стража**. Диапазон записи
|
||
`[kid_win_lo+2 … kid_win_lo+2 + ширина_окна_стража − 1]`; при обычной ширине
|
||
4 он уезжает за `flags[13]`, когда `kid_win_lo > 8` — то есть **когда Кид
|
||
стоит у правого края комнаты** (взаимное расположение Кида и стража ни при
|
||
чём, стража даёт только длину). Сразу за массивом лежит сохранённый IX;
|
||
затёртый младший байт и превращал `0xBFFA` в `0xBF00`.
|
||
|
||
Перелёт в отрицательные индексы был невозможен: `scan_off = kid_win_lo + 2`,
|
||
а `kid_win_lo >= COLL_C0 = −2` из-за клампа в `calc_coll_window`.
|
||
|
||
**Почему не ловилось раньше.** На уровне 1 (комната 3) бой идёт левее
|
||
середины — запись оставалась внутри массива. Ничего не портилось, но флаги
|
||
стража всё равно читались по чужим смещениям и с иксами Кида: тихо неверные
|
||
данные, без последствий (чомперов там нет).
|
||
|
||
**Фикс.** `coll_scan_prepare()` в начале `get_row_collision_data()` — теперь
|
||
запись всегда ложится в `[win_lo+2 … win_hi+2]`, а `calc_coll_window` клампит
|
||
окно в `[−2 … 11]`, то есть индексы гарантированно `0…13`. Заодно чинится
|
||
сам расчёт: `scan_left0` задаёт x колонок, и чомпер-коллизия стража считалась
|
||
по координатам Кида.
|
||
|
||
**Как ловилось (приём на будущее).** Детекторы `ix != 0xBFFA` на границах
|
||
фаз `PROF()` с починкой IX в верху цикла: игра остаётся живой, а первый
|
||
сработавший детектор называет фазу. Дальше — инструкционная трасса MAME
|
||
(`trace file,0`), включаемая на подозрительном вызове и выключаемая сразу
|
||
после, со стопом только на плохом кадре: в файле остаётся ровно тот вызов,
|
||
где IX испортился. В хвосте трассы видно `ld sp,ix / pop ix`, достающий
|
||
`BF00` вместо `BFFA`. См. memory `z80_profiling_method`.
|
||
|
||
**Проверка.** Бой со стражем в комнате 18 уровня 4 (пользователь) —
|
||
детекторы IX висели без починки и не сработали ни разу; `make -C tests-host`
|
||
— все 5 наборов.
|
||
|
||
---
|
||
|
||
<a id="bug-gate-seam-row1"></a>
|
||
## BUG-GATE-SEAM-ROW1. Решётка в шве не анимируется, если ворота НЕ в ряду 0 — **ЗАКРЫТ 2026-08-10**
|
||
|
||
**Симптом (пользователь, уровень 4, стартовая комната).** Плита нажимается,
|
||
ворота физически открываются (Кид проходит), но визуально с решёткой ничего
|
||
не происходит. Отдельно: чёрный треугольник над воротами (верх решётки под
|
||
ковром) статичен, тогда как в оригинале он ездит вместе с барами.
|
||
|
||
**Корень.** Зонд на `pop_add_trob` показал `room=8 tp=19 type=1`: плита
|
||
комнаты 1 открывает ворота **комнаты 8 в (1,9)** — соседней комнаты, видимые
|
||
через ЛЕВЫЙ шов (бары ворот в col9 рисует col0 соседа справа). Наш
|
||
change-driven редрой шва смотрел только один байт `m[9]` (ряд 0), а
|
||
`pop_room_redraw_seam_left()` перерисовывал жёстко `draw_tile(0, 0)`.
|
||
Изменение приходило в `m[19]` — сигнатура его не видела.
|
||
|
||
На уровне 1 та же связка комнат 6/8 работала только потому, что решётка
|
||
соседа стояла в **(0,9)**. Тайлсет ни при чём: в подземелье просто не
|
||
попадалось ворот в ряду 1 у шва (и треугольника не было — над решётками
|
||
всегда потолок).
|
||
|
||
**Фикс.**
|
||
1. `seam_sig` — массив на три ряда, сравниваются все три; накопленная маска
|
||
изменившихся рядов живёт в `seam_rows` и переживает оба кадра
|
||
дабл-буфера.
|
||
2. `pop_room_redraw_seam_left(uint8_t rows)` берёт маску и перерисовывает
|
||
только помеченные ряды — не все три, чтобы не платить каждый кадр
|
||
анимации.
|
||
3. В маску добавляется ряд ВЫШЕ (`changed | (changed >> 1)`): верх решётки
|
||
(`draw_tile_anim_topright`, seg008:0568 — маска 68 + кадр
|
||
`DOOR_FRAM_TOP`) рисует не сам тайл ворот, а тайл над-справа от него, у
|
||
нас `(r−1, 0)`. Без этого бары ездили, а треугольник над ними стоял.
|
||
|
||
**Проверка.** Уровень 4, стартовая комната: нажатие плиты — решётка шва
|
||
поднимается/опускается вместе с верхом (пользователь).
|
||
|
||
|
||
<a id="bug-guard-color-1"></a>
|
||
## BUG-GUARD-COLOR-1. Страж и его полоса HP — всегда одного цвета — **ЗАКРЫТ 2026-08-07**
|
||
|
||
**Симптом (пользователь, прогон уровня 2).** Комната 4: страж не того цвета,
|
||
что в SDLPoP, и полоса его HP тоже.
|
||
|
||
**Корень.** Оригинал держит ОДИН набор спрайтов стража и подменяет 16 цветов
|
||
палитры: `redraw_screen` (seg003:255) зовёт `set_chtab_palette(chtab_5_guard,
|
||
&guard_palettes[0x30*curr_guard_color − 0x30], 16)` ПЕРЕД отрисовкой комнаты,
|
||
где `curr_guard_color = level.guards_color[room−1] & 0x0F` (enter_guard,
|
||
seg002:184), а `guard_palettes` — ресурс 10 из `PRINCE.DAT` (7 палитр × 16
|
||
цветов, 6-битные каналы). У нас `pop_pack_guard.py` брал `COLOR = 2`
|
||
константой (цвет стражей уровня 1) и запекал одну палитру в слоты
|
||
`0x90..0x9F`. Полоса HP рисуется тем же атласом — отдельного бага не было.
|
||
|
||
**Фикс.**
|
||
1. `pop_pack_guard.py` выгружает ВСЕ 7 палитр в `pop_guard_pal.h`
|
||
(7 × 16 × 4 = 448 Б, записи `B,G,R,0` — формат `gfx_pal_load`);
|
||
каналы масштабируются `scale6to8`, как в `pop_pack_kid` для `kid.pal`.
|
||
2. `pop_guard_set_palette(color)` (`pop_gdraw.c`) заливает 16 слотов в ОБЕ
|
||
палитры дабл-буфера; `color == 0` — не трогать (так и оригинал).
|
||
3. Зовётся из `pop_guard_enter` сразу после чтения данных комнаты, то есть
|
||
ДО отрисовки — как `redraw_screen`. `pop_level_guard` цвет отдавал уже
|
||
давно, его просто никто не использовал.
|
||
|
||
**Грабли, стоившие итерации (записать на будущее).** Первая версия передавала
|
||
`gfx_pal_load` указатель прямо на таблицу — а таблица лежит в rodata
|
||
БАНКОВОГО модуля, то есть по 0xC000+. `gfx_pal_load` отдаёт указатель в BIOS
|
||
(`$A4` через `rst #0x08`), а BIOS читает только `#4000-#BFFF` (корневой
|
||
CLAUDE.md). BIOS забирал мусор с чужой страницы, палитра уезжала в тёмное и
|
||
страж становился НЕВИДИМЫМ. Лечится копией записи в локальный буфер — стек
|
||
гарантированно в W2.
|
||
|
||
**Проверено в MAME (уровень 2, ROOMNAV):**
|
||
|
||
| комната | цвет из уровня | что на экране |
|
||
|---------|----------------|---------------|
|
||
| 11 | 1 | страж сине-фиолетовый, полоса HP синяя |
|
||
| 7 | 3 | страж оранжевый, полоса HP оранжевая |
|
||
| ур. 1 | 2 | охра, как было (регрессии нет) |
|
||
|
||
Цвета сходятся с таблицей `pop_guard_pal.h`: цвет 1 = (72,145,255),
|
||
цвет 3 = (255,80,0), цвет 2 = (170,48,0).
|
||
|
||
**Смежное, НЕ входит сюда:** палитра КЛАДКИ уровня 3 (в оригинале зелёная) —
|
||
другой механизм и другой ресурс, заведена отдельной задачей
|
||
[L3-COLOR](TASKS_OPEN.md#l3-color).
|
||
|
||
---
|
||
|
||
<a id="bug-sword-ghost-1"></a>
|
||
## BUG-SWORD-GHOST-1. Переход комнаты В БОЮ: Кид прячет меч и дерётся пустой рукой — **ЗАКРЫТ 2026-08-07**
|
||
|
||
**Проверено в игре (пользователь):** «вроде проблема не воспроизводится —
|
||
всё корректно». Замер на замороженном кадре: оба персонажа в комнате 2,
|
||
ряд 1, колонки 4 и 7, `Kid.sword = 2`, `Guard.sword = 2`, луч видимости
|
||
прошёл, счётчик уборок меча за прогон — **0**.
|
||
|
||
**Симптом.** Кида вытеснили из комнаты 3 в комнату 2 в бою, он спрятал
|
||
меч; страж вошёл следом, и дальше Кид «отбивался» без клинка. Зависимость
|
||
от расстояния между Кидом и стражем в момент перехода: близко — бой
|
||
продолжался нормально, далеко — меч убирался и страж не шёл, а в
|
||
промежутке страж приходил, но меч оставался в ножнах.
|
||
|
||
**Корень — мнимая стена за краем комнаты, а не боёвка.** Ложным было
|
||
`can_guard_see_kid`. Луч видимости (`check_can_guard_see_kid`, seg003:688)
|
||
идёт по тайлам между колонками Кида и стража, а колонка считается из `x`:
|
||
`floor((x − 7 − 58) / 14)`. На всём диапазоне `x` 0..255 это даёт `col`
|
||
от **−5 до 13**, то есть колонка регулярно уходит ЗА комнату. Оригинал
|
||
такие колонки резолвит через `find_room_of_tile` (seg006:005D), гуляя по
|
||
`roomlinks` на любую глубину; наш `get_tile` знал соседей только на две
|
||
колонки (`−2..11`), а дальше отдавал `TILE_WALL`. Луч упирался в эту
|
||
мнимую стену, `can_guard_see_kid` падал в 0 — и `control_with_sword`
|
||
(ветка «противника не видно») штатно убирал меч.
|
||
|
||
Тот же ноль не давал достать меч обратно: `control_standing` зовёт
|
||
`draw_sword` только при `can_guard_see_kid >= 2`. Отсюда и «драка без
|
||
меча», и зависимость от расстояния — на самом деле не от расстояния, а от
|
||
того, вышла ли колонка за кэш.
|
||
|
||
**Артефакт (регистратор в памяти, снят с живой сцены).** Брейкпоинт тут
|
||
бесполезен — баг ловится руками и редко, поэтому в код были временно
|
||
вшиты байты «кто убрал меч и почему оборвался луч»; читались после сцены:
|
||
|
||
```
|
||
src = 2 control_with_sword, ветка «противника не видно»
|
||
why = 2 луч упёрся в стену
|
||
tile = 0x14 = 20 = TILE_WALL
|
||
col = 12 ← колонка стража ЗА кэшем (было −2..11)
|
||
kid_col = 8 guard_col = 12
|
||
```
|
||
|
||
**Фикс.** `pop_map` кэширует `fg` соседних комнат слева и справа ЦЕЛИКОМ
|
||
(по 30 байт, раскладка комнаты) и резолвит `col` от −10 до 19 в реальный
|
||
тайл соседа; за этими пределами — по-прежнему стена (у оригинала там
|
||
следующий `roomlink`, у нас край кэша). Данные забирает сама
|
||
`pop_map_set_edges(left, right, up, down)`, читая уровень — отдельной
|
||
копии в приложении нет. Заодно `gate_modif` перестал читать за границу
|
||
массива модификаторов: openness ворот на дальних колонках берётся из
|
||
`room_modif` СОСЕДА (`pop_gate_modif`, им же пользуется луч).
|
||
|
||
Цена: **+48 байт** в W2 (кэш 6+6 → 30+30), код W1 даже уменьшился.
|
||
Комнаты сверху/снизу оставлены как были (`above_fg` / `below_fg`): по
|
||
вертикали `curr_row` за −1..3 не выходит, мнимых стен там не возникает.
|
||
|
||
Попутно вернули строку оригинала, потерянную при порте: `control_with_sword`
|
||
сбрасывает `holding_sword` для живого Кида (seg005:980) — от неё зависит
|
||
индикатор HP стража.
|
||
|
||
**Регресс:** `tests-host` — все 5 наборов зелёные, характеризационные
|
||
трассы Кида не сдвинулись (`[phys] ok: 1723`). Новый набор проверок
|
||
`char_tiles_resolve_across_rooms` в `t_char` держит резолв колонок
|
||
−11..20 в тайлы соседей и стену на краю кэша — чтобы кэш нельзя было
|
||
молча сузить обратно.
|
||
|
||
**Почему в SDLPoP не воспроизводилось** (пользователь пробовал): там этой
|
||
границы просто нет — `find_room_of_tile` уходит по `roomlinks` сколько
|
||
нужно, и луч никогда не встречает мнимой стены.
|
||
|
||
---
|
||
|
||
## BUG-GRAB-1. Прыжок с места через провал в 3 тайла: зацепа нет — **ЗАКРЫТ 2026-08-05**
|
||
|
||
**Проверено в игре (пользователь):** «зацеп работает». Физика была верна с
|
||
самого начала, чинить пришлось клавиатуру — [BUG-KBD-5](#bug-kbd-5).
|
||
|
||
> **Итог 2026-08-05.** Физика тут ни при чём — виновата клавиатура.
|
||
> Зажатый Shift снимался автоповтором зажатой стрелки, поэтому к кадрам
|
||
> 102…106 (окно зацепа) движок видел Shift отпущенным. Полный разбор и
|
||
> фикс — [BUG-KBD-5](BUGS_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](BUGS_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). На зацеп не влияет.
|
||
|
||
---
|
||
|
||
---
|
||
|
||
## BUG-GATEMOD-1. Ворота стартуют закрытыми, хотя в уровне открыты — **ЗАКРЫТ 2026-08-05**
|
||
|
||
**Проверено в игре (пользователь).**
|
||
|
||
**Симптом (пользователь, 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,
|
||
файловое значение.
|
||
|
||
---
|
||
|
||
---
|
||
|
||
## BUG-GUARD-DEAF-1. Страж не оборачивался на вернувшегося Кида — **ЗАКРЫТ 2026-08-05**
|
||
|
||
**Наблюдение (пользователь, 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` (автопилота стража в наборах нет).
|
||
|
||
**Проверено в игре (пользователь, 2026-08-05):** тот же сценарий — страж
|
||
выталкивает Кида в комнату 22, Кид возвращается бегом — **страж
|
||
оборачивается**.
|
||
|
||
**Что осталось «как в оригинале» и багом не является:** подкрасться к
|
||
стражу СТОЯ по-прежнему можно (шумит движение, а не присутствие), и после
|
||
возврата в комнату страж всегда поднимается с УБРАННЫМ мечом в неактивной
|
||
стойке, повёрнутый туда же, куда смотрел при выходе Кида — это `enter_guard`
|
||
(seg002:0112), а не потеря состояния.
|
||
|
||
---
|
||
|
||
---
|
||
|
||
## BUG-JUMPWALL-1. Недолетевший прыжок проходил СКВОЗЬ стену — **ЗАКРЫТ 2026-08-05**
|
||
|
||
**Симптом (пользователь, приёмка уровня 3).** Комната 14: Кид на (0,8)
|
||
лицом влево, прыжок с места — пролетает сквозь кладку и падает в комнату 13
|
||
на (2,3). Обобщение пользователя (оно и оказалось верным): «если Кид
|
||
прыгает, недолетает и должен врезаться в стену и упасть ВДОЛЬ неё, у нас он
|
||
летит по свободной траектории, как будто ему никто не мешает».
|
||
|
||
**Корень — упрощение в `check_collisions`, про которое там же стояла
|
||
оговорка.** Оригинал (seg004:0004) каждый кадр считает флаги перекрытия для
|
||
ТРЁХ рядов (`curr`/`above`/`below`), а `move_coll_to_prev` (seg004:00DF) в
|
||
начале следующего кадра выбирает из них тот, что соответствует ряду прошлого
|
||
кадра. Так «прошлые» флаги остаются настоящими и на кадре СМЕНЫ РЯДА.
|
||
|
||
У нас ряд был один, и на смене ряда `prev` заполнялся тройками («уже
|
||
перекрывал всё»), что подавляло бамп на этом кадре. В падении ряд меняется
|
||
почти каждый кадр, а переход флага 0→1 на стене приходится ровно на него —
|
||
поэтому удар не регистрировался и `bumped_fall` (который и гасит `fall_x`)
|
||
не вызывался.
|
||
|
||
**Как ловили.** Не в MAME, а на харнессе: набор
|
||
[`tests-host/t_wall.c`](tests-host/t_wall.c) — комната 14 уровня 3 с
|
||
подложенным дном шахты, свип по всей ширине стартовой плиты (x = 177..196).
|
||
Из 20 стартовых позиций 4 давали проход сквозь кладку (Кид оказывался в
|
||
колонке 4 при стене в колонке 5). Первый вариант сценария (одна стартовая
|
||
X, из центра плиты) БАГА НЕ ПОКАЗАЛ — отсюда свип.
|
||
|
||
**Фикс.** Три ряда как в оригинале + дословный `move_coll_to_prev`
|
||
(включая условия ±3 на заворот ряда при смене комнаты). Цена — 3×14
|
||
`get_tile` на кадр вместо 14, банк 3 +115 Б.
|
||
|
||
**Чем проверено.** `t_wall` зелёный; **все 1723 существующие трассы
|
||
`t_phys` не изменились ни на байт** — правка поведение-сохраняющая для
|
||
всего остального. В живой игре подтвердил пользователь: «ударились и
|
||
падаем вертикально вниз».
|
||
|
||
---
|
||
|
||
## BUG-SEAM-WEDGE-1. Клин кладки в пустом углу (2,0) — **ЗАКРЫТ 2026-08-05**
|
||
|
||
**Симптом (пользователь, приёмка уровня 3).** Комната 18: в тайле (2,0)
|
||
нарисован кусок кладки, хотя по данным там пусто.
|
||
|
||
**Корень.** У тайла (2,0) «сосед снизу-слева» — это колонка −1 ряда 3, то
|
||
есть тайл ЧУЖОЙ комнаты (`room_BL` — левая от нижней). `load_rowbelow`
|
||
(seg008:368) резолвит его через `get_tile_to_draw(room_left, 9, 0, …)` и
|
||
подставляет стену ТОЛЬКО когда такой комнаты нет. Мы клали стену
|
||
безусловно, а `draw_topright` для стены рисует угловой кусок — он и был
|
||
клином. Для комнаты 18 диагональ — комната 13, её тайл (0,9) пуст, значит
|
||
рисовать нечего.
|
||
|
||
**Фикс.** Ряд «снизу» стал одиннадцатибайтным: `[0..9]` — колонки комнаты
|
||
снизу, `[10]` — тайл (0,9) комнаты снизу-слева (дефолт «стена», если её
|
||
нет). Затронуты `pop_level.c` (заполнение), `pop_bg.c` (чтение),
|
||
`roomtest.c` (размер массива).
|
||
|
||
**Проверено** в MAME: комната 18 уровня 3, клина нет.
|
||
|
||
---
|
||
|
||
## BUG-LOOSE-3. Чёрный бар под упавшей плитой-потолком — **ЗАКРЫТ 2026-08-05**
|
||
|
||
**Симптом (пользователь, приёмка уровня 2).** Комната 6: Кид сбивает
|
||
плиту-потолок (−1,2) — плита падает, но на её месте остаётся чёрный бар.
|
||
Два уточнения пользователя оказались диагностическими:
|
||
|
||
1. «бар попал в фоновое изображение» — Кид прыгает поверх, уходит, бар
|
||
остаётся → чернота лежит в ОЗУ-копии, и heal возвращает её каждый кадр;
|
||
2. «когда возвращаемся в комнату после выхода — бара нет» → статическая
|
||
отрисовка комнаты рисует всё правильно, виновата ЗАПЕЧКА.
|
||
|
||
**Корень.** `pop_ceil_bake_empty` стирал полосу потолка чёрной плитой
|
||
(`bar`, банк NORMAL — пишет и в ОЗУ-копию) шириной 64 px, а восстанавливал
|
||
только ДВА тайла ряда −1: `(−1,col)` и `(−1,col+1)`. Но куски тайлов
|
||
рисуются ВВЕРХ от своей нижней грани и свисают ВПРАВО, поэтому в стёртую
|
||
полосу попадает графика и соседа СЛЕВА, и тайлов ряда 0 — их верхушки.
|
||
Незакрашенным оставался прямоугольник ~13×3 px.
|
||
|
||
**Замер (он же метод).** Плита роняется без игры — записью
|
||
`pop_ceil_modif[col] = 1` в отладчике MAME (это ровно то, что делает
|
||
`make_loose_fall` для потолка). Дальше попиксельная сверка скриншота
|
||
«после запечки» со скриншотом «комната перерисована заново» (выйти и
|
||
вернуться): различие локализовалось в прямоугольник **экранные x 230..255,
|
||
y 47..52** при полосе потолка y 44..52.
|
||
|
||
**Фикс.** Запечка восстанавливает всё, чья графика попадает в полосу:
|
||
ряды −1 И 0, колонки `col−1..col+1`.
|
||
|
||
**Проверено:** та же попиксельная сверка после фикса даёт **0 различий** в
|
||
полосе; пользователь подтвердил в игре.
|
||
|
||
---
|
||
|
||
## BUG-RJUMP-1. Разбег-прыжок не берёт провал в три тайла — **ЗАКРЫТ 2026-08-04**
|
||
|
||
**Симптом (пользователь, приёмка уровня 2).** Комната 1: Кид с разбегу
|
||
обязан перелететь колодец с (0,5) на (0,1) — у нас он долетает до колодца и
|
||
валится вертикально вниз. Комната 9: то же с (1,5) на (1,1). Обобщение
|
||
пользователя оказалось точным: **провал ровно в три пустых тайла наш Кид не
|
||
перепрыгивал никогда**, а провалы поменьше брал.
|
||
|
||
**Почему «никогда», а не «иногда».** Суммарный `dx` последовательности
|
||
`seq_4_run_jump` (кадры 34..44) — 62 пикселя при ширине тайла 14. Это
|
||
ровно 4 колонки и меньше половины тайла запаса. То есть перелёт трёх
|
||
пустых тайлов возможен ТОЛЬКО если оттолкнуться почти точно от кромки; из
|
||
случайной фазы бегового цикла он не получается никогда.
|
||
|
||
**Корень.** Оригинал именно поэтому и не даёт прыгать откуда попало:
|
||
`run_jump` (seg005:0AA8) перед стартом **выравнивает Кида по кромке**.
|
||
|
||
```c
|
||
short xpos = char_dx_forward(4);
|
||
short col = get_tile_div_mod_m7(xpos);
|
||
for (short tiles_forward = 0; tiles_forward < 2; ++tiles_forward) {
|
||
col += dir_front[Char.direction + 1];
|
||
get_tile(Char.room, col, Char.curr_row);
|
||
if (curr_tile2 == tiles_2_spike || !tile_is_floor(curr_tile2)) {
|
||
pos_adjustment = distance_to_edge(xpos) + TILE_SIZEX * tiles_forward - TILE_SIZEX;
|
||
if ((word)pos_adjustment < (word)-8 || pos_adjustment >= 2) {
|
||
if (pos_adjustment < 128) return; // ПРЫЖКА НЕТ
|
||
pos_adjustment = -3;
|
||
}
|
||
Char.x = char_dx_forward(pos_adjustment + 4);
|
||
break;
|
||
}
|
||
}
|
||
control_up = release_arrows();
|
||
seqtbl_offset_char(seq_4_run_jump);
|
||
```
|
||
|
||
**У нас этой половины не было** — стояла заглушка с честным комментарием
|
||
«оригинал выравнивает Kid по краю пола (нужны tile-запросы) — это полировка
|
||
K3; K2b просто запускает run-jump». Полировкой это не оказалось: без
|
||
выравнивания три тайла непроходимы в принципе.
|
||
|
||
**Две тонкости, которые легко потерять при порте.**
|
||
|
||
1. **Беззнаковое сравнение.** `(word)pos_adjustment < (word)-8 || pos_adjustment >= 2`
|
||
означает ровно «`pos_adjustment` НЕ попал в `[-8,-1]`». Ветка
|
||
`pos_adjustment = -3` недостижима: `distance_to_edge ∈ [0,13]`,
|
||
`tiles_forward ∈ {0,1}`, значит `pos_adjustment ∈ [-14,13]`, а туда нужно
|
||
`>= 128`. В порте она записана как мёртвая, с объяснением.
|
||
2. **Отказ НЕ гасит `control_up`.** `return` выходит из `run_jump` до
|
||
`release_arrows()`, поэтому Кид бежит дальше с зажатой «вверх» и пробует
|
||
снова на следующем кадре. Именно так игрок и ловит фазу — просто
|
||
удерживая клавишу. Если погасить, прыжок у кромки станет одноразовым и
|
||
почти всегда неудачным.
|
||
|
||
**Фикс.** Тайловая половина — `pop_run_jump_align()` в `pop_map` (там живут
|
||
`get_tile`/`distance_to_edge`), диспетчерская — в `pop_ctrl.run_jump`.
|
||
Разделение то же, что у `pop_jump_up_seq`: pop_map правит `Kid.x` напрямую,
|
||
и `pop_savekid_state` эту правку намеренно не затирает.
|
||
|
||
**Проверка — на харнессе, а не в MAME.** Сценарий
|
||
`phys_running_jump_over_3tile_gap` в [`tests-host/t_phys.c`](tests-host/t_phys.c)
|
||
(комната с провалом в колонках 2–4). Было: прыжок со старта в кадре 34 при
|
||
`x=165`, кадр 44 приходится на колонку 3 — провал, падение. Стало: Кид
|
||
пробегает лишние 5 кадров, выравниватель ловит фазу, прыжок стартует при
|
||
`x=149`, кадр 44 даёт `x=87, col=1, row=1` — приземление на пол и бег
|
||
дальше. Остальные 8 сценариев набора не изменились ни на байт: правка
|
||
трогает только ветку разбег-прыжка у кромки.
|
||
|
||
---
|
||
|
||
<a id="bug-fall-sword-1"></a>
|
||
## BUG-FALL-SWORD-1. Отход с мечом в провал: не та последовательность падения — **ЗАКРЫТ 2026-08-04**
|
||
|
||
**Симптом (приёмка уровня 2, комната 4).** Кид с вынутым мечом отступает
|
||
от стража к дыре от упавших loose-плит. Наблюдение пользователя: он
|
||
«проваливается раньше времени», летит **с клинком в руке**, и падает **по
|
||
другим X**, чем в оригинале — «почти на целый тайл левее».
|
||
|
||
**Разбор.** Сверка `seg006:1044 start_fall` показала, что наш порт
|
||
пропустил ТРИ вещи из оригинала, и все три бьют именно по этому сценарию:
|
||
|
||
```c
|
||
void start_fall() {
|
||
Char.sword = sword_0_sheathed; // (1) меч В НОЖНЫ
|
||
inc_curr_row(); start_chompers();
|
||
...
|
||
} else if (frame >= 81 && frame < 86) { // (2) срыв при приземлении
|
||
seq_id = seq_19_fall; // после прыжка вверх
|
||
Char.x = char_dx_forward(5);
|
||
load_fram_det_col();
|
||
} else if (frame >= 150 && frame < 180) { // (3) кадры С МЕЧОМ
|
||
droppedout = 1;
|
||
if (Char.direction < dir_0_right && distance_to_edge_weight() <= 7)
|
||
Char.x = char_dx_forward(-5);
|
||
seq_id = seq_81_kid_pushed_off_ledge;
|
||
}
|
||
```
|
||
|
||
У нас все они падали в общий `else` → `seq_7` (stepfall). Разница между
|
||
`seq_7` и `seq_81` в `seqtbl.c` и объясняет ВЕСЬ симптом:
|
||
|
||
```
|
||
seq_7 stepfall : dx(1) dy(3) | 102 | dx(2) dy(6) | dx(-1) dy(9) | dy(12) | dx(-2) set_fall(1,15)
|
||
seq_81 fightfall : dy(-1)| 102 | dx(-2) dy(6)| dx(-2) dy(9) | dx(-1) dy(12) | dx(-3) set_fall(0,15)
|
||
```
|
||
|
||
`set_fall(1, 15)` против `set_fall(0, 15)` — **горизонтальный дрейф**: в
|
||
`seq_7` во время всего свободного падения `Char.x` уходит на 1 в сторону
|
||
КАЖДЫЙ кадр. За два этажа падения это и есть тот самый «почти тайл».
|
||
Оригинал в бою падает строго вниз.
|
||
|
||
**Сверка по числам** (лог SDLPoP `DBG shot`, кадры 102..105):
|
||
|
||
```
|
||
SDLPoP: 102 x=155 103 x=157 104 x=159 105 x=160 ← +2 +2 +1 = seq_81
|
||
у нас: 102 x=151 ← seq_7
|
||
```
|
||
|
||
Обратный счёт: у SDLPoP в момент решения `x = 150`, дальше
|
||
`char_dx_forward(-5)` при `dir=-1` даёт `+5` → 155. У нас решение при
|
||
`x = 152` — то есть **по самому правилу срыва расхождения нет**: обе
|
||
позиции лежат в колонке 7 (`dx_weight = x + 14`, кадр 157 имеет
|
||
`weight_x = 14`; колонка 7 — это `x ∈ [149,163)`). Двухпиксельная разница
|
||
— фаза отхода (шаг отступления `dx(-3)+dx(-2)` = 5 пикселей за цикл), а она
|
||
зависит от RNG стража и между движками совпасть не обязана. «Раньше
|
||
времени» — это не срыв не там, а seq_7 вместо seq_81.
|
||
|
||
**Что подтвердилось попутно.** Старт уровня 2 у нас **байт в байт** как в
|
||
SDLPoP: `frame=15 x=107 y=118 dir=-1 col=3 row=1 room=5`. Таблицы кадров и
|
||
`seqtbl` вынуты из оригинального бинарника, так что расходиться могут
|
||
только РЕШЕНИЯ движка — искать надо всегда там.
|
||
|
||
**Связь с [BUG-LAND-SWORD-1](#bug-land-sword-1).** Тот фикс (`land()`
|
||
даёт `seq_63` при вынутом мече) остаётся — он есть в оригинале, — но для
|
||
Кида он теперь почти недостижим: `start_fall` убирает меч в ножны, и после
|
||
приземления кадр 109 разбирает обычный `control_crouched`. То есть
|
||
настоящий корень вечного приседа был здесь, а не в `land()`.
|
||
|
||
**Метод.** Пустышка `pop_dbg_trap()` в резиденте W1 (идея пользователя):
|
||
брейкпоинт на банковый код ставить нельзя — 0xC000+ это окно, куда мапятся
|
||
все банки, и точка ловит чужие функции. Оставлена в `pop_state.c` как
|
||
многоразовый инструмент.
|
||
|
||
---
|
||
|
||
## BUG-CTRL-FRAME-1. Геометрия Кида считалась по кадру СТРАЖА — **ЗАКРЫТ 2026-08-04**
|
||
|
||
Самый неприятный класс: не косметика, а неверные РЕШЕНИЯ движка, причём
|
||
плавающие — одна и та же поза Кида давала разный результат в зависимости от
|
||
того, в какой фазе анимации находится страж.
|
||
|
||
**Симптом (приёмка уровня 2, комната 4).** Кид под сплошной плитой (1,6),
|
||
справа от него дыра от упавшей loose-плиты. По ↑ он то прыгает вверх
|
||
впустую (упираясь головой в плиту), то пытается зацепиться — но **не с той
|
||
координаты X**, с которой запрыгивает оригинал. Наблюдение пользователя,
|
||
оказавшееся точным: «похоже, дело в страже — в комнатах без стража то же
|
||
самое рисуется и работает правильно».
|
||
|
||
**Замер (брейкпоинт на входе `pop_jump_up_seq`, состояние снято в момент
|
||
решения).**
|
||
|
||
```
|
||
_Kid frame=15 (стойка) x=156 y=181 dir=влево curr_col=6 curr_row=2
|
||
кадр 15 Кида (kid_data.bin): dx=0 weight_x=3
|
||
pop_gframe (кадр СТРАЖА, image 17): dx=-1 weight_x=8
|
||
```
|
||
|
||
Считаем `dx_weight()` обоими кадрами:
|
||
|
||
| | кадр Кида (правильно) | кадр стража (что было) |
|
||
|---|---|---|
|
||
| `dx_weight()` | 156+3 = **159** | 156+9 = **165** |
|
||
| `m7()` | (159−7−58)/14 = 6 ост. **10** | (165−7−58)/14 = 7 ост. **2** |
|
||
| `curr_col` | 6 | **7** ← намерено |
|
||
| `distance_to_edge_weight()` | **10** | **2** |
|
||
| ветка `jump_up_or_grab` | 10 ≥ 6 → шаг назад + зацеп | 2 < 6 → `jump_up_plain` |
|
||
| результат | x 156 → **160**, `seq_24/8` | x без изменений, **`seq_28`** ← намерено (A=28 на выходе) |
|
||
|
||
Обе строки «намерено» совпали с предсказанием по кадру стража до единицы —
|
||
диагноз подтверждён, а не выведен.
|
||
|
||
**Корень.** `cur_frame` (`pop_kid.c:200`) — ОДИН глобал на всех персонажей
|
||
(так и в оригинале), его владелец — тот, кто последним прошёл `load_frame`.
|
||
В нашем кадре последним тикает страж (`pop_guard_tick`), поэтому к моменту
|
||
управления Кидом там лежит кадр стража. А `kid_cur_dx/dy/flags` читают этот
|
||
глобал напрямую, и через него считается ВСЯ геометрия управления:
|
||
`dx_weight` → `determine_col` → `distance_to_edge_weight` →
|
||
`get_edge_distance` → выбор ветки в `check_jump_up`.
|
||
|
||
Оригинал страхуется явно и симметрично: `play_kid_frame` (seg000:1209) и
|
||
`play_guard_frame` (seg000:1246) сразу после `loadkid`/`loadshad` зовут
|
||
**`load_fram_det_col()`** (= `load_frame` + `determine_col`, seg006:0144) —
|
||
ДО `control()`. У нас этого вызова не было; в `pop_ctrl_tick` стоял
|
||
комментарий «кадр не перезагружаем — play_seq в kid_tick сделает это
|
||
следующим шагом», и он был неверен: `control()` читает `cur_frame` РАНЬШЕ,
|
||
чем `play_seq` его обновит.
|
||
|
||
**Фикс.** `pop_load_fram_det_col()` (`pop_kid.c`) — порт связки; зовётся
|
||
после `pop_loadkid()` в `pop_ctrl_tick` и после `pop_loadshad_and_opp()` в
|
||
`pop_guard_tick`. Вторая половина связки (`determine_col`) выполняется
|
||
только на ветке Кида: `determine_col` у нас существует лишь для него —
|
||
`pop_map` работает с `Kid` напрямую, а не с абстрактным `Char`.
|
||
|
||
**Что это объясняет задним числом.** Плавающее поведение прыжка — кадр
|
||
стража меняется каждый тик, вместе с ним `dx`/`weight_x`, и `distance`
|
||
Кида скакал через порог 6. А «голова Кида поверх плиты (1,6)», с которой
|
||
начался разбор, — не баг отрисовки: Кид просто оказывался в позе, которой
|
||
в оригинале в этом месте не бывает.
|
||
|
||
**Урок.** Любой глобал, который в оригинале «принадлежит активному
|
||
`Char`», у нас обязан перезагружаться на КАЖДОМ входе в окно `Char` — иначе
|
||
между персонажами течёт состояние, и баг проявляется только когда в комнате
|
||
есть второй персонаж. Здесь такой глобал уже ловили однажды: см. запись
|
||
про кэш кадра отрисовки в `load_frame` (`pop_kid.c:342`).
|
||
|
||
---
|
||
|
||
<a id="bug-land-sword-1"></a>
|
||
## BUG-LAND-SWORD-1. Кид навсегда застревает в приседе после падения с мечом — **ЗАКРЫТ 2026-08-04**
|
||
|
||
**Симптом (приёмка уровня 2).** Страж ударил Кида, Кид потерял HP,
|
||
провалился через loose-плиту на этаж ниже — и **сел в присед, из которого
|
||
не выходит**: клавиши не действуют вообще.
|
||
|
||
**Что показало ЖИВОЕ состояние в MAME** (мост `mame-z80`, чтение по
|
||
адресам из `roomtest.noi` — окно застало багу в момент):
|
||
|
||
```
|
||
_Kid frame=109 x=144 y=181 dir=-1 col=6 row=2 action=1 room=4
|
||
sword=2 (ВЫНУТ) alive=-1 curr_seq=0x2037
|
||
_holding_sword=1 _can_guard_see_kid=0 control_x/y/shift = 0
|
||
```
|
||
|
||
`curr_seq = 0x2037` — это внутри метки `softland_crouch`
|
||
(`SEQTBL_BASE + 1736 = 0x2036`). Смотрим `seqtbl.c:916`:
|
||
|
||
```c
|
||
LABEL(softland) // seq_17_soft_land
|
||
act(actions_5_bumped), SEQ_KNOCK_DOWN, dx(1), frame_107_fall_land_1,
|
||
dx(2), frame_108_fall_land_2,
|
||
act(actions_1_run_jump), LABEL(softland_crouch) frame_109_crouch,
|
||
jmp(softland_crouch), // ВЕЧНЫЙ ЦИКЛ на кадре 109
|
||
```
|
||
|
||
То есть `seq_17_soft_land` **не заканчивается сам** — он крутится на кадре
|
||
109, и вывести из него может ТОЛЬКО `control_crouched()`.
|
||
|
||
**Корень.** `control()` (seg005:252) до `control_crouched` при вынутом
|
||
мече не доходит — раньше срабатывает ветка
|
||
|
||
```c
|
||
} else if (Char.sword == sword_2_drawn) {
|
||
control_with_sword();
|
||
```
|
||
|
||
а `control_with_sword` (seg005:964) в мирной обстановке
|
||
(`can_guard_see_kid < 2`, под ногами не loose) умеет ровно одно: если кадр
|
||
== 171 (стойка с мечом) — убрать меч. Кадр 109 он не знает. **Замкнутый
|
||
круг: последовательность ждёт control_crouched, а диспетчер туда не
|
||
пускает.**
|
||
|
||
Оригинал такой позы просто не допускает — `land()` (seg005:176):
|
||
|
||
```c
|
||
if (Char.charid >= charid_2_guard || Char.sword == sword_2_drawn) {
|
||
Char.sword = sword_2_drawn;
|
||
seq_id = seq_63_guard_active_after_fall; // боевая стойка
|
||
} else {
|
||
seq_id = seq_17_soft_land; // присед
|
||
}
|
||
```
|
||
|
||
**У нас этой ветки не было** — `pop_map.c land()` ставил
|
||
`SEQ_17_SOFT_LAND` безусловно. На уровне 1 не всплывало, потому что там
|
||
меч подбирается поздно и падать с ним особо негде; на уровне 2 меч у Кида
|
||
**с первого кадра** (`have_sword = level >= 2`), а в комнате 4 страж стоит
|
||
в одном ряду с двумя loose-плитами — сценарий собирается сам.
|
||
|
||
**Фикс.** Порт недостающей ветки: при `Kid.sword == SWORD_2_DRAWN`
|
||
падение на один этаж даёт `seq_63` (боевая стойка), а не присед. Ветка
|
||
`charid >= charid_2_guard` нам не нужна — наш `land()` работает только с
|
||
Кидом.
|
||
|
||
**Почему не задело падение на ДВА этажа** (`seq_20_medium_land`): там
|
||
`jmp` в конце нет — 29 кадров приседа и автоматический подъём
|
||
(`seqtbl.c:934`), поэтому оно развязывается само. Вечный цикл только у
|
||
`softland`.
|
||
|
||
**Урок на будущее.** Симптом «персонаж не реагирует на управление» стоит
|
||
диагностировать не по кадру, а по `curr_seq`: адрес прямо показывает, в
|
||
какой метке `seqtbl` он завис, и дальше видно, кто обязан был его оттуда
|
||
вывести.
|
||
|
||
---
|
||
|
||
## Проверено в MAME 2026-08-01 (ревизия L1-TRIAGE)
|
||
|
||
Три бага стояли как **Critical** с 2026-07-21 и по исходникам выглядели
|
||
закрытыми, но переподтверждены не были. Прогон в MAME (roomtest, `ROOMNAV`,
|
||
чтение `_Kid` через мост) закрыл все три.
|
||
|
||
### BUG-1. Боковой переход через ворота: Kid проваливается на row 1 — **ЗАКРЫТ (не воспроизводится)**
|
||
|
||
**Был симптом:** при проходе через ОТКРЫТЫЕ ворота в соседнюю комнату
|
||
(через шов) Kid оказывался на ряду row 1 вместо row 0.
|
||
|
||
**Проверка 2026-08-01, точный сценарий бага.** Комната 6, Kid на ряду 0;
|
||
осторожный шаг на кнопку (0,2) — решётка room8 (0,9) поднимается; удержание
|
||
← через открытый шов:
|
||
|
||
| момент | `room` | `x` | `y` | `curr_col` | `curr_row` |
|
||
|--------|--------|-----|-----|-----------|-----------|
|
||
| на кнопке в room6 | 6 | 97 | 55 | 2 | 0 |
|
||
| после перехода | **8** | 181 | **55** | 8 | **0** |
|
||
|
||
Ряд и Y сохранены, Kid стоит на полу ряда 0 (скриншот `triage_06.png`).
|
||
Провала нет.
|
||
|
||
**Заодно снят и сам диагноз записи** — он был неверен. В записи стояло:
|
||
«Y/`curr_row` при боковом переходе НЕ репроецируются, в отличие от
|
||
`check_leave_below`». Это не дефект, а **точное поведение оригинала**:
|
||
`goto_other_room` (`SDLPoP/src/seg002.c:390`) для направлений left/right
|
||
меняет ТОЛЬКО `Char.x` (±140), а `Char.y`/`curr_row` трогает исключительно
|
||
для up/down. Наш `check_leave` (`pop_map.c:1402`) делает ровно то же.
|
||
Реальной причиной симптома была, судя по всему, кромочная коллизия — её
|
||
закрыл `char_x_forward_edge` (см. BUG-SEAM-PINGPONG ниже).
|
||
|
||
### BUG-2. Возврат из комнаты назад: Kid отбрасывается обратно (ping-pong) — **ЗАКРЫТ (не воспроизводится)**
|
||
|
||
**Был симптом:** после перехода в соседнюю комнату попытка сразу вернуться
|
||
приводила к тому, что Kid снова закидывался в ту же комнату — выйти нельзя.
|
||
|
||
**Проверка 2026-08-01.** Шов room2↔room3 (ряд 1, без ворот — чистый
|
||
горизонтальный переход), с намеренным разворотом СРАЗУ после пересечения:
|
||
|
||
| действие | `room` | `x` | `curr_col` |
|
||
|----------|--------|-----|-----------|
|
||
| старт в room2 | 2 | 86 | 1 |
|
||
| держим → | **3** | 122 | 3 |
|
||
| сразу держим ← | **2** | 195 | 9 |
|
||
| сразу держим → | **3** | 85 | 1 |
|
||
| сразу держим ← | **2** | 183 | 8 |
|
||
|
||
Комната меняется РОВНО один раз на пересечение, туда и обратно, без
|
||
осцилляции. Механизм на месте: `pop_leave_timer` (порт `exit_room_timer`,
|
||
`pop_map.c:112,1553`) + `char_x_forward_edge` (`pop_map.c:365`).
|
||
|
||
### BUG-3. Climb-up на тайл-кнопку: неправильная окклюзия — **ЗАКРЫТ фиксом от 2026-07-28**
|
||
|
||
**Был симптом:** при подтягивании на тайл, верх которого — кнопка
|
||
(opener/closer), Kid рисовался ПОВЕРХ кнопки вместо того, чтобы быть
|
||
перекрытым её передней гранью.
|
||
|
||
**Почему закрыт.** Запись требовала: «трактовать нажатую кнопку как
|
||
floor-тайл в climb-overlay, учесть подстановку из `get_tile_to_draw`».
|
||
Ровно это и сделано `tile_code_drawn()` — `climb_overlay_tile`
|
||
(`pop_bg.c:1346`) берёт ПОДСТАВЛЕННЫЙ код тайла, а не сырой. То есть
|
||
BUG-3 — дубль пункта «спуск с кнопки (room8, кромка (0,6))» из раздела
|
||
«Исправлено», заведённый до фикса.
|
||
|
||
**Оговорка, чтобы не выдавать желаемое:** покадрово в MAME снимался СПУСК
|
||
(кадры 148..138). Подъём идёт через ту же ветку и ту же таблицу
|
||
`FLOOR_LEFT_OVERLAY[fidx]`, поэтому отдельного дефекта тут быть не может,
|
||
но визуально направление «вверх» не переснималось. Если при сквозном
|
||
прохождении (L1-PASS) увидишь Kid поверх кнопки на подъёме — заводи заново.
|
||
|
||
---
|
||
|
||
## Косметика окклюзии — закрыта кодом, список отставал (сверено 2026-08-01)
|
||
|
||
Четыре записи от 2026-07-22 висели как открытые Medium. Каждая описывала
|
||
недостающий кусок порта; каждый из них с тех пор написан, но записи никто не
|
||
снял. Ниже — что именно закрывает каждую.
|
||
|
||
### BUG-CEIL-1. Прыжок вверх: руки Kid рисуются ПОВЕРХ потолка — **ЗАКРЫТ**
|
||
|
||
**Был симптом:** при прыжке вверх (SEQ up, кадры 67..79) руки/голова Kid
|
||
заходили в полосу кладки у потолка (row −1) и рисовались ПОВЕРХ неё.
|
||
|
||
**Чем закрыт:** `ceil_over_kid_tile()` (`pop_bg.c:1275`) — порт «нижняя грань
|
||
потолка и кадр плиты-потолка идут в FOREtable», т.е. поверх персонажа.
|
||
Зовётся из `pop_fore_over_kid` (`pop_bg.c:1575`) и из fore-прохода стража
|
||
(`:1607`). Комментарий на месте прямо называет причину: «иначе руки
|
||
прыгающего Kid лезут на кромку потолка».
|
||
|
||
### BUG-CEIL-2. Тряска/разбитие loose-плиты в потолке (row −1) — **ЗАКРЫТ**
|
||
|
||
**Был симптом:** loose-плита в ряду 2 верхнего соседа (room5 (2,5) → потолок
|
||
room6 над (0,5)) при прыжках Kid на (0,5) не тряслась и не разбивалась —
|
||
loose-состояние соседней комнаты не тянулось.
|
||
|
||
**Чем закрыт:** отдельное состояние плиты-потолка `pop_ceil_modif[10]`
|
||
(`pop_bg.c:547`, ведёт `pop_map`) + пара `pop_ceil_shake_draw()` /
|
||
`pop_ceil_bake_empty()` (`pop_bg.c:815,827`): дрожание рисуется на текущей
|
||
странице поверх фона, а провал «запекается» в ОЗУ-копию, чтобы heal его
|
||
сохранял. В полосе у потолка виден только НИЗ плиты (`draw_tile_aboveroom`),
|
||
всё выше режется клипом `POP_YOFF` — как в оригинале.
|
||
|
||
Из этого следует, что и запись «требует персистентного per-room modifier
|
||
соседей, это Фаза P0 gates_spikes_plan» больше не верна: понадобился не общий
|
||
механизм, а один массив на 10 байт под конкретный случай.
|
||
|
||
### BUG-CEIL-3. Анимация ворот стирает потолок над ними — **ЗАКРЫТ**
|
||
|
||
**Был симптом:** при анимации решётки шва полоса кладки у потолка НАД
|
||
воротами пропадала — чёрный `bar` по col0 стирал ряд −1, а redraw его не
|
||
восстанавливал.
|
||
|
||
**Чем закрыт:** `pop_room_redraw_seam_left()` (`pop_bg.c:793`) больше не
|
||
трогает полосу потолка — `bar` идёт с `POP_YOFF + 3`, а не с `POP_YOFF`:
|
||
низ полосы кладки на room-space y=2, бары ворот начинаются с y=3
|
||
(`gate_top_y = dby − 62`). Приятный побочный эффект: отпала необходимость
|
||
перерисовывать `draw_tile(-1,0)` на КАЖДОМ кадре анимации решётки — то есть
|
||
фикс не только косметический, но и вдвое дешевле прежнего.
|
||
|
||
### BUG-OCCL-1. Тень дальней колонны перекрывает Kid — **ЗАКРЫТ**
|
||
|
||
**Был симптом:** Kid у (0,4)-(0,5) частично перекрыт тёмной штриховкой —
|
||
боковой гранью ДАЛЬНЕЙ колонны, которая окклюдить персонажа не должна
|
||
(over-occlusion fore-слоя, рисовавшего fore футпринт-тайлов без учёта
|
||
глубины).
|
||
|
||
**Чем закрыт:** разделение слоёв по признаку из оригинала, а не по нашим
|
||
соображениям о глубине (`overlay_mid_tile`, `pop_bg.c:1367`). Ключ,
|
||
подтверждённый трассой оригинала: `draw_tile_right`, `draw_tile_anim_right` и
|
||
`draw_loose` кладут спрайты через `add_backtable` НАПРЯМУЮ, минуя
|
||
`ptr_add_table` — значит правая грань левого соседа («шахматка» столба 93,
|
||
blueline, грани пик/loose соседа, кадр loose) **всегда** рисуется ПОД
|
||
персонажем. Через `ptr_add_table` (→ midtable при `draw_other_overlay`) идут
|
||
только «floor B» 42 при левом соседе-поле, `draw_tile_base`,
|
||
`draw_tile_anim` (свои пики) и `draw_tile_bottom`.
|
||
|
||
---
|
||
|
||
## BUG-DOOR-CLIP. Подъём по лестнице двери уровня: нет обрезки по правому косяку — **ИСПРАВЛЕН 2026-08-01**
|
||
|
||
**Был симптом (найден пользователем сразу после L1-EXIT):** при подъёме по
|
||
лестнице за дверью уровня (кадры 224..228) силуэт Кида вылезал ПРАВЕЕ правого
|
||
косяка проёма. По высоте обрезка была корректна.
|
||
|
||
**Причина — недопортированная половина `clip_char`** (seg006:1231). Для
|
||
кадров двери оригинал ставит ДВА клипа, у нас был только первый:
|
||
|
||
```c
|
||
if (frame >= frame_224_exit_stairs_8 && frame < 229) {
|
||
obj_clip_top = leveldoor_ybottom + 1;
|
||
obj_clip_right = leveldoor_right;
|
||
}
|
||
```
|
||
|
||
Отдельная ловушка: комментарий в SDLPoP говорит «frames 217..228», а КОД
|
||
проверяет `>= frame_224_exit_stairs_8`, то есть **224..228**. Портировано по
|
||
коду.
|
||
|
||
**Почему именно обрезка, а не fore-слой:** створка и косяк уходят в оригинале
|
||
ЦЕЛИКОМ в backtable (`draw_leveldoor`, все `add_backtable`), то есть рисуются
|
||
ПОД персонажем и перекрыть его не могут. Единственный способ спрятать
|
||
поднимающегося — срезать сам спрайт.
|
||
|
||
**Фикс:**
|
||
- libbgi: `gfx_blit_cols_part_w(..., uint8_t maxw)` — обрезка СПРАВА у
|
||
колоночного блита. Для column-major это ровно уменьшение числа колонок,
|
||
то есть внутри ядра механизм уже был (так же клипается край экрана:
|
||
`w = _bgi_maxx + 1 - x`), наружу не было выведено. Тело блита переехало
|
||
туда, `gfx_blit_cols_part` стал тонкой обёрткой (maxw=0) — тем же приёмом,
|
||
каким `gfx_blit_cols` уже обёрнут вокруг `gfx_blit_cols_part`. Работает и
|
||
при flip: первые `maxw` нарисованных колонок всегда ложатся в ЛЕВУЮ часть
|
||
футпринта.
|
||
- PoP: `pop_leveldoor_right` / `pop_leveldoor_ybottom` (порт одноимённых
|
||
глобалов) пишет `draw_leveldoor` в `pop_state` — их читает `clip_char` из
|
||
другого банка; `pop_clip_char_right()` отдаёт границу, `kid_draw`
|
||
превращает её в `maxw` и уводит эти кадры с noclip-пути на общий.
|
||
`kid_lw` (прямоугольник heal) тоже сужается — стираем ровно нарисованное.
|
||
|
||
**Проверено в MAME:** `pop_leveldoor_right = 176` — ровно
|
||
`(draw_xh<<3) + 48` для двери комнаты 9; `pop_leveldoor_ybottom` = 112 у
|
||
закрытой створки и 69 у поднятой (сходится с формулой оригинала).
|
||
Отрисовка подтверждена пользователем на живом подъёме.
|
||
|
||
---
|
||
|
||
## BUG-SEAM-PINGPONG (#4). Пинг-понг drawn_room у шва с закрытыми воротами — **РЕШЁН 2026-07-22**
|
||
|
||
Настоящий корень найден потиковой трассой ЖИВОГО SDLPoP 1.23
|
||
(lldb-брейкпоинты на leave_room/bumped/safe_step с логом Char +
|
||
char_x_left/right; fixes выключены = vanilla). Прежние гипотезы оставлены
|
||
ниже для истории — они НЕ были причиной.
|
||
|
||
### КОРЕНЬ (подтверждён трассой + исходником)
|
||
|
||
`set_char_collision` (seg006:0723): `char_x_right = obj_x/2 + 58`, где
|
||
`load_frame_to_obj` (seg008:1728) считает `obj_x = 2*char_dx_forward(dx) - 116`
|
||
и **добавляет +1** для кадров «чётного пикселя»:
|
||
`if ((sbyte)(cur_frame.flags ^ obj_direction) >= 0) ++obj_x;`
|
||
(бит 0x80 флагов кадра XOR направление; вправо: +1 если бит НЕ стоит).
|
||
Деление `obj_x/2` — C-усечение К НУЛЮ, поэтому при `e = x+dx <= 57`
|
||
(obj_x < 0, зона левого шва) поправка +1 даёт `char_x_right = e+1`, а при
|
||
e >= 58 формула сокращается к чистому `e`.
|
||
|
||
Итог: Kid, осевший после отскока от ворот шва на x=57 (frame15, флаги 0x43 —
|
||
бит 0x80 не стоит), имеет **char_x_right = 58** и порога leave-left (<=57)
|
||
НЕ достигает. Наш движок считал передний край как `Kid.x + dx` без поправки
|
||
→ 57 → ложный leave → пинг-понг.
|
||
|
||
Эталонный цикл SDLPoP (нормализовано по трассе): стойка x=61 → тап вправо →
|
||
safe_step(d=0) → step, на первом dx(1) x=62 → bump (edge-триггер) → align 61
|
||
→ seq47 dx(-4) → **x=57** (скрыт за кромкой) → кадры 50/51/52 (cxr 61/60/58,
|
||
у всех бит 0x80 снят, e>57 — без сдвига) → стойка cxr=58 → leave НЕ
|
||
срабатывает; тап → safe_step d=3 → x=60 (1/3 видно); тап → step1 → x=61
|
||
(2/3 видно); тап → bump → 57 … по кругу. Char.room и drawn_room НЕ меняются.
|
||
|
||
### Фикс (pop_map.c)
|
||
|
||
`char_x_forward_edge()`: `e = char_dx_forward(dx); if (((flags ^ (dir<0 ?
|
||
0x80 : 0)) & 0x80) == 0 && e <= 57) e++;` — используется в `char_front_coll`
|
||
(коллизия/bump/edge_distance) и в `check_leave` (порог ухода). Плюс порт
|
||
doortop-гарда leave-right из leave_room (тайл (9,row) = doortop → правого
|
||
выхода нет). `pop_leave_timer` (exit_room_timer) оставлен — он реален в
|
||
seg002/seg003.
|
||
|
||
### Симптом (как выглядел)
|
||
|
||
Kid стоит за решёткой закрытых ворот шва (левый сосед room8 виден в кромке
|
||
room6). При удержании/нажатии ВПРАВО экран пинг-понгует между двумя
|
||
состояниями:
|
||
- **A**: показывается room6, Kid у левой кромки за решёткой (спрайт на 2/3);
|
||
- **B**: показывается room8, Kid у его правой кромки.
|
||
|
||
Эталон SDLPoP: drawn_room **всегда остаётся room6**, Kid осциллирует у
|
||
кромки (1/3→2/3→отступил→по кругу), в room8 экран НЕ переключается.
|
||
|
||
### Инструментальный диагноз (watchpoint на pop_leave_dir)
|
||
|
||
В момент лишнего свитча A→B: `pop_leave_dir=1 (LEFT)`, Kid **frame=15 (СТОЯ,
|
||
не transient!), Kid.x=57** (до репроекции +140). То есть:
|
||
- `char_x_right = char_dx_forward(kid_cur_dx()) = Kid.x + frame15.dx = 57+0 = 57`.
|
||
- Порог leave-left (взгляд вправо): `char_x_right <= 57` → срабатывает РОВНО на 57.
|
||
- Грань ворот (где их держит коллизия) = **61** (`wall_dist_from_left[1]=10 +
|
||
coll_tile_left_xpos=51`). Между 57 и 61 — **зазор 4px**: Kid НЕ удержан
|
||
воротами (d=61−57=4≥0 → check_bumped не бампит), но уже на пороге ухода.
|
||
- Kid оседает на 57 из-за recoil отскока: seq_47 = `act(bumped), dx(-4),
|
||
frame_50, 51, 52`; SEQ_DX(−4) двигает Char.x на −4 суммарно; frame_50.dx=4
|
||
компенсирует ТОЛЬКО точку коллизии НА кадре 50, но при возврате в стойку
|
||
(frame15, dx=0) `char_x_right = Char.x = aligned−4 = 57`.
|
||
|
||
### Что было ИСКЛЮЧЕНО (сверено с исходниками SDLPoP, НЕ причина)
|
||
|
||
- Формула char_x: `char_x_right = obj_x/2+58 = Char.x+frame.dx` (seg006
|
||
set_char_collision) — совпадает с нашим char_dx_forward.
|
||
- Позиция грани ворот: `get_left_wall_xpos = wall_dist_from_left[1](10) +
|
||
xpos_in_drawn_room(x_bump[9+5])+7 = 10+(184−140)+7 = 61` — совпадает с нашим
|
||
(`x_bump[−1+5]=44`, +7, +10 = 61).
|
||
- Порог leave: SDLPoP leave_room looking-right `char_x_right<=57` — совпадает.
|
||
- Данные кадров: frame_50 (image=49,dx=4,flags=0x67) и frame_15
|
||
(image=14,dx=0,flags=0x43,sword=9) — БАЙТ-В-БАЙТ как в SDLPoP frame_table_kid.
|
||
- seq_47 (act bumped, dx(-4), frame 50/51/52) — совпадает.
|
||
|
||
### Почему точечные фиксы НЕ работали
|
||
|
||
- Гард в check_leave (подавить leave на закрытых воротах) — это ОТСЕБЯТИНА,
|
||
не SDLPoP (в leave_room такого нет); откачено.
|
||
- `exit_room_timer=2` (порт seg002 exit_room — РЕАЛЬНЫЙ механизм, оставлен как
|
||
pop_leave_timer): блокирует leave 2 кадра после входа в комнату. НЕ спасал:
|
||
положение Char.x=57 **устойчивое** (Kid стоит), а не transient.
|
||
|
||
### Задел, который НЕ понадобился: порт coll_room + отложенный drawn_room
|
||
|
||
Трасса показала, что в эталоне `Char.room`/`drawn_room` вообще не меняются,
|
||
поэтому план S2/S3 для этого бага не потребовался. Остаётся заготовкой под
|
||
стражей/двух персонажей в кадре:
|
||
|
||
1. **Раздельные комнаты.** `Char.room` ≠ `drawn_room`. Уже есть `kid_room`
|
||
(S1) + рендер-смещение `pop_kid_set_render_dx(∓140)`.
|
||
2. **Коллизия по Char.room через coll_room (seg004, S2).** Портировать
|
||
`check_collisions` → `get_row_collision_data` → `get_left_wall_xpos`/
|
||
`get_right_wall_xpos` → `curr_row_coll_room[]`/`_flags[]`,
|
||
`bump_col_left_of_wall`/`bump_col_right_of_wall` → `check_bumped_look_*` →
|
||
`bumped()`. Тайлы — из РЕАЛЬНОЙ комнаты колонки, НЕ из снапшота.
|
||
3. **Отрисовка по drawn_room**, персонаж со сдвигом ±140.
|
||
4. **Отложенная смена drawn_room (seg002/seg000, S3):** `leave_room` →
|
||
`goto_other_room` → `exit_room` (`next_room`) → `check_the_end`.
|
||
|
||
Реализовано на 2026-07-22: S1 + `pop_leave_timer`.
|
||
Память: [[pop_seam_room_model]], [[sdlpop_odd_pixel_char_x]].
|
||
|
||
---
|
||
|
||
## Решено НЕ делать
|
||
|
||
### OPT-1. Хирургический редрой левого шва (ворота соседа) — 2026-07-22
|
||
|
||
**Возможность:** `pop_room_redraw_seam_left()` (pop_bg.c) на каждое изменение
|
||
openness рисует `bar(BLACK)` по всему col0 + **полный `draw_tile(0,0)`**
|
||
(стены, `topright`, `wall_pattern` с `prandom()` — десятки блитов). Реально
|
||
анимируются только бары решётки — `draw_gate_back` (~9 `env_b`).
|
||
|
||
**Стоимость:** seam-блок (синий io_border) занимает ~30-50% кадрового периода,
|
||
**но только пока openness меняется** — во время открытия и медленного
|
||
авто-закрытия (~5 сек после схода с кнопки). В покое — 0%. Замерено в MAME:
|
||
брейк на `_pop_room_redraw_seam_left` (0x5A54) срабатывает ⟺ сегмент дорогой.
|
||
|
||
**Приём** (heal НЕ годится: печёные бары устаревшие, поэтому и стоит
|
||
`bar(BLACK)`+redraw): `bar(BLACK)` только по полосе баров (x=0,
|
||
gate_top..gate_bot) + `draw_gate_back(lmod,...)` + дорисовать статику тайла
|
||
(0,0), задетую полосой. **Пиксель-чувствительно** — обязательна выверка в
|
||
MAME по кромкам.
|
||
|
||
**Решение:** оставляем как есть — стоимость транзиентная, в бюджет
|
||
помещаемся. Делать, только если упрёмся в кадровый бюджет на сценах с
|
||
воротами.
|
||
|
||
---
|
||
|
||
## Исправлено
|
||
|
||
- **Спуск с кнопки (room8, кромка (0,6)): Кид просвечивал в щель, ближняя рука
|
||
срезана до одного пикселя** — ИСПРАВЛЕНО 2026-07-28. Две причины:
|
||
(а) не был портирован `clip_char()` (seg006:1749) — верхняя обрезка спрайта по
|
||
`y_clip[curr_row+1]`, когда тайл над головой стена/пол; сделано
|
||
(`pop_clip_char_top` в pop_map.c + `gfx_blit_cols_part` в libbgi, heal чистит
|
||
уже обрезанный прямоугольник);
|
||
(б) `climb_overlay_tile` выбирал ветку `draw_floor_overlay` (seg008:1E3A) по
|
||
СЫРОМУ коду тайла — а нажатая кнопка в `get_tile_to_draw` (seg008:240)
|
||
подменяется на floor/stuck. Тайл-кнопка не проходил тест floor, уходил в
|
||
`draw_other_overlay` и закрашивал Kid ЦЕЛЫМ тайлом вместо узкой кромки
|
||
`floor_left_overlay[frame-137]`. Фикс — `tile_code_drawn()` (одна подстановка
|
||
на все слои). Проверено покадрово в MAME (кадры 148..138). **Этим же
|
||
фиксом закрыт BUG-3** (см. выше).
|
||
|
||
- **Шов ворот жёг 50% кадра в покое (закрытая решётка)** — ИСПРАВЛЕНО
|
||
2026-07-22. Причина — **баг кодогенератора SDCC z80** (memory
|
||
`sdcc_z80_cmp_store_a_bug`): `if (m[9] != seam_sig) seam_sig = m[9];`
|
||
компилировался в `sub (seam_sig)` (A ← разность) + `ld (seam_sig),a` —
|
||
сохранял РАЗНОСТЬ `m[9]-seam_sig`, не `m[9]`. `seam_sig` осциллировала
|
||
(напр. 25↔231), `m[9]!=seam_sig` истинно каждый кадр → `draw_tile(0,0)`
|
||
каждый кадр даже у неподвижной решётки. Фикс: store-до-сравнения
|
||
(`seam_sig = g;` из чистого `g` ДО `sub`), подтверждён в .asm. Прочёс всех
|
||
модулей PoP: других случайных compare-then-store нет.
|
||
|
||
- **Пики: 2×полный `draw_tile` на кадр анимации** → хирургический редрой,
|
||
2026-07-22. `pop_spike_redraw`: `heal_off` уже возвращает всю печёную
|
||
статику (база 127, пол, грани); поверх анимируются только два острия
|
||
(`SPIKES_FRAM_LEFT` в своей ячейке + `SPIKES_FRAM_RIGHT` в соседней).
|
||
Заменили 2×`draw_tile` на 2×`env_b` — пиксель-в-пиксель тот же результат,
|
||
в разы дешевле (шахта из нескольких пик больше не съедает полный кадр).
|
||
|
||
- **Отрисовка нажатой кнопки (0,2)/(0,3)** — ИСПРАВЛЕНО. Причина: `fore_tile`
|
||
(pop_bg.c, fore-слой поверх Kid) рисовал переднюю грань КНОПКИ (bottom_id
|
||
149) поверх уже нарисованной грани пола (43) — «остаток нажатой кнопки».
|
||
Фикс: `fore_tile` применяет ту же подстановку нажатой кнопки, что и
|
||
`draw_tile` (opener→floor / closer→stuck при таймере связи >1). Плюс
|
||
`pop_button_redraw` — wipe своей ячейки + правой грани (дальний угол в
|
||
0,3), низ строго yb+64 (не залезать в стену ряда 1).
|
||
|
||
---
|
||
|
||
## НЕ БАГИ (кривая картинка, но совпадает с оригиналом — НЕ чинить)
|
||
|
||
- **Кадр падения с мечом: голова/руки поверх кромки пола — САМ РЕНДЕР верен.**
|
||
Сверено с SDLPoP v1.24 (2026-08-04): там кадр падения выглядит один в один.
|
||
⚠ Речь ТОЛЬКО о виде кадра. Расхождение по X, которое было отмечено здесь
|
||
как открытое, разобрано и закрыто: [BUG-FALL-SWORD-1](#bug-fall-sword-1) —
|
||
дело не в моменте срыва (он верен), а в том, что мы играли `seq_7` вместо
|
||
`seq_81` и получали горизонтальный дрейф `set_fall(1,15)` за всё падение.
|
||
|
||
Механика точки веса, чтобы не разбирать заново. У стоек с мечом она
|
||
ОГРОМНАЯ:
|
||
|
||
```
|
||
кадр 158/170/171 (stand_with_sword): dx=0 weight_x=13
|
||
кадр 157 (walk_with_sword): dx=0 weight_x=14
|
||
кадр 15 (обычная стойка): dx=0 weight_x=3
|
||
```
|
||
|
||
`dx_weight()` = `char_dx_forward(dx − weight_x)`, а при взгляде ВЛЕВО знак
|
||
меняется — точка веса уезжает на 13 px ВПРАВО от `Char.x`. Замер нашего
|
||
кадра падения: `x=151`, взгляд влево → `dx_weight = 164` → `m7(164)` = колонка
|
||
**7**, а (1,7) — дыра. `check_on_floor` честно видит «под ногами не пол».
|
||
С обычной стойкой (weight_x=3) вышло бы 154 → колонка 6 → пол.
|
||
|
||
То есть с мечом персонаж «стоит» на 10 px правее, чем выглядит, и кромку
|
||
переступает раньше. Данные кадров у нас совпадают с `frame_table_kid`
|
||
(seg006:127) байт в байт, `swordfight`/`back_with_sword` портированы дословно
|
||
(включая `control_backward = CONTROL_IGNORE` — один нажим = один шаг).
|
||
Чинить нечего.
|
||
|
||
**Важное отличие от остальных записей этого раздела.** Там (труп стража,
|
||
падающая плита) картинка кривая, и совпадает с оригиналом лишь потому, что
|
||
оригинал сам так рисует — артефакт порядка midtable. **Здесь картинка
|
||
ПРАВИЛЬНАЯ**: Кид реально уже за кромкой, и спрайт перекрывает её ровно
|
||
настолько, насколько персонаж туда зашёл (наблюдение пользователя,
|
||
2026-08-04). Геометрия и рендер согласованы; «неправильно выглядит» —
|
||
только если считать позой то, что видит глаз, а не точку веса.
|
||
|
||
⚠ **Не путать** с тем, что БЫЛО нашим багом рядом: расширение футпринта
|
||
перерисовки под клинок (`redraw_at_char`, seg003:0430) — его не было, и меч
|
||
оставлял след/лез поверх столба. Исправлено 2026-08-04.
|
||
|
||
- **Голова стоящего Кида поверх падающей на него loose-плиты.** Комната 12:
|
||
зацеп не удался, Кид остался стоять, сбитая плита падает прямо на него —
|
||
голова рисуется ПОВЕРХ плиты. Сверено покадрово с SDLPoP v1.24 (2026-07-29):
|
||
там ровно то же самое. Артефакт оригинального движка (порядок midtable), а
|
||
не наш баг; «починка» увела бы от эталона. Отличать от соседних случаев,
|
||
которые БЫЛИ нашими багами и исправлены: вис/подтягивание на кромке плиты и
|
||
падение вместе с плитой — там плита обязана быть поверх Кида.
|
||
|
||
- **Ноги стоящего Кида поверх головы лежащего трупа стража.** Комната 21:
|
||
страж убит, комната покинута и открыта заново (у трупа своя запомненная X),
|
||
Кид СТОИТ на одну колонку правее тела — его ноги рисуются поверх головы.
|
||
Проверено в SDLPoP (2026-08-03): там ровно то же.
|
||
|
||
Корень — устройство движка, а не наш порт. `set_objtile_at_char`
|
||
(seg006:13F3) приписывает персонажа РОВНО ОДНОМУ тайлу, и ширина спрайта на
|
||
выбор тайла не влияет никак; дальше `redraw_needed_tiles` (seg008:1B06)
|
||
обходит тайлы рядами 2,1,0 и колонками 0..9, и кто позже — тот поверх.
|
||
Стоящий Кид укладывается примерно в колонку, поэтому у него это незаметно, а
|
||
лежащий труп занимает две-три колонки, но «принадлежит» левой. Всё, что
|
||
стоит правее, перекрывает выступающую часть тела. Механизма «широкий объект
|
||
участвует в нескольких тайлах» в оригинале нет — сверено с
|
||
`draw_objtable_items_at_tile`, `sort_curr_objs` и веткой
|
||
`tile_object_redraw == 0xFF` (последняя про оверлеи пола, не про персонажей).
|
||
|
||
Отличать от того, что БЫЛО нашими багами в этом же месте и исправлено
|
||
(BUG-DRAWORDER-1): колонка трупа считалась из тайла вместо X, ветка
|
||
`actions_1_run_jump` не была портирована, окно fore-клипа затиралось стражем.
|
||
Если когда-нибудь захочется «починить» и это — минимальный вариант — брать
|
||
тайл по ЦЕНТРУ габарита, а не по левой границе; но это осознанный отход от
|
||
эталона, и в других позах порядок поменяется в обратную сторону.
|
||
|
||
- **Комнаты 13, 18, 24 недостижимы в обычной игре** — свойство ДАННЫХ уровня 1,
|
||
не наш баг. Обход графа от стартовой комнаты (по `res2001.bin`, links @1952)
|
||
показывает: у всех трёх ссылки наружу есть, а на них не ссылается никто
|
||
(24: `L→9`, но у 9 `R=0`; 13 и 18 связаны только друг с другом). Признак
|
||
«комнату выкинули из компоновки, связи не почистили» — несимметричные ссылки
|
||
ровно у этих трёх, у остальных 21 симметрия полная:
|
||
|
||
```
|
||
13 L→22, у 22 R=16 | 18 L→15, у 15 R=12 | 24 L→9, у 9 R=0
|
||
13 R→16, у 16 L=22 | 18 R→12, у 12 L=15
|
||
| 18 D→19, у 19 U=12
|
||
```
|
||
|
||
Следствие: в 13/18/24 возможен «мусор в шве» — наш рендер кромки читает
|
||
крайнюю колонку соседа ПО ССЫЛКЕ, а сосед этих комнат соседом себя не
|
||
считает. Приоритет багов в этих трёх комнатах — низкий (в игре не видно).
|
||
|
||
---
|
||
|
||
## Проверено в MAME 2026-08-03 (прогон уровня 1: 11 наблюдений → 6 корней)
|
||
|
||
Сырой список наблюдений с обхода всех комнат разобран по корням.
|
||
Проверка — MAME + мост `mame-z80`: чтение `_Kid` по адресу из
|
||
`.sprinter-cc-roomtest/roomtest.noi`, `ROOMNAV` для навигации, потиковые
|
||
трассы, скриншоты.
|
||
|
||
**Оговорка о полноте проверки.** Каждый корень закрыт тем, что его СОБСТВЕННЫЙ
|
||
сценарий больше не воспроизводится; сквозного прохождения уровня и поиска
|
||
регрессий в соседней механике автоматика не делала. Чек-лист для ручной
|
||
перепроверки — в [`BUGS_OPEN.md`](BUGS_OPEN.md), раздел «Ручная перепроверка
|
||
фиксов». Особое внимание — порту `check_collisions`: он переписал ВСЮ
|
||
горизонтальную коллизию.
|
||
|
||
### BUG-LVLSTATE-1. Уровень был немутабельным — **ЗАКРЫТ**
|
||
|
||
**Симптомы:** выпитое зелье / поднятый меч / разбитая плита возвращались при
|
||
возврате в комнату; иногда кувшина нет, а пузырёк над ним крутится; в
|
||
комнате 15 раз в 1–2 с мигал контур кладки. «Первое остаётся, остальное
|
||
возвращается».
|
||
|
||
**Корень.** Уровень лежал в EMM-странице только на чтение, `enter_room`
|
||
перезаливал `room_fg[30]` из неё при каждом входе, а персистентность держала
|
||
таблица переопределений на **8 записей** (`OVR_MAX` в `roomtest.c`) — девятая
|
||
и дальше молча терялись. Второй хвост: `pop_trob.c` брал тип тайла из
|
||
СТРАНИЦЫ (`pop_level_tile_raw`), поэтому заводил trob зелья/меча там, где
|
||
предмет уже поднят — отсюда пузырёк без кувшина и мигание от `animate_sword`.
|
||
|
||
**Фикс.** `pop_level_set_tile()` пишет тайл ПРЯМО в страницу уровня (порт
|
||
`curr_room_tiles[…] = …`: `do_pickup` seg006:1671, `remove_loose` seg007:0EB8,
|
||
`loose_land` seg007:11E8). Таблица `ovr_*` удалена. Эталонная копия
|
||
foretable — в той же странице по смещению `0x1000` (страница 16 КБ, данных
|
||
2.3 КБ).
|
||
|
||
**Проверено:** комната 22, зелье (0,6) выпито → выход в 23 → возврат: кувшина
|
||
нет, пузырька нет; ROOMNAV ставит Кида уже на (0,6), потому что тайл стал
|
||
полом.
|
||
|
||
### BUG-RESPAWN-1. Респавн не перезагружал уровень — **ЗАКРЫТ**
|
||
|
||
**Как в оригинале:** цикл `play_level` (seg003:57) на КАЖДОЙ итерации, в том
|
||
числе после смерти, зовёт `load_level()` — уровень читается заново.
|
||
|
||
**Фикс.** `pop_level_reset_tiles()` (восстановление foretable из эталонной
|
||
копии) в `pop_start_level` рядом с `pop_trob_reset()`.
|
||
|
||
**Проверено:** после смерти от стража зелье в комнате 22 снова на месте.
|
||
|
||
### BUG-DEATH-1. Смерть от меча не доводилась до конца — **ЗАКРЫТ**
|
||
|
||
**Симптом:** страж убивает Кида, тот «воскресает» на месте и его убивают
|
||
снова, по кругу.
|
||
|
||
**Корень.** `hurt_by_sword` ставил seq_85 и обнулял HP, но `Kid.alive`
|
||
оставался −1, а `pop_kid_dead` (по нему главный цикл делает респавн) взводил
|
||
только путь пик/падения. В оригинале это первая строка `control_kid`
|
||
(seg006:0CD1): `if (Char.alive < 0 && hitp_curr == 0) Char.alive = 0;`.
|
||
|
||
**Фикс.** Порт этой ветки в начало `pop_ctrl_tick` + `pop_kid_dead = 1`.
|
||
|
||
**Проверено:** страж в комнате 21 убивает Кида → респавн в стартовой позиции
|
||
уровня (комната 1), цикла нет.
|
||
|
||
### BUG-GATE-ANIM-1. Ворота в отрисованной комнате не перерисовывались — **ЗАКРЫТ**
|
||
|
||
**Корень.** `pop_process_trobs` продвигал модификатор ворот, но пометки
|
||
перерисовки не ставил; ворота рисовались только при полной отрисовке комнаты
|
||
и в `pop_room_redraw_seam_left`. В оригинале `animate_door` (seg007:0522)
|
||
заканчивается `draw_trob()` (seg007:01E6).
|
||
|
||
**Фикс.** Вид перерисовки `POP_RD_GATE` + `pop_gate_redraw(row,col)` в
|
||
`pop_bg.c` (wipe зоны решётки в ячейке ПРАВОГО соседа + `draw_tile`), пометка
|
||
из `pop_process_trobs` (текущая страница каждый кадр, обе — на последнем).
|
||
|
||
**На уровне 1 это ровно ОДНА решётка:** room5 (0,5). Все остальные стоят в
|
||
колонке 9, их бары рисуются уже в соседней комнате — там работает
|
||
`pop_room_redraw_seam_left`.
|
||
|
||
**Проверено:** кнопка room5 (0,4) — решётка (0,5) поднимается на экране.
|
||
|
||
### BUG-COLL-1. Bump искал стену только в колонке переднего края — **ЗАКРЫТ**
|
||
|
||
**Симптомы:** пробегание сквозь закрытую решётку (комната 12, room5 (0,9));
|
||
влёт внутрь стены на длинном прыжке (комната 6).
|
||
|
||
**Два корня.**
|
||
1. `check_bumped` брал ОДНУ колонку — ту, в которой оказался передний край.
|
||
Для решётки в col9 окно этой колонки всего 4 px (201..204), беговой кадр
|
||
его перескакивает — бампа нет, а `check_leave` тут же уводит в соседнюю
|
||
комнату. Оригинал (`check_collisions`, seg004:0004) считает флаги
|
||
перекрытия для ВСЕХ колонок ряда и берёт колонку с переходом флага 0→1.
|
||
2. Наш guard `action == FREEFALL || MIDAIR → return` в `check_bumped`. В
|
||
оригинале таких гардов НЕТ: `bumped_fall` (seg004:04E4) специально
|
||
разбирает `actions_4_in_freefall`. Отсюда влёт в стену в прыжке.
|
||
|
||
**Фикс.** Полный порт `check_collisions` + `get_row_collision_data` +
|
||
`is_obstacle` + `bumped(delta, push_dir)`; колонки считаются от −2 до 11,
|
||
чтобы решётки СОСЕДНЕЙ комнаты (швы) бампили как свои; гарды в `check_bumped`
|
||
приведены к оригинальным (только вис и подтягивание).
|
||
|
||
**Проверено:** комната 5, бег вправо в закрытую решётку (0,9) — Кид упирается
|
||
(x встаёт на 60 в системе комнаты 1 и дальше не растёт).
|
||
|
||
**Остаток:** экран при этом перелистывается на соседнюю комнату, и Кид в шве
|
||
не рисуется — отдельный баг BUG-SEAM-DRAW-1 (модель straddle S3), см.
|
||
`BUGS_OPEN.md`.
|
||
|
||
### BUG-STANDUP-1. Вставание из приседа у стены роняло сквозь пол — **ЗАКРЫТ**
|
||
|
||
**Симптом:** комната 5, падение с кнопки (0,6) на щебень (2,7) с уроном,
|
||
присед — и при вставании провал в комнату 6.
|
||
|
||
**Корень — лишний guard `if (Kid.action == ACT_BUMPED) return;` в
|
||
`bumped_floor`** (в оригинале, seg004:0520, там `if (Char.alive)`). Цепочка:
|
||
вставание двигает Кида на 1 px в стену → бамп → `bumped_floor` прижимает `y`
|
||
к полу, отменяя `dy(−2)` из начала `medland` → наш guard возвращает
|
||
управление вместо `seq_47`, `medland` доигрывает свои `dy(+1)`,`dy(+1)` уже
|
||
ОТ пола → Кид НИЖЕ пола → следующий бамп читает беззнаковую разность как
|
||
«высоко над полом» → `bumped_fall` → выпадение вниз. У оригинала `seq_47`
|
||
обрывает `medland`, лишних `dy` нет.
|
||
|
||
Найдено потиковой трассой + временной диагностикой (колонка бампа = 8, тайл
|
||
после разрешения = 14 щебень, ветка = `bumped_floor`).
|
||
|
||
**Заодно** убран полу-порт опционального `FIX_STAND_ON_THIN_AIR`: у нас была
|
||
взята только его первая часть (кадры вставания 110..119 требуют пол), без
|
||
парной правки seqtbl (`dx(1)→dx(0)`, `dx(−4)→dx(−3)`), которую применить
|
||
нельзя — seqtbl извлечён из данных оригинала. Вернулись к ванильному
|
||
`seg006:909` (только кадр 109).
|
||
|
||
---
|
||
|
||
# Вторая волна прогона 2026-08-03 (вечер)
|
||
|
||
<a id="bug-kbd-5"></a>
|
||
## BUG-KBD-5. Зажатый Shift снимается автоповтором стрелки — **ЗАКРЫТ 2026-08-05**
|
||
|
||
**Симптом (пользователь).** Прыжок с места с зацепом (уровень 2, комната 9)
|
||
не выходит: Кид прыгает, но за кромку не цепляется. Дальше уточнения,
|
||
которые и указали на клавиатуру, а не на физику:
|
||
|
||
- «отпускаю стрелки в полёте и жму Shift один — зацеп есть»;
|
||
- «жму Shift не сразу, а когда бо́льшая часть прыжка позади — зацеп есть»;
|
||
- «зажимаю Shift, потом стрелки — прыжок есть, зацепа нет»;
|
||
- в SDLPoP та же комбинация в том же порядке работает всегда.
|
||
|
||
Обобщение: **Shift работал в одиночку и не работал вместе со стрелками.**
|
||
Ровно то же ломало и осторожный шаг — игрок держит Shift, тапает стрелку, а
|
||
Кид на каком-то тапе уходит в бег (это же поведение раньше описывалось как
|
||
BUG-KBD-4, см. ниже).
|
||
|
||
**Замер (MAME, `roomtest`, чтение карты `_kbdraw_down` + breakpoint на выходе
|
||
из `in a,($18)` в декодере трамплина).**
|
||
|
||
1. Зажать LShift → байт 2 карты = `04` (LSh взведён), поток `12 12 12 …`
|
||
(Shift автоповторяется сам, пока он последняя нажатая клавиша).
|
||
2. Добавить ↑ → байт 46 = `20` (↑ взведена), **байт 2 = `00` — Shift снят,
|
||
хотя физически зажат.**
|
||
3. Добавить ещё → байт 46 = `30` (обе стрелки видны, 3-key rollover в
|
||
порядке), Shift по-прежнему `00`.
|
||
4. КОРОТКИЙ тап ↑ при зажатом Shift — Shift выживает. Это и сбивало с
|
||
толку: бит сносился, но тут же восстанавливался, потому что после
|
||
отпускания стрелки Shift снова становился «последней клавишей» и его
|
||
автоповтор `12` взводил бит обратно за ~30 мс.
|
||
5. Сам поток при зажатом Shift и зажатой ↑:
|
||
|
||
```
|
||
… E0 E0 E0 75 E0 75 E0 75 ← ни одного F0/12
|
||
```
|
||
|
||
**Причина.** Декодеры (`_irq_tramp.c`, `kbd_raw_poll.c`) делали из «fake
|
||
shift» ДВА вывода, и второй был неверен:
|
||
|
||
- обёртка `E0 F0 12` / `E0 12` есть → Shift зажат → взвести бит ✔ верно;
|
||
- расширенный make **без** обёртки → Shift отпущен → снять биты обоих
|
||
шифтов ✘ **неверно**.
|
||
|
||
Обратный вывод опирался на «клавиатура обёртывает КАЖДЫЙ расширенный код».
|
||
Замер это опровергает: клавиатура MAME-Sprinter (`pc_kbd ms_naturl`) обёртку
|
||
не шлёт вовсе, и уж точно её не бывает на typematic-повторах — а повторы идут
|
||
непрерывно, пока стрелка зажата. Значит каждый повтор снимал реально зажатый
|
||
Shift, и к кадрам 102…106 (окно `check_grab`) движок видел Shift отпущенным.
|
||
Отсюда и «работает, если нажать Shift позже»: бит успевал постоять несколько
|
||
кадров до ближайшего повтора стрелки.
|
||
|
||
**Фикс.** Обратный вывод убран целиком — расширенная клавиша идёт обычным
|
||
путём и о состоянии Shift не судит. Прямой вывод оставлен (обёртка, если
|
||
клавиатура её всё-таки шлёт, подтверждает «Shift зажат» и стоит дёшево).
|
||
Состояние Shift теперь ведут его собственные make/break `12` / `F0 12` —
|
||
они приходят всегда. Заодно ушла ставшая ненужной переменная
|
||
`_kbdraw_fakesh`, и оба декодера стали короче (трамплину это на пользу: его
|
||
клавиатурный блок упирается в диапазон `jr`).
|
||
|
||
Файлы: `libc/irq/_irq_tramp.c`, `libc/kbd/kbd_raw_poll.c`, `libc/kbd/_kbdraw.h`,
|
||
`libc/kbd/_kbdraw_state.c`, `libc/kbd/kbd_raw_sync.c`.
|
||
|
||
**Чем платим.** Потерянный при Rx-overrun break Shift снять теперь нечем —
|
||
модификатор может залипнуть до перенажатия (это старый BUG-KBD-3). Размен
|
||
осознанный и решён в ту же сторону, что и раньше в `kbd_raw_sync`: **лучше
|
||
залипание, чем отвал** — залипший Shift игрок снимает нажатием Shift, а
|
||
сорванный посреди игры Shift в PoP стоит жизни. Вероятность overrun'а сильно
|
||
снижена дренажом FIFO опросом из главного цикла (`kbd_raw_poll`, KBD-1).
|
||
|
||
**Проверено в MAME после фикса** (карта `_kbdraw_down` при `roomtest`):
|
||
|
||
| действие | LSh (байт 2) | стрелки (байт 46) |
|
||
|---|---|---|
|
||
| зажать LShift | `04` | `00` |
|
||
| + зажать ↑ и → | `04` | `30` |
|
||
| держать 5 с (автоповтор идёт) | `04` | `30` |
|
||
| отпустить Shift, стрелки держать | `00` | `30` |
|
||
| отпустить стрелки | `00` | `00` |
|
||
|
||
`make size-check`: 70 программ, роста нет.
|
||
|
||
**Осталось наблюдением, не багом этой задачи.** В карте изредка остаётся
|
||
взведённым бит в PLAIN-половине для кода стрелки (напр. байт 14 бит 4 =
|
||
`0x74` «Right без E0»). Это потерянный префикс `E0` — след старого рассинхрона
|
||
FIFO. Игру не задевает (движок читает `KBD_RIGHT = EXT|0x74`, то есть
|
||
расширенную половину), но если всплывёт — искать здесь.
|
||
|
||
---
|
||
|
||
<a id="bug-kbd-4"></a>
|
||
## BUG-KBD-4 (+ BUG-KBD-3). Shift: сначала залипал, потом стал отваливаться — **ЗАКРЫТ (с поправкой, см. BUG-KBD-5)**
|
||
|
||
> **Поправка 2026-08-05.** Вывод «клавиатура обёртывает КАЖДЫЙ расширенный
|
||
> код, пока зажат Shift» оказался неверным, и построенный на нём обратный
|
||
> вывод («расширенный make без обёртки ⇒ Shift отпущен») убран — он ломал
|
||
> Shift вместе со стрелками. Разбор — [BUG-KBD-5](#bug-kbd-5). Проверка
|
||
> «пять тапов стрелки подряд» ниже проходила не потому, что вывод был верен,
|
||
> а потому что между тапами бит восстанавливал автоповтор самого Shift.
|
||
> Актуальное поведение `kbd_raw_sync` — вариант 1 (модификаторы не сбрасываем).
|
||
|
||
**Симптом (вторая редакция).** Залипание ушло, но появилось обратное: при
|
||
зажатом Shift второй-третий-четвёртый тап стрелки отрабатывал уже не
|
||
осторожным шагом, а бегом. Для PoP это ХУЖЕ залипания: игрок рассчитывает
|
||
на короткий шаг, а Кид убегает в яму или на пики.
|
||
|
||
**Почему обе прежние редакции были неправильны.** Это был размен между
|
||
двумя способами «починить» потерю байта при Rx-overrun SIO (FIFO 3 байта):
|
||
|
||
1. исключать модификаторы из сброса — потерянный break Shift снять нечем,
|
||
Shift залипает навсегда (BUG-KBD-3);
|
||
2. сбрасывать всю карту, как DSS (`KBD_Receiver_Overrun` в `KEYINTER.ASM`
|
||
чистит и `KEYCTRL`, и `KEY_FLG`) — залипания нет, но Shift сносится
|
||
каждым overrun'ом (BUG-KBD-4).
|
||
|
||
Причина, по которой вариант 2 бил так часто, замерена: при зажатом Shift
|
||
клавиатура обёртывает КАЖДЫЙ расширенный код служебной парой, поэтому тап
|
||
стрелки — это не 5 байт, а 10, и overrun почти гарантирован.
|
||
|
||
**Что нашлось (замер в MAME, watchpoint на порт данных SIO + чтение карты
|
||
`_kbdraw_down`).** Эта самая обёртка — «fake shift» — и есть решение, а не
|
||
помеха. Клавиатура шлёт `E0 F0 12` перед расширенным make и `E0 12` после
|
||
его break, и шлёт ТОЛЬКО пока Shift реально зажат. Проверено обоими
|
||
шифтами: правый обёртывается своим кодом `0x59` (bit 1 байта 11 карты), левый
|
||
— `0x12` (bit 2 байта 2). Это непрерывное и прямое свидетельство реального
|
||
состояния Shift — единственное доступное, потому что опросить состояние у
|
||
PS/2 нельзя, а typematic повторяет только ПОСЛЕДНЮЮ нажатую клавишу, то есть
|
||
стрелку, а не Shift.
|
||
|
||
**Фикс.** Оба декодера (`libc/irq/_irq_tramp.c`, `libc/kbd/kbd_raw_poll.c`)
|
||
читают обёртку в обе стороны:
|
||
|
||
- увидели fake shift → реальный Shift ЗАЖАТ → взвести plain-бит (бит кладёт
|
||
общий писатель: достаточно обнулить префиксы, и он попадёт в plain-половину
|
||
карты как make; в расширенную половину не пишем — это не клавиша);
|
||
- расширенный make БЕЗ предшествующей обёртки → Shift ОТПУЩЕН → снять
|
||
plain-биты обоих шифтов.
|
||
|
||
`kbd_raw_sync` вернулся к исключению модификаторов — теперь это безопасно:
|
||
состояние Shift подтверждается независимо от того, что съел overrun, а
|
||
залипание снимается первым же нажатием стрелки.
|
||
|
||
**Побочно найдена и исправлена своя ошибка** в новом коде `kbd_raw_poll`:
|
||
после `bit 1, a` в `A` лежал `pending`, а не скан-код, — обычные
|
||
(нерасширенные) клавиши декодировались бы из мусора. Оба декодера приведены
|
||
к одной логике.
|
||
|
||
**Раскладка трамплина.** Клавиатурный блок перевалил за 127 байт, а `jp`
|
||
внутри трамплина запрещён (код копируется в W2 побайтно, абсолютные
|
||
само-ссылки сломают копию). Поэтому префиксные обработчики переехали вплотную
|
||
к своим `cp`, а посередине тела стоят три ретранслятора (`tr_kbd_hub`,
|
||
`tr_hub_notkbd`, `tr_hub_dss`) — до них дотягиваются `jr` и сверху, и снизу.
|
||
В `kbd_raw_poll` такого ограничения нет, там три перехода стали `jp`.
|
||
|
||
**Проверено в MAME:**
|
||
|
||
- Shift зажат, пять тапов стрелки подряд → `LSh` остаётся `04` во всех пяти,
|
||
`ovr = 0`, расширенная половина карты чистая;
|
||
- штатное отпускание Shift → `LSh = 00`;
|
||
- искусственно залипший Shift (бит записан в карту отладчиком) → снимается
|
||
ПЕРВЫМ же тапом стрелки;
|
||
- `make size-check`: 70 программ, роста нет.
|
||
|
||
<a id="bug-respawn-2"></a>
|
||
## BUG-RESPAWN-2. После respawn стражи остаются мёртвыми — **ЗАКРЫТ**
|
||
|
||
**Вопрос из отчёта:** «после respawn — должны ли оживать стражники?»
|
||
**Ответ по SDLPoP: да.** Цикл `play_level` (seg003:57) на КАЖДОЙ итерации —
|
||
в том числе после смерти Кида — делает `load_level()`, следом `pos_guards()`
|
||
(seg003:83), а затем `Guard.charid = charid_2_guard; Guard.direction =
|
||
dir_56_none`. То есть стражи — такая же часть данных уровня, как тайлы, и
|
||
перезагрузка файла возвращает их всех.
|
||
|
||
**Корень у нас.** `gstate_init()` (живая копия таблицы стражей: тайл,
|
||
направление, мастерство, поза трупа) вызывался ТОЛЬКО при загрузке уровня.
|
||
Рестарт возвращал тайлы (`pop_level_reset_tiles`), но не стражей: в `gstate`
|
||
оставался сохранённый `leave_guard`'ом `seq_hi != 0`, и `pop_guard_enter`
|
||
поднимал стража трупом с `guardhp_curr = 0`.
|
||
|
||
**Фикс.** `pop_level_reset_guards()` (тот же `gstate_init`) + `pop_guard_reset()`
|
||
в `pop_start_level`, рядом с `pop_level_reset_tiles()`. Туда же уехал
|
||
`pop_loose_mob_reset()` — настоящий `mobs_count = 0` из `start_level`.
|
||
|
||
**Проверено в MAME:** комната 21, страж жив (`alive = -1`, hp 3) → убит читом
|
||
`K` (`alive = 0`, hp 0) → Кид убит стражем → респавн → возврат в комнату 21:
|
||
страж снова `alive = -1`, hp 3.
|
||
|
||
<a id="bug-draworder-1"></a>
|
||
## BUG-DRAWORDER-1. Кид рисовался поверх тела стража — **ЗАКРЫТ**
|
||
|
||
**Симптом.** В оригинале Кид проходит ЗА телом убитого стража; у нас — перед ним.
|
||
|
||
**Три разных корня, найденные по очереди.** Стоит того, чтобы перечислить: два
|
||
первых захода были неполны, и каждый следующий кадр от тестера вскрывал новый
|
||
слой.
|
||
|
||
1. **Порядок задавался ролью, а не тайлом.** Мы безусловно рисовали
|
||
`pop_guard_draw()` → `kid_draw()`, то есть Кид был сверху всегда. В
|
||
оригинале никто не «поверх» по определению: оба персонажа попадают в midtable
|
||
при обработке СВОЕГО тайла (`set_objtile_at_char`, seg006:13F3), а тайлы
|
||
обходятся строго — `redraw_needed_tiles` (seg008:1B06) идёт рядами 2,1,0 и
|
||
внутри ряда колонками 0..9; кто позже, тот поверх. Совпали тайлы — решает
|
||
`sort_curr_objs` (seg008:203C): ниже по `obj_y` = позже.
|
||
Фикс: `guard_over_kid()` в `roomtest.c`.
|
||
|
||
2. **Своя же регрессия: Кид полез поверх передних столбов.** Окно fore-клипа
|
||
(`pop_fore_set_clip`) одно на всех, и его ставит каждый, кто рисует
|
||
персонажа. Пока страж рисовался строго ДО Кида, к моменту
|
||
`pop_fore_over_kid` в окне оказывался Кид и всё сходилось само. Как только
|
||
порядок стал переменным, при «страж поверх» окно оставалось СТРАЖЬИМ, и
|
||
fore-проход Кида отсекался целиком.
|
||
Фикс: `kid_fore_clip_restore()` — окно восстанавливается явно, а не
|
||
«по счастливому порядку вызовов».
|
||
|
||
3. **Колонка трупа была нулевой.** `leave_guard` сохраняет тайл как
|
||
`get_tilepos(0, row)`, то есть колонку 0 всегда, а `enter_guard` у нас брал
|
||
`curr_col` оттуда и следом перетирал X запомненным значением. В памяти это
|
||
было видно прямо: `col=0` при `x=95`. Оригинал (seg002:180) выводит колонку
|
||
ИЗ X: `Char.curr_col = get_tile_div_mod_m7(Char.x)`. У живого стража
|
||
незаметно (X там сама считается из колонки), у запомненного ТРУПА — ломало
|
||
порядок. Колонка нужна не только отрисовке: на неё смотрят коллизия и
|
||
`check_can_guard_see_kid`.
|
||
|
||
4. **Ветка `actions_1_run_jump` оказалась не «упрощаемой».** Сначала я решил,
|
||
что её можно не портировать. Кадр в движении показал `ACT=1`: в беге
|
||
оригинал берёт тайл не из `curr_row`/`curr_col`, а из нижнего ряда и ЛЕВОЙ
|
||
колонки ГАБАРИТА (`char_bottom_row`/`char_col_left` из `set_char_collision`,
|
||
seg006:0723). При беге вправо левая колонка меньше `curr_col` примерно на
|
||
тайл — потому бегущий Кид и уходит за объекты справа. Отсюда «некоторые
|
||
кадры бега рисовали Кида поверх тела». Считается для ОБОИХ персонажей:
|
||
`enter_guard` ставит `action = 1` и стражу, односторонний учёт дал бы
|
||
перекос в другую сторону. Тонкость: `char_col_left` берётся по НЕ
|
||
утоньшённой границе — поправку THIN оригинал применяет только к `*_coll`.
|
||
|
||
**Проверено в MAME:** после возврата в комнату у трупа `col=5` при `x=135`
|
||
(было `col=0`); Кид слева от тела — и стоя, и в кадре бега — рисуется за телом;
|
||
передние столбы снова перекрывают Кида.
|
||
|
||
**Остаток, который НЕ баг.** Ноги стоящего Кида поверх головы трупа, когда он
|
||
стоит колонкой правее тела, — врождённое свойство движка (один спрайт = один
|
||
тайл при спрайте шире колонки). Сверено в SDLPoP: там так же. Подробности — в
|
||
разделе «НЕ БАГИ» выше.
|
||
|
||
<a id="bug-loose-2"></a>
|
||
## BUG-LOOSE-2. Осколки только от одной из двух падающих плит — **ЗАКРЫТ**
|
||
|
||
**Симптом.** Комната 12, соседние падающие плиты (0,1) и (0,2): после
|
||
пробежки осколки появляются только на (1,2), на (1,1) — нет. В комнате 7
|
||
(плиты 0,5/0,6 → 2,5/2,6) обе дают осколки.
|
||
|
||
**Корень.** Гипотеза автора отчёта подтвердилась чтением кода: Кид уходит в
|
||
комнату 15 раньше, чем долетает плита (0,1). Наш падающий кусок был привязан
|
||
к ТЕКУЩЕЙ комнате — `pop_loose_reset()` на смене комнаты звал
|
||
`pop_loose_mob_reset()` и гасил всё, что ещё летит, а тайл под куском читался
|
||
из карты отрисованной комнаты. Кусок уничтожался, `loose_land` не случался,
|
||
щебень не клался. В комнате 7 обе плиты успевают сесть до ухода — потому там
|
||
и выглядело правильно.
|
||
|
||
В оригинале `mobs[]` чистится ТОЛЬКО в `start_level` (seg003:88); `do_mobs`
|
||
(seg007:1063) прокручивает все куски независимо от `drawn_room`, `move_loose`
|
||
работает по `curmob.room`, а `redraw_at_cur_mob` (seg007:132C) сверяется с
|
||
`drawn_room` лишь для ОТРИСОВКИ.
|
||
|
||
**Фикс.** У куска появилось поле `room` (`curmob.room`). На смене комнаты
|
||
зовётся `pop_loose_mob_room_changed()` — сбрасывает только heal-историю, а
|
||
полёт продолжается. Тайл читается через `mob_tile_at()` (своя комната —
|
||
`tile_code`, чужая — `pop_level_tile`). Приземление в покинутой комнате пишет
|
||
щебень прямо в данные уровня: сигнальная пара `pop_loose_landed`/
|
||
`pop_debris_at` обслуживает только текущую комнату. Отрисовка и
|
||
`check_loose_fall_on_kid` (там оригинал начинает с `Char.room == curmob.room`)
|
||
— только для своей комнаты. Настоящий `mobs_count = 0` переехал в
|
||
`pop_start_level`.
|
||
|
||
**Проверено вручную (2026-08-03).** Автоматикой гонку воспроизвести не
|
||
удалось: мост MAME шлёт нажатия рывками, и «уйти из комнаты раньше, чем
|
||
долетит плита» через него не набирается. Подтверждено живым прогоном.
|
||
|
||
Это ровно тот случай, ради которого заведены host-тесты: на уровне логики
|
||
проверка занимает несколько строк — заспавнить кусок, сменить комнату, тикать
|
||
до приземления, проверить щебень в данных уровня. Стоит первым в
|
||
`../docs/host_tests_plan.md`.
|
||
|
||
<a id="bug-seam-draw-1"></a>
|
||
## BUG-SEAM-DRAW-1. Кид, упершийся в решётку на шве, не рисуется — **ЗАКРЫТ (не воспроизводится)**
|
||
|
||
**Как было заведено.** Комната 5: Кид добегает до закрытой решётки (0,9),
|
||
решётка его держит, но экран переключается на комнату 1, и в ней Кида не
|
||
видно — он стоит на `x = 57..60`, левее кромки комнаты (col 0 начинается с 58).
|
||
|
||
**Проверка 2026-08-03.** Не воспроизводится: Кид упирается и встаёт на
|
||
`x = 61`, `curr_col = -1`, `room = 1` — и в комнате 1 РИСУЕТСЯ, за решёткой,
|
||
как и должен.
|
||
|
||
Почему сходится: при `x = 61` персонаж уже внутри системы координат комнаты 1
|
||
(`obj_x = 2*61 − 116 = 6`), поэтому никакого straddle-смещения не требуется и
|
||
обычная отрисовка справляется. Отрицательная `curr_col` тут не противоречие —
|
||
это правило `get_tile_div_mod_m7`: `(61 − 7 − 58) / 14 = −1`.
|
||
|
||
**Что осталось за скобками.** Само переключение экрана штатное: `leave_room`
|
||
(seg002:423) уводит вправо при `char_x_right >= 201`, а левая грань решётки как
|
||
раз 201 — оригинал в этой позе тоже перелистнёт.
|
||
|
||
И остаётся теоретический остаток: при `x <= 60` (`obj_x <= 4`) спрайт уходил бы
|
||
за левую кромку и обрезался. В текущей сборке Кид туда не встаёт, поэтому баг
|
||
и не воспроизводится. Полное лекарство — довести модель straddle (S3 в
|
||
`../docs/room_model_plan.md`): держать `Kid.room` отдельно от `drawn_room` и
|
||
рисовать со смещением ±140, как `xpos_in_drawn_room` (seg004:0405). Пока
|
||
поводов для этого нет — заводить обратно только по живому наблюдению.
|
||
|
||
---
|
||
|
||
# Прогон всех комнат уровней 1 и 2 (пользователь, 2026-08-07) — ЗАКРЫЛ ТРИ РЕВИЗИИ РАЗОМ
|
||
|
||
Результат прогона: **крупных багов нет**. Тем самым закрыты и переехали сюда
|
||
из `BUGS_OPEN.md` три накопившихся хвоста — чек-листы ручной перепроверки
|
||
фиксов, таблица обхода 24 комнат уровня 1 и таблица сырых наблюдений прогона
|
||
2026-08-03. Всё, что с прогона 2026-08-07 осталось открытым, — три записи
|
||
уровня 2 в [`BUGS_OPEN.md`](BUGS_OPEN.md) (BUG-GUARD-COLOR-1,
|
||
BUG-GUARD-SPLASH-1, BUG-CHEAT-FIGHT-1); ни одна из них не мешает играть.
|
||
|
||
Сырые формулировки пользователя лежат рядом: [`bugs_level1.md`](bugs_level1.md)
|
||
и [`bugs_level2.md`](bugs_level2.md).
|
||
|
||
<a id="ручная-перепроверка-2026-08-03"></a>
|
||
## Ручная перепроверка фиксов (2026-08-03) — ПРОЙДЕНА
|
||
|
||
Шесть корней прогона 2026-08-03 были закрыты автоматической проверкой в MAME
|
||
(мост `mame-z80`: чтение `_Kid`, потиковые трассы, скриншоты) — этого хватает,
|
||
чтобы показать, что конкретный сценарий больше не воспроизводится, но НЕ
|
||
хватает, чтобы поймать регрессии в соседней механике. Отсюда список сценариев
|
||
ровно в тех формулировках, в которых баги были заведены. Прогон 2026-08-07
|
||
прошёл все комнаты уровней 1 и 2 и ни одного из этих симптомов не показал.
|
||
|
||
| # | что проверялось | ожидаемо |
|
||
|---|-----------------|----------|
|
||
| 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) |
|
||
| 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 | **[BUG-KBD-4]** долго играть Shift+стрелка, много тапов подряд | Shift не «отваливается»; если однажды залипнет — снимается первым же нажатием стрелки |
|
||
| 18 | **[BUG-RESPAWN-2]** убить стража (комнаты 3/21), умереть, вернуться в ту же комнату | страж снова жив, стоит на исходном тайле, HP полные |
|
||
|
||
Отдельно смотрели **регрессии от порта `check_collisions`** — он трогает всю
|
||
горизонтальную коллизию: бамп в стену на бегу и в прыжке, осторожный шаг у
|
||
стены, проход через ОТКРЫТЫЕ ворота, разворот в проёме решётки, переходы через
|
||
швы (старый BUG-SEAM-PINGPONG). Не всплыло.
|
||
|
||
<a id="обход-всех-24-комнат-уровня-1"></a>
|
||
## Обход всех 24 комнат уровня 1 — ЗАКРЫТ прогоном 2026-08-07
|
||
|
||
Инструмент: `#define ROOMNAV` в `roomtest.c` — `+`/`-` (цифровой блок либо
|
||
`=`/`-` основного ряда) переключают комнату по номеру (1..24, с обёрткой),
|
||
Kid ставится на первый пол. Номер комнаты — полосками в верхнем борте: слева
|
||
десятки, справа единицы (`||` `||||` = 24). Инструмент остаётся в сборке:
|
||
он же нужен для приёмки уровня 3.
|
||
|
||
Таблица заполнялась по ходу отладки и осталась незакрытой на 19 строк из 24;
|
||
её закрыл ручной прогон всех комнат уровней 1 и 2 (2026-08-07, крупных багов
|
||
нет). Ниже — то, что таблица успела зафиксировать: это не «отчёт по
|
||
комнатам», а разбор корней, полезный при похожем симптоме.
|
||
|
||
| комната | статус | что не так |
|
||
|---------|--------|------------|
|
||
| 1 | пофикшено | падающая плита (2,6): правая грань видна через пол (2,7) и перекрывает его переднюю грань — `mob_render` брал ряд соседа из `m->row` (счётчик, уже ушедший на ряд вперёд), а не из координаты |
|
||
| 5 | пофикшено | прыжок в решётку: Kid оставался стоять на 6 px ВЫШЕ пола и без приземления-приседания — от `bumped()` (seg004) был портирован только хвост (`seq_47`), не хватало `bumped_floor` (прижатие Y к полу + `seq_46_hardbump` на кадрах прыжка 24/25/40..42/102..106) и `bumped_fall` |
|
||
| 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 дверь не рисует) |
|
||
| 12 | пофикшено | вис/подтягивание на кромке loose-плиты: плита рисовалась ПОД Кидом. Не хватало двух кусков `draw_tile`: (а) `draw_loose` кладёт нижнюю грань плиты И в foretable (поверх персонажа), (б) `draw_tile_base` подставляет верх плиты из `loose_fram_left`, а в нашем midtable-оверлее стоял голый `base_id` (у loose он 0). Голова Кида поверх падающей на него плиты — см. «НЕ БАГИ» выше |
|
||
| 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` |
|
||
| 13, 18, 24 | недостижимы в игре | свойство данных уровня, разбор — «НЕ БАГИ» выше |
|
||
|
||
<a id="сырые-наблюдения-прогон-2026-08-03"></a>
|
||
## Сырые наблюдения (прогон 2026-08-03) → корень
|
||
|
||
Одиннадцать формулировок с прогона свелись к шести корням; все шесть закрыты
|
||
и проверены в MAME — разборы выше по файлу.
|
||
|
||
| # | наблюдение (кратко) | корень |
|
||
|---|---------------------|--------|
|
||
| 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 |
|
||
| 8 | залипает Shift | BUG-KBD-3 / BUG-KBD-4 (поправка — BUG-KBD-5) |
|
||
| 9 | комната 5: кнопка (0,4) не открывает решётку (0,5) | BUG-GATE-ANIM-1 |
|
||
| 10 | комната 5: с кнопки (0,6) на (2,7), присед — провал в комнату 6 | BUG-STANDUP-1 |
|
||
| 11 | страж убил Кида → Кид воскресает на месте и его убивают снова | BUG-DEATH-1 |
|
||
|
||
Вторая волна того же прогона (вечер) дала ещё четыре наблюдения — BUG-KBD-4,
|
||
BUG-RESPAWN-2, BUG-DRAWORDER-1, BUG-LOOSE-2, — все закрыты, разборы выше.
|
||
Тогда же закрыт как невоспроизводящийся BUG-SEAM-DRAW-1 и заведён
|
||
BUG-GATE-PASS-1 (он закрыт ниже, 2026-08-09).
|
||
|
||
---
|
||
|
||
<a id="bug-guard-splash-1"></a>
|
||
## BUG-GUARD-SPLASH-1. Нет «брызг» при попадании по стражу — **ЗАКРЫТ 2026-08-07**
|
||
|
||
**Как было заведено (пользователь, 2026-08-07).** При попадании по стражу
|
||
должен рисоваться сплеш («звёздочка»), чтобы игрок видел, что удар дошёл до
|
||
цели, не переводя взгляд на полосу HP. У Кида такое есть, у стража — нет.
|
||
|
||
**Корень.** `draw_hurt_splash` (seg006:16CE) в оригинале зовётся ИЗ ДВУХ
|
||
мест: `draw_kid` при `hitp_delta < 0` (seg008:1643) и `draw_guard` при
|
||
`guardhp_delta < 0` (seg008:1654). У нас была портирована только первая
|
||
половина — `kid_draw_splash()`; для стража вызова не было вовсе, хотя
|
||
`guardhp_delta` считался и уже использовался.
|
||
|
||
**Фикс** (`pop_guard.c`, `pop_gdraw.c`, `roomtest.c`):
|
||
|
||
- флаг `pop_guard_hurt` — симметрия `pop_kid_hurt`: ставится в
|
||
`pop_do_delta_hp`, когда `guardhp_delta < 0`, снимается главным циклом
|
||
после отрисовки (стража могло не оказаться в отрисованной комнате —
|
||
тогда брызги просто пропадают, как в оригинале);
|
||
- отрисовка — ВНУТРИ `pop_guard_draw`, между спрайтом стража и клинком:
|
||
`draw_guard` кладёт брызги в objtable сразу после стража и ДО клинка, а
|
||
порядок записей задаёт порядок рисования. Заодно это решает fore-слой:
|
||
`pop_fore_over_char` в конце той же функции накрывает и брызги;
|
||
- спрайт — `chtab_5_guard` `obj_id = 1`, то есть **image 1 нашего атласа
|
||
стража** (`res752.png`, звезда 28×26): у `add_midtable` аргумент
|
||
`obj_id + 1`, а `get_image` вычитает единицу обратно — тот же off-by-one,
|
||
что расписан в шапке `pop_pack_guard.py`. У Кида это image 218;
|
||
- смещения — три ветки оригинала: кадр 178 (chomped) брызг не даёт вовсе;
|
||
185 и 106..110 (смерть/падение) — `obj_y + 4`; 177 (пики) — сдвиг НАЗАД
|
||
на 5; иначе `obj_y − 11` (у Кида 15: `((charid == kid) << 2) + 11`);
|
||
- свой прямоугольник heal по страницам (`qx_l/qvalid`) — брызги живут один
|
||
кадр, но стирать их обязана та страница, в которую рисовали.
|
||
|
||
**Побочно исправлено:** `pop_kid_hurt` взводился при ЛЮБОЙ ненулевой дельте
|
||
HP, поэтому зелье здоровья (+1) давало Киду и брызги, и красную вспышку.
|
||
И `draw_kid` (seg008:1643), и `flash_if_hurt` (seg003:785) смотрят именно
|
||
`hitp_delta < 0` — условие приведено к оригиналу.
|
||
|
||
**Проверено в MAME (2026-08-07)**, мост `mame-z80`, уровень 1, комната 21,
|
||
живой страж (HP 3/3):
|
||
|
||
```
|
||
bp 51BA ← адрес снятия pop_guard_hurt в main: срабатывает ТОЛЬКО
|
||
в кадре удара, то есть уже после отрисовки брызг
|
||
key k ← чит «убить стража» = guardhp_delta < 0
|
||
→ state=stop PC=0x51BA, guardhp: curr=0 max=3 delta=0 hurt=1
|
||
bp 8BDF (_gfx_set_visible_page) + out ← домотать до флипа страницы
|
||
snap ← звезда нарисована поверх стража
|
||
```
|
||
|
||
Следующие два кадра (обе страницы дабл-буфера) — чисто, следов брызг нет.
|
||
|
||
Приём на будущее: снятие флага сделано **по факту** (`if (pop_guard_hurt)
|
||
pop_guard_hurt = 0;`), а не безусловно — это и экономит запись каждый кадр,
|
||
и даёт готовую точку останова для отладки в MAME. Условные брейкпоинты
|
||
мост не принимает (`bp ADDR cond` вешает плагин), поэтому точка останова,
|
||
срабатывающая сама по себе только в нужном кадре, — рабочий обходной путь.
|
||
|
||
**Цена:** `_CODE` 24 179 → 24 209 (+30 Б резидента), BANK4 (`pop_gdraw`)
|
||
2407 → 3369 (+962 Б, банк занят на 20.6 %, свободно 13 015 Б).
|
||
|
||
**Цвет брызг** зависит от палитры стража (слоты `0x90..0x9F`), поэтому он
|
||
изменится вместе с [BUG-GUARD-COLOR-1](BUGS_CLOSED.md#bug-guard-color-1) —
|
||
отдельной работы не требует.
|
||
|
||
|
||
---
|
||
|
||
<a id="bug-gate-pass-1"></a>
|
||
## BUG-GATE-PASS-1. Проход сквозь закрывшуюся решётку — **ЗАКРЫТ 2026-08-09**
|
||
|
||
**Приёмка:** смоук-прогон уровня 1 (пользователь, 2026-08-09) — с проблемного
|
||
`x` Кид больше сквозь решётку не пробегает, пинг-понг у шва не вернулся.
|
||
|
||
> **ВОСПРОИЗВЕДЁН И РАЗОБРАН.** Сценарий: уровень 1, комната 8, ряд 0,
|
||
> `Kid.x = 170`, лицом ВПРАВО, решётка шва (комната 8, колонка 9) ЗАКРЫТА.
|
||
> Разбежаться вправо и НЕ ОТПУСКАТЬ клавишу. Кид проходит сквозь решётку и
|
||
> убегает вглубь комнаты 6.
|
||
>
|
||
> Ниже — новый разбор; всё, что было раньше (гипотеза про x > 205), не
|
||
> подтвердилось и оставлено в конце для истории.
|
||
|
||
### Корень: история флагов коллизии индексируется НЕ ТАК, как в оригинале
|
||
|
||
Сняты две трассы — наша (брейкпоинт на резидентном `kid_tick`) и оригинала
|
||
(SDLPoP, собран с `POP_TRACE=1`, печать в `play_kid_frame` + маркеры в
|
||
`bumped` и `exit_room`). Стартовая позиция совпала до пикселя, поэтому
|
||
сравнение построчное.
|
||
|
||
**Оригинал:**
|
||
|
||
```
|
||
LEAVE dir=1 x=61 cxl=184 cxr=201 room=6 смена комнаты — ШТАТНАЯ
|
||
BUMPED dx=-2 push=-1 x=63 tile=4 col=9 и СРАЗУ бамп о решётку, откат
|
||
KID f=15 x=60 act=0 col=-1 room=6 drawn=6 сел за решёткой, стоит
|
||
```
|
||
|
||
**Мы:**
|
||
|
||
```
|
||
x 170 -> 196 бегом, col 7 -> 8 -> 9, НИ ОДНОГО бампа
|
||
x=198 -> room 8 -> 6
|
||
при удержании ВПРАВО доезжает до col 1 комнаты 6 и бежит дальше
|
||
```
|
||
|
||
Смена комнаты в этой точке правильна и у нас, и у оригинала: `leave_room`
|
||
(seg002:423) вправо блокируют ТОЛЬКО `doortop`-тайлы, решётки в списке нет, а
|
||
порог `char_x_right >= 201` совпадает с плоскостью ворот
|
||
(`x_bump[9+FIRST_ONSCREEN_COLUMN] + TILE_MIDX + wall_dist_from_left[1]`
|
||
= 191 + 10). Оригинал полагается не на запрет ухода, а на бамп СРАЗУ ПОСЛЕ
|
||
перехода.
|
||
|
||
**Почему бамп есть у них и нет у нас.** `check_collisions` (seg004:0004)
|
||
хранит флаги так:
|
||
|
||
```c
|
||
row_coll_flags_ptr[tile_col] = curr_flags; // индекс — колонка В РАЗРЕШЁННОЙ комнате
|
||
row_coll_room_ptr [tile_col] = curr_room; // и рядом — НОМЕР ЭТОЙ КОМНАТЫ
|
||
...
|
||
if (curr_row_coll_room[column] >= 0 &&
|
||
prev_coll_room[column] == curr_row_coll_room[column]) { /* только тогда сравниваем */ }
|
||
```
|
||
|
||
То есть история привязана к паре **(колонка внутри своей комнаты, комната)**.
|
||
Решётка комнаты 8 и до перехода, и после лежит в слоте 9 с `room = 8` —
|
||
переход 8->6 историю НЕ рвёт, переход флага 0->1 виден, бамп срабатывает.
|
||
|
||
У нас массив `coll_prev/coll_curr` индексируется колонкой ОТНОСИТЕЛЬНО
|
||
отрисованной комнаты (−2…11). При смене комнаты все индексы уезжают на 10,
|
||
сопоставить прошлый кадр с текущим нечем, и `enter_room` вынужден звать
|
||
`pop_coll_invalidate()`; тот ставит «прошлые флаги = уже перекрывал» (3), то
|
||
есть ПОДАВЛЯЕТ бамп на кадре входа. А к следующему кадру Кид уже за
|
||
плоскостью ворот — перехода 0->1 не будет никогда.
|
||
|
||
### Фикс сделан 2026-08-09 (вариант B — сдвиг истории), ЖДЁТ ПРИЁМКИ В MAME
|
||
|
||
Рассматривались два пути:
|
||
|
||
- **A, дословный порт.** 10 слотов + параллельный массив номера комнаты,
|
||
индекс по колонке разрешённой комнаты, бамп только при совпадении номеров;
|
||
`pop_coll_invalidate()` тогда не нужен вовсе. **Не взят:** оригиналу
|
||
хватает десяти слотов только потому, что он перебирает узкое окно вокруг
|
||
Кида, а мы намеренно считаем все четырнадцать колонок (FIX_COLL_FLAGS,
|
||
чтобы не оставалось протухших ячеек) — при таком переборе колонки −2/−1 и
|
||
8/9 сядут в одни слоты 8/9. То есть A тянет за собой ещё и сужение окна.
|
||
- **B, сдвиг истории (сделано).** Индексацию оставляем свою, но при
|
||
БОКОВОМ переходе историю не выбрасываем, а перенумеровываем на 10 слотов:
|
||
`check_leave` двигает `x` ровно на ∓140 = 10 тайлов, координата грани
|
||
(`pop_x_bump`) едет на те же 140 вместе с габаритом Кида, значит сами
|
||
флаги инвариантны — меняется только номер слота.
|
||
|
||
Реализация: `pop_coll_shift(int8_t dcol)` в `pop_map.c` сдвигает
|
||
`coll_curr/above/below` (не `prev` — его на следующем кадре всё равно
|
||
перезапишет `move_coll_to_prev`), освободившиеся слоты = 3 («уже
|
||
перекрывал», бампа нет — ровно то, что в оригинале даёт несовпадение номера
|
||
комнаты). `enter_room_side` зовёт её сразу после `pop_map_set_edges`:
|
||
`side == 0` (ушёл влево) → `+10`, `side == 1` (вправо) → `−10`. Переходы
|
||
вверх/вниз и все прочие входы в комнату остаются на полной инвалидации —
|
||
там колонки не сдвигаются, но тайлы под ними уже из другой комнаты.
|
||
|
||
Расхождение с дословным вариантом A записано в `../docs/impl_diff.md` (D-1).
|
||
|
||
**Осторожно:** это сердце коллизии, вокруг которого разбирался
|
||
BUG-SEAM-PINGPONG (см. `BUGS_CLOSED.md`). Приёмка:
|
||
|
||
1. Комнаты 6 ↔ 8 уровня 1, закрытая решётка, **обе** стороны.
|
||
2. Мелким шагом (упереться) И с разбега (не пройти насквозь) — сценарий
|
||
выше воспроизводится с `Kid.x = 170`, комната 8, ряд 0, лицом вправо.
|
||
3. Пинг-понг у шва не вернулся (экран не перескакивает туда-сюда).
|
||
4. `make -C tests-host` — прошли все 5 наборов (2026-08-09).
|
||
|
||
**Инструмент для сверки готов:** SDLPoP собран с трассой, включается
|
||
`POP_TRACE=1` (правки в `seg000.c` play_kid_frame, `seg004.c` bumped,
|
||
`seg002.c` exit_room, чтение переменной — `main.c`). Сборка на macOS:
|
||
`make CFLAGS="-std=gnu99 -O2 -I/opt/homebrew/include -Wno-implicit-function-declaration" LIBS="-L/opt/homebrew/lib -lSDL2 -lSDL2_image"`
|
||
(в системе нет pkg-config, штатный Makefile без этого не собирается).
|
||
|
||
---
|
||
|
||
### История: прежний разбор (гипотеза НЕ подтвердилась)
|
||
|
||
**Статус: наблюдался один раз (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()`: если он берётся не от того кадра, решётка может
|
||
перестать считаться препятствием раньше времени.
|
||
|
||
---
|
||
|
||
## BUG-TORCH-CHOMP-2. Застывший чомпер накрывается пламенем факела — ЗАКРЫТ 2026-08-10
|
||
|
||
Симптом (уровень 4, скриншот пользователя): пока чомпер щёлкает, всё в
|
||
порядке; как только его анимация кончается, пламя соседнего факела ложится
|
||
ПОВЕРХ челюстей.
|
||
|
||
Регрессия от фикса [BUG-TORCH-CHOMP-1](#bug-torch-chomp-1): там пламя
|
||
перевели на ЗАПЕКАНИЕ в фон (`GFX_BANK_NORMAL`), чтобы `heal` соседнего
|
||
чомпера не съедал огонь. Аргумент «запекать безопасно, кадры пламени
|
||
самонакрываются» верен для СОБСТВЕННЫХ пикселей факела, но не для чужой
|
||
графики в той же ячейке: пламя рисуется в клетке ПРАВОГО СОСЕДА
|
||
(`x = (col+1)*32 + 8`, seg008:560), и если сосед — чомпер, огонь ложится на
|
||
его челюсти. Пока trob чомпера жив, `pop_set_redraw(tp, POP_RD_CHOMP, ...)`
|
||
возвращает их поверх каждый кадр; как только анимация кончилась и trob
|
||
умер, возвращать стало нечем.
|
||
|
||
**Как это устроено в оригинале** (и почему у него бага нет).
|
||
`animate_torch` (seg007:0241) заканчивается вызовом `set_redraw_anim_right()`
|
||
— он метит `redraw_frames_anim` у ПРАВОГО СОСЕДА (seg007:0101). А
|
||
`redraw_needed` (seg008:0178) рисует помеченный слой строго в порядке
|
||
|
||
```
|
||
draw_tile_anim_topright();
|
||
draw_tile_anim_right(); <- пламя ЛЕВОГО соседа (факела)
|
||
draw_tile_anim(); <- СВОЯ графика тайла: челюсти чомпера
|
||
```
|
||
|
||
То есть челюсти возвращаются поверх огня **каждый кадр факела**, независимо
|
||
от того, жива ли собственная анимация чомпера. У нас пламя рисуется
|
||
напрямую из `pop_process_trobs`, минуя механизм пометок, — этой второй
|
||
половины не было.
|
||
|
||
**Фикс** (`pop_trob.c`): после `pop_torch_draw` метим правого соседа
|
||
`pop_set_redraw(tp + 1, POP_RD_CHOMP, 1)`, если там чомпер. Одна страница:
|
||
факел анимируется каждый кадр, значит обе страницы дабл-буфера получат свою
|
||
перерисовку по очереди. Порядок сходится сам — блок факелов стоит ДО
|
||
`pop_redraw_needed`. Код соседа читается в том же префетче кодов тайлов
|
||
(`trob_rcode[]`), чтобы не свапать W0 второй раз за кадр. Банк 6: +104 Б.
|
||
|
||
**Осознанное расхождение** — оригинал метит соседа БЕЗУСЛОВНО, мы только под
|
||
чомпера. Безусловная пометка = перерисовка тайла каждый кадр на каждый
|
||
факел; слой `draw_tile_anim` рисует ещё пики, зелье и меч (seg008:0644),
|
||
поэтому теоретически «застыть под пламенем» могут и они — заведено открытым
|
||
в `BUGS_OPEN.md` (TORCH-ANIM-RIGHT). На уровнях 1–4 такого соседства не
|
||
встретилось.
|
||
|
||
---
|
||
|
||
<a id="t-2"></a>
|
||
## T-2. Idle-skip — **ЗАКРЫТ 2026-08-08**
|
||
|
||
Сделан как шаг 1 задачи [DRAW-COST](TASKS_OPEN.md#draw-cost) (коммит
|
||
`a25ce58`), и шире, чем формулировался здесь: пропускается не только Кид, а
|
||
ЛЮБОЙ персонаж, у которого с прошлой отрисовки этой страницы дабл-буфера не
|
||
изменился ни один вход отрисовки, — включая труп стража и ждущего стража.
|
||
Условие «обе страницы уже получили это состояние», которого требовала эта
|
||
запись, выполнено само собой: снимок входов хранится ПО СТРАНИЦАМ.
|
||
|
||
Замер: комната 1.3 с трупом стража, Кид стоит — 210 % -> 116 % кадрового
|
||
периода, ноль вызовов `pop_heal_fast` за кадр. Контракт — шапка
|
||
`pop_cdraw.h`, разбор и что делать дальше — `TASKS_OPEN.md#draw-cost`.
|
||
|
||
Связь с T-1 сработала как и предсказано: пока Кида не перерисовываем, heal'а
|
||
нет, стирать пики нечем. Но T-1 остаётся открытым — трогать пики
|
||
безусловно мы всё ещё продолжаем, когда Кид рядом ДВИЖЕТСЯ.
|
||
|
||
---
|
||
|
||
<a id="mirror-fg-stale"></a>
|
||
## MIRROR-FG-STALE. Зеркало и снимок комнаты — **НЕ БАГ, закрыт 2026-08-11**
|
||
|
||
Заводился как краевой случай от [L4-MIRROR](TASKS_CLOSED.md#l4-mirror):
|
||
`place_mirror` (`pop_trob.c`) пишет тайл зеркала в ДАННЫЕ УРОВНЯ
|
||
(`pop_level_set_tile`), а коллизия работает со СНИМКОМ комнаты `room_fg`,
|
||
который `pop_room_load` делает при входе в комнату. Опасение: если тайл
|
||
поставлен, пока игрок УЖЕ в комнате 4, снимок останется старым и зеркало
|
||
будет невидимо для коллизии — Кид пробежит сквозь него, тень не родится.
|
||
|
||
**Проверка пользователем (2026-08-11), чит ROOMNAV:** нажал кнопку открытия
|
||
двери, телепортировался в комнату 4 — **зеркала не было вовсе**; вышел из
|
||
комнаты и вошёл обычным путём — зеркало появилось и **непроходимо, кроме
|
||
правильного прыжка**, то есть коллизия видит его корректно.
|
||
|
||
**Вывод: сценария нет.** Тайл ставится в момент, когда дверь ДОРИСОВАЛА
|
||
открытие (`animate_leveldoor`, переход `pop_leveldoor_open` 0/2 → 1, 43
|
||
тика анимации) — телепорт успевает раньше, поэтому «постановки при игроке в
|
||
комнате» в этом прогоне не случилось вообще. А при обычном входе комната
|
||
рисуется целиком из данных уровня и `room_fg` снимается заново — графика и
|
||
коллизия согласованы по построению.
|
||
|
||
**Решение пользователя:** телепорт по комнатам — ОТЛАДОЧНЫЙ режим; в реальном
|
||
прохождении дверь выхода стоит не в комнате зеркала, игрок физически не может
|
||
быть там в момент постановки, и зеркало с тенью появляются всегда штатно.
|
||
Чинить нечего.
|
||
|
||
**Если симптом всё же всплывёт** (например, появится мод/уровень, где кнопка
|
||
и зеркало в одной комнате): лечение — обновлять в `place_mirror` не только
|
||
данные уровня, но и живую карту (в `pop_map.c` уже есть внутренние точки
|
||
записи `g_fg[tilepos]`, нужна публичная «поставить тайл в текущей комнате»).
|
||
|
||
---
|
||
<a id="seam-button-stale"></a>
|
||
## SEAM-BUTTON-STALE. Кнопка в шве не меняла вид при нажатии/отжатии — ЗАКРЫТ
|
||
|
||
> **Симптом (пользователь, 2026-08-11, уровень 5).** Кнопка нижних ворот
|
||
> комнаты 24 стоит в шве: в комнате 11 это (1,9), в комнате 24 — (1,−1).
|
||
> «Нажали её из комнаты 11, она проанимировалась — зайдя в 24, она ВСЕГДА
|
||
> нарисована нажатой, хотя по статусу отжимается. Не нажимали, вошли в 24 и
|
||
> наступили — ворота открываются, а визуально кнопка остаётся отжатой.»
|
||
|
||
**Корень.** Change-driven редрой шва (`seam_sig` в `roomtest.c`) сравнивал
|
||
`room_modif` колонки 9 соседа. У кнопки `modif` — это ИНДЕКС LINKLOC,
|
||
константа уровня; само нажатие живёт в `doorlinks2`
|
||
(`get_doorlink_timer`, seg007:0BB6 — младшие 5 бит). Изменение в сигнатуру
|
||
не приходило НИКОГДА, редрой не заказывался, и шов оставался нарисованным
|
||
таким, каким был на входе в комнату — отсюда оба симптома сразу.
|
||
|
||
Видимой разницу делает `draw_tile`: нажатый opener рисуется как ПОЛ
|
||
(порт `get_tile_to_draw`, seg008:253 — `pop_room.c:256`), а у пола правая
|
||
грань есть, у кнопки её нет.
|
||
|
||
**Фикс.** `seam_row_sig()` (roomtest.c) подмешивает в сигнатуру ряда бит
|
||
«кнопка нажата» для тайлов `0x0F`/`0x06`. Оригинал шов в этом случае не
|
||
перерисовывает вовсе (`get_trob_pos_in_drawn_room` возвращает 30 для чужой
|
||
комнаты) — осознанное расхождение, записано как D-2 в
|
||
[`../docs/impl_diff.md`](../docs/impl_diff.md).
|
||
|
||
Цена: резидент +200 Б (`_CODE` 24 739 → 24 939), куча W2 1795 → 1595 Б.
|
||
|
||
**Проверено (пользователь, 2026-08-11):** «кнопка теперь отрисовывается
|
||
правильно в обоих случаях» — оба направления — нажать из комнаты 11 и войти в 24;
|
||
войти в 24 поверху и наступить на кнопку, стоя в шве. Картинка кнопки
|
||
обязана совпадать со статусом ворот.
|
||
|
||
---
|
||
|
||
---
|
||
|
||
<a id="bug-potion-stripe"></a>
|
||
## BUG-POTION-STRIPE. Узор паласа пропадал на месте выпитого зелья — ЗАКРЫТ
|
||
|
||
**Наблюдение (пользователь, 2026-08-11, уровень 5).** «На стенке паласа есть
|
||
узор — синяя в цветочек полоса, она запекается в фон и должна быть всегда.
|
||
Но если на её фоне Кид (или Тень) выпивает зелье — полоса пропадает.»
|
||
Воспроизведено в комнате 14 (два зелья в ряду 2, колонки 4 и 5): после
|
||
каждого глотка узор исчезал в ячейке СПРАВА от выпитого тайла и **не
|
||
возвращался даже после выхода из комнаты и повторного входа**.
|
||
|
||
**Корень.** У оригинала `curr_room_tiles` — это сама таблица уровня, поэтому
|
||
`curr_room_tiles[curr_tilepos] = tiles_1_floor` в `do_pickup` (seg006:1671)
|
||
виден всем в тот же миг. У нас `fg` живёт в ДВУХ местах — буфер отрисовки
|
||
(`g_fg`) и EMM-страница уровня, — и страницу правил главный цикл ПОЗЖЕ в
|
||
кадре, уже после `pop_process_trobs`.
|
||
|
||
В этот зазор успевал проснуться trob поднятого зелья: код тайла он читает из
|
||
страницы уровня (`pop_level_tile_raw`), там ещё зелье — и `animate_potion`
|
||
крутил фазу пузырька поверх только что обнулённого модификатора,
|
||
`bubble_next_frame(0)` = **1**. Модификатор оставался единицей навсегда
|
||
(`room_modif` персистентен, `room_seen` не даёт переинициализации).
|
||
|
||
Дальше срабатывало правило самого оригинала: для ПОЛА в паласе `modif == 1`
|
||
означает «узор не рисовать» — `if (num == !!level_type) return;`
|
||
(seg008:499, у нас `pop_room.c:322`). Отсюда и «не лечится перезаходом».
|
||
|
||
**Как подтверждено (MAME, чтение памяти, а не рассуждение):** буфер тайлов
|
||
комнаты (`0xA342`) после подбора — `01` = пол на обеих позициях, всё верно;
|
||
а в модификаторах на позициях 24 и 25 стояли `01 01`.
|
||
|
||
**Фикс.** `do_pickup` (`pop_map.c`) ставит тайл в странице уровня СРАЗУ
|
||
(`pop_level_set_tile`). Тогда trob того же кадра уходит в `default` и
|
||
снимается (`type = −1`), модификатор остаётся нулём. Дублирующая запись из
|
||
главного цикла убрана. Тем же фиксом лечится МЕЧ: там `animate_sword`
|
||
делал `--mod[tp]` из нуля и получал 255.
|
||
|
||
**Проверено (пользователь, 2026-08-11):** «узор теперь на месте».
|
||
|
||
**Ложный след, для протокола:** первым подозреваемым был `pop_floor_bake` —
|
||
после подбора он чистит `bar` шириной 60 px при 64-пиксельной колонке, и
|
||
уцелевший хвостик узора в 4 px выглядел как его подпись. Отвергнуто
|
||
перезаходом в комнату: полная перерисовка идёт мимо `bake`, а узор всё
|
||
равно не появлялся.
|
||
|
||
---
|
||
|
||
<a id="shadow-fight-l5"></a>
|
||
## SHADOW-FIGHT-L5. Кид и Тень вставали в боевую стойку в комнате с зельем — ЗАКРЫТ
|
||
|
||
**Наблюдение (пользователь, 2026-08-11).** Тень выходит и пьёт зелье
|
||
штатно, но если Кид успевал подняться в ряд 0, пока она пьёт: (а) Кид
|
||
доставал меч, (б) Тень тоже вставала в боевую стойку и **рисовалась
|
||
спрайтами стража**.
|
||
|
||
**Корень.** В `pop_check_can_guard_see_kid` (порт `check_can_guard_see_kid`,
|
||
seg003:702) первым множителем стояло `Guard.charid != 0`, тогда как оригинал
|
||
пишет:
|
||
|
||
```c
|
||
if ((Guard.charid != charid_1_shadow || current_level == 12) && ...
|
||
```
|
||
|
||
То есть ТЕНЬ — боевой персонаж только на 12-м уровне; на 4/5/6 она Кида «не
|
||
видит». Это не косметика: `can_guard_see_kid >= 2` разворачивается в оба
|
||
симптома через одну ветку `control_standing` (seg005:352) — Кид достаёт меч
|
||
сам, а Тень с `charid 1 < CHARID_2_GUARD` и убранным мечом идёт в
|
||
диспетчере `pop_control` ТОЙ ЖЕ веткой, что и Кид; с кадра 150 боевые кадры
|
||
лежат в атласе стража.
|
||
|
||
**Фикс.** Порт условия целиком + возвращён пропущенный множитель
|
||
`Guard.direction != dir_56_none` (им оригинал гасит выключенного
|
||
персонажа). Константа `SHADOW_FIGHT_LEVEL` в `pop_guard.h`.
|
||
|
||
**Проверено (пользователь, 2026-08-11):** тень выпивает и уходит, боя нет.
|
||
|
||
---
|
||
|
||
<a id="bug-lattice-doortop"></a>
|
||
## BUG-LATTICE-DOORTOP. Чёрная дыра на месте арки у ворот — ЗАКРЫТ
|
||
|
||
**Наблюдение (пользователь, 2026-08-11).** Уровень 5, комната 12, тайл
|
||
(0,9): чёрный квадрат вместо арки.
|
||
|
||
**Корень.** `draw_tile_base` (seg008:622) начинается со СПЕЦСЛУЧАЯ, которого
|
||
у нас не было:
|
||
|
||
```c
|
||
if (tile_left == tiles_26_lattice_down && curr_tile == tiles_12_doortop) {
|
||
id = 6; // Lattice + door A
|
||
ybottom += 3;
|
||
}
|
||
```
|
||
|
||
У `doortop` своей базы нет (`base_id == 0`), поэтому рядом с аркой пара
|
||
«арка + верх двери» рисуется ОДНИМ спрайтом 6, опущенным на 3 px. Без
|
||
ветки тайл не рисовал ничего, кроме полоски `bottom_id` 85 (32×4).
|
||
|
||
**Фикс.** Ветка добавлена в `pop_room.c` (draw_tile) И в
|
||
`toolchain/render_room.py` — последнее обязательно: `pop_pack_bg.py`
|
||
собирает атлас по обходу `render_room`, поэтому без правки скрипта спрайт 6
|
||
просто не попал бы в `pop_env*.atl` (см. memory `pop_atlas_dynamic_ids`).
|
||
|
||
**Как найдено — методика, годная для любого «не так нарисовано»:**
|
||
|
||
1. `./prince megahit <N> --screenshot-level` в SDLPoP рисует карту ВСЕГО
|
||
уровня (комнаты разложены по `roomxs`/`roomys`, клетка 320×192).
|
||
2. Вырезать нужную комнату и совместить со снимком MAME перебором смещения
|
||
(у нас совпало на `crop(101, 50, +640, +192)`, масштаб 2×1).
|
||
3. Разница по порогу яркости даёт карту расхождений: до фикса — белый
|
||
прямоугольник ровно в (0,9) и 1892 пикселя, после — 995 (Кид, факел,
|
||
статус-полоса, то есть динамика).
|
||
4. Что именно рисует оригинал в тайле — трасса `POP_TRACE_BT=1` (печать в
|
||
`add_backtable`, `src/seg008.c`, выводит room/row/col/id/размер). Она
|
||
сразу показала `id=6 w=32 h=63` рядом со «своим» `id=85 w=32 h=4`.
|
||
|
||
---
|
||
|
||
<a id="bug-belowrow-wall"></a>
|
||
## BUG-BELOWROW-WALL. Жёлтые треугольники в шахте падения — ЗАКРЫТ
|
||
|
||
**Наблюдение (пользователь, 2026-08-11).** Уровень 6, комната 3 (шахта под
|
||
комнатой 1): в нижнем ряду под пустыми тайлами (2,4)-(2,6) торчат жёлтые
|
||
треугольники.
|
||
|
||
**Корень.** `load_rowbelow` (seg008:368) при ОТСУТСТВУЮЩЕЙ комнате снизу
|
||
подставляет РАЗНЫЕ кромки: колонкам 1..9 — `tiles_0_empty`, и только левому
|
||
краю (тайл из `room_BL`) — `tiles_20_wall`. У нас `pop_room_load` забивал
|
||
стеной все одиннадцать позиций `below_fg`, а стена в роли соседа снизу-слева
|
||
даёт `topright` стены — её грань и лезла треугольниками.
|
||
|
||
**Фикс.** `below_fg[0..9] = 0`, `below_fg[10] = 20`.
|
||
|
||
**Проверено:** комната 3 совпала с картой SDLPoP — 300 пикселей расхождения,
|
||
все они силуэт Кида. Опасение «не пропадёт ли законный треугольник в (1,3)»
|
||
снято тем же сравнением: грань стены в колонке 3 и скос у её основания на
|
||
месте у обоих. Оговорка пользователя: эталон брался из `--screenshot-level`,
|
||
живая сверка в SDLPoP — потом.
|
||
|
||
---
|
||
|
||
<a id="bug-balcony-right"></a>
|
||
## BUG-BALCONY-RIGHT. Правая половина портала не рисовалась — ЗАКРЫТ
|
||
|
||
**Наблюдение (пользователь, 2026-08-11).** Уровень 6, комната 18: «закрытый
|
||
портал показывается некорректно» — у арки балкона нет правой половины,
|
||
решётка обрывается, справа чёрный провал.
|
||
|
||
**Корень — не в движке, а в УПАКОВЩИКЕ АТЛАСОВ.** `id 12` стоял в
|
||
`FORE_ENV_IDS` (`toolchain/pop_pack_bg.py`), потому что в `tile_table` он
|
||
числится `fore_id` ЗЕЛЬЯ (0x0A). Но зелье рисуется из `chtab_1`
|
||
(`add_foretable(id_chtab_1_...)`; у нас `pop_potion_flask`), а в `chtab_6`
|
||
под номером 12 лежит ПРАВАЯ ЧАСТЬ АРКИ БАЛКОНА 32x62, и она идёт в
|
||
backtable. Из-за списка спрайт уезжал в `pal_fore.atl`, а `pal_env0.atl`
|
||
получал дырку: `idx 12: w=0 h=0`. Движок берёт правую грань соседа из
|
||
env-атласа — и не рисовал ничего.
|
||
|
||
**Как найдено.** Трасса оригинала `POP_TRACE_BT=1` показала в спорном тайле
|
||
`BT room=18 r=1 c=7 ch=6 id=12 w=32 h=62`; дальше — чтение каталога
|
||
`pal_env0.atl` напрямую (запись 12 пустая) и `pal_fore.atl` (она там).
|
||
|
||
**Фикс.** `id 12` убран из `FORE_ENV_IDS`; в env-атласе он занял своё место.
|
||
Дифф комнаты с оригиналом 1151 -> 375 (остаток — Кид и пламя).
|
||
**Проверено пользователем:** «портал отрисовался корректно».
|
||
|
||
**Урок:** список `FORE_ENV_IDS` собирается по `tile_table.fore_id`, но
|
||
`fore_id` НЕ значит «спрайт из chtab_6» — у зелья, меча и пламени он
|
||
указывает в `chtab_1`. Прежде чем вносить номер в этот список, проверять,
|
||
из какой таблицы спрайт берётся.
|
||
|
||
---
|
||
|
||
<a id="bug-mob-multiroom"></a>
|
||
## BUG-MOB-MULTIROOM. Плита не пролетала несколько комнат — ЗАКРЫТ
|
||
|
||
**Наблюдение (пользователь, 2026-08-11).** Уровень 6: плита (1,7) комнаты 6
|
||
должна пролететь НЕСКОЛЬКО комнат и упасть на кнопку, открывающую решётку.
|
||
«Плита просто падает и ничего не открывается.»
|
||
|
||
**Корень.** У оригинала кусок живёт в списке mobs и спускается ряд за рядом:
|
||
`mob_down_a_row` (seg007:1387) из НИЖНЕГО ряда переводит его в комнату снизу
|
||
(`y -= 192`, ряд 0), и так сколько угодно комнат. У нас кусок на границе
|
||
комнаты ГАСИЛСЯ, а «переход вниз» подменялся сигналом главному циклу — он
|
||
искал посадку в комнате снизу через `pop_room_col_landing`. Для колонки 7
|
||
комнаты 7 (сквозная шахта) посадки нет — кусок исчезал.
|
||
|
||
**Фикс.** Честный `mob_down_a_row` в `mob_tick_one` (`pop_room.c`);
|
||
приземление в НЕ отрисованной комнате сигналит комнатой и тайлом, а щебень
|
||
и кнопку ставит главный цикл (порт `loose_land`, seg007:11E8).
|
||
|
||
Путь в этом сценарии: комната 6 (1,7) -> сквозная шахта комнаты 7 -> кнопка
|
||
(2,7) комнаты 11, а она по LINKLOC открывает ворота (1,9) комнаты 18 — те
|
||
самые у портала. **Проверено пользователем:** ворота открываются.
|
||
|
||
---
|
||
|
||
<a id="bug-mob-stale-page"></a>
|
||
## BUG-MOB-STALE-PAGE. Улетевшая плита оставалась на одной странице — ЗАКРЫТ
|
||
|
||
**Наблюдение (пользователь, 2026-08-11).** Сразу после предыдущего фикса:
|
||
«в одном из кадров дабл-буфера осталась падающая плита».
|
||
|
||
**Корень.** Чистка прошлого кадра куска стояла под гейтом «кусок в
|
||
ОТРИСОВАННОЙ комнате»: `if (here && m->prev_y[pg] != MOB_Y_NONE)`. Как
|
||
только кусок уходил вниз через `mob_down_a_row`, гейт закрывался — и след,
|
||
оставленный им на этой странице, не стирал уже никто.
|
||
|
||
**Фикс.** Гейт убран: `prev_y[pg]` — след ИМЕННО НА ЭТОЙ странице, стирать
|
||
его надо независимо от того, где кусок сейчас. Плюс `clean = 2` во всех
|
||
ветках гашения, чтобы дочищались обе страницы. **Проверено пользователем.**
|
||
|
||
---
|
||
|
||
<a id="bug-char-stale-page"></a>
|
||
## BUG-CHAR-STALE-PAGE. Тень застывала на двух страницах в РАЗНЫХ позах
|
||
|
||
**Наблюдение (пользователь, 2026-08-11, редкий — пойман дважды).** Уровень
|
||
6, комната с тенью: «Тень застряла в двух разных кадрах в разных позициях».
|
||
|
||
**Замер в отладчике (состояние было живым).** `Guard` = кадр 15 (stand),
|
||
x = 0x51 — то есть персонаж В ПОКОЕ. А слот отрисовки `pop_cd[OPP]`:
|
||
|
||
```
|
||
страница 0: x=25 y=85 w=34 h=38 valid=1 <- ЛЕЖАЩАЯ поза
|
||
страница 1: x=41 y=78 w=12 h=41 valid=1 <- стойка
|
||
```
|
||
|
||
**Корень.** В `pop_char_draw` (`pop_cdraw.c`) прямоугольник и `valid[dp]`
|
||
пишутся ВНУТРИ `if (w && h)`, а снимок состояния для пропуска перерисовки
|
||
(`cd_sig_make`) брался БЕЗУСЛОВНО, в самом конце функции. Если спрайт
|
||
кадра нулевой (пустая запись атласа), блок рисования пропускался — `x/y/w/h`
|
||
и `valid` оставались от ПРОШЛОГО кадра этой страницы, а сигнатура
|
||
обновлялась на текущее состояние. Дальше `cd_quiet` видел `valid = 1` и
|
||
совпавшую сигнатуру, то есть считал «на странице нарисовано ровно то, что
|
||
надо», и слот залипал навсегда: `heal` не звался, старый спрайт оставался.
|
||
|
||
**Фикс.** `cd_sig_make` вызывается только когда кадр реально рисовали
|
||
(`if (w && h)`). Без снимка слот не будет «тихим», и следующий кадр
|
||
начнётся с `heal` — старое сотрётся само.
|
||
|
||
**ОСТАЁТСЯ ОТКРЫТЫМ (в BUGS_OPEN):** почему спрайт кадра оказался нулевым.
|
||
Фикс убирает залипание, но кадр, для которого атлас отдал 0x0, всё равно
|
||
не нарисуется. Подозрение — конкуренция за окно W0 между `atlas_image` и
|
||
чтением данных уровня; проверять трассировкой маппинга.
|
||
|
||
---
|
||
|
||
<a id="bug-cheat-imm-1"></a>
|
||
## BUG-CHEAT-IMM-1. Кид залипал в боевой стойке без меча — ЗАКРЫТ 2026-08-17
|
||
|
||
**Наблюдение (пользователь, уровень 2, комната 4).** Кида с мечом сталкивают
|
||
ударами с ряда 1 на ряд 2; страж падает следом и оказывается у него за
|
||
спиной. Кид встаёт в боевую стойку, но: **меча в руке нет** (не
|
||
отрисовывается), и **к сопернику он не разворачивается** — на клавиши не
|
||
реагирует вовсе. «Иногда Кид успевает развернуться, а вот сейчас не успел».
|
||
|
||
**Два симптома — один отказ.** Диспетчер `control()` (`pop_ctrl.c`,
|
||
порт seg005:252) уходит в `control_with_sword` только при
|
||
`Char.sword == SWORD_2_DRAWN`. При `sword == 0` управление проваливается в
|
||
обычные ветки, а среди них для кадров стойки с мечом (158/170/171) нет ни
|
||
одной — каждый кадр не делается НИЧЕГО. Отсюда и «не разворачивается»:
|
||
разворот к сопернику за спиной (`SEQ_60_TURN_WITH_SWORD` при
|
||
`char_opp_dist() < -4`) живёт внутри `control_with_sword`.
|
||
|
||
**Корень — наш чит бессмертия, а не механика боя.** `hurt_by_sword`
|
||
(`guards.c`, порт seg002) перехватывался читом в самом начале и
|
||
**безусловно**, не глядя на меч, ставил `SEQ_74_HIT_BY_SWORD`:
|
||
|
||
```c
|
||
if (Char.charid == CHARID_0_KID && pop_immortal) {
|
||
pop_char_set_seq(SEQ_74_HIT_BY_SWORD); /* ← без проверки sword */
|
||
```
|
||
|
||
А `seq_74` — это анимация «получил удар **В БОЕВОЙ СТОЙКЕ**», её кадры
|
||
(150..179) все с мечом. В оригинале в неё попадают ТОЛЬКО из ветки
|
||
`sword == 2`; безоружного там убивают (seg002, дословный комментарий:
|
||
*«Being hurt when not in fighting pose means death»*, `take_hp(100)`).
|
||
Чит подменял эту смерть выживанием и оставлял Кида в кадрах с мечом при
|
||
`sword == 0` — состояние, которого в оригинале не существует и для которого
|
||
у диспетчера нет ветки.
|
||
|
||
**Как доказано** (важно для метода — гипотез было три, и две неверные):
|
||
|
||
1. Кодоген `draw_sword` проверен по `bank5_pop_ctrl.asm`: `ld hl,#_Char+12 /
|
||
ld (hl),#0x02` — store меча на месте, версия «SDCC потерял запись»
|
||
(ср. [[sdcc_z80_cmp_store_a_bug]]) отпала.
|
||
2. Watchpoint на `Kid.sword` в MAME: `0 → 2` при доставании и `2 → 0` в
|
||
`start_fall` проходят штатно (`start_fall` меч гасит и в оригинале,
|
||
seg006:1102).
|
||
3. **Решающее:** с ВЫКЛЮЧЕННЫМ читом (`pop_immortal = 0`) тот же сценарий
|
||
даёт корректную смерть — то есть порт совпадает с оригиналом, а баг
|
||
живёт только под читом.
|
||
|
||
Грабли метода, стоившие времени: ловушки прошлой сессии печатали
|
||
`b@0x992A` **без** префикса `0x10000`, то есть мимо логического вида Z80 —
|
||
`charid/frame/dir` в той трассе были мусором, отсюда ложный вывод «записей
|
||
в `sword` нет вовсе». И ловушка на `Char.sword` бесполезна: `Char` —
|
||
переключаемое окно, у стража меч вынут, поэтому `2 → 0` там происходит на
|
||
каждом переключении окна. Ещё одно: `pop_char_set_seq` пишет
|
||
`ld ($992D),hl`, запись 16-битная — байтовое условие `wpdata==0xDC` не
|
||
совпало бы никогда.
|
||
|
||
**Фикс** (решение пользователя: «повторяем поведение оригинала, наш иммортал
|
||
работает только во время боя, с мечом»). Спецветка чита удалена целиком —
|
||
она оказалась **избыточной**: под бессмертием `pop_take_hp(1)` и так
|
||
возвращает 0, и обычный путь сам приводит к `seq_74` с сохранённым мечом.
|
||
Осталось запретить читу действовать в безоружной ветке:
|
||
|
||
```c
|
||
if (Char.sword != SWORD_2_DRAWN) {
|
||
uint8_t imm = pop_immortal;
|
||
pop_immortal = 0; /* чит не действует вне боевой стойки */
|
||
pop_take_hp(100);
|
||
pop_immortal = imm;
|
||
pop_char_set_seq(SEQ_85_STABBED_TO_DEATH);
|
||
}
|
||
```
|
||
|
||
Глушится именно вызов, а не `pop_take_hp`: тот общий с физикой (падения,
|
||
пики, чомперы), и там бессмертие обязано работать при любом положении меча.
|
||
|
||
**Подтверждение.** Пользователь, 2026-08-17: «всё сработало как надо»,
|
||
проверено обоими концами — «во время боя удары, пропускаемые Кидом, урон не
|
||
приносят; если Кид без меча — смерть с одного удара». То есть чит в бою
|
||
работает как работал, а вне боевой стойки поведение снова совпадает с
|
||
оригиналом.
|
||
Взведена регресс-ловушка на сам инвариант: Киду пишется кадр стойки
|
||
(158/170/171), а `Char.sword != 2`. Условие смотрит на `Char`, а НЕ на
|
||
`Kid`: `ldir` копирует байт кадра (0) раньше байта меча (12), поэтому по
|
||
`Kid` ловушка слепа ровно в тот тик, когда меч теряется, и ложно срабатывает
|
||
на каждом законном доставании.
|
||
|
||
---
|
||
|
||
<a id="leveldoor-palace-clip"></a>
|
||
## LEVELDOOR-PALACE-CLIP. Кид скрывался за кромкой дворцового портала раньше времени — ЗАКРЫТ 2026-08-17
|
||
|
||
**Наблюдение (пользователь, уровень 4, обход после оптимизации фаз).**
|
||
Порталы, через которые Кид входит на уровень и уходит на следующий, в
|
||
подземелье и во дворце РАЗНОЙ ширины — дворцовый шире. В анимации ухода с
|
||
уровня 4 на 5-й Кид убегает по лестнице, и его контур обрезается не правой
|
||
гранью портала, а раньше, «как будто он прячется проходом». Диагноз
|
||
пользователя сразу верный: взята ширина подземного портала.
|
||
|
||
**Корень.** `draw_leveldoor` (`pop_room.c`, порт seg008:1D29) считал правую
|
||
кромку проёма без дворцовой поправки:
|
||
|
||
```c
|
||
pop_leveldoor_right = xh * 8 + 48;
|
||
```
|
||
|
||
В оригинале строкой ниже стоит (seg008:1429):
|
||
|
||
```c
|
||
leveldoor_right = (draw_xh<<3)+48;
|
||
if (custom->tbl_level_type[current_level]) leveldoor_right += 8;
|
||
```
|
||
|
||
Это значение читает `clip_char` (у нас `pop_map.c:2668`) как правую границу
|
||
клипа персонажа — отсюда ранняя обрезка.
|
||
|
||
Расхождение было **осознанным и отложенным**: в коде стоял комментарий
|
||
«+8 у palace-уровней (tbl_level_type) — на уровне 1 не применяется». На
|
||
момент написания дворцовых уровней в порту не было, и условие не дописали.
|
||
`tbl_level_type[4] = 1` (`pop_level_cold.c:52`), то есть уровень 4 —
|
||
дворцовый, и на нём это наконец проявилось.
|
||
|
||
**Фикс.** `if (pop_palace) pop_leveldoor_right += 8;` Флаг `pop_palace`
|
||
выставляет `pop_bg_load(set)` из того же `tbl_level_type`, который читает
|
||
оригинал, так что эквивалент дословный и рассинхронизироваться не может.
|
||
|
||
**Найдено попутно, НЕ починено:**
|
||
[LEVELDOOR-STARTROOM-WIPE](BUGS_OPEN.md#leveldoor-startroom-wipe) — в той же
|
||
функции оригинал в СТАРТОВОЙ комнате кладёт затирающий прямоугольник вместо
|
||
лестницы (и там своя дворцовая/подземная разница 48/39 и сдвиг 2 px), а мы
|
||
рисуем марш 144 безусловно. Заведено отдельным багом.
|
||
|
||
---
|
||
|
||
<a id="shadow-stale-frame"></a>
|
||
## SHADOW-STALE-FRAME. Тень мигала между двумя РАЗНЫМИ позами на страницах дабл-буфера — ЗАКРЫТ 2026-08-18
|
||
|
||
**Наблюдение (пользователь, 2026-08-18).** Уровень 6, комната 1 (Кид и через
|
||
большой провал — тень). «У Тени на двух экранах отрисованы разные позиции —
|
||
одна стоящая, одна бегущая; увидел сразу, как вошёл в комнату. Повторяется
|
||
нечасто, но очень неприятно». Гипотеза пользователя — «в один из экранов
|
||
попала случайная позиция Тени» — оказалась дословно верной.
|
||
|
||
**Улика (живое состояние, снято через мост MAME до перезапуска).** Слот
|
||
соперника `pop_cd[POP_CH_OPP]`:
|
||
|
||
```
|
||
стр.0: x=41 y=78 w=12 h=41 valid=1 <- кадр 15 «стойка»
|
||
стр.1: x=25 y=85 w=34 h=38 valid=1 <- ЧТО-ТО ДРУГОЕ
|
||
cd_sig[OPP][0] == cd_sig[OPP][1] == { frame=15, x=81, y=118, dir=0, charid=1 }
|
||
```
|
||
|
||
Обе страницы «тихие» (снимок совпал с живым `Guard`), поэтому ни одна больше
|
||
не перерисовывается — мигание навсегда. Прямоугольник страницы 1
|
||
восстанавливается однозначно: `bx = scr_x(obj_x) − w` и `top = obj_y − h + 1`
|
||
при `Guard.x = 81`, `Guard.y = 118`, `dir = 0` дают `dx = 3`, `dy = 4`,
|
||
`flags ≥ 0x80` и картинку 34×38. В таблице кадров ровно одна такая запись —
|
||
**`frame_tbl_guard[36]`, то есть кадр 185 «страж мёртв»** (`image = 33`,
|
||
`dx = 3`, `dy = 4`, `flags = 0xC9`; 34×38 — это размер `image 33` в атласе
|
||
КИДА, что сходится: набор спрайтов выбирается по `Guard.frame = 15`, а
|
||
`image` приходил из кэша).
|
||
|
||
**Корень.** Оригинал грузит кадр В САМОЙ ОТРИСОВКЕ: `add_kid_to_objtable` и
|
||
`add_guard_to_objtable` (seg008:22F0/2324) первым делом после
|
||
`loadkid`/`loadshad` зовут `load_fram_det_col()`. У нас `kid_frame` /
|
||
`pop_gframe` были КЭШЕМ, который наполняет тик (`play_seq` → `load_frame`), а
|
||
отрисовка брала готовое. Кэш отстаёт от `Char.frame` у всех, кто пишет кадр
|
||
мимо `play_seq` — например `do_init_shad` (`guards.c`) кладёт тени
|
||
`frame = 15` прямой записью. А `pop_gframe` в этот момент держал кадр 185
|
||
убитого стража с прошлого уровня: после смерти соперника `pop_guard_tick`
|
||
выходит по `charid == 0` РАНЬШЕ `pop_load_fram_det_col`, и кэш не трогается
|
||
вообще, сколько бы комнат Кид ни прошёл.
|
||
|
||
Само по себе это моргнуло бы один кадр. Смертельным его делает **пропуск
|
||
неизменившегося кадра** (DRAW-COST): снимок `cd_sig` пишется по
|
||
`Guard.frame`, то есть страница запоминает «нарисован кадр 15», хотя на ней
|
||
чужой спрайт. Тень дальше стоит, снимок совпадает — страница не
|
||
перерисовывается НИКОГДА.
|
||
|
||
**Фикс.** `pop_char_draw` зовёт `pop_load_frame()` сразу после
|
||
`pop_loadkid`/`pop_loadshad` — там же, где это делает оригинал. После этой
|
||
строки `image` однозначно определяется парой `(charid, frame)`, то есть
|
||
снимок `cd_sig` снова описывает ровно то, что нарисовано, и расхождение
|
||
«пиксели против снимка» становится невозможным независимо от того, кто и как
|
||
испортил кэш. Цена: +3 байта в банке 4, кадровый бюджет не трогает
|
||
(отрисовка и так пропускается у «тихого» слота).
|
||
|
||
Заодно приведена к оригиналу ветка «кадра нет в таблице»: `load_frame`
|
||
(`pop_kid.c`) ставила `cur_frame.image = 255` и выходила, НЕ обновив кэш
|
||
владельца, — то есть вместо «не рисовать» рисовалась прошлая картинка.
|
||
Оригинал кладёт туда `blank_frame {255,0,0,0,0}` (`get_frame_internal`,
|
||
seg006:507). Выход из функции теперь один на обе ветки: +46 байт резидента.
|
||
|
||
**Проверка (MAME, уровень 6 комната 1).** Брейкпоинт на трамплине
|
||
`___sdcc_bcall_ehl` с условием `hl == _pop_char_draw` и действием
|
||
«записать в `pop_gframe` кадр 185 и продолжить» — то есть кэш травится
|
||
ровно перед КАЖДОЙ отрисовкой персонажа. Слот при принудительной
|
||
перерисовке (`valid[0] = valid[1] = 0`) остаётся `12×41` на обеих
|
||
страницах, `pop_gframe` после кадра — снова кадр 15. До фикса та же
|
||
подмена дала бы `34×38 @ (25,85)` — ровно числа из улики.
|
||
|
||
**Что осталось невыясненным.** Не удалось воспроизвести САМ МОМЕНТ порчи:
|
||
вход в комнату с заведомо испорченным кэшем (подмена `pop_gframe` в соседней
|
||
комнате + возврат читом обхода) самолечится — метка «фон трогали» от
|
||
перерисовки комнаты стоит на обеих страницах, и слот честно перерисовывается
|
||
следующим кадром. Значит порча случилась в кадре, где фон НЕ трогали, и
|
||
конкретный путь остался неизвестен. На вывод это не влияет: фикс закрывает
|
||
класс целиком, а не найденный путь. Ловушка на будущее, если симптом
|
||
всплывёт снова: `wp <адрес pop_cd[OPP].w>,4,w` — сработает на первой же
|
||
записи «чужой» ширины.
|
||
|
||
---
|
||
|
||
<a id="blueline-noblue"></a>
|
||
## BLUELINE-NOBLUE. Кусок синего узора на стене там, где оригинал его не рисует — ЗАКРЫТ 2026-08-18
|
||
|
||
**Наблюдение (пользователь, 2026-08-18).** Уровень 6, комната 1, тайл (2,3):
|
||
у правой кромки левого блока висит небольшой кусочек тёмно-синего узора,
|
||
которого в оригинале нет. Диагноз пользователя («первоначальная отрисовка
|
||
фона, фикс не должен повлиять на скорость») подтвердился полностью.
|
||
|
||
**Корень.** Полоску рисует `draw_tile_right` (`pop_room.c`, порт
|
||
seg008:0666) — её кладёт ПРАВЫЙ сосед стены:
|
||
|
||
```c
|
||
case tiles_20_wall:
|
||
if (tbl_level_type[current_level] && (modifier_left & 0x80) == 0)
|
||
add_backtable(chtab_6, 84 /*wall stripe*/, draw_xh + 3, 0, draw_main_y - 27, ...);
|
||
```
|
||
|
||
Проверка `& 0x80` у нас была, а вот значения, которое она проверяет, — нет.
|
||
Модификатор стены В ФАЙЛЕ уровня и В РАНТАЙМЕ у оригинала разные:
|
||
`load_alter_mod` (seg008:1216) переводит их при загрузке комнаты —
|
||
`*curr_tile_modif <<= 7`, то есть единственный бит уровня («no blue»,
|
||
авторский флаг «не рисовать полоску на моей правой кромке») переезжает в
|
||
СТАРШИЙ. Наш порт `load_alter_mod` (`pop_trob.c`) ветку стен пропускал
|
||
НАМЕРЕННО, и в комментарии это было записано: связи кладки мы считаем по
|
||
типам соседей (`pop_wall_modifier`), сохранённый модификатор стены «не
|
||
читается вовсе».
|
||
|
||
Ошибка в слове «вовсе»: у модификатора стены ДВА независимых смысла, и
|
||
из соседей выводится только один. `noblue` проставлен вручную в данных
|
||
уровня и не выводится ниоткуда. В комнате 1 уровня 6 стена (2,2) имеет в
|
||
файле модификатор 1, то есть ровно «без полоски»; без перевода отрисовка
|
||
видела сырую единицу, `(1 & 0x80) == 0` проходило — и полоска вылезала.
|
||
|
||
**Фикс.** `case TILE_WALL: *m = (uint8_t)(*m << 7);` в порте
|
||
`load_alter_mod`. Перевод одноразовый, при первом входе в комнату
|
||
(`room_seen`), — кадровый бюджет не трогает вообще; +24 байта в банке 6,
|
||
резидент не изменился.
|
||
|
||
**Проверка (MAME, уровень 6 комната 1).** Попиксельный диф скриншотов до и
|
||
после: изменения ровно в боксе `(344,199)-(358,206)`, 38 пикселей — сам
|
||
артефакт и ничего кроме. На подземных уровнях изменений нет по построению:
|
||
полоска целиком под `if (PALACE)`.
|
||
|
||
---
|
||
|
||
<a id="spike-baked"></a>
|
||
## SPIKE-BAKED. Выдвинутые пики консервировались в фон и оставались навсегда
|
||
|
||
**Наблюдение (пользователь, 2026-08-18).** Уровень 7, комната 19. Пики стоят
|
||
в `(0,6)` и из-за изометрии частью выходят в ячейку `(0,7)`. Выдвигаются
|
||
корректно все; **прячутся корректно только те, что в `(0,7)`**, а в `(0,6)`
|
||
остаются два вида мусора: куски остриёв на фоне дальней стены и белые точки в
|
||
дырах пола. Пользователь сразу указал и на класс («первоначальная отрисовка
|
||
фона, фикс не должен повлиять на скорость»), и на причину («в `(0,5)` не пол,
|
||
а кнопка») — оба попадания верные.
|
||
|
||
**Артефакты, снятые в MAME на живом кадре с мусором.**
|
||
|
||
| что | значение |
|
||
|-----|----------|
|
||
| `pop_t_bg[6]` (живой modif пики) | `0x00` — пики УБРАНЫ |
|
||
| `pop_t_fg[0..9]` | `03 04 13 0F 01 06 02 04 01 03` → `(0,5)` = `0x06` closer-кнопка, `(0,6)` = `0x02` spike |
|
||
| мусор на экране | x 198..215, y 63..88 |
|
||
|
||
Модификатор нулевой, а пиксели пик есть — значит рисует их не живое
|
||
состояние. И `pop_spike_redraw` каждый кадр делает `pop_heal_off(192, 62,
|
||
64, 40)` — ровно поверх этого прямоугольника. Мусор переживает СВОЙ ЖЕ heal,
|
||
а heal восстанавливает ОЗУ-копию фона: пики в ней и лежат.
|
||
|
||
**Корень.** Кнопка при смене состояния метится `POP_RD_FLOOR`, а это
|
||
`pop_floor_bake(0,5)` — ЗАПЕЧКА, то есть рисование в банке, который пишет и в
|
||
ОЗУ-копию. Запечка перерисовывает свой тайл **и правого соседа**:
|
||
|
||
```c
|
||
pop_t_bake_rest = 1;
|
||
draw_tile(row, col);
|
||
if (col + 1 < 10) draw_tile(row, col + 1); /* <- это пики (0,6) */
|
||
```
|
||
|
||
`pop_t_bake_rest` для того и заведён — «в фон кладём состояние ПОКОЯ», — но
|
||
читал его ТОЛЬКО `pop_loose_frame`. Кадр пик считался прямо по месту,
|
||
`(m & 0x80) ? 5 : m`, в ЧЕТЫРЁХ слоях, и ни один флага не спрашивал. Так
|
||
выдвинутая пика попадала в ОЗУ-копию, и дальше heal возвращал её вечно.
|
||
|
||
Всё сходится численно. Окно клипа запечки — `x 160..219` (ширина 60 от
|
||
кнопки), спрайт пики — `x 192..222`, `y 62..88`. Пересечение `x 192..219` —
|
||
это и есть наблюдаемые 198..215; а `(0,7)` начинается с 224, за окном
|
||
запечки, — потому там и чисто. Правая часть пики живёт только в видео-ОЗУ и
|
||
честно стирается.
|
||
|
||
**Фикс (`pop_tile.c`).** Порт `get_spike_frame` (SDLPoP seg008:08A0), которого
|
||
у нас не было, — плюс наша платформенная оговорка про запечку:
|
||
|
||
```c
|
||
uint8_t pop_spike_frame(uint8_t m)
|
||
{
|
||
if (pop_t_bake_rest) return 0;
|
||
return (uint8_t)((m & 0x80) ? 5 : m);
|
||
}
|
||
```
|
||
|
||
Все четыре слоя (`draw_tile_anim`, `draw_tile_anim_right` соседа, fore-проход,
|
||
mid-оверлей) переведены на него. Заодно `pop_chomp_pose` тоже стал отдавать
|
||
позу покоя в запечке: у чомпера ровно та же схема, а рядом с кнопкой он
|
||
встретится так же легко.
|
||
|
||
Одна точка вместо четырёх лечит ВСЕ пять путей запечки сразу, включая
|
||
`pop_room_draw` — вход в комнату с выдвинутыми пиками тоже консервировал бы их.
|
||
|
||
**Цена.** Резидент +28 Б, банки без изменений; в кадре — один `call` на слой,
|
||
и только на тайлах-пиках. Скорость не затронута, как и предполагал
|
||
пользователь.
|
||
|
||
**Проверка.** Пользователь прошёл сценарий вручную на пересобранном билде —
|
||
мусора нет.
|
||
|
||
**Урок (записан в `pop_tile.h` над `pop_t_bake_rest`).** Флаг «сейчас
|
||
запечка» обязана спрашивать САМА функция «кадр по модификатору», а не
|
||
вызывающий слой: слоёв отрисовки у тайла четыре, и забыть один слишком легко.
|
||
Для новой анимированной ловушки это теперь обязательный пункт.
|
||
|
||
---
|
||
|
||
<a id="died-on-button"></a>
|
||
## DIED-ON-BUTTON. Смерть на кнопке не ломала её: решётка закрывалась обратно
|
||
|
||
**Наблюдение (пользователь, 2026-08-18).** Уровень 7 начинается падением, и
|
||
если не зацепиться за кромку, Кид разбивается на `(2,1)` комнаты 3 — а там
|
||
кнопка открытия решётки. В оригинале решётка после этого открыта НАСОВСЕМ,
|
||
у нас отжималась.
|
||
|
||
Баг был заведён ещё при закрытии
|
||
[BUG-LOOSE-BUTTON-1](#bug-loose-button-1) как «`died_on_button` (seg007:776)
|
||
не портирован», но оказался ДВОЙНЫМ — до недостающей функции ещё и не было
|
||
пути.
|
||
|
||
### Половина 1: физика трупа выключена целиком
|
||
|
||
```c
|
||
void pop_phys_tick(void) __banked
|
||
{
|
||
if (pop_kid_dead) return; /* <- вся цепочка, включая check_press */
|
||
```
|
||
|
||
У оригинала `play_kid_frame` (seg000:1231) гейт ровно один — `Char.room != 0`;
|
||
весь список крутится и на трупе (по «жив/мёртв» разведены `check_spiked`/
|
||
`check_chomped_kid`, и то через `resurrect_time`). Ранний выход выглядел
|
||
безобидной экономией — «труп не шевелится», — но `check_press` на мёртвом
|
||
персонаже делает СОВСЕМ ДРУГОЕ, чем на живом, и именно это выключалось.
|
||
|
||
Отсюда наблюдаемое: кнопка получала РОВНО ОДНО нажатие — в кадре самой
|
||
смерти, потому что `pop_kid_dead` ставит `land()` уже ВНУТРИ цепочки, после
|
||
раннего возврата. Решётка приоткрывалась и закрывалась.
|
||
|
||
Снято в MAME до фикса: Кид `frame 185, action 5 (bumped), alive 0`, тайл
|
||
`(2,1)` = `0x0F` opener с модификатором `0x12`, openness решётки ползёт
|
||
вниз 146 → 112. Кадр смерти при этом проходит все проверки `check_press`
|
||
штатно (`action == ACT_BUMPED`, флаги кадра 185 = `0x49`, бит
|
||
`FRAME_NEEDS_FLOOR` есть) — то есть путь был закрыт ровно одним `return`.
|
||
|
||
«Труп не шевелится» держится и без него, тем же, чем в оригинале: кадр
|
||
смерти не двигается сам, а уход из комнаты и сотрясение отсечены ВНУТРИ
|
||
`kid_phys` (там свой `if (pop_kid_dead) return;` перед `pop_check_knock`).
|
||
|
||
### Половина 2: сам `died_on_button`
|
||
|
||
```c
|
||
if (Char.alive < 0) pop_trigger_button(...); /* жив — нажатие на 5 кадров */
|
||
else died_on_button(...); /* мёртв — кнопка СЛОМАНА */
|
||
```
|
||
|
||
Открывалка превращается в обычный пол с нулевым модификатором, а связь
|
||
дёргается типом «щебень» (`TILE_DEBRIS`) — у `trigger_gate` это отдельная
|
||
ветка «открыть насовсем». Любая другая кнопка становится заклиненной
|
||
(`TILE_STUCK` = 5). Тайл пишем и в страницу уровня (переживает выход из
|
||
комнаты), и в живую `g_fg`, с пометкой на перерисовку своей ячейки и правой.
|
||
|
||
Инфраструктура была готова давно: `TILE_DEBRIS` и ветка «насовсем» в
|
||
`trigger_gate` написаны ещё тогда, с комментарием-ссылкой именно на
|
||
`died_on_button` — из-за чего механизм и казался реализованным. Опасение
|
||
старой записи, что нужен «тайл заклиненной кнопки в атласе, а его там нет»,
|
||
не подтвердилось: `pop_tile_table[5]` (stuck floor) заполнен и уже
|
||
рисуется — на него подставляется НАЖАТАЯ кнопка-закрывалка.
|
||
|
||
### Половина 3 (вылезла сразу): таймер связи переживал рестарт уровня
|
||
|
||
Пользователь поймал на первом же прогоне: **до первого падения кнопка
|
||
рисуется правильно, после — выглядит нажатой с самого начала уровня**, хотя
|
||
логически не нажата.
|
||
|
||
`died_on_button` ставит таймер связи в 5 и заводит trob кнопки — но тайл к
|
||
этому моменту УЖЕ пол, и trob умирает в первом же обходе
|
||
(`default: type = -1`), ни разу не уменьшив таймер. У оригинала ровно то же
|
||
самое, только там `load_level()` перечитывает файл ЦЕЛИКОМ, а наш
|
||
`pop_level_reset_tiles` восстанавливал одну foretable. Остаток `dl2[18] = 5`
|
||
переживал респавн, а `pop_tile_code_drawn` рисует кнопку нажатой, пока
|
||
таймер > 1.
|
||
|
||
Фикс — восстанавливать в рестарте и LINKMAP. Отдельная эталонная копия не
|
||
нужна: в странице уровня LINKMAP остаётся нетронутой, игра правит только
|
||
ОЗУ-копию `pop_dl2`.
|
||
|
||
**Проверка в MAME (полный цикл).**
|
||
|
||
| момент | тайл `(2,1)` | `dl2[18]` | `pop_kid_dead` | решётка `(2,6)` |
|
||
|--------|--------------|-----------|----------------|-----------------|
|
||
| труп на кнопке | `01` floor | 5 | 1 | `FF` открыта навсегда |
|
||
| после респавна | `0F` opener | **`00`** | 0 | `00` закрыта |
|
||
| второе падение | `01` floor | 5 | 1 | открывается заново |
|
||
|
||
Плюс попиксельная сверка кадров: решётка, до фикса стоявшая опущенной,
|
||
поднята. Цена: резидент без изменений, банк 3 +193 Б, банк 8 +22 Б.
|
||
|
||
**Осталось невыясненным.** На СТАРОМ поведении openness решётки замирал на
|
||
112 и дальше не убывал, хотя `animate_door` (совпадает с seg007:0522) обязан
|
||
досчитать до 0 и снять trob. С фиксом сцена так не воспроизводится
|
||
(решётка уходит в `FF`), проверить нечем; если всплывёт где-то ещё — это
|
||
отдельный баг, к `died_on_button` отношения не имеющий.
|
||
|
||
---
|
||
|
||
<a id="guard-respawn-col0"></a>
|
||
## GUARD-RESPAWN-COL0. Страж при возврате в комнату телепортировался в колонку 0 и падал
|
||
|
||
**Наблюдение (пользователь, 2026-08-18).** Уровень 8, комната 24. Первый
|
||
вход из левой комнаты — страж около `(0,7)`, стоит. Кид уходит вправо и
|
||
возвращается — страж уже «в `(0,-1)`». Ещё раз туда-обратно — страж на
|
||
`(0,0)`, идёт биться, падает на два ряда и разбивается. Второй сценарий:
|
||
вход из комнаты 23, страж на `(0,7)` неподвижен; Кид уходит влево и
|
||
возвращается — страж падает на `(2,1)` и разбивается.
|
||
|
||
Формулировка пользователя — «страж, если не может биться с Кидом, не должен
|
||
менять позиции; иначе он будет ждать Кида прямо на входе, и тот не успеет
|
||
достать меч» — совпадает с тем, что гарантирует оригинал.
|
||
|
||
**Как это устроено в оригинале.** Позиция стража живёт в ТРЁХ полях данных
|
||
уровня, и колонки среди них нет:
|
||
|
||
- `pos_guards` (seg003:0913), один раз сразу после `load_level`:
|
||
`guards_x[room] = x_bump[(tile % 10) + FIRST_ONSCREEN_COLUMN] + TILE_SIZEX`.
|
||
То, что лежит в `guards_x` В ФАЙЛЕ, оригинал ВЫБРАСЫВАЕТ (в комнате 24
|
||
уровня 8 там 255);
|
||
- `leave_guard` (seg002:02F5) при уходе Кида:
|
||
`guards_tile = get_tilepos(0, row)` — колонка обнуляется НАМЕРЕННО, тайл
|
||
несёт только РЯД, — и `guards_x = Guard.x`, то есть настоящая позиция;
|
||
- `enter_guard` (seg002:0112) при возврате: `curr_row = tile / 10`,
|
||
`y = y_land[row + 1]`, **`x = guards_x`**, `curr_col = f(x)`.
|
||
|
||
Носитель колонки — `guards_x`, и только она.
|
||
|
||
**Что было у нас.** `pop_guard_leave` тайл обнуляла верно, а
|
||
`pop_guard_enter` считала X из `tile % 10` — из колонки, которой там уже нет.
|
||
Запомненную X брали ТОЛЬКО у трупа. Значит после ПЕРВОГО же выхода из
|
||
комнаты живой страж возвращался в колонку 0.
|
||
|
||
Численно: тайл стража комнаты 24 = 6 (ряд 0, колонка 6), ряд 0 =
|
||
`13 00 00 00 0F 01 01 01 01 01`, то есть **колонки 1..3 ПУСТЫЕ**, а в
|
||
колонке 0 факел. Телепорт давал `x = x_bump[5] + 14 = 72`, а 72 по таблице
|
||
деления — это колонка **1**: страж оказывался над дырой, падал с ряда 0 на
|
||
ряд 2 (щебень в `(2,1)`) — два ряда, смерть. Оба сценария пользователя
|
||
ложатся сюда ровно, включая «упал на 2,1».
|
||
|
||
**Фикс.** Два места, оба — приведение к оригиналу:
|
||
|
||
- `pop_gstate_init` — порт `pos_guards`: при загрузке уровня `GS_X`
|
||
считается из колонки тайла, а не читается из файла;
|
||
- `pop_guard_enter` — `Guard.x` берётся из запомненной ВСЕГДА, не только у
|
||
трупа; колонка, как и раньше, считается из X.
|
||
|
||
**Проверка в MAME** (уровень 8, комната 24, три входа через `enter_room`):
|
||
|
||
| | room | x | y | col | row | alive |
|
||
|---|---|---|---|---|---|---|
|
||
| первый вход | 24 | 156 | 55 | 7 | 0 | жив |
|
||
| возврат | 24 | 156 | 55 | 7 | 0 | жив |
|
||
| второй возврат | 24 | 156 | 55 | 7 | 0 | жив |
|
||
|
||
Позиция не меняется, страж стоит на полу. Host-тесты — 5106 проверок без
|
||
расхождений. Резидент без изменений, банк 8 −15 Б.
|
||
|
||
**Что осталось невыясненным.** Наблюдение «страж был в `(0,-1)`» отдельно
|
||
не воспроизведено: телепорт даёт колонку 0/1, а −1 требует `x < 65`.
|
||
Вероятнее всего это кадр уже НАЧАВШЕГОСЯ падения (при падении X сносит), но
|
||
доказательства нет. Если `(0,-1)` появится на исправленном билде — это
|
||
отдельный баг.
|
||
|
||
---
|
||
|
||
<a id="seam-fight-flicker"></a>
|
||
## SEAM-FIGHT-FLICKER. Бой у шва: комната перерисовывалась то одна, то другая
|
||
|
||
**Наблюдение (пользователь, 2026-08-18).** Уровень 8, шов комнат 24/18: во
|
||
время схватки со стражем экран постоянно переключался между комнатами —
|
||
«драться очень неудобно». Отдельно пользователь заметил, что уходит Кид,
|
||
даже когда просто держит «вверх» (защита), и предположил верную причину:
|
||
стойка защиты не одна, Кид периодически проходит через обычную.
|
||
|
||
**Корень.** `leave_room` (seg002:0490) запрещает смену комнаты не только на
|
||
развороте, подъёме-с-зацепа (135..149) и вставании из приседа (110..119), но
|
||
и на ВСЕЙ боевой анимации — кадры **150..162** и **166..168**. У нас были
|
||
только первые три условия.
|
||
|
||
Разрешены при этом 163..165, 169 (`begin_block`) и 170/171 (`stand with
|
||
sword`) — то есть уйти можно, но только на «легальном» кадре. Цикл защиты
|
||
(seqtbl: `readyblock` = 169 -> `blocking` = 150, петля) как раз проходит
|
||
через 169, а между атаками Кид возвращается в 158/170 — отсюда и ощущение
|
||
«стоять в защите нельзя».
|
||
|
||
Побочный эффект известен и в оригинале эксплуатируется — **Trick 35,
|
||
«retreat without leaving the room»**: определённым ритмом «назад» кадр 170 не
|
||
наступает никогда, и Кид пятится из комнаты, не переключая её. В SDLPoP это
|
||
чинит `FIX_RETREAT_WITHOUT_LEAVING_ROOM`, по умолчанию ВЫКЛЮЧЕННЫЙ, — мы
|
||
портируем оригинал.
|
||
|
||
**Сторона стража проверена отдельно, расхождений нет.** `play_guard_frame`
|
||
(seg000:0F48) ухода из комнаты не содержит вовсе (ни `leave_room`, ни
|
||
`check_leave_below`), физика загейчена окном `Char.x ∈ [44, 211)`. Комнату
|
||
страж меняет только через `follow_guard` (у нас `pop_guard_follow`) и
|
||
`check_guard_fallout` (у нас `pop_guard_fallout`) — оба портированы.
|
||
|
||
**Проверка: покадровая сверка с SDLPoP.** Пользователь добавил в SDLPoP
|
||
вывод в конце `add_kid_to_objtable` (seg008:1679); мы навесили такой же
|
||
(`pop_dbg_kidobj`, см. ниже). Окно перехода совпало КАДР В КАДР:
|
||
|
||
| | SDLPoP | наша |
|
||
|---|---|---|
|
||
| | `tilepos=10 frame=161 col=-1` | `tilepos=10 frame=161 col=-1` |
|
||
| | `tilepos=10 frame=160 col=-1` | `tilepos=10 frame=160 col=-1` |
|
||
| | `tilepos=10 frame=157 col=-2` | `tilepos=10 frame=157 col=-2` |
|
||
| | `tilepos=10 frame=158 col=-2` | `tilepos=10 frame=158 col=-2` |
|
||
| **смена** | `tilepos=18 frame=169 col=9` | `tilepos=18 frame=169 col=9` |
|
||
|
||
Прогон уровня 1 комнаты 3 (1779 кадров): за весь бой комната сменилась
|
||
ОДИН раз; 34 боевых кадра прошли за краем комнаты (`col<0`), самый длинный
|
||
непрерывный прогон — 17 кадров, и комната при этом не переключалась — ровно
|
||
как в трассе SDLPoP.
|
||
|
||
Прогон уровня 8, шов 24/18 (1433 кадра, 360 кадров с мечом): внутри боя
|
||
**4 смены комнаты на 387 кадров** — то есть примерно раз в 6 секунд, и это
|
||
настоящее выдавливание, а не мерцание. Все четыре пришлись на РАЗРЕШЁННЫЕ
|
||
кадры (170, 164, 170, 165); ни одной смены на запрещённом кадре во всей
|
||
трассе нет. Кид подолгу держался на `col=-1` и `col=10`, комната не
|
||
переключалась.
|
||
|
||
**Инструмент (оставлен в дереве, выключен).** `pop_dbg_kidobj`
|
||
(`roomtest_cold.c`) + резидентный буфер `pop_dbg_obj` (`pop_state.c`),
|
||
включается `#define DBG_KIDOBJ 1` в `roomtest.c`. Считает то же, что
|
||
`set_char_collision` + `set_objtile_at_char`. Вывод забирает брейкпоинт
|
||
MAME на резидентном `pop_dbg_trap`; сборщик — `collect_kidobj.py`.
|
||
|
||
Грабли, стоившие времени:
|
||
|
||
- `#define DBG_KIDOBJ` сперва положили внутрь `#ifndef PROF_BORDER`, а
|
||
`PROF_BORDER` задаётся ключом сборки — блок не выполнялся, трасса молча
|
||
не включалась. Нужен свой `#ifndef`.
|
||
- Знаковые значения через выражения отладчика MAME получить не вышло: `b@`
|
||
связывается слабее `+`, и `b@ADDR + 128` читает ДРУГОЙ адрес; скобки не
|
||
помогают. Зонд печатает сырые байты, перевод — в сборщике.
|
||
- Кольцо `clog` не потребляющее, одинаковые строки от повторов не отличить —
|
||
в строку добавлен `totalcycles`, по нему и склеиваются чанки.
|
||
- Замерено: один зонд с коротким `printf` — **98 % полной скорости**
|
||
эмуляции, играть можно (в отличие от четырёх зондов профилировщика).
|
||
|
||
**Осталось невыясненным.** Поле `cols` (`char_col_left..char_col_right`) у
|
||
нас систематически на единицу меньше, чем в SDLPoP, и дважды дало `0..-1`,
|
||
чего в оригинале не бывает. Все поля, которые считает ДВИЖОК (`tilepos`,
|
||
`frame`, `act`, `col`, точка перехода), совпали полностью — расхождение
|
||
только в величине, которую выводит сам отладочный помощник из ширины кадра
|
||
`pop_cd[].fpw`. Подозрение на паддинг кадров в атласе
|
||
(`png_strip_padding_tradeoff`) либо на мой пересчёт переднего края мимо
|
||
боевого `char_x_forward_edge`. Проверять отдельно.
|
||
|
||
---
|
||
|
||
<a id="mob-neighbour-room"></a>
|
||
## MOB-NEIGHBOUR-ROOM. Плиты нижнего ряда пропадали без кадров падения
|
||
|
||
**Наблюдение (пользователь, 2026-08-18).** Уровень 11, комната 14: Кид бежит
|
||
по ряду проваливающихся плит, они трясутся и «падают сразу полностью
|
||
пропадая, а должен быть промежуточный кадр». Догадка пользователя — «для
|
||
плит на 2-ом ряду мы рисуем только тряску» — оказалась верной по симптому и
|
||
почти верной по причине.
|
||
|
||
**Корень.** Порт `draw_mob` (seg007:13E5) был неполным. У оригинала кусок
|
||
рисуется НЕ ТОЛЬКО в своей комнате:
|
||
|
||
```c
|
||
if (curmob.room == drawn_room) { if (curmob.y >= 210) return; }
|
||
else if (curmob.room == room_B) { if (ABS((sbyte)ypos) >= 18) return; curmob.y += 192; }
|
||
else if (curmob.room == room_A) { if (curmob.y < 174) return; ypos = curmob.y - 189; }
|
||
else return;
|
||
```
|
||
|
||
У нас на этом месте стояло `if (!mt_here) return;` — «чужая комната: считаем,
|
||
но не рисуем».
|
||
|
||
Почему страдал ИМЕННО ряд 2: кусок спавнится на `y_loose_land[row+1]`, для
|
||
ряда 2 это **191**, а граница ряда `y_something[3]` — **188**. То есть на
|
||
ПЕРВОМ же `move_loose` он уходит в комнату снизу и у нас переставал
|
||
рисоваться мгновенно. Ряды 0 и 1 летят внутри комнаты, поэтому там всё было
|
||
видно. Промежуточных кадров ровно три — пока `|y| < 18`.
|
||
|
||
**Фикс.** У куска появилась отдельная ЭКРАННАЯ координата `draw_y` (своя
|
||
комната / +192 из комнаты снизу / −189 из комнаты сверху, `MOB_Y_NONE` =
|
||
не рисуется). По ней идут отрисовка, `prev_y`/heal, порядок относительно
|
||
Кида, пометки соседа и оверлей; отбор в проходе отрисовки — по `draw_y`, а не
|
||
по комнате. Ссылки вверх/вниз кэшируются (иначе `pop_room_link` мапил бы W0
|
||
каждый кадр).
|
||
|
||
Вторым заходом пришлось поправить `mob_overlay_neighbour`: ряд там уже считался
|
||
по `draw_y`, а габарит спрайта и вызов `pop_mob_overlay_tile` остались по `y` —
|
||
для куска из соседней комнаты это расходится на 192, и пересечение с габаритом
|
||
тайла считалось заведомо неверно (порядок переднего слоя).
|
||
|
||
**Цена — и урок про то, ЧТО именно дорого.** Замер 13/23:
|
||
|
||
| секция | до | первый вариант | после гейта |
|
||
|---|---:|---:|---:|
|
||
| работа | 880 272 | 898 578 | **888 984** |
|
||
| зелёная | 419 562 | 445 248 | **427 242** |
|
||
| циан | 380 202 | 379 818 | 378 864 |
|
||
|
||
Первый вариант метил «фон трогали» по ПРОШЛОЙ нарисованной позиции
|
||
безусловно. У куска, видимого из соседней комнаты, она отличается от `y` на
|
||
192 — объединение прямоугольников растягивалось почти на весь экран и тянуло
|
||
за собой лишние перерисовки. Гейт `m->room != pop_t_room` вернул 18 000 из
|
||
26 000.
|
||
|
||
Я сперва списал рост на клипованный блит (кусок у кромки по определению
|
||
клипуется, а клипованный путь дороже: 14 088 против 10 422) — замер это
|
||
опроверг: дело было в пометке. Остаток +7 680 в зелёной (+1,8 %) —
|
||
собственно отрисовка трёх кадров на плиту; зелёная остаётся ниже растрового
|
||
кадра 430 000.
|
||
|
||
Host-тесты 5106 проверок без расхождений (`pop_room.c` они не линкуют —
|
||
проверка визуальная, в MAME: уровень 11 комната 14, отделившаяся плита видна
|
||
три кадра).
|
||
|
||
### Продолжение: в мид-оверлее не было переднего торца плиты
|
||
|
||
Пользователь тут же нашёл следующий слой (2026-08-18, та же комната): правый
|
||
конец падающего куска рисовался ПОВЕРХ соседней плиты `(2,6)`, хотя торец той
|
||
плиты при этом лежал поверх куска — то есть порядок был противоречивым в
|
||
разных частях перекрытия.
|
||
|
||
Сначала я проверил, не нужен ли клип объекта: `add_mob_to_objtable`
|
||
(seg007:1161) ставит куску `clip.right = 40`. Оказалось — **не нужен и в
|
||
оригинале не работает**: клип применяется только если
|
||
`chtab_flip_clip[chtab_id]`, а таблица (`data.h:196`,
|
||
`{1,0,1,1,1,1,0,0,0,0}`) для `chtab_6_environment` даёт **0**. Поле
|
||
выставляется и молча игнорируется, так что «клипа нет» у нас — верно. Пункт
|
||
MOB-CLIP-RIGHT можно закрывать как несуществующий.
|
||
|
||
Настоящая причина — состав мид-оверлея. У оригинала это `draw_tile2()`:
|
||
|
||
```c
|
||
draw_tile_right(); draw_tile_anim_right(); draw_tile_base();
|
||
draw_tile_anim(); draw_tile_bottom(0); draw_loose(0);
|
||
```
|
||
|
||
а у нашего `overlay_mid_tile` последнего вызова не было. У loose в
|
||
`pop_tile_table` `bottom_id = 0`, поэтому передний торец плиты рисует ТОЛЬКО
|
||
`draw_loose` — и в оверлее его не рисовал никто. Добавлено; кадр берётся по
|
||
живому modif, чтобы дрожащая плита осталась дрожащей.
|
||
|
||
**Осталось невыясненным:** в `overlay_mid_tile` по-прежнему нет полного
|
||
`draw_tile_right` (есть только случай пола, спрайт 42) и нет
|
||
`draw_tile_anim_right` — то есть анимированной правой грани СОСЕДА (пики,
|
||
ворота, loose). Симптома на это пока не видели; если всплывёт «правая грань
|
||
соседа под персонажем/куском» — смотреть сюда.
|
||
|
||
### Вариант B: порт корзины 30 (2026-08-18)
|
||
|
||
Реализовано то, что разобрано в коммите `272cf8f`. Кусок, у которого ряд
|
||
объекта вне 0..2 (`y_to_row_mod4` даёт −1 и «выше потолка», и «ниже пола»),
|
||
считается объектом ТАЙЛА 30 — а такие оригинал рисует ПЕРВЫМИ, до всего
|
||
обхода тайлов. Два следствия:
|
||
|
||
- `defer = 0` — кусок идёт под всем, включая Кида (раньше сравнение рядов
|
||
давало обратное: «−1 обходится последним» читалось как «поверх»);
|
||
- оверлею отдаётся ориентир **3** («раньше любого ряда обхода 2,1,0»)
|
||
вместо сырого −1, и в набор перекрываемых тайлов добавляется **своя
|
||
клетка** — у куска в обычном ряду её перерисовывать нельзя, там объект
|
||
вливается в midtable уже после частей своего тайла.
|
||
|
||
**Замер 13/23 (3032 кадра, зонды фаз + детализация зелёной):**
|
||
|
||
| секция | тег `mob-order-B-start` | после B | дельта |
|
||
|---|---:|---:|---:|
|
||
| работа | 888 984 | **913 848** | +24 864 |
|
||
| синяя | 159 804 | 159 810 | +6 |
|
||
| зелёная | 427 242 | **440 418** | +13 176 |
|
||
| циан | 378 864 | 393 000 | +14 136 |
|
||
|
||
Период кадра не изменился: 4 растра в 23 кадрах, 5 в двух — как и до правки.
|
||
|
||
**Зелёная вышла за растровый кадр** (440 418 против 430 000) при цели
|
||
400 000. Детализация показывает, где именно:
|
||
|
||
| участок зелёной | максимум |
|
||
|---|---:|
|
||
| `pop_loose_tick` целиком | 185 826 |
|
||
| — из них `pop_loose_mob_tick` | **168 180** |
|
||
| — циклы по тайлам | 23 736 |
|
||
| — `check_loose_fall_on_kid` | 5 868 |
|
||
| тробы + `pop_redraw_needed` | 337 800 |
|
||
| шов и прочее | 11 964 |
|
||
|
||
(максимумы взяты по разным кадрам, поэтому не складываются в общий).
|
||
|
||
Основной кандидат на возврат этих тактов — задача
|
||
[HEAL-WIDTH](TASKS_OPEN.md#heal-width): heal'ы кусков и плит берут ширину по
|
||
клеткам, а фактический след — 58/57.
|