Пользователь проверил в SDLPoP v1.24: оригинал падает с той же позиции, по той же траектории и с тем же видом кадра падения (голова/руки поверх кромки пола). Кадры совпадают один в один. Механика записана с числами: у стоек с мечом weight_x = 13-14 против 3 у обычной стойки, поэтому при взгляде влево точка веса уезжает на 13 px вправо от Char.x, и кромку персонаж переступает раньше, чем выглядит. Замер: x=151 -> dx_weight=164 -> колонка 7 (дыра) вместо 6 (пол). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
70 KiB
roomtest — архив закрытых багов
Сюда переезжает всё, что закрыто: подтверждённые фиксы, снятые
диагнозы, осознанные решения «не делать». Открытые баги — в
bug_list.md, текущие задачи — в TASKS.md.
Файл существует не ради истории как таковой: половина записей ниже — это
разбор КОРНЯ (odd-pixel арифметика char_x, подстановка тайла нажатой
кнопки, баг кодогенератора SDCC), и он экономит часы, когда похожий симптом
всплывёт снова. Прежде чем заводить новый баг — грепни здесь по симптому.
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).
НЕ БАГИ (кривая картинка, но совпадает с оригиналом — НЕ чинить)
-
Отход с ВЫНУТЫМ МЕЧОМ роняет Кида в яму «раньше, чем кажется» (комната 4 уровня 2, кромка ряда 1 у дыры от упавшей loose-плиты). Сверено с SDLPoP v1.24 пользователем (2026-08-04): оригинал падает с той же позиции, по той же траектории и с тем же видом кадра падения (голова/руки поверх кромки пола). Наш кадр и кадр SDLPoP совпадают один в один.
Механика, чтобы не разбирать заново. У стоек с мечом ОГРОМНАЯ точка веса:
кадр 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— один нажим = один шаг). Чинить нечего.⚠ Не путать с тем, что БЫЛО нашим багом рядом: расширение футпринта перерисовки под клинок (
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-4 (+ BUG-KBD-3). Shift: сначала залипал, потом стал отваливаться — ЗАКРЫТ
Симптом (вторая редакция). Залипание ушло, но появилось обратное: при зажатом 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). Пока
поводов для этого нет — заводить обратно только по живому наблюдению.