BUG-FALL-SWORD-1 — start_fall (seg006:1044) был портирован не целиком: не хватало трёх веток выбора последовательности и уборки меча в ножны. Из-за seq_7 с set_fall(1,15) (дрейф 1 px/кадр) Кид с мечом уезжал примерно на тайл вбок; в оригинале это seq_81_fightfall — падение строго вниз. Сверено по логу SDLPoP: кадры 102..105 дают x = 155,157,159,160. BUG-RJUMP-1 — run_jump (seg005:0AA8) не выравнивал Кида по кромке пола перед толчком, а был заглушкой «полировка K3». Суммарный dx seq_4 — 62 px при тайле 14, то есть провал ровно в три тайла берётся ТОЛЬКО с кромки: без выравнивания Кид не перепрыгивал его никогда. Порт — pop_run_jump_align() в pop_map (беззнаковое сравнение оригинала = «сдвиг не попал в [-8,-1]»). На харнессе: было — толчок с x=165, кадр 44 в колонке 3 (провал); стало — 5 кадров добега, толчок с x=149, кадр 44 даёт x=87 col=1 row=1. Остальные 8 сценариев не изменились. BUG-GATEMOD-1 — load_alter_mod (seg008:198E) был портирован только для зелий, поэтому ворота с bg=1 («Open» по спецификации DAT, табл. 8) стартовали закрытыми. Добавлены ветки gate (1 -> 188) и loose. Ветка wall намеренно НЕ портируется: связи стен наш pop_bg считает по типам соседей в момент отрисовки. На уровнях 1-3 таких ворот всего двое (ур. 1 комн. 5 (0,9); ур. 2 комн. 13 (1,5)) — у остальных bg=2, а 2 и 0 ведут себя одинаково. Убран временный трассировщик pop_dbg_trace/pop_dbg_draw; pop_dbg_trap() оставлен как многоразовый инструмент. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
88 KiB
roomtest — архив закрытых багов
Сюда переезжает всё, что закрыто: подтверждённые фиксы, снятые
диагнозы, осознанные решения «не делать». Открытые баги — в
bug_list.md, текущие задачи — в TASKS.md.
Файл существует не ради истории как таковой: половина записей ниже — это
разбор КОРНЯ (odd-pixel арифметика char_x, подстановка тайла нажатой
кнопки, баг кодогенератора SDCC), и он экономит часы, когда похожий симптом
всплывёт снова. Прежде чем заводить новый баг — грепни здесь по симптому.
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). Пока
поводов для этого нет — заводить обратно только по живому наблюдению.