Files
Sprinter-SDCC/applications/SprPoP/CLAUDE.md
T
snark13 aaa480d0f2 SprPoP: в CLAUDE.md — игра на образе лежит в D:\GAMES\SPRPOP
С обобщения HDD-сборки (ea8efdb) один образ рассчитан на несколько
приложений, и EXE переехал в подкаталог.  В инструкции по запуску это не
отразили, поэтому старая последовательность (`d:` + `sprpop`) отвечает
`Bad command or file name`, а `dir D:\` показывает только каталог GAMES —
на это можно потратить время впустую.

Записаны и сам путь, и исправленная цепочка клавиш для моста.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018T2k4VZrSyERwk6H97Sfi1
2026-09-02 17:41:40 +03:00

224 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`).
**Игра лежит на образе в `D:\GAMES\SPRPOP\`**, а НЕ в корне диска (так с
обобщения HDD-сборки, коммит ea8efdb — один образ рассчитан на несколько
приложений). `dir D:\` показывает только каталог `GAMES`; запуск из корня
отвечает `Bad command or file name`.
**Тайминги моста** (не ждать дольше, см. `docs/mame-autotest.md` §10):
старт `run_bridge.sh` → 6 с`go` → 8 с`keyseq d:{ENTER}` +
`keyseq cd games\sprpop{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`.