diff --git a/applications/PoP/roomtest/bug_closed.md b/applications/PoP/roomtest/bug_closed.md index 822258e..836ce51 100644 --- a/applications/PoP/roomtest/bug_closed.md +++ b/applications/PoP/roomtest/bug_closed.md @@ -11,6 +11,155 @@ --- +## 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`: + +```c +LABEL(softland) // seq_17_soft_land + act(actions_5_bumped), SEQ_KNOCK_DOWN, dx(1), frame_107_fall_land_1, + dx(2), frame_108_fall_land_2, + act(actions_1_run_jump), LABEL(softland_crouch) frame_109_crouch, + jmp(softland_crouch), // ВЕЧНЫЙ ЦИКЛ на кадре 109 +``` + +То есть `seq_17_soft_land` **не заканчивается сам** — он крутится на кадре +109, и вывести из него может ТОЛЬКО `control_crouched()`. + +**Корень.** `control()` (seg005:252) до `control_crouched` при вынутом +мече не доходит — раньше срабатывает ветка + +```c +} else if (Char.sword == sword_2_drawn) { + control_with_sword(); +``` + +а `control_with_sword` (seg005:964) в мирной обстановке +(`can_guard_see_kid < 2`, под ногами не loose) умеет ровно одно: если кадр +== 171 (стойка с мечом) — убрать меч. Кадр 109 он не знает. **Замкнутый +круг: последовательность ждёт control_crouched, а диспетчер туда не +пускает.** + +Оригинал такой позы просто не допускает — `land()` (seg005:176): + +```c +if (Char.charid >= charid_2_guard || Char.sword == sword_2_drawn) { + Char.sword = sword_2_drawn; + seq_id = seq_63_guard_active_after_fall; // боевая стойка +} else { + seq_id = seq_17_soft_land; // присед +} +``` + +**У нас этой ветки не было** — `pop_map.c land()` ставил +`SEQ_17_SOFT_LAND` безусловно. На уровне 1 не всплывало, потому что там +меч подбирается поздно и падать с ним особо негде; на уровне 2 меч у Кида +**с первого кадра** (`have_sword = level >= 2`), а в комнате 4 страж стоит +в одном ряду с двумя loose-плитами — сценарий собирается сам. + +**Фикс.** Порт недостающей ветки: при `Kid.sword == SWORD_2_DRAWN` +падение на один этаж даёт `seq_63` (боевая стойка), а не присед. Ветка +`charid >= charid_2_guard` нам не нужна — наш `land()` работает только с +Кидом. + +**Почему не задело падение на ДВА этажа** (`seq_20_medium_land`): там +`jmp` в конце нет — 29 кадров приседа и автоматический подъём +(`seqtbl.c:934`), поэтому оно развязывается само. Вечный цикл только у +`softland`. + +**Урок на будущее.** Симптом «персонаж не реагирует на управление» стоит +диагностировать не по кадру, а по `curr_seq`: адрес прямо показывает, в +какой метке `seqtbl` он завис, и дальше видно, кто обязан был его оттуда +вывести. + +--- + ## Проверено в MAME 2026-08-01 (ревизия L1-TRIAGE) Три бага стояли как **Critical** с 2026-07-21 и по исходникам выглядели diff --git a/applications/PoP/roomtest/guards.c b/applications/PoP/roomtest/guards.c index d3dc601..daa5d6a 100644 --- a/applications/PoP/roomtest/guards.c +++ b/applications/PoP/roomtest/guards.c @@ -491,6 +491,9 @@ void pop_guard_tick(void) __banked { if (Guard.charid == 0) return; /* стража в комнате нет */ pop_loadshad_and_opp(); /* Char = Guard, Opp = Kid */ + /* Симметрично Киду (play_guard_frame, seg000:1248): cur_frame один на + * всех, и до этой строки в нём лежит кадр КИДА — он тикал раньше. */ + pop_load_fram_det_col(); play_guard(); /* Анимация: та же seqtbl, что у Кида, но своя таблица кадров — * выбирается по Char.charid внутри load_frame (seg006:0293). */ diff --git a/applications/PoP/roomtest/pop_ctrl.c b/applications/PoP/roomtest/pop_ctrl.c index ebe3ff4..0c03e42 100644 --- a/applications/PoP/roomtest/pop_ctrl.c +++ b/applications/PoP/roomtest/pop_ctrl.c @@ -707,9 +707,16 @@ void pop_ctrl_tick(void) read_user_control(); /* Окно Char вокруг диспетчера: control() работает с АКТИВНЫМ * персонажем (как в оригинале), поэтому ввод игрока применяется к - * Киду только внутри loadkid/savekid. Кадр не перезагружаем — - * play_seq в kid_tick сделает это следующим шагом. */ + * Киду только внутри loadkid/savekid. + * + * load_fram_det_col ОБЯЗАТЕЛЕН здесь — порт play_kid_frame + * (seg000:1209), где он стоит ровно между loadkid_and_opp() и + * play_kid(). cur_frame один на всех персонажей, и в кадре последним + * его перезаписывает страж; без перезагрузки диспетчер считает + * геометрию Кида (dx_weight -> determine_col -> distance_to_edge_weight + * -> get_edge_distance) по кадру СТРАЖА. Замерено: BUG-CTRL-FRAME-1. */ pop_loadkid(); + pop_load_fram_det_col(); user_control(); /* -> pop_control(): может сменить Char.curr_seq */ pop_savekid_state(); save_ctrl_1(); diff --git a/applications/PoP/roomtest/pop_kid.c b/applications/PoP/roomtest/pop_kid.c index 81aa19a..3694d59 100644 --- a/applications/PoP/roomtest/pop_kid.c +++ b/applications/PoP/roomtest/pop_kid.c @@ -450,6 +450,40 @@ void play_seq(void) } } +/* load_fram_det_col (seg006:0144) = load_frame() + determine_col(). + * + * Зачем это отдельной публичной точкой. cur_frame — ОДИН глобал на всех + * персонажей (так и в оригинале), и его владелец — тот, кто последним + * прошёл load_frame. В кадре последним тикает страж, поэтому к моменту + * управления Кидом в cur_frame лежит кадр СТРАЖА. А через cur_frame + * считается вся геометрия: 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() — ДО control(). У нас этого не было, и Кид считал + * свой прыжок по кадру стража (BUG-CTRL-FRAME-1): при distance 2 вместо 10 + * check_jump_up уходил в «пустой прыжок вверх» вместо «шаг назад и + * зацеп». Результат менялся от кадра к кадру — вместе с анимацией стража. + * + * determine_col у нас существует только для Кида: pop_map работает с Kid + * напрямую, а не с абстрактным Char. Поэтому вторую половину связки делаем + * только на его ветке — стражу пока хватает перезагрузки самого кадра (его + * колонку ведёт pop_guard_enter/leave). Char.curr_col синхронизируем + * руками: окно Char — копия Kid. */ +void pop_load_fram_det_col(void) +{ + if (!kdat_ok) return; + gfx_w0_map(kdat_page); + load_frame(); + gfx_w0_unmap(); + if (Char.charid == CHARID_0_KID) { + pop_determine_col(); + Char.curr_col = Kid.curr_col; + } +} + /* Прокрутить последовательность КОНКРЕТНОГО персонажа (порт связки * loadkid/play_seq/savekid, seg006). Звать вместо голого play_seq: тот * работает с Char и сам по себе не знает, чей это персонаж. */ diff --git a/applications/PoP/roomtest/pop_kid.h b/applications/PoP/roomtest/pop_kid.h index 5d97eef..3a8e44a 100644 --- a/applications/PoP/roomtest/pop_kid.h +++ b/applications/PoP/roomtest/pop_kid.h @@ -97,6 +97,13 @@ void pop_char_set_seq(uint8_t seq_id); /* seq активного персон void pop_kid_play(void); void pop_guard_play(void); +/* load_fram_det_col (seg006:0144) — перезагрузить cur_frame под АКТИВНОГО + * персонажа (Char) и пересчитать его колонку. Звать сразу после pop_loadkid/ + * pop_loadshad и ДО диспетчера control(), как это делают play_kid_frame / + * play_guard_frame (seg000:1209/1246): cur_frame один на всех, и без этого + * геометрия одного персонажа считается по кадру другого. */ +void pop_load_fram_det_col(void); + /* Поля текущего кадра kid_frames[Kid.frame] (pop_map, без дублирования * таблицы во втором TU). */ int8_t kid_cur_dx(void); diff --git a/applications/PoP/roomtest/pop_map.c b/applications/PoP/roomtest/pop_map.c index 9e88fc0..b5b361e 100644 --- a/applications/PoP/roomtest/pop_map.c +++ b/applications/PoP/roomtest/pop_map.c @@ -31,6 +31,7 @@ #include "pop_state.h" #include "pop_trob.h" /* room_modif + trob-список (пики/ворота) */ #include "pop_level.h" /* pop_level_start_room — вход в дверь уровня */ +#include "pop_guard.h" /* SWORD_2_DRAWN — ветка приземления с мечом */ /* ---- Тайлы / действия / seq id (подмножество PoP) ------------------ */ #define TILE_EMPTY 0 @@ -80,6 +81,7 @@ #define SEQ_73_CLIMB_UP_TO_GATE 73 /* подъём к закрытой калитке */ #define SEQ_51_SPIKED 51 /* напороться на пики (смерть) */ #define SEQ_22_CRUSHED 22 /* разбиться при падении (смерть) */ +#define SEQ_63_ACTIVE_AFTER_FALL 63 /* стойка С МЕЧОМ после приземления */ #define TILE_FLOOR 1 #define FRAME_109_CROUCH 109 @@ -365,6 +367,11 @@ static void determine_col(void) Kid.curr_col = get_tile_div_mod_m7(dx_weight()); } +/* determine_col наружу — для pop_load_fram_det_col (pop_kid): порт связки + * load_frame + determine_col (seg006:0144), которую оригинал зовёт перед + * control(). См. разбор там. */ +void pop_determine_col(void) __banked { determine_col(); } + /* расстояние до края тайла (для in_wall). */ static int distance_to_edge(int xpos) { @@ -478,7 +485,20 @@ static void land(void) * приложение рисует «брызги» И даёт КРАСНУЮ вспышку фона (flash_if_hurt, * seg003:0AFC). */ if (Kid.fall_y < 22) { - seq = SEQ_17_SOFT_LAND; + /* seg005:176 — ВЕТКА С МЕЧОМ, и она не косметика. seq_17_soft_land + * заканчивается ВЕЧНЫМ циклом на кадре 109 (`jmp softland_crouch` в + * seqtbl), из которого выводит только control_crouched(). А + * control() при `Char.sword == sword_2_drawn` до него не доходит: + * управление уходит в control_with_sword, который знает лишь кадр + * 171 (стойка с мечом). То есть Кид, упавший на этаж С ВЫНУТЫМ + * МЕЧОМ, в этот присед садится НАВСЕГДА — ровно то, что поймали на + * приёмке уровня 2 (BUG-LAND-SWORD-1). Оригинал такой позы не + * допускает: с мечом приземление даёт боевую стойку. */ + if (Kid.sword == SWORD_2_DRAWN) { + seq = SEQ_63_ACTIVE_AFTER_FALL; + } else { + seq = SEQ_17_SOFT_LAND; + } } else { uint8_t deadly = (uint8_t)(Kid.fall_y >= 33); /* СНЯТЬ до обнуления */ pop_kid_hurt = 1; diff --git a/applications/PoP/roomtest/pop_map.h b/applications/PoP/roomtest/pop_map.h index 5831fd0..606cd5b 100644 --- a/applications/PoP/roomtest/pop_map.h +++ b/applications/PoP/roomtest/pop_map.h @@ -35,6 +35,10 @@ void pop_map_set_room(uint8_t room) __banked; * (guards.c): он идёт по ряду между Кидом и стражем. */ uint8_t pop_tile_at(int8_t col, int8_t row) __banked; +/* determine_col (seg006:014D): Kid.curr_col = m7(dx_weight()). Наружу — для + * pop_load_fram_det_col (pop_kid.c), порт load_fram_det_col. */ +void pop_determine_col(void) __banked; + /* HP/смерть. pop_kid_dead=1 когда Kid убит (пики); hitp_curr — текущее HP. * pop_kid_hp_reset() ставит старт HP и снимает смерть (звать в kid_init/ * респавн). */