Скелет (L3-SKEL, ассеты + механика): - pop_pack_guard.py получил параметр набора (GUARD/SKEL): атлас скелета poc/res/skel/g0..g3.atl (28 кадров), палитра — из его res750.pal (на ур. 3 curr_guard_color = 0, оригинал палитру не подменяет); - pop_guard_load выбирает набор по tbl_guard_type и перезагружается ПРИ СМЕНЕ УРОВНЯ (load_lev_spr, seg000:1092) — без этого скелет рисовался атласом стража и был невидим; - load_frame: charid_4_skeleton идёт по таблице стража (seg006:529), тень — только в кадрах 150..189. Пока ветка была одна (charid_2_guard), скелет получал image из таблицы Кида (180 при 28 спрайтах) и не рисовался; - check_skel (seg002:1042), ветка charid_4 в enter_guard, возрождение в комнате 3 при падении (seg002:252), autocontrol_skeleton; - leveldoor_open (seg007:456) — новый флаг, сбрасывается стартом уровня. Цвета стражей (BUG-GUARD-COLOR-1, закрыт): - все 7 палитр res10.bin -> pop_guard_pal.h, заливка 16 слотов по guards_color комнаты перед отрисовкой (set_chtab_palette, seg003:257). Проверено в MAME: ур. 2 комн. 11 = цвет 1, комн. 7 = цвет 3, полоса HP меняется вместе со стражем. Грабля: gfx_pal_load отдаёт указатель в BIOS, а тот читает только #4000-#BFFF — таблицу из банка копируем в стек. Кэш соседних комнат (BUG-SWORD-GHOST-1, закрыт): - pop_map кэширует fg соседей слева/справа ЦЕЛИКОМ и резолвит col -10..19. Было -2..11, дальше мнимая стена: луч видимости упирался в неё (страж после follow_guard в col 12), Кид прятал меч посреди боя и не мог достать обратно. +48 байт W2. Окклюзия соперника: - pop_fore_over_char получил проход other_overlay_tile (порядок midtable, seg008:1B06) и расширение перебора объединённым прямоугольником «персонаж + клинок + брызги» — падающий скелет больше не рисуется поверх кладки и верхней грани пола; - клип полем 192 строк (reset_obj_clip, seg006:0507) для спрайта, клинка (общий pop_sword_draw) и брызг — спрайт не залезает на полосу HP; - ROOMNAV после смерти Кида делает честный pop_start_level: телепорт «оживлял» мёртвого мимо старта уровня, оставляя живого скелета рядом с вернувшейся кучей костей. Ассеты чомпера (под L3-CHOMP): весь набор кадров в атласе явным списком (101-105 низ, 111-113 верх, 106-110 фронт, 114-123 кровь mono-силуэтом) — render_room анимированные тайлы пропускает, и в атласе не было ни одного. Число EMM-страниц не изменилось. Тесты: tests-host все 5 наборов зелёные, t_char вырос до 65 проверок (резолв колонок за краем комнаты, возрождение скелета); в testkit добавлен гард «код наехал на данные» (DATA_LOC). Доски: TASKS.md разнесён на TASKS_OPEN/TASKS_CLOSED, закрытые баги с разбором корней — в bug_closed.md; заведены DRAW-CHAR (отрисовка одна на всех Char, как физика после GUARD-PHYS) и L3-COLOR (зелёная кладка уровня 3: level_var_palettes = ресурс 20, есть в MSDOS/PRINCE.DAT). В roomtest.c временно оставлен автостоп на падении соперника (отладка падений скелета) — помечен ВРЕМЕННО. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
137 KiB
roomtest — архив закрытых багов
Сюда переезжает всё, что закрыто: подтверждённые фиксы, снятые
диагнозы, осознанные решения «не делать». Открытые баги — в
bug_list.md, текущие задачи — в
TASKS_OPEN.md, закрытые задачи с протоколами — в
TASKS_CLOSED.md.
Файл существует не ради истории как таковой: половина записей ниже — это
разбор КОРНЯ (odd-pixel арифметика char_x, подстановка тайла нажатой
кнопки, баг кодогенератора SDCC), и он экономит часы, когда похожий симптом
всплывёт снова. Прежде чем заводить новый баг — грепни здесь по симптому.
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 рисуется тем же атласом — отдельного бага не было.
Фикс.
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.pop_guard_set_palette(color)(pop_gdraw.c) заливает 16 слотов в ОБЕ палитры дабл-буфера;color == 0— не трогать (так и оригинал).- Зовётся из
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.
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.
Итог 2026-08-05. Физика тут ни при чём — виновата клавиатура. Зажатый Shift снимался автоповтором зажатой стрелки, поэтому к кадрам 102…106 (окно зацепа) движок видел Shift отпущенным. Полный разбор и фикс — BUG-KBD-5; поведение Shift в MAME проверено замером карты
_kbdraw_down. Осталось подтвердить сам зацеп живой игрой; версии 2 и 3 ниже проверять только если он всё ещё не выйдет.
Симптом. Уровень 2, комната 9. Перепрыгнув на (1,1), Кид должен вернуться обратно: разбегаться негде, поэтому он встаёт на самый край плиты, прыгает с места и цепляется руками за (1,5), после чего подтягивается. У нас Кид с зажатым Shift всё равно срывается.
Что уже точно известно (и не надо перепроверять).
-
Физика прыжка у нас совпадает с оригиналом кадр в кадр. Сверено по логу 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. -
В оригинале зацеп срабатывает на кадре 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()из этой ветки тоже зовёт — то есть структура на месте, расходится что-то внутри. -
По харнессу зацеп у нас РАБОТАЕТ: окно стартовых
x = 91…95, и короткий шаг ставит Кида ровно туда (91 после первого нажатия, 95 после второго). Зафиксировано тестомt_grab.
Отсюда главный вопрос был: почему харнесс говорит «работает», а живая машина — «нет». Расхождение между ними и оказалось уликой; версии выдвигались по убыванию правдоподобия, и сработала первая:
- Shift не доезжает до движка — ПОДТВЕРЖДЕНО, это и была причина.
Харнесс подменяет клавиатуру и потому этот путь не проверяет вовсе, а у
нас есть история проблем ровно с «Shift + стрелки» (KBD-1, BUG-KBD-3/4).
Замер в MAME: при зажатом Shift и зажатой стрелке бит
LShв_kbdraw_downстоял в нуле. Разбор — 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:
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) —
// 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:
- Выход Кида из комнаты «усыпляет» стража — так и в оригинале.
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). - Дальше решает
autocontrol_guard_inactive(seg002:0876), и он Кида за спиной ИГНОРИРУЕТ. Кид вернулся справа, страж смотрит влево →char_opp_dist()отрицательна → веткаelse if (distance < 0) return;. Единственный выход из неё — флагis_guard_notice: «Кид нашумел». При нём страж оборачивается (move_4_down). Направление вcheck_can_guard_see_kidНЕ участвует вовсе, так что «не видит» — это не про луч видимости, а именно про этот флаг. - У нас
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 — комната 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) — плита падает, но на её месте остаётся чёрный бар. Два уточнения пользователя оказались диагностическими:
- «бар попал в фоновое изображение» — Кид прыгает поверх, уходит, бар остаётся → чернота лежит в ОЗУ-копии, и heal возвращает её каждый кадр;
- «когда возвращаемся в комнату после выхода — бара нет» → статическая отрисовка комнаты рисует всё правильно, виновата ЗАПЕЧКА.
Корень. 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) перед стартом выравнивает Кида по кромке.
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». Полировкой это не оказалось: без выравнивания три тайла непроходимы в принципе.
Две тонкости, которые легко потерять при порте.
- Беззнаковое сравнение.
(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. В порте она записана как мёртвая, с объяснением. - Отказ НЕ гасит
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
(комната с провалом в колонках 2–4). Было: прыжок со старта в кадре 34 при
x=165, кадр 44 приходится на колонку 3 — провал, падение. Стало: Кид
пробегает лишние 5 кадров, выравниватель ловит фазу, прыжок стартует при
x=149, кадр 44 даёт x=87, col=1, row=1 — приземление на пол и бег
дальше. Остальные 8 сценариев набора не изменились ни на байт: правка
трогает только ветку разбег-прыжка у кромки.
BUG-FALL-SWORD-1. Отход с мечом в провал: не та последовательность падения — ЗАКРЫТ 2026-08-04
Симптом (приёмка уровня 2, комната 4). Кид с вынутым мечом отступает от стража к дыре от упавших loose-плит. Наблюдение пользователя: он «проваливается раньше времени», летит с клинком в руке, и падает по другим X, чем в оригинале — «почти на целый тайл левее».
Разбор. Сверка seg006:1044 start_fall показала, что наш порт
пропустил ТРИ вещи из оригинала, и все три бьют именно по этому сценарию:
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. Тот фикс (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).
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:
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 при вынутом
мече не доходит — раньше срабатывает ветка
} 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):
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). Для
кадров двери оригинал ставит ДВА клипа, у нас был только первый:
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 для этого бага не потребовался. Остаётся заготовкой под
стражей/двух персонажей в кадре:
- Раздельные комнаты.
Char.room≠drawn_room. Уже естьkid_room(S1) + рендер-смещениеpop_kid_set_render_dx(∓140). - Коллизия по 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(). Тайлы — из РЕАЛЬНОЙ комнаты колонки, НЕ из снапшота. - Отрисовка по drawn_room, персонаж со сдвигом ±140.
- Отложенная смена 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 — дело не в моменте срыва (он верен), а в том, что мы играли
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=3dx_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, но у 9R=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 для навигации, потиковые
трассы, скриншоты.
Оговорка о полноте проверки. Каждый корень закрыт тем, что его СОБСТВЕННЫЙ
сценарий больше не воспроизводится; сквозного прохождения уровня и поиска
регрессий в соседней механике автоматика не делала. Чек-лист для ручной
перепроверки — в bug_list.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).
Два корня.
check_bumpedбрал ОДНУ колонку — ту, в которой оказался передний край. Для решётки в col9 окно этой колонки всего 4 px (201..204), беговой кадр его перескакивает — бампа нет, аcheck_leaveтут же уводит в соседнюю комнату. Оригинал (check_collisions, seg004:0004) считает флаги перекрытия для ВСЕХ колонок ряда и берёт колонку с переходом флага 0→1.- Наш 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), см.
bug_list.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 (вечер)
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) в декодере трамплина).
-
Зажать LShift → байт 2 карты =
04(LSh взведён), поток12 12 12 …(Shift автоповторяется сам, пока он последняя нажатая клавиша). -
Добавить ↑ → байт 46 =
20(↑ взведена), байт 2 =00— Shift снят, хотя физически зажат. -
Добавить ещё → байт 46 =
30(обе стрелки видны, 3-key rollover в порядке), Shift по-прежнему00. -
КОРОТКИЙ тап ↑ при зажатом Shift — Shift выживает. Это и сбивало с толку: бит сносился, но тут же восстанавливался, потому что после отпускания стрелки Shift снова становился «последней клавишей» и его автоповтор
12взводил бит обратно за ~30 мс. -
Сам поток при зажатом 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, то есть
расширенную половину), но если всплывёт — искать здесь.
BUG-KBD-4 (+ BUG-KBD-3). Shift: сначала залипал, потом стал отваливаться — ЗАКРЫТ (с поправкой, см. BUG-KBD-5)
Поправка 2026-08-05. Вывод «клавиатура обёртывает КАЖДЫЙ расширенный код, пока зажат Shift» оказался неверным, и построенный на нём обратный вывод («расширенный make без обёртки ⇒ Shift отпущен») убран — он ломал Shift вместе со стрелками. Разбор — BUG-KBD-5. Проверка «пять тапов стрелки подряд» ниже проходила не потому, что вывод был верен, а потому что между тапами бит восстанавливал автоповтор самого Shift. Актуальное поведение
kbd_raw_sync— вариант 1 (модификаторы не сбрасываем).
Симптом (вторая редакция). Залипание ушло, но появилось обратное: при зажатом Shift второй-третий-четвёртый тап стрелки отрабатывал уже не осторожным шагом, а бегом. Для PoP это ХУЖЕ залипания: игрок рассчитывает на короткий шаг, а Кид убегает в яму или на пики.
Почему обе прежние редакции были неправильны. Это был размен между двумя способами «починить» потерю байта при Rx-overrun SIO (FIFO 3 байта):
- исключать модификаторы из сброса — потерянный break Shift снять нечем, Shift залипает навсегда (BUG-KBD-3);
- сбрасывать всю карту, как 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 программ, роста нет.
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.
BUG-DRAWORDER-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. -
Своя же регрессия: Кид полез поверх передних столбов. Окно fore-клипа (
pop_fore_set_clip) одно на всех, и его ставит каждый, кто рисует персонажа. Пока страж рисовался строго ДО Кида, к моментуpop_fore_over_kidв окне оказывался Кид и всё сходилось само. Как только порядок стал переменным, при «страж поверх» окно оставалось СТРАЖЬИМ, и fore-проход Кида отсекался целиком. Фикс:kid_fore_clip_restore()— окно восстанавливается явно, а не «по счастливому порядку вызовов». -
Колонка трупа была нулевой.
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. -
Ветка
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: там так же. Подробности — в разделе «НЕ БАГИ» выше.
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.
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) — ЗАКРЫЛ ТРИ РЕВИЗИИ РАЗОМ
Результат прогона: крупных багов нет. Тем самым закрыты и переехали сюда
из bug_list.md три накопившихся хвоста — чек-листы ручной перепроверки
фиксов, таблица обхода 24 комнат уровня 1 и таблица сырых наблюдений прогона
2026-08-03. Всё, что с прогона 2026-08-07 осталось открытым, — три записи
уровня 2 в bug_list.md (BUG-GUARD-COLOR-1,
BUG-GUARD-SPLASH-1, BUG-CHEAT-FIGHT-1); ни одна из них не мешает играть.
Сырые формулировки пользователя лежат рядом: bugs_level1.md
и bugs_level2.md.
Ручная перепроверка фиксов (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). Не всплыло.
Обход всех 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 | недостижимы в игре | свойство данных уровня, разбор — «НЕ БАГИ» выше |
Сырые наблюдения (прогон 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, который остаётся открытым (ждёт сценария).
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_guardobj_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 —
отдельной работы не требует.