4e43890fce
Пройденная игра доходила до таблицы рекордов и умирала: программа
исчезала, машина следом вставала намертво (di;halt на 0x0000) либо уходила
в reset. Одинаково из Flex Navigator и из голого DSS.
КОРЕНЬ. pop_ui.h объявлял группу pop_text_*_mapped БЕЗ __banked. Пока
pop_hof.c лежал в банке 9 рядом с pop_ui.c, прямой call был верен; после
переноса pop_hof/pop_config/pop_pal в банк 10 тот же call стал уходить в
пустой хвост чужого банка. Процессор полз по 0xFF до 0x0000, где ловушка
DSS ставит B=0x27 и сворачивает процесс — подмена страниц W1/W2/W3,
которую было видно на трупе, оказалась уборкой, а не причиной.
Точную инструкцию (call $E503 = _pop_text_map банка 9) дала трассировка
MAME на узком участке: trace включалась брейкпоинтом на входе в
pop_hof_show и выключалась на процедуре завершения процесса DSS (0x1E56).
ЧТО СДЕЛАНО
* pop_ui.h/.c — группа text_*_mapped помечена __banked.
* toolchain/check_bank_calls.py — две проверки банкового кода:
1) прямой call в чужой банк (доказательна, ВАЛИТ сборку — проверено
намеренной поломкой);
2) указатель на данные своего банка, отданный в чужой (эвристика по
форме кода, только предупреждает).
Встроена в app.mk, запускается сразу после линковки.
* pop_hof.c — курсор ввода строится на стеке: литерал "_" лежал в _BANK10
и после пометки __banked уезжал из-под ног чужому банку, заливая экран
знаками вопроса.
* libc: kbd_raw_keypad_as_ext() — kbd_raw_sync переносит голые коды
нумпада в EXT-половину карты. Лечит залипание стрелок (потерянный
префикс E0 сажал make в PLAIN как код нумпада, и снять его было нечем),
заодно нумпад стал управлением: 7/8/9, 4/6, 2 и 5 = вниз.
* pop_pace.c — цикл ожидания луча зовёт тот же idle-хук, что и
gfx_wait_vsync: без этого F10 в геймплее не работал вовсе.
* pop_hof.c — Esc в таблице рекордов отменяет запись (расхождение с
оригиналом записано в docs/impl_diff.md).
* Экран версии показывается только через Menu/Settings/About: стартовый
показ и Ctrl+V убраны, мёртвый код снят.
* sprpop_cold.c — pop_start_level зовёт pop_hp_invalidate: после Ctrl+A с
выросшим за уровень максимумом полоса HP моргала между страницами.
Разбор всех четырёх багов — в applications/PoP/roomtest/BUGS_CLOSED.md
(FINAL-BANKCALL, FINAL-HOF-GARBAGE, KBD-ARROW-PHANTOM, F10-GAMEPLAY),
правило про банки — в applications/SprPoP/CLAUDE.md.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
206 lines
18 KiB
Markdown
206 lines
18 KiB
Markdown
# SprPoP — правила приложения
|
||
|
||
Порт Prince of Persia (DOS/Apple II) на Sprinter Sp2000 поверх нашего
|
||
sprinter-cc / libc / libbgi. Действуют правила корневого `CLAUDE.md`
|
||
(сборка, libc, ABI, MAME-автотест); ниже — только специфика порта.
|
||
Общение и комментарии — на русском.
|
||
|
||
Приложение **автономно**: код, оригинальные данные, конверторы и сборка
|
||
лежат внутри этой папки. Наружу оно знает ровно один путь — корень
|
||
тулчейна, `SPRINTER_ROOT` (по умолчанию `../..`).
|
||
|
||
История: SprPoP выделен из `applications/PoP/roomtest` 2026-08-26. Та
|
||
папка ЗАМОРОЖЕНА и остаётся архивом (закрытые задачи и баги с разбором
|
||
корней — `../PoP/roomtest/TASKS_CLOSED.md` и `BUGS_CLOSED.md`; там же
|
||
исполненные планы в `../PoP/docs/`). Разработка идёт только здесь.
|
||
|
||
## Главное правило: SDLPoP — источник истины. Сначала читай, потом кодь
|
||
|
||
**`assets/orig/SDLPoP/src/` (github.com/NagyD/SDLPoP, GPLv3) — ЕДИНСТВЕННЫЙ
|
||
авторитетный источник того, как оригинальный движок это делает.** Правило
|
||
без исключений:
|
||
|
||
1. **Перед реализацией ЛЮБОЙ функции** (движение, коллизия, окклюзия,
|
||
падение, loose-полы, стражники, отрисовка, тайминги, любые числовые
|
||
константы) — СНАЧАЛА найди и прочитай соответствующий код в SDLPoP и
|
||
портируй по нему. Не пиши по памяти, не выводи логику «из общих
|
||
соображений», не угадывай значения — это источник багов, которые потом
|
||
ловятся в MAME часами.
|
||
2. **По любому вопросу «как в оригинале должно быть»** (что окклюдит что,
|
||
в каком порядке слои, когда меняется тайл, какая скорость/задержка, что
|
||
делает такой-то кадр анимации) — ответ ищи в SDLPoP, а не строй гипотезу.
|
||
Если не нашёл — это повод копать дальше в исходнике, а не додумывать.
|
||
3. Расхождение нашей реализации с SDLPoP — по умолчанию **баг у нас**, пока
|
||
не доказано обратное (наша платформа/ABI требует отличия — тогда явно
|
||
зафиксировать почему в комментарии **и записью в `docs/impl_diff.md`**:
|
||
что делает оригинал, что делаем мы, чем платим, что проверять при
|
||
регрессе).
|
||
|
||
Карта сегментов: `seg005` control-диспетчер, `seg006` play_kid/коллизия/
|
||
seqtbl, `seg007` mob/loose/падающие объекты, `seg008` отрисовка тайлов/
|
||
слои/окклюзия, `seg009` чтение ресурсов. Слои окклюзии у нас = слои SDLPoP.
|
||
См. memory `pop_check_sdlpop_first`.
|
||
|
||
Вторичные референсы (когда в SDLPoP непонятно или нужен другой ракурс)
|
||
лежат вне приложения, в `../PoP/`: `Prince-of-Persia-Apple-II/` —
|
||
оригинальный 6502-исходник 1989 (Мехнер), `PR/` — Princed Resources,
|
||
`mininim/` — независимая реализация. Все они — **справочник логики,
|
||
структур и констант**, но НЕ код для копирования (лицензии несовместимы,
|
||
наш ABI другой): читаем и переписываем под наш движок.
|
||
|
||
## Доски
|
||
|
||
**Что в работе сейчас — `docs/TASKS_OPEN.md`** (приоритеты, критерии
|
||
готовности). Баги — `docs/BUGS_OPEN.md` (только ОТКРЫТЫЕ).
|
||
|
||
Правило разнесения: как только задача или баг закрыт — запись целиком
|
||
переезжает в архив roomtest (`../PoP/roomtest/TASKS_CLOSED.md` /
|
||
`BUGS_CLOSED.md`), а в открытом файле остаётся ссылка. Перед заведением
|
||
нового бага грепни архив по симптому: корень мог уже разбираться.
|
||
Открытые файлы обязаны читаться целиком за раз.
|
||
|
||
Индекс документации с отметками актуальности — **`docs/README.md`**,
|
||
начинать чтение оттуда.
|
||
|
||
## Сборка и запуск
|
||
|
||
```
|
||
make собрать build/sprpop.exe и разложить build/
|
||
make hdd + образ build/hdd/sprpop.chd
|
||
make mame-link однократно: подставить образ в MAME (см. ниже)
|
||
make -C tests/host модульные тесты движка под ucsim_z80 (секунды, без MAME)
|
||
make resources перегенерировать ресурсы из assets/orig/
|
||
make music-mp3 музыка из другого набора (flac|mp3|ogg|mt32)
|
||
make clean снести build/ (ассеты не трогает)
|
||
make distclean clean + снести assets/packed/ (вернуть — make resources)
|
||
```
|
||
|
||
`MEMORY=huge`, `--gfx 256`, 11 банков кода в W3. Отладочные ключи сборки:
|
||
`make LEVEL=9` (стартовый уровень), `make ROOM=15 POS=2` (стартовая комната
|
||
и тайл `row*10+col`), `make PROF=1` (профиль фаз кадра полосами бордюра),
|
||
`make ALLOCS=100000` (плотная упаковка регистров — для замеров размера).
|
||
Сравнивать занятость банков можно **только при одном `ALLOCS`**.
|
||
|
||
**Ловушка `make mame-link`:** он подменяет `mame/v306/IMG/test_hdd.chd`
|
||
символьной ссылкой на наш образ. Пока ссылка стоит, `make hdd` любого
|
||
другого приложения (в т.ч. замороженной roomtest) писал бы через неё в
|
||
наш `build/hdd/` — если понадобится собрать чужой образ, ссылку сначала
|
||
убрать.
|
||
|
||
## Раскладка папки
|
||
|
||
| Папка | Что |
|
||
|---|---|
|
||
| `src/` | рукописный C: главный цикл `sprpop.c`, холодная половина `sprpop_cold.c`, движок `pop_*.c` |
|
||
| `gen/` | генерируемые заголовки (индексы архивов, таблицы кадров, шрифт, палитры). **Руками не править** — их печатают упаковщики; лежат в репозитории, потому что без них `src/` не собрать |
|
||
| `assets/orig/` | оригинальные данные (SDLPoP, MSDOS, записи музыки). Вне репозитория — копирайт; что и откуда взять, написано в `assets/orig/README.md` |
|
||
| `assets/packed/` | то, что ложится на диск игры, уже в раскладке диска (`BG/`, `KID/`, …). В репозитории — иначе из чистого клона не собрать |
|
||
| `tools/` | конверторы ресурсов. Раскладку путей знает ОДИН файл — `tools/paths.py`; менять пути нужно там, а не в отдельных упаковщиках |
|
||
| `docs/` | планы, доски, справочники; `docs/PoP/` — форматы ресурсов оригинала |
|
||
| `tests/host/` | модульные тесты движка под ucsim_z80 |
|
||
| `build/` | выход: `sprpop.exe`, каталоги ресурсов, `hdd/`, промежуточные `atl/` |
|
||
|
||
Музыка собирается из одного из четырёх наборов записей
|
||
(`assets/orig/PoP1_DOS_music/`): `flac` по умолчанию, плюс `mp3`, `ogg` и
|
||
`mt32` (исполнение Roland MT-32 — звучит иначе, длина треков другая). Набор
|
||
входит в имя stamp'а, поэтому `make music-mt32` пересобирает музыку сам, без
|
||
`-B`; разовая сборка без смены умолчания — `make MUSIC_FMT=ogg resources-music`.
|
||
|
||
Ресурсы: упаковщик пишет промежуточные атласы в `build/atl/<набор>/`, из них
|
||
Makefile склеивает архивы прямо в `assets/packed/<КАТАЛОГ>/`, а `make`
|
||
копирует их в `build/`. Порядок файлов в списках `*_ATL` Makefile — это
|
||
порядок индексов внутри архива, и он же напечатан в `gen/*_arc.h`: менять
|
||
порядок нельзя, не перегенерировав заголовок.
|
||
|
||
**Каноническая спецификация форматов `.DAT`** —
|
||
`docs/PoP/POP-DAT-FormatSpecifications.pdf` (грепаемая копия — `.txt`):
|
||
первоисточник Princed для DAT v1.0. Наши разборы
|
||
(`docs/PoP/MSDOS_RESOURCE_FORMAT.md`, `docs/PoP/APPLEII_RESOURCE_FORMAT.md`) —
|
||
практические заметки; при расхождении источник истины — спецификация.
|
||
|
||
## Проверка в MAME
|
||
|
||
Только через `toolchain/mame_interactive.py` или MCP-мост `mame-z80`
|
||
(memory `mame_autotest`, `mame_mcp_bridge`, `mame_hdd_test_disk`).
|
||
Пересобрал образ → MAME ОБЯЗАН полный рестарт (`mame_hdd_rebuild_restart`).
|
||
|
||
**Тайминги моста** (не ждать дольше, см. `docs/mame-autotest.md` §10):
|
||
старт `run_bridge.sh` → 6 с → `go` → 8 с → `keyseq d:{ENTER}` +
|
||
`keyseq sprpop{ENTER}` → 5 с → программа работает.
|
||
|
||
Отладочные тумблеры в живой сессии (`src/sprpop.c`): **1** — заморозить
|
||
кадр, **2** — продолжить (разбор позы/окклюзии); **ESC** — выход. Читы
|
||
(`src/pop_cheat.h`): **K** — убить стража, **I** — бессмертие,
|
||
**Shift+L** — следующий уровень, **+/−** — обход комнат (`ROOMNAV`).
|
||
Не чит, а штатная клавиша оригинала: **Ctrl+S** — вкл/выкл звук (пурпурная
|
||
палочка в борте = звука нет). Целевая раскладка управления, к которой
|
||
подгоняем SprPoP, — `docs/keys.txt`.
|
||
|
||
Логику, которую можно проверить без железа, покрывать в `tests/host/`
|
||
(обвязка — `testkit/`). MAME остаётся для отрисовки, банков, таймингов и
|
||
клавиатуры.
|
||
|
||
## Модули
|
||
|
||
| Файл | Роль (порт SDLPoP) |
|
||
|------|--------------------|
|
||
| `sprpop.c` | Главный цикл: дабл-буфер (2 страницы + флип на vsync), порядок tick→draw. |
|
||
| `sprpop_cold.c` | Холодная половина главного цикла (банк 8): старт уровня, отладочная навигация. |
|
||
| `pop_tile.c/.h` | Общие ЛИСТЬЯ слоя фона — РЕЗИДЕНТ W1: блит куска атласа, чтение тайла комнаты, `tile_table`, метка «фон трогали вот здесь» (`pop_cd_touch`). Резидент потому, что их зовут обе половины слоя фона из разных банков, а `const`-таблицы банка из чужого банка не видны. |
|
||
| `pop_bg.c/.h` | ГОРЯЧАЯ половина слоя фона (банк 2, каждый кадр): fore-окклюзия поверх ЛЮБОГО персонажа (`seg003` redraw_at_char/char2 — один проход на всех Char), оверлеи кромки, кладка стены (`wall_pattern`), клип. `pop_bg.h` — публичный API ВСЕГО слоя. |
|
||
| `pop_room.c` | ХОЛОДНАЯ половина слоя фона (банк 7, раз на комнату/событие): полная отрисовка комнаты (`seg008` draw_tile), точечные перерисовки тайлов (пики, ворота, кнопки, дверь уровня, дрожащие плиты), падающие куски loose (`seg007` mob), загрузка атласов. |
|
||
| `_pop_bg.h` | Внутренний контракт между половинами: общее состояние + три тонкие `__banked`-обёртки; там же почему рез прошёл именно здесь и сколько стоит. |
|
||
| `pop_kid.c/.h` | Анимация/движение: интерпретатор seqtbl `play_seq` + frame_table (`seg006`), окна `Char` (loadkid/loadshad), атласы Кида. |
|
||
| `pop_cdraw.c/.h` | ОТРИСОВКА персонажей — одна на всех Char (порт `add_kid_to_objtable`/`add_guard_to_objtable`): спрайт, clip_char, брызги, клинок, heal; полоса HP и палитры соперника. Там же ПРОПУСК неизменившегося кадра (`pop_char_skip_mask`). |
|
||
| `pop_ctrl.c/.h` | Управление: диспетчер `control()` (`seg005`) + ввод `read_user_control` (`seg006`) поверх `<kbd_raw.h>`. |
|
||
| `pop_map.c/.h` | Коллизия с картой + физика падения/приземления/стены + loose-полы (`seg005/006/007`). |
|
||
|
||
## Банки: две мины, которых компилятор не видит
|
||
|
||
Обе стреляют молча, обе поймали нас 2026-08-27 (разбор — в
|
||
`../PoP/roomtest/BUGS_CLOSED.md`, поиск «FINAL-BANKCALL»).
|
||
|
||
**1. Прямой вызов в чужой банк.** Трамплин SDCC выбирает по `__banked`
|
||
в ОБЪЯВЛЕНИИ, а не по тому, где функция лежит. Функция без пометки
|
||
зовётся прямым `call` — верно ровно пока вызывающий и вызываемый в одном
|
||
банке. Перенесли модуль ради разгрузки банка — и тот же `call` уходит в
|
||
пустой хвост чужого банка, процессор ползёт по 0xFF до 0x0000, DSS убивает
|
||
процесс. Ловит `toolchain/check_bank_calls.py`, она встроена в сборку и
|
||
ВАЛИТ её (проверено намеренной поломкой).
|
||
|
||
**2. Указатель на данные своего банка, отданный в чужой.** Обратная
|
||
сторона: пока обе стороны в одном банке, указатель на литерал или
|
||
const-таблицу работает; пометили функцию `__banked` — и трамплин на время
|
||
вызова переключает W3, а указатель показывает уже в чужой банк. Так экран
|
||
таблицы рекордов залило знаками вопроса: в `pop_text_draw_mapped` (банк 9)
|
||
уходил литерал `"_"` из банка 10.
|
||
|
||
**Правило:** всё, что уходит указателем в другой банк, обязано лежать в
|
||
W2 — стек, `_DATA` или копия. `const`-таблицы и строковые литералы
|
||
банкового модуля наружу отдавать нельзя (то же, что уже записано про
|
||
`pop_tile.c`). Вторая проверка скрипта предупреждает о явных случаях, но
|
||
она эвристическая и гарантией не является.
|
||
|
||
## Порядок слоёв в кадре (важно для окклюзии)
|
||
|
||
`sprpop.c` каждый тик рисует в СКРЫТУЮ страницу: `pop_char_heal` (стереть
|
||
прошлый кадр — по слоту на персонажа) → `kid_tick` → `pop_phys_tick` →
|
||
`pop_loose_tick` (loose СЛОЙ ФОНА — ДО персонажей, чтобы они были поверх
|
||
плиты) → `pop_char_draw`/`pop_char_fore` для соперника и Кида (кто позже —
|
||
тот поверх, порядок задаёт обход тайлов) → `pop_room_clip_borders` (чистка
|
||
бортов, гейт по флагу — только в кадрах падения) → флип на vsync.
|
||
|
||
**Дабл-буфер:** у каждой страницы своя видео-ОЗУ и теневая ОЗУ-копия; heal
|
||
берёт чистый фон из копии ТОЙ страницы, в которую рисуем. Любой
|
||
динамический элемент (Kid, loose-плита, падающий кусок) обязан чистить свой
|
||
прошлый кадр на КАЖДОЙ из двух страниц — иначе остаток виден через кадр как
|
||
**мерцание**. Типовой источник багов «остаётся кусочек» (memory
|
||
`pop_fall_debug_baseline`).
|
||
|
||
## Ключевые memory
|
||
|
||
`pop_port_project`, `pop_check_sdlpop_first`, `pop_banking_architecture`,
|
||
`pop_background_strategy`, `pop_kid_plan`, `pop_hang_state`, `pop_fore_layer`,
|
||
`pop_fall_debug_baseline`, `accel_vertical_copy`, `kbd_raw_fifo_drain`,
|
||
`pop_perf_registry`.
|