3a0e847353
КОД НЕ МЕНЯЛСЯ. Построчный разбор наших реализаций против оригинала: 26 позиций за пять проходов, каждая с рангом вероятности (А..Д), с описанием «чем грозит» и сценарием проверки. Расхождения группируются в три узла, и это главный вывод аудита: 1. СМЕРТЬ ПРИ АКТИВНОЙ ФИЗИКЕ — здесь все находки ранга А. У нас смерть это флаг, а физика продолжает вести персонажа как живого: нет ветки «убит и сброшен с уступа» (оригинал выбирает её по тайлу позади), прижатие к полу в hurt_by_sword стало безусловным (в оригинале только для выжившего удара), в land лишний пересчёт колонки. Этим объясняется наблюдение пользователя: заколотый на краю Кид доезжает этажом ниже и садится в присед. 2. ГРАНИЦЫ МОДУЛЕЙ — pop_map не отдаёт наружу тайловые запросы от произвольного Char, wall_type и загрузку кадра. Три ветки упрощены НЕ по логике, а по доступности функций: отсутствующая ветка уступа, «стена впереди» сужена у стражей до одного тайла (оригинал считает преградой ещё ворота, верх двери, зеркало и чомпер), in_wall не перезагружает кадр. Чинить это заплатками неправильно — сначала расширять интерфейс pop_map. 3. МОМЕНТ ПОБОЧНЫХ ДЕЙСТВИЙ — делаем то же самое, но раньше или позже: сброс fall_x, побудка чомперов (у нас отложена до конца play_seq), звук удара, перезагрузка кадра. По отдельности мелочь, вместе — сдвиг состояния на кадр. Восемь позиций СВЕРЕНЫ И СОВПАДАЮТ (диспетчер control, все 15 опкодов seqtbl, control_with_sword, parry, swordfight, sword_strike, check_sword_hurt, check_hurting, bumped_fall, таблицы кадров) — их не нужно перепроверять. Дважды по ходу работы едва не записана ложная находка из-за чтения отфильтрованного вывода; отсюда правило: фиксировать расхождение только после чтения обеих реализаций целиком. Отдельно: второе наблюдение пользователя (падение частично в стене) — у SDLPoP есть ТРИ опциональных фикса ровно про это, то есть в ванили баг присутствует, и мы его намеренно повторяем. Но найдены два места, где мы можем быть хуже ванили (гард curr_row<=2 в do_fall и in_wall выше). Незакрытое перечислено в файле: тела autocontrol_*, check_grab, check_bumped_look_left, старшие биты байта клинка. Также отмечено, что ни одно найденное осознанное отличие не занесено в docs/impl_diff.md, хотя правило проекта этого требует. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011MsUsEFAQfsjjQpJ7RtKVY
550 lines
40 KiB
Markdown
550 lines
40 KiB
Markdown
# Аудит расхождений с 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`: не перезагружается кадр — ранг **Б**
|
||
|
||
*Оригинал:* `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. **ИИ стражей** и особенности скелета/Тени/Джафара.
|