Files
Sprinter-SDCC/applications/SprPoP/CLAUDE.md
T
snark13 df5071a967 SprPoP: музыка без перелинковки — длины и длительности уехали на диск
Часть I плана music_runtime_index_plan.md (MI0..MI5).  gen/pop_music_tbl.h
и gen/pop_music_ticks.h УДАЛЕНЫ: длины треков и длительности реплик
читаются из MUS/mus.idx (формат PMI1, tools/pop_idx.py, тесты в
make test-tools).  Один и тот же sprpop.exe работает с любым из четырёх
наборов записей — sha256 бинарника при смене MUSIC_FMT не меняется.

ГДЕ ЖИВЁТ ИНДЕКС.  228 байт таблицы в W2 не положить (свободной кучи там
порядка двух сотен), поэтому индекс лежит в одной странице EMM, а в
резиденте от него два байта.  Данные в странице — со смещения 0x100:
gfx_w0_page_prepare пишет в неё стабы прерываний (0x38 и 0x66), и с нуля
они попали бы прямо в записи id 10 и 21.  Со смещением работает штатная
защита, а не запрет прерываний (тот же приём, что CFG_BASE в
pop_config.c).  Число страниц в индексе не хранится — считается из blocks,
чтобы не разъехалось.

ПАУЗА КОНЦА УРОВНЯ — СОСТОЯНИЕМ, А НЕ СЧЁТЧИКОМ.  pop_endmus_left и
POP_MUS_TICKS_32/41 удалены; главный цикл ждёт pop_music_active() —
«заявка лежит, идёт загрузка или трек звучит».  Одного busy мало: между
заявкой и первой нотой 190-230 мс (замер в sound_plan §9).  Прежний
счётчик закрывал эту щель ценой зависимости EXE от набора и жёсткого
делителя /4, который врал в режимах FAST/FASTEST (там логический кадр 3
кадра луча, а не 4).  Побочно исправилось расхождение с SDLPoP: при
выключенном звуке заявка не кладётся, и уровень меняется сразу, как в
оригинале (seg006:651 + seg003:387) — раньше игра держала пройденный
уровень лишние 12 секунд в тишине.

PV-СЦЕНА — на четырёх якорях (8 байт статики), которые считаются из
индекса при входе в сцену; прежние выражения шкалы не изменились.  План
предлагал протащить структуру времён через пять функций — для сцены,
которая идёт раз за запуск, это того не стоит.

ПАМЯТЬ.  За обе фазы резидент не вырос, а освободился: _CODE 23865 ->
23544, куча 239 -> 256 Б.  Банк 9 похудел на 118 Б (ушла pop_mus_tbl из
rodata), банк 11 — на длительности реплик.

ПРОВЕРЕНО В MAME: exe побайтово одинаков для flac и mt32; все 22 трека в
индексах различаются, и контрольные значения совпали с предсказанными
планом (m41 732->685, m50 831->867, m53 985->1044, m56 9865->10462
блоков, 78->82 страницы); на mt32 PV-сцена проходит целиком по его
длительностям; без mus.idx музыки нет, эффекты работают, игра проходима.

НЕ ПРОВЕРЕНО: потоковый m56 на 82 страницах — до финала надо дойти в
игре.  Единственный оставшийся пункт приёмки, отмечен в sound_plan §11.5.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011MsUsEFAQfsjjQpJ7RtKVY
2026-08-31 17:17:59 +03:00

218 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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) — ЕДИНСТВЕННЫЙ
авторитетный источник того, как оригинальный движок это делает.** В
репозитории его нет: заполняется `make fetch` (`make fetch-check` — что уже
на месте). Правило без исключений:
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 test-tools тесты упаковщиков на хосте (форматы, которые читает Z80)
make fetch скачать внешние данные в assets/orig/ (SDLPoP + музыка)
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 — необязательно). Вне репозитория — копирайт; вместо них в репозитории лежит `tools/fetch_orig.py`, который их качает (`make fetch`). Правила — в `assets/orig/README.md` |
| `assets/packed/` | то, что ложится на диск игры, уже в раскладке диска (`BG/`, `KID/`, …). В репозитории — иначе из чистого клона не собрать |
| `tools/` | конверторы ресурсов. Раскладку путей знает ОДИН файл — `tools/paths.py`; менять пути нужно там, а не в отдельных упаковщиках. Адреса ВНЕШНИХ источников — так же в одном: `tools/fetch_orig.py` |
| `docs/` | планы, доски, справочники; `docs/PoP/` — форматы ресурсов оригинала |
| `tests/host/` | модульные тесты движка под ucsim_z80 |
| `build/` | выход: `sprpop.exe`, каталоги ресурсов, `hdd/`, промежуточные `atl/` |
Звуковые ЭФФЕКТЫ по умолчанию берутся из SDLPoP (`SND_SRC=sdlpop`) — сборка
обязана работать без оригинального дистрибутива DOS. У кого лежит
`assets/orig/MSDOS/`, включает его набор явно (`make SND_SRC=msdos`): там
оцифровка полнее — в SDLPoP пуст звук 48 `spiked` (насаживание на пики).
Разбор — `docs/sound_plan.md`.
Музыка собирается из одного из четырёх наборов записей
(`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_sfx.c` + `pop_sfx_cold.c` | Звук: насос CBL в резиденте + холодная загрузка набора. Раскладка эффектов НЕ компилируется в EXE — читается с диска (`SND/snd.idx`, формат `PSI1`); см. `docs/sound_plan.md` §10. |
| `pop_music.c` | Музыка (банк 9): загрузка треков, кольцо для финальной темы. Длины и длительности — из `MUS/mus.idx` (EMM-страница + accessor `pop_music_info`), в EXE их нет; см. `docs/sound_plan.md` §11. |
| `pop_snd_tbl.h` + `pop_snd_data.c` | Тип записи набора и инварианты (рукописный заголовок) + резидентные `pop_snd_tbl`/`pop_snd_page`/`pop_snd_pages`. Размер записи 5 байт — часть дискового контракта, проверяется статически. |
| `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`.