# 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`), а в открытом файле остаётся ссылка. Перед заведением нового бага грепни архив по симптому: корень мог уже разбираться. Открытые файлы обязаны читаться целиком за раз. **CHANGELOG. Ставишь релизный тег — добавь запись в `CHANGELOG.md`.** Версия, дата И КОММИТ в заголовке берутся ИЗ GIT, руками не проставляются: git for-each-ref \ --format='%(refname:short) | %(creatordate:short) | %(*objectname:short)' \ refs/tags/<тег> Заголовок = `<имя тега> — <дата> — <коммит>`. Коммит обязателен: тег можно передвинуть или переименовать, а ревизия, на которой релиз собран, должна оставаться в записи однозначно. Берётся именно `*objectname` (дереференс аннотированного тега), а не `objectname` — последний вернёт хеш самого объекта тега, а не коммита. В запись идёт только то, что видно ИГРОКУ или меняет сборку/запуск, — по одному пункту «симптом -> причина одной фразой -> хеш коммита». Полный разбор живёт в сообщении коммита, дублировать его сюда не надо. Индекс документации с отметками актуальности — **`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`) поверх ``. | | `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`.