Files
Sprinter-SDCC/applications/SprPoP/CLAUDE.md
T
snark13 31b82661eb SprPoP: автономное приложение, выделенное из roomtest
Порт PoP переехал в applications/SprPoP — приложение, которое собирается
само: код, оригинальные данные, конверторы ресурсов и сборка внутри одной
папки.  Наружу знает единственный путь — корень тулчейна (SPRINTER_ROOT,
по умолчанию ../..).  applications/PoP/roomtest ЗАМОРОЖЕНА и остаётся
архивом закрытых задач, багов и исполненных планов.

Скопировано из applications/PoP/roomtest@4b74478.  Перенос проверен
побайтово: собранный sprpop.exe совпал с roomtest.exe того же коммита,
все 39 дисковых ресурсов и все 16 генерируемых заголовков — тоже, host-
тесты зелёные (15/15).

Раскладка:
  src/           рукописный C (roomtest.c -> sprpop.c)
  gen/           генерируемые заголовки, в репозитории
  assets/orig/   оригинальные данные игры, вне репозитория (копирайт)
  assets/packed/ то, что ложится на диск, в раскладке диска
  tools/         конверторы; все пути — в одном tools/paths.py
  build/         выход: exe, каталоги ресурсов, hdd/, промежуточные atl/

Сборка ресурсов: assets/packed и gen — версионируемые ВХОДЫ, а не то, что
пересчитывается каждым make.  Автоматика построена на ОТСУТСТВИИ файла, а
не на таймстемпах: git не хранит времена, и в свежем клоне сравнение по
времени превращалось бы в лотерею.  Недостающий ресурс или заголовок
чинится сам, рекурсивным вызовом в ветку генерации.

Музыка собирается из любого из четырёх наборов записей (make music-mp3,
music-mt32, ...); набор входит в имя stamp'а, поэтому смена набора сама
делает музыку устаревшей.  Длины реплик больше не захардкожены: упаковщик
печатает их в gen/pop_music_ticks.h, и шкала сцены выражена через них —
иначе mt32 (реплики на 6% длиннее) молча ломал катсцену.

Тулчейн: в app.mk два обратносовместимых крючка (SRC_DIR/BUILD_DIR),
HDD_IMG стал ?=; команда сборки roomtest не изменилась.  Корневой
make host-tests переключён на SprPoP.

Подгонка тайминга катсцены с принцессой (PV_MAGIC_LEAD): сцена
render-bound и идёт ~49 тиков/с вместо 60, из-за чего кода реплики
приходила раньше молнии.  Это обход, а не лечение; разбор с замерами —
docs/BUGS_OPEN.md, записи SND-PACE-DEAD, PV-RENDER-BOUND, MUS-LEFT-TEAR.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 12:12:28 +03:00

16 KiB
Raw Blame History

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: менять порядок нельзя, не перегенерировав заголовок.

Каноническая спецификация форматов .DATdocs/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).

Порядок слоёв в кадре (важно для окклюзии)

sprpop.c каждый тик рисует в СКРЫТУЮ страницу: pop_char_heal (стереть прошлый кадр — по слоту на персонажа) → kid_tickpop_phys_tickpop_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.