# Аудит расхождений с SDLPoP: Кид, стражи, seqtbl, отрисовка > Начат 2026-08-31. КОД НЕ МЕНЯЛСЯ — это только разбор. Задача: найти > места, где наш движок может вести себя иначе, чем оригинал, и оценить > вероятность того, что расхождение реально. ## Как читать Ранги вероятности того, что расхождение ЕСТЬ и проявляется в игре: | ранг | смысл | |---|---| | **А** | гарантированное различие: код объективно разный, эффект понятен | | **Б** | весьма вероятное: код разный, эффект вероятен, но не доказан | | **В** | средневероятное: код разный, но эффект может гаситься другим местом | | **Г** | маловероятное: различие есть в форме, эффект скорее отсутствует | | **Д** | почти невероятное: сходство подтверждено, остаётся крайний случай | Ссылки вида `seg005:114` — строка в `assets/orig/SDLPoP/src/`. Наши ссылки — `файл:строка` в `src/`. ## Метод и охват Сравниваются НАШИ реализации с оригиналом построчно по функциям. Первый проход (2026-08-31) охватил: * диспетчер `control()` (seg005:251) — целиком; * `land()` (seg005:114) и `start_fall()` (seg006:1099); * цепочку смерти: `control_kid` (seg006:1390), `play_kid` (seg006:1348), `take_hp` (seg006:986), `control()` ветка `alive >= 0`. НЕ охвачено первым проходом (список для следующих): * интерпретатор `play_seq` и полный набор опкодов seqtbl; * `frame_table` и модификаторы кадров; * бой целиком: `control_with_sword`, парирование, `strike`, `hurt_by_sword`; * ИИ стражей (`guard_ai`), особенности скелета, Тени, Джафара; * `add_kid_to_objtable`/`add_guard_to_objtable`, порядок слоёв, `clip_char`; * `check_bumped` — сверен ЧАСТИЧНО (находки 14, 15); `check_grab`, `in_wall`, `check_bumped_look_left` — нет; * `do_fall` целиком (проверен только вход). --- ## Симптом, с которого начат аудит **Наблюдение (пользователь, 2026-08-31):** идёт бой, за Кидом провал на этаж. Страж колет на последнем HP, Кид отшатывается назад и падает. Кид умирает — но на экране он этажом ниже В ПРИСЕДЕ, как после мягкого приземления. **Что говорит код SDLPoP.** Разбор цепочки: 1. `start_fall` (seg006:1099) ПЕРВЫМ ДЕЛОМ убирает меч (`Char.sword = sword_0_sheathed`) — для любого падения, независимо от здоровья. Значит к моменту приземления меч уже в ножнах. 2. `land` (seg005:114) при падении на один ряд выбирает `seq_63_guard_active_after_fall`, только если `charid >= guard` ИЛИ меч вынут; иначе — `seq_17_soft_land`, то есть ПРИСЕД. Из-за п.1 для Кида это всегда присед. 3. `land` НЕ смотрит ни на `alive`, ни на `hitp_curr` вовсе. 4. `control` (seg005:251) при мёртвом персонаже (`alive >= 0`) не диспетчеризует ничего; он переводит в `seq_71_dying` ТОЛЬКО из четырёх кадров стойки (15, 166, 158, 171). Присед в этот список не входит. **Вывод:** по букве оригинала мёртвый Кид, застигнутый смертью в полёте, тоже долетает, приземляется в присед и остаётся в нём — `control` его не трогает. То есть наблюдаемое, СКОРЕЕ ВСЕГО, воспроизводится и в SDLPoP. Гипотеза «hp стал нулевым, поэтому спрятали меч» кодом НЕ подтверждается: единственное место, где SDLPoP связывает `hitp_curr == 0` с чем-либо, — `control_kid` (seg006:1395), и там взводится только `Char.alive = 0`. **Как проверить окончательно:** прогнать сцену на живом SDLPoP (он собирается в `assets/orig/SDLPoP/`), где для этого уже есть наши врезки `POP_TRACE`. До проверки считаю симптом НЕ доказанным расхождением — ранг **В** (см. находку 5). --- ## Находки первого прохода ### 1. `land()`: лишний `determine_col()` — ранг **А** *Оригинал:* `land` (seg005:114) заканчивается тремя действиями: `seqtbl_offset_char(seq_id)`, `play_seq()`, `Char.fall_y = 0`. Никакого пересчёта колонки. *У нас:* `pop_map.c:715` (и в ветке смерти `:705`) после `play_seq()` дополнительно вызывается `determine_col()`. *Чем грозит:* `determine_col` пересчитывает `Char.curr_col` из `Char.x`. Оригинал делает это в другом месте и в другой момент — `load_fram_det_col` (seg006:0144) перед разбором кадра. Лишний пересчёт сразу после приземления может дать другую колонку, если `play_seq` успел сдвинуть `x` первым же `dx`. Колонка — вход для проверок тайла, пик и коллизий. *Замечание:* приём применён у нас системно (`pop_map.c` — 8 вызовов), то есть это, вероятно, осознанная адаптация, но в `docs/impl_diff.md` она НЕ записана. Нужно либо обосновать и записать, либо снять. ### 2. `land()`: обнуляется ещё и `fall_x` — ранг **Б** *Оригинал:* в `land` обнуляется ТОЛЬКО `fall_y`, и притом в самом конце, ПОСЛЕ `play_seq()`. *У нас:* `pop_map.c:697` и `:713` обнуляют пару `Char.fall_x = Char.fall_y = 0`, причём ДО `pop_char_set_seq`/`play_seq`. *Чем грозит:* два отличия сразу. Во-первых, `fall_x` в оригинале переживает приземление — если наш сброс лишний, теряется горизонтальный импульс, влияющий на последующие кадры. Во-вторых, момент: если `play_seq` читает `fall_y` (а он читает при обработке своих опкодов движения), оригинал видит ещё НЕ обнулённое значение, а мы — уже ноль. ### 3. `land()`: проверка пик до коррекции X — ранг **Б** *Оригинал:* сначала (внутри ветки «тайл под ногами не пика») делается коррекция `Char.x = char_dx_forward(-3)` при `distance_to_edge_weight() < 3`, и лишь ПОТОМ проверяется падение на пики, причём условие для пики ПОЗАДИ использует `distance_to_edge_weight() >= 12`. *У нас:* `pop_map.c:664` — `fell_on_spikes()` вызывается ПЕРВЫМ, до коррекции X. *Чем грозит:* `distance_to_edge_weight()` считается от `Char.x`, а коррекция этот `x` меняет на 3 пикселя. У края тайла порядок решает, попадёт ли персонаж в ветку «пика позади» — то есть умрёт он или нет. ### 4. Смерть: у нас свой флаг вместо счётчика — ранг **В** *Оригинал:* `play_kid` (seg006:1348) после смерти ведёт СЧЁТЧИК `Char.alive`, и по его значениям запускает музыку смерти (`alive == 6`) и надпись «Press Button to Continue» (`alive == 7`), причём переход задерживается, пока играет звук (`check_sound_playing`). *У нас:* `pop_ctrl.c` (`ctrl_kid_death`) взводит `pop_kid_dead` — сигнал главному циклу на респавн; счётчика стадий нет. *Чем грозит:* момент респавна и порядок «музыка смерти → сообщение → рестарт» могут отличаться, особенно если смерть застала персонажа в длинной анимации (падение). Сюда же относится симптом выше: в оригинале поза сохраняется, пока крутится счётчик. ### 5. Мёртвый доигрывает приземление — ранг **В** Разобрано выше. Код у нас и в оригинале В ЭТОМ МЕСТЕ совпадает, поэтому ранг не выше среднего: расхождение может сидеть не в `land`, а в моменте взведения смерти (находка 4) — тогда оригинал успевает поставить кадр смерти до приземления, а мы нет. Проверяется прогоном на живом SDLPoP. ### 6. Диспетчер `control()` — ранг **Д** Сверен ветка в ветку (seg005:251 против `pop_ctrl.c:618`): совпадают и порядок проверок (`bumped/freefall` → меч → charid → кадры), и границы диапазонов кадров, и обработка мёртвого. Расхождений не видно; остаются только опциональные `#ifdef`-фиксы SDLPoP, которых у нас нет намеренно. ### 7. `JMP_IF_FEATHER`: у нас эффект только для Кида — ранг **Б** *Оригинал:* опкод `SEQ_JMP_IF_FEATHER` (seg006, play_seq) смотрит ТОЛЬКО на глобальный `is_feather_fall`. Кто именно проигрывает последовательность, роли не играет. *У нас:* `pop_kid.c:306` добавляет условие `Char.charid != CHARID_0_KID` — для всех, кроме Кида, ветка «пера» не берётся никогда. *Чем грозит:* под зельем медленного падения любой НЕ-Кид, попавший в последовательности `stepfloat`/`bumpfloat`, у нас пойдёт по обычной ветке (с уроном), а в оригинале — по парящей. Практически это Тень (charid 1) на уровне 4-6 и скелет; страж в эти seq попадает редко, но попадает через `bumpfloat` при отскоке. *Замечание:* отличие ОСОЗНАННОЕ (в комментарии сказано «эффект достаётся только Киду — как и сама физика пера»), но в `docs/impl_diff.md` не записано, хотя правило проекта этого требует. ### 8. `start_chompers` отложен до конца `play_seq` — ранг **Б** *Оригинал:* опкоды `SEQ_UP`/`SEQ_DOWN` меняют ряд и ТУТ ЖЕ зовут `start_chompers()` — то есть внутри цикла интерпретатора, до разбора следующих опкодов. *У нас:* `pop_kid.c:318-325` только взводит `chomp_pending`, а сам вызов происходит после выхода из цикла (`:396`). Причина архитектурная и описана в коде: seqtbl читается через окно W0, а `start_chompers` лезет в другое окно. *Чем грозит:* два следствия. Во-первых, последовательность с ДВУМЯ сменами ряда (`UP` `UP`, спуск/подъём по лестнице) в оригинале будит чомперов в обоих рядах, у нас — только в конечном. Во-вторых, между `SEQ_UP` и концом цикла успевают отработать `dx`/`dy`/`action`, то есть оригинал будит чомперов с ДРУГИМИ координатами персонажа. Симптом «челюсти не заводятся» уже ловился в этом проекте (memory `pop_chomper_needs_trigger`), и это место — кандидат в его причины. ### 9. Набор опкодов seqtbl — ранг **Д** Сверены все пятнадцать кодов (`0xF1`..`0xFF`): совпадают и значения, и семантика, включая проваливание `JMP_IF_FEATHER` в `JMP` и то, что `SEQ_DIE` — пустышка в обоих движках. Отличия только в находках 7 и 8. ### 10. Отрисовка: шаги те же, но разнесены — ранг **В** *Оригинал:* `add_kid_to_objtable` (seg008:1667) и его двойник для стража — это строго упорядоченная цепочка: `loadkid`/`loadshad` → `load_fram_det_col` → `load_frame_to_obj` → `stuck_lower` → `set_char_collision` → `set_objtile_at_char` → `redraw_at_char` → `redraw_at_char2` → `clip_char` → `add_objtable`. *У нас:* все звенья присутствуют, но распределены по слоям: `clip_char`, `load_frame_to_obj`, `check_mirror`, брызги — в `pop_cdraw.c`; `redraw_at_char`/`set_objtile_at_char`/`set_char_collision` — в `pop_bg.c` (единый проход на всех Char, см. CLAUDE.md). *Чем грозит:* сам по себе перенос не ошибка, но ПОРЯДОК внутри цепочки влияет на результат: `set_char_collision` и `set_objtile_at_char` готовят данные, которыми пользуются `redraw_at_char` и `clip_char`. Если наш общий проход выполняет их для ОБОИХ персонажей до отрисовки, а оригинал — для каждого непосредственно перед его выводом, то при наложении Кида и стража состояние на момент клипа будет разным. *Отдельно:* `stuck_lower` найден только в `pop_cdraw.h` — надо убедиться, что он реализован, а не только объявлен. Если его нет, персонаж, застрявший на границе тайла, будет рисоваться на пиксель выше. ### 11. Порядок вывода Кида и стража — ранг **В** *Оригинал:* `draw_people` (seg008:1635) всегда ставит сначала Кида (`draw_kid`), затем стража (`draw_guard`), а КТО ОКАЖЕТСЯ СВЕРХУ решает `add_objtable` — таблица объектов упорядочена по позиции тайла. *У нас:* по CLAUDE.md порядок задаёт обход тайлов, «кто позже — тот поверх». Это близко по смыслу, но не тождественно сортировке objtable. *Чем грозит:* при наложении персонажей (бой вплотную, страж перед Кидом) верхний может оказаться другим. Проверять сравнением кадров боя вплотную с эталонным SDLPoP (метод — memory `pop_pixel_diff_vs_sdlpop`). ### 12. `hurt_by_sword`: ветка «сбит с уступа» не портирована — ранг **А** *Оригинал:* `hurt_by_sword` (seg002:911) при уколе ВООРУЖЁННОГО персонажа выбирает одну из двух смертей по обстановке ПОЗАДИ: * тайл позади не пустой ИЛИ до кромки меньше 4 → `seq_85_stabbed_to_death` (заколот на месте); * иначе → `seq_81_kid_pushed_off_ledge` — отдельная последовательность «убит и сброшен с уступа», которая сама отыгрывает падение замертво. *У нас:* `guards.c:1015` — ветки `seq_81` НЕТ вовсе, всегда `seq_85`. Упрощение ЗАДОКУМЕНТИРОВАНО в комментарии (`guards.c:1011`): ей нужны тайловые запросы от `Char`, а `pop_map` умеет их только от `Kid`. *Чем грозит:* именно тем, что наблюдал пользователь. Заколотый на краю обрыва Кид в оригинале уходит в собственную анимацию падения с уступа; у нас он получает «смерть на месте», продолжая при этом висеть в воздухе — дальше им распоряжается обычная физика падения, и он приземляется этажом ниже по общим правилам (а с убранным в `start_fall` мечом — в присед, находка 5). *Как проверить:* поставить Кида спиной к обрыву с 1 HP и дать стражу уколоть. В оригинале — падение замертво (кадры seq_81), у нас — смерть на месте с последующим отдельным падением. ### 13. `hurt_by_sword`: прижатие к полу стало безусловным — ранг **А** *Оригинал:* `Char.y = y_land[Char.curr_row + 1]` и `Char.fall_y = 0` выполняются ТОЛЬКО в ветке выжившего удара (seg002:962, рядом с `seq_74_hit_by_sword`). Смертельные ветки координату не трогают. *У нас:* `guards.c` — те же две строки стоят ПОСЛЕ всего `if/else`, то есть выполняются и при смерти тоже. *Чем грозит:* персонажа, убитого в воздухе, мы принудительно ставим на пол текущего ряда и обнуляем накопленную скорость падения. Дальше физика обнаруживает, что пола под ним нет, и запускает падение ЗАНОВО — уже без `fall_y`, то есть с другой высотой и другим исходом приземления. Это вторая половина механизма из находки 12 и вероятная причина того, что мёртвый Кид доезжает до нижнего этажа «своим ходом». *Как проверить:* тот же сценарий; в отладчике смотреть `Char.y` и `fall_y` сразу после попадания — оригинал их не меняет. ### 14. Отскок с мечом: нет `seq_64` — ранг **Б** *Оригинал:* при отскоке (`bumped`, seg004:328) живой персонаж с вынутым мечом получает ОДНУ ИЗ ДВУХ последовательностей по направлению толчка: толкнули вперёд — `seq_65_bump_forward_with_sword`, отбросило назад — `seq_64_pushed_back_with_sword`. *У нас:* `pop_map.c` знает только `SEQ_65_BUMP_FWD_SWORD` (объявлен на `:103`, используется на `:2231`); константы и ветки `seq_64` нет вовсе. *Чем грозит:* персонаж, отброшенный назад с мечом (страж у стены, Кид в тесной комнате), проигрывает не ту анимацию — либо ветку без меча, либо `seq_65`. Кадры разные, а вместе с ними расходятся и смещения `dx` в последовательности, то есть итоговая позиция после отскока. *Как проверить:* бой вплотную к стене, толчок в сторону стены и от неё; сверять кадры с эталонным прогоном SDLPoP. ### 15. `check_bumped_look_right`: гейт по направлению — ранг **В** В нашей реализации (`pop_map.c:2148`) стоит ранний выход по `Char.direction` с пометкой «(меча в руке у нас нет)». Пометка означает, что ветка писалась до появления боя, а оригинал в этом месте учитывает и меч, и `push_direction` (находка 14). Область `check_bumped_look_left` не сверялась вовсе — её надо пройти отдельно. ### 16. `control_with_sword` — ранг **Д** Сверен целиком (seg005:964 против `pop_ctrl.c:587`): гейт по `action`, условие «пол под ногами loose ИЛИ страж видит Кида», пороги дистанции (90 и −4), `seq_60_turn_with_sword`, ветка «соперник умер» с `seq_92_put_sword_away`, разделение по `charid`. Совпадает. Отдельно отмечу: в оригинале сравнение дистанции сделано ЗНАКОВО-НЕЯВНО (приведением к `word`), из-за чего ветка «соперник за спиной» вообще достижима. У нас то же самое выражено явными знаковыми сравнениями — и диапазоны совпадают, включая «вплотную за спиной» (−4..−1), где обе реализации ведут бой, а не разворачиваются. ### 17. `parry` — ранг **Д** Сверен целиком (seg005:1064 против `pop_ctrl.c:513`): список кадров стойки, порог 32 для не-Кида, обработка кадров соперника (151/152/162, особый случай 153 с отложенным `play_seq`), ветка стража по кадру 152, ветка `frame_167_blocked` с `seq_61`, сброс автоповтора `control_up`. Совпадает вплоть до порядка условий. ### 18. `check_hurting`: звук «меч в движении» в других условиях — ранг **Б** *Оригинал:* звук 11 играется в САМОМ КОНЦЕ `check_hurting` (seg002) и только при трёх условиях сразу: направление персонажа не `none`, его кадр — укол (154), а соперник при этом НЕ парирует и НЕ ранен. Первое условие добавлено в SDLPoP специально против зацикливания звука. *У нас:* `guards.c:1068` — звук играется в начале ветки укола, безусловно, ещё до того, как определено попадание. *Чем грозит:* лишние срабатывания в двух ситуациях, где оригинал молчит — когда удар парирован и когда он попал. То есть в самой частой части боя звук звучит чаще, чем должен. Плюс отсутствует защита от зацикливания при `direction == none`. *Как проверить:* бой с парирующим стражем; считать срабатывания звука 11 на серии ударов и сравнить с эталонным прогоном SDLPoP. ### 19. Остальной бой сверен — ранг **Д** Прочитаны целиком и совпадают: * `swordfight` (seg005:998) — включая ветку кадра 161, `sword_strike`, побочные эффекты уборки меча (`offguard`, `guard_refrac`, `holding_sword`), разделение `seq_93`/`seq_92`/`seq_87` по `charid` и хвост (`parry` / `forward_with_sword` / `back_with_sword`); * `sword_strike` (seg005:1037) — список кадров, выбор `seq_75`/`seq_58`, `seq_66` после парирования, сброс автоповтора; * `check_sword_hurt` (seg002:971) — включая ПРИОРИТЕТ СТРАЖА при одновременном ранении и сброс `Kid.action` в бег, а также `refractimer` по навыку; * `check_hurting` в основной части — гейты по мечу, ряду и кадрам, пороги дистанции (29), `min_hurt_range` 8/12 по мечу соперника, ветка парирования с `justblocked` и `seq_69`. Единственное расхождение — звук, находка 18. ### 20. Стражи: «стена впереди» сужена до одного тайла — ранг **Б** *Оригинал:* `guard_follows_kid_down` (seg002:811) и соседние ветки ИИ спрашивают `wall_type(tile) != 0`. Эта функция (seg006:1626) считает преградой ПЯТЬ видов тайлов: ворота, верх двери с полом, верх двери, зеркало, чомпер и собственно стену — с разной стороной блокировки. *У нас:* `guards.c:683` и `:685` сравнивают тайл напрямую с `TILE_WALL` (тип 20). Ворота, верх двери, зеркало и чомпер преградой не считаются. *Причина:* `wall_type` реализована у нас (`pop_map.c:504`, таблица на `:498`), но НЕ экспортирована — в `pop_map.h` её нет, поэтому `guards.c` до неё не дотягивается. То есть это не пробел в портировании логики, а следствие границы модулей. *Чем грозит:* страж, преследующий упавшего Кида, у нас шагнёт вперёд там, где оригинал отступает — перед закрытыми воротами, верхом двери, зеркалом и чомпером. Отсюда возможны и проход стража сквозь препятствие, и падение туда, куда оригинал его не пускает. *Как проверить:* уровень с воротами (например, 3-й) — заманить стража к закрытым воротам после падения Кида и сравнить, отступает ли он. *Замечание:* в `guards.c` таких мест ЧЕТЫРЕ (`:148`, `:683`, `:685`); одно из них (`:148`) уже перечисляет три тайла вручную, то есть расхождение частично компенсировано, но не везде одинаково. --- ## Область: столкновение со стенами и падение внутри стены Заведена по наблюдению пользователя (2026-08-31): разбег, прыжок сделан рано, Кид не долетел, врезался в стену и начал падать — но по X он оказался ВНУТРИ стены и падал частично в ней. ### 21. Фикс «скольжения сквозь стену» не портирован — ранг **Г** (соответствие ванили) *Оригинал:* в `do_fall` (seg005:37) есть блок `FIX_GLIDE_THROUGH_WALL` с собственным комментарием SDLPoP: «Кид падает сквозь стены после разворота в беге, особенно в невесомости». Блок опциональный — то есть в ВАНИЛЬНОЙ игре этот баг ЕСТЬ, а SDLPoP его чинит по желанию. Рядом такие же опциональные `FIX_JUMP_THROUGH_WALL_ABOVE_GATE` и `FIX_DROP_THROUGH_TAPESTRY`. *У нас:* ни один из трёх не портирован — мы намеренно повторяем ваниль. *Вывод по симптому:* «падение частично в стене» — с большой вероятностью ОРИГИНАЛЬНОЕ поведение PoP, а не наша ошибка. Ранг Г означает: различия с ванилью, скорее всего, нет. Но проверить стоит другое — не ХУЖЕ ли у нас, чем в ванили (см. находки 22 и 15). *Как проверить:* повторить сцену на живом SDLPoP с выключенными фиксами (они выключаемы в его настройках) и сравнить глубину захода в стену. ### 22. `do_fall`: наш гард `curr_row <= 2` — ранг **В** *Оригинал:* в `do_fall` ветка «достигли нового ряда» выполняется БЕЗ условия на номер ряда: проверка тайла стены с вызовом выталкивания, затем `land()` либо переход на ряд ниже. *У нас:* `pop_map.c:909` — вся ветка обёрнута в `if (Char.curr_row <= 2)`. Причина задокументирована (`:768`): наш `get_tile` за нижней кромкой комнаты отдаёт СТЕНУ как сентинель, тогда как в оригинале там комната снизу, и без гарда выталкивание срабатывало ложно, смещая падение на тайл. *Чем грозит:* гард гасит не только ложные срабатывания. Если персонаж достиг `curr_row == 3` легитимно (падение между комнатами по вертикали), у нас не выполнится ни выталкивание из стены, ни `land()`, ни переход ряда — всё это ляжет на следующий кадр и другую ветку. Именно такая комбинация (падение у границы комнаты рядом со стеной) даёт кандидата в причины наблюдения пользователя. *Как проверить:* падение вдоль стены точно на стыке комнат по вертикали; в отладчике смотреть `curr_row`, `Char.x` и факт вызова выталкивания. ### 23. `bumped_fall` — ранг **Д** Сверен (seg004 против `pop_map.c`): откат X на 4 пикселя назад, обнуление горизонтальной скорости в свободном падении, иначе `seq_45_bumpfall` с проигрыванием, звук удара. Совпадает; у нас добавлен только флаг «стражи услышали», который в оригинале ставится внутри звуковой функции. **Замечание по области:** глубина отката при столкновении — ровно 4 пикселя в обоих движках. Если Кид вошёл в стену глубже (а при недолёте с разбега скорость по X велика), одного отката не хватит ни там, ни у нас — и дальше всё зависит от того, сработает ли выталкивание из стены на следующем кадре. У нас его может съесть гард из находки 22. Это главная зацепка по симптому. ### 24. `in_wall`: не перезагружается кадр — ранг **Б**, ОТЛОЖЕНА > **Правка сделана и откачена 2026-08-31.** По букве оригинала находка > верна, но практического эффекта показать не удалось: все 15 наборов > host-тестов дали одинаковый результат до и после, включая специально > написанный тест на заход в кладку (`t_wall`). При этом правка не > бесплатна — добавляет маппинг окна и перезагрузку кадра на каждое > выталкивание из стены. Платить за недоказанное не стали. > > Задача переехала в `docs/vanilla_vs_bugfixed.md`: вернуться к ней при > работе над двумя режимами поведения, где появится сценарий, в котором > кадр меняется перед выталкиванием. *Оригинал:* `in_wall` (seg006) после выталкивания персонажа из стены делает `load_fram_det_col()` — ЗАГРУЖАЕТ КАДР и следом определяет колонку, затем перечитывает тайл. *У нас:* `pop_map.c` (`in_wall`) вызывает только `determine_col()`. Пороги (`>= 8`), формулы смещения (`6 - d` и `d + 4`), условие по тайлу впереди и финальное чтение тайла совпадают — расходится только этот шаг. *Чем грозит:* после выталкивания данные кадра (картинка, смещения, флаги — включая «нужен пол» и «чётный пиксель») остаются от позиции ДО коррекции, а ими пользуются проверки того же кадра: падение, клип, коллизия. Это ровно область, где наблюдалось падение внутри стены (находки 21, 22). *Как проверить:* недолёт с разбега в стену; в отладчике сравнить `Char.x`, колонку и поля текущего кадра сразу после выталкивания. ### 25. Таблицы кадров и seqtbl — ранг **Д** `frame_table_kid`, `original_seqtbl` и таблица смещений извлекаются АВТОМАТИЧЕСКИ из исходников SDLPoP (`tools/pop_extract_kid_data.py` → `gen/kid_data.h`), поэтому расхождение в данных маловероятно по построению. Применение тоже сверено: используются все четыре флага кадра («нужен пол» 0x40, вес по X 0x1F, «тонкий» 0x20, чётный пиксель 0x80), а байт клинка маскируется как в оригинале (`& 0x3F`, `pop_kdraw.c:31`). Не сверено: старшие два бита байта клинка (номер набора спрайтов) — у Кида он нулевой, у прочих персонажей стоит проверить отдельно. ### 26. Полнота автоуправления — ранг **Д** Из двенадцати функций `autocontrol_*` оригинала у нас есть одиннадцать. Отсутствующая — тривиальная обёртка над общей логикой стража; у нас она встроена в вызывающего. Расхождения нет. Не сверены ПОСТРОЧНО тела: `autocontrol_guard_kid_armed`, `autocontrol_guard_kid_far`, `autocontrol_shadow*`, `autocontrol_skeleton`, `check_grab`, `check_bumped_look_left`, `back_with_sword`, `forward_with_sword`. --- ## ИТОГ АУДИТА Проверено 26 позиций за пять проходов. | ранг | находки | суть | |---|---|---| | **А** | 1, 12, 13 | лишний пересчёт колонки в `land`; нет ветки «убит и сброшен с уступа»; безусловное прижатие к полу при смерти | | **Б** | 2, 3, 7, 8, 14, 18, 20, 24 | `fall_x` и момент сброса; порядок «пики / коррекция X»; перо только для Кида; отложенные чомперы; нет `seq_64`; звук удара; «стена» сужена до одного тайла; нет перезагрузки кадра в `in_wall` | | **В** | 4, 5, 10, 11, 15, 22 | флаг смерти вместо счётчика стадий; мёртвый доигрывает приземление; разнесённая цепочка отрисовки; порядок Кид/страж; гейт в `check_bumped_look_right`; гард `curr_row <= 2` в `do_fall` | | **Г** | 21 | опциональные фиксы SDLPoP не портированы — соответствие ванили | | **Д** | 6, 9, 16, 17, 19, 23, 25, 26 | сверено и совпадает | ### Три узла, вокруг которых группируются расхождения 1. **Смерть при активной физике** (1, 12, 13, 4, 5). Здесь все находки ранга А. Общая причина: у нас смерть — это флаг, а физика продолжает работать с персонажем как с живым. 2. **Границы модулей** (12, 20, 24). `pop_map` не отдаёт наружу то, что нужно `guards.c` и работе с произвольным `Char`: тайловые запросы от `Char`, `wall_type`, загрузку кадра. Ветки упрощались не по логике, а по доступности функций. 3. **Момент побочных действий** (2, 8, 18, 24). Делаем то же самое, но раньше или позже оригинала: сброс скорости, побудка чомперов, звук, перезагрузка кадра. По отдельности мелочь, вместе — сдвиг состояния на кадр. ### Что делать дальше 1. Проверить находки А и Б в MAME по сценариям из их описаний — начиная с 12/13 (смерть на краю) и 24 (выталкивание из стены). 2. Те же сцены прогнать на живом SDLPoP: часть наблюдений может оказаться ванильным поведением (как находка 21). 3. Подтверждённые осознанные отличия записать в `docs/impl_diff.md` — сейчас там нет ни одного из найденных, хотя правило проекта требует. Порядок по ожидаемой отдаче: 1. **Бой** — ПРОЙДЕН. Находки: 12, 13 (ранг А), 18 (Б); совпадают `control_with_sword`, `parry`, `swordfight`, `sword_strike`, `check_sword_hurt`, `check_hurting` (кроме звука). Не сверены мелочи: `back_with_sword`, `forward_with_sword`, `check_skel`. 2. **`play_seq` и опкоды** — самая опасная область: ошибка в одном опкоде меняет все последовательности разом. 3. **Отрисовка** — `add_kid_to_objtable`/`add_guard_to_objtable`, порядок слоёв, `clip_char`. 4. **`do_fall`/`check_bumped`/`check_grab`** — остаток физики. 5. **ИИ стражей** и особенности скелета/Тени/Джафара. --- # Глубокое ревью находок А и Б: можно ли починить и чем платим > 2026-08-31. КОД ПО-ПРЕЖНЕМУ НЕ МЕНЯЛСЯ. Здесь только оценка. > > **Главное ограничение (требование пользователя): фикс не должен заметно > замедлять игру.** Поэтому у каждой находки первым делом указана ЧАСТОТА > вызова места, а уже потом сама правка. > > **Оговорка к ограничению:** для КРИТИЧНЫХ фиксов скорость — не вето. > Если такой фикс всерьёз бьёт по производительности, он выносится в > отдельный разбор, где ищется способ получить правильное поведение > дёшево (иной момент вызова, кэш, предвычисление, перенос в холодный > путь). То есть порядок такой: сначала решаем, критично ли поведение, и > только потом — какой ценой его добиться. ## Снятое препятствие Обоснование сразу двух упрощений (`guards.c:1011` — «нужны тайловые запросы ОТ Char, а pop_map умеет только от Kid») **устарело**. Проверено: `get_tile_at_char`, `get_tile_infrontof_char`, `get_tile_behind_char` и `distance_to_edge_weight` в `pop_map.c` УЖЕ работают от `Char` (строки 460, 465, 477, 561). Они лишь не выведены в `pop_map.h`. Так же обстоит с данными: `pop_char_set_seq()` ставит любую из 115 последовательностей по индексу, то есть `seq_81` и `seq_64` доступны без единого нового байта данных — таблица генерируется из оригинала целиком. То есть три находки (12, 14, 20) упираются не в архитектуру, а в четыре строки объявлений. ## Классификация мест по частоте вызова | место | частота | вывод | |---|---|---| | `play_seq` (находки 7, 8) | КАЖДЫЙ кадр каждого персонажа | правка обязана быть бесплатной | | `check_hurting` (18) | каждый кадр боя, дважды | почти горячий | | ИИ стража (20) | каждый кадр, пока страж активен | почти горячий | | `land`, `in_wall`, `bumped` (1, 2, 3, 14, 24) | событие раз в несколько секунд | холодный, цена не важна | | `hurt_by_sword` (12, 13) | момент попадания | холодный | ## Разбор по находкам ### 12 + 13 (ранг А) — смерть на краю. ТОЛЬКО ВМЕСТЕ, НЕ ПООТДЕЛЬНОСТИ > **Проверено на живой машине 2026-08-31 и провалилось.** Правка 13 была > сделана в изоляции (прижатие к полу перенесено в ветки пережитого > удара) — и сломала смерть: страж бьёт Кида, тот погибает, а вместо > нормальной смерти идут вспышка, стопкадр и немедленный выход в > заставку. > > Причина: у нас прижатие к полу работало КОМПЕНСАЦИЕЙ отсутствующей > ветки «убит и сброшен с уступа» (находка 12). Убрав компенсацию и не > добавив то, что она компенсировала, мы оставляем мёртвого персонажа с > ненулевой `fall_y` и незакреплённой `Char.y` — физика продолжает вести > его вниз, он проваливается за пределы уровня, и срабатывает аварийный > путь. > > Вывод: обе находки — ОДНА правка. Оценка «чистое перемещение строк, > риск низкий» была неверной; риск ВЫСОКИЙ, пока ветка 12 отсутствует. > Порядок внутри правки: сперва добавить ветку `seq_81` (с экспортом > тайловых запросов), убедиться, что смерть на краю отыгрывается ею, и > только затем убирать безусловное прижатие. *Место:* `guards.c`, `hurt_by_sword` — холодный путь. *Правка:* (а) перенести две строки прижатия к полу внутрь ветки выжившего удара — это чистое перемещение, минус ноль байт; (б) добавить ветку выбора `seq_81` по тайлу позади и расстоянию до кромки. *Что нужно:* экспорт `get_tile_behind_char()` и `distance_to_edge_weight()` из `pop_map.c` в `pop_map.h` как `__banked`. *Цена скорости:* два межбанковых вызова (`guards.c` — банк 1, `pop_map.c` — банк 3) в момент попадания мечом, то есть несколько раз за бой. Незаметно. *Цена памяти:* банк 1 занят на 19,8 % (13 142 Б свободно) — места вдоволь; банк 3 занят на 81,1 % (3 100 Б), но там прибавятся только две обёртки. *Риск:* низкий. Ветка симметрична существующей, данные есть. ### 1 (ранг А) — лишний `determine_col()` в `land` *Место:* холодный путь. *Правка:* убрать вызов и проверить, не понадобился ли он нам вместо оригинального `load_fram_det_col`, который оригинал делает в другом месте цепочки. *Цена:* отрицательная (кода меньше). *Риск:* СРЕДНИЙ — вызов мог компенсировать наш иной порядок загрузки кадра; убирать только с прогоном сцен падения и приземления. ### 2, 3 (ранг Б) — `land`: `fall_x` и порядок проверки пик *Место:* холодный. *Правка 2:* сбрасывать только `fall_y` и после `play_seq`, как оригинал. *Правка 3:* перенести проверку пик после коррекции X. *Цена:* нулевая, это перестановка строк. *Риск:* низкий, но обе меняют поведение на краю тайла — нужны прогоны с пиками. ### 24 (ранг Б) — `in_wall` не перезагружает кадр. Одна строка *Место:* холодный. *Правка:* заменить `determine_col()` на `pop_load_fram_det_col()` — он УЖЕ экспортирован (`pop_kid.h:109`) и, что важно, НЕ банковый, то есть вызов прямой. *Цена скорости:* одна перезагрузка кадра при выталкивании из стены — доли процента кадра. *Риск:* низкий; это возврат к оригиналу. ### 14 (ранг Б) — нет `seq_64` *Место:* `bumped`, холодный. *Правка:* добавить выбор между 64 и 65 по направлению толчка. *Цена:* нулевая. *Риск:* низкий. ### 18 (ранг Б) — звук удара *Место:* `check_hurting` — дважды за кадр боя. *Правка:* перенести звук в конец функции и обвесить тремя условиями оригинала. *Цена:* ОТРИЦАТЕЛЬНАЯ — звук перестанет играть в двух случаях из трёх, то есть уменьшится и число обращений к звуковой очереди. *Риск:* низкий. ### 20 (ранг Б) — «стена» у стражей. Требует осторожности со скоростью *Место:* ИИ стража — вызывается каждый кадр, пока страж активен. *Плохой вариант:* экспортировать `wall_type` из `pop_map.c` и звать из `guards.c`. Это МЕЖБАНКОВЫЙ вызов (банк 1 → банк 3) в почти горячем пути — трамплин с переключением W3 на каждый шаг ИИ. Против требования по скорости. *Хороший вариант:* завести копию таблицы `wall_type_tbl` (32 байта) в rodata банка 1 и обращаться к ней напрямую — стоимость чтения байта, ноль переключений банка. Дублирование данных здесь оправдано: таблица константная и вшита в формат уровней. *Риск:* низкий, но нужно следить, чтобы копия не разошлась с оригиналом — лучше генерировать обе из одного места или снабдить перекрёстным комментарием. ### 7 (ранг Б) — перо только для Кида. Правка ускоряет *Место:* `play_seq`, самый горячий путь. *Правка:* убрать лишнее условие по `charid`. *Цена:* ОТРИЦАТЕЛЬНАЯ — из горячего цикла уходит сравнение. *Риск:* средний: надо убедиться, что физика пера у нас применяется к любому персонажу так же, как в оригинале, иначе анимация разойдётся с физикой. ### 8 (ранг Б) — отложенные чомперы. Чинить ДЕШЁВЫМ способом *Место:* `play_seq`, горячий путь. *Почему отложено:* `play_seq` маппит страницу байткода в окно W0 ОДИН раз перед циклом (`pop_kid.c:284`) и снимает после (`:389`). Вызвать `start_chompers` внутри цикла — значит снять окно, позвать, вернуть окно, и так на каждый переход ряда. Это прямая деградация горячего пути и против требования по скорости. *Дешёвая замена:* сейчас копится ОДИН флаг, из-за чего теряются промежуточные ряды. Достаточно копить не флаг, а НОМЕРА рядов — один байт-битовую маску (рядов всего 0..3) плюс запомненную колонку. После цикла пройти по взведённым битам и разбудить чомперов в каждом. Цена в цикле: одна операция «выставить бит» вместо присваивания флага, то есть ноль. Разница с оригиналом останется только в МОМЕНТЕ побудки (после цикла, а не внутри), но ряды перестанут теряться. *Риск:* низкий. Полное совпадение с оригиналом здесь недостижимо без потери скорости — это осознанный компромисс, который надо записать в `docs/impl_diff.md`. ## Сводка: что делать в каком порядке | приоритет | находки | почему | |---|---|---| | 1 | 24 | одна строка, риск низкий, готовая экспортированная функция | | 2 | 12+13 ВМЕСТЕ | порознь ломают смерть (проверено); нужен экспорт двух функций | | 3 | 18, 8 | обе УСКОРЯЮТ или бесплатны; 8 — по дешёвому варианту | | 4 | 2, 3, 20 | перестановки и копия таблицы; нужны прогоны | | 5 | 1, 7 | риск средний: обе могут компенсировать наши отличия в другом месте | **Ни один фикс не требует переделки архитектуры и ни один не ложится на горячий путь с накладными расходами** — при условии, что находка 20 делается копией таблицы, а находка 8 — битовой маской рядов.