applications/PoP: порт Prince of Persia — PoC (roomtest) + пайплайн

Порт PoP на Sprinter.  Текущий PoC — applications/PoP/roomtest/:
комната 1 (фон-композиция тайлов) + Kid с управлением на raw-клавиатуре
и коллизией с картой.

- roomtest — pop_bg (фон), pop_kid (спрайты Kid, column-major флип,
  seqtbl-анимация), pop_ctrl (порт control() PoP на held-state
  kbd_raw), pop_map (коллизия seg004/005: бег/стоп у стены,
  падение/приземление, отскок seq_47, вертикальный прыжок K4.1).
  MEMORY=small (DATA сразу за CODE, ~23КБ кода не лезет в huge).
- toolchain (PoP) — pop_pack_kid/pop_pack_bg/render_room/extract —
  распаковка res-графики MSDOS в атласы + композиция комнат.
- toolchain/ (корень) — make_hdd.sh (быстрый HDD-тест вместо FDD),
  png_strip.py / room_compose.py (ассет-пайплайн).
- docs — PORT_PLAN, KID_PLAN, форматы ресурсов (Apple II / MSDOS / DAT).
- bgtest/coltest/poc — ранние PoC (фон, коллизия, первый прототип).

.gitignore: build-артефакты applications/*/*/*; исключены внешние
референс-репозитории (SDLPoP/mininim/PR/Apple-II — свои git-клоны) и
оригинальные game-данные MSDOS/ (копирайт, только для реверса форматов).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 17:51:08 +03:00
parent 484b18d10c
commit cd8d566d82
196 changed files with 6630 additions and 0 deletions
@@ -0,0 +1,283 @@
# Формат ресурсов Prince of Persia (Apple II, оригинальные исходники 1989)
Источник — официально опубликованные Джорданом Мехнером исходники
(`Prince-of-Persia-Apple-II/`, 6502-ассемблер). В отличие от DOS-версии, здесь
формат восстановлен **напрямую по коду**, а не по догадкам о байтах —
уверенность высокая везде, где указана ссылка на конкретный файл/строки.
---
## 1. Формат уровня (`01 POP Source/Levels/LEVEL0`…`LEVEL14`, 2304 байта)
Файлы уровня — это побайтовый дамп структуры `blueprnt`, которая грузится по
фиксированному адресу `$b700` (`EQ.S:28`) и объявлена как `dum blueprnt` в
`EQ.S:258-266`. Никакого отдельного заголовка файла нет — это чистый образ
структуры в памяти:
| Поле | Размер | Смещение в файле | Описание |
|---|---|---|---|
| `BLUETYPE` | 720 Б | 0719 | 24 экрана × 30 тайлов: тип объекта/тайла |
| `BLUESPEC` | 720 Б | 7201439 | 24 экрана × 30 тайлов: доп. байт состояния объекта |
| `LINKLOC` | 256 Б | 14401695 | Таблица связей нажимных плит/дверей, часть 1 |
| `LINKMAP` | 256 Б | 16961951 | Таблица связей, часть 2 |
| `MAP` | 96 Б | 19522047 | 24 экрана × 4 байта: граф соседних экранов |
| `INFO` | 256 Б | 20482303 | Метаданные уровня: старт Кида, стражников и т.д. |
Сумма: 720+720+256+256+96+256 = **2304** — точно совпадает с размером файла,
что подтверждает: это чистый дамп структуры, без обёртки.
### 1.1 Сетка тайлов (`BLUETYPE` / `BLUESPEC`)
Каждый экран — ровно **30 тайлов** (10 столбцов × 3 ряда): подтверждено
таблицами `BlockTable`/`BlockEdge` (`TABLES.S:74-154`) и логикой перехода
между экранами в `CTRLSUBS.S:218-234` (при переходе через край экрана
`tempblockx` меняется на ±10, `tempblocky` — на ±3).
Функция `CALCBLUE` (`GRAFIX.S:1757-1784`) вычисляет для экрана 1–24:
`BlueType = blueprnt + (screen-1)*30`, `BlueSpec = BlueType + 24*30`,
используя таблицу `Mult30` (`TABLES.S:131-140`).
Байт `BLUETYPE` упакован битовыми полями (`EQ.S:484-486`):
```
бит 7-6: secmask (%11000000) — назначение не установлено по доступному коду
(возможно, служебное поле редактора)
бит 5: reqmask (%00100000) — флаг "необходимая опорная плитка"
(проверяется в BREAKLOOSE, MOVER.S:395-397)
бит 4-0: idmask (%00011111) — тип тайла/объекта, 0-29
```
Перечень 30 типов объектов (`MOVEDATA.S:8-37`):
```
0 space 8 pillarbottom 16 exit 24 window2
1 floor 9 pillartop 17 exit2 25 archbot
2 spikes 10 flask 18 slicer 26 archtop1
3 posts 11 loose 19 torch 27 archtop2
4 gate 12 panelwof 20 block 28 archtop3
5 dpressplate 13 mirror 21 bones 29 archtop4
6 pressplate 14 rubble 22 sword
7 panelwif 15 upressplate 23 window
```
Проверено вручную на дампе начала `LEVEL1` (`00 00 00 21 01 21 21 21 34 34
33 33 21 23 00 34 14 14 14 34 14 34 34 2e 23 0b 01 21 34`) — например,
`0x33 → id=0x13=19 (torch)`+reqmask, `0x34 → id=20 (block)`+reqmask —
декодирование по таблице сходится чисто.
`BLUESPEC` — доп. байт, чья семантика зависит от типа тайла (единой схемы
нет, разбирается объект-специфичным кодом):
- **gate** (дверь, `FRAMEADV.S:2222-2234`): на диске — маленький enum (1 =
начинает открытой сверху, 2 = снизу, …), который через `initsettings`
(`FRAMEADV.S:22-23`, диапазон `gminval=0`..`gmaxval=188`, из
`MOVEDATA.S:56-57`) при инициализации уровня превращается в живой счётчик
"высоты двери" 0–188.
- **loose** (шаткая плитка, `FRAMEADV.S:2224-2237`): при инициализации всегда
принудительно обнуляется, независимо от значения на диске.
- **flask** (зелье, `FRAMEADV.S:2226,2239-2246`): значение×32 выбирает
цвет/тип зелья.
- **spikes** (шипы, `MOVER.S:365-382`, константы `spikeExt=5, spikeRet=9` в
`MOVEDATA.S:45-46`): 0 = безопасно/убраны, 1–8 — кадр анимации
выдвижения/втягивания, `$FF` = навсегда заклинило (тело наколото).
- **pressplate/upressplate** (нажимные плиты, `MOVER.S:425-464`,
`FRAMEADV.S:2059-2098`): значение — это **индекс в цепочке связей**
`LINKLOC`/`LINKMAP` (см. ниже); младшие 5 бит `LINKMAP` по этому индексу
одновременно служат счётчиком таймера плиты (0–31), определяющим
состояние "поднято/опущено".
### 1.2 `LINKLOC` / `LINKMAP` — граф триггеров (нажимные плиты → двери и т.п.)
Два параллельных массива по 256 байт кодируют цепочки "нажатие плиты X →
сработать объект на экране S, блок B". Восстановлено из `MOVER.S:506-537`
(цикл `trigger`) и `MOVER.S:1549-1581` (`gettimer/chgtimer/getloc/
getlastflag/getscrn`):
```
LINKLOC[i]: бит 7 = флаг "последнее звено цепочки"
биты 6-5 = младшие 2 бита номера целевого экрана
биты 4-0 = номер целевого блока (0-29); $FF = "никуда не привязано"
LINKMAP[i]: биты 7-5 = старшие 3 бита номера целевого экрана
(вместе с LINKLOC биты 6-5 → полный номер экрана 0-31)
биты 4-0 = таймер обратного отсчёта плиты (0-31, значим только
по индексу самой плиты)
```
`BLUESPEC` плиты хранит индекс `i` её *первого* звена; `getlastflag` идёт
вперёд (`inc linkindex`), пока не встретит бит 7 в `LINKLOC`. Сверено на
`LEVEL1`: байт по смещению 1440 (`0x89 = 10001001` → флаг конца цепочки,
целевой блок 9) и параллельно байт по смещению 1696 (`0x60 = 01100000`
старшие биты номера экрана) — согласуется с этой раскладкой. Заполнены
реально используемые уровнем звенья, остальное — "мусорные" повторяющиеся
байты-заполнители.
### 1.3 `MAP` — граф соседних экранов
24 записи × 4 байта = 96 байт: для каждого экрана (1–24)
`MAP[(scrn-1)*4 + 0..3] = левый, правый, верхний, нижний соседние экраны`,
читается через `GETLEFT/GETRIGHT/GETUP/GETDOWN` (`CTRLSUBS.S:244-274`,
индексация `MAP-4..MAP-1,x` при `x = scrn*4`). Экран `0` зарезервирован как
"нет экрана" (проверка `beq ]rts` в этих же процедурах).
### 1.4 `INFO` — метаданные уровня (256 байт, база = смещение файла 2048)
Объявлено как `dum INFO` в `EQ.S:272-288`:
| Смещение (от начала INFO) | Поле | Размер |
|---|---|---|
| 0 | "число экранов + 1" (используется в `SETINITIALS`, `SUBS.S:1441-1445`) | 1 |
| 1–63 | резерв/не используется | 63 |
| 64 | `KidStartScrn` | 1 |
| 65 | `KidStartBlock` | 1 |
| 66 | `KidStartFace` (направление; при загрузке инвертируется XOR `$ff`, `SUBS.S:1516-1518`) | 1 |
| 67 | заполнитель | 1 |
| 68 | `SwStartScrn` (стартовый экран меча) | 1 |
| 69 | `SwStartBlock` | 1 |
| 70 | заполнитель | 1 |
| 7194 | `GdStartBlock[1..24]` — стартовый блок стражника на экране; **≥30 = "стражника нет"** (`AUTO.S:1832-1834`, `SUBS.S:1677-1679`) | 24 |
| 95118 | `GdStartFace[1..24]` (86 = "стражника нет", см. `ShadFace cmp #86` по всему `AUTO.S`) | 24 |
| 119142 | `GdStartX[1..24]` — пересчитывается заново из блока при старте уровня, значение на диске почти не используется (`SUBS.S:1674-1690`) | 24 |
| 143166 | `GdStartSeqL[1..24]` | 24 |
| 167190 | `GdStartProg[1..24]` — "программа"/поведение ИИ стражника | 24 |
| 191214 | `GdStartSeqH[1..24]` — обнуляется при старте (`SUBS.S:1685-1686`) | 24 |
| 215–255 | резерв/не используется | 41 |
Проверено на `LEVEL1`: байт по смещению файла 0x800 = `0x18`=24 (число
активных экранов = 23+1); по смещению 0x840 — `01 00 ff 00 00 00 ff 1e 1e
11 1e 1e ...``KidStartScrn=1, KidStartBlock=0, KidStartFace=$FF,
SwStartScrn=0, SwStartBlock=0`, далее 24 байта `GdStartBlock`, в основном
`0x1e`(30, "нет стражника"), с реальной расстановкой только на экране 3
(`0x11`=17) и экране 23 (`0x06`) — согласуется с уровнем, где всего два
стражника.
### 1.5 Как уровень попадает с диска (важно: имя файла — не игровой механизм)
В рантайме нет чтения "по имени файла LEVELn" — это чисто утилита для
экспорта в этом репозитории. Реально `LOADLEVELX` (`MISC.S:795-809`)
использует фиксированные таблицы по номеру уровня `bluepTRKlst`/
`bluepREGlst` (`MISC.S:776-787`), дающие физическую **дорожку (1-33)** и
**регион (0/1)**, затем `rdbluep` (`MASTER.S:598-616`) вызывает
низкоуровневое чтение `rw18` (`RdGrpErr`) 9 физических групп по 256 байт
(`$b7-$bf`) — 9×256=2304 байта — прямо в буфер blueprint; регион 0/1 выбирает
половину 18-секторной дорожки (два уровня делят одну дорожку). Файлы
`LEVELn` в этом репозитории — реконструкция этого сырого блока для удобства
работы с инструментами.
---
## 2. Формат изображений/спрайтов (`IMG.CHTAB1-7`, `IMG.BGTAB1/2.DUN/.PAL`)
Каждый такой файл грузится целиком по **фиксированному адресу**, заданному
константами `chtableN`/`bgtableN` (`GAMEEQ.S:9-18`):
```
chtable1=$6000 chtable2=$8400 chtable3=$0800 chtable4=$9600
chtable5=$a800 chtable6=$6000 chtable7=$9f00
bgtable1=$6000 bgtable2=$8400
```
— то есть смещения внутри файла один-в-один совпадают с адресами в памяти
после загрузки.
### 2.1 Раскладка контейнера
Восстановлено из заголовка-комментария "Image table format" в `HIRES.S:181-186`,
процедуры разрешения указателя `setimage` (`HIRES.S:263-277`) и
`GETWIDTH`/`PREPREP` (`HIRES.S:283-339`):
```
Смещение 0 : 1 байт — число изображений в таблице (максимум 127,
в образцах встречается 0x7f)
Смещение 1..254 : 127 × 2-байтных little-endian указателей
(указатель на изображение N — по смещению 1+(N-1)*2,
N=1..127) — АБСОЛЮТНЫЕ адреса в адресном пространстве
фиксированной загрузки этой таблицы, указывающие на
запись данных этого изображения
Смещение 255 : заполнитель (таблица указателей занимает ровно 256 байт)
Смещение 256 (база+0x100) и далее:
последовательно идущие записи данных изображений:
байт 0: ширина (в байтах на строку)
байт 1: высота (число строк)
байты 2..(2+ширина*высота-1): сырые байты пикселей,
слева направо, сверху вниз, БЕЗ сжатия
```
Проверено вручную на `IMG.BGTAB1.DUN`: с точной арифметикой индексов из
`setimage` (`Y = image*2 - 1`, `HIRES.S:264-267`) первые ~30 записей дают
строго возрастающую последовательность указателей `0x6101, 0x6133, 0x6159,
0x618b, 0x61c9, 0x61fb, 0x6221, 0x6313, 0x63c9, ...` — указатель
изображения #1 приходится ровно на `bgtable1 ($6000) + 0x100`, то есть точно
на конец 256-байтной таблицы указателей. Это независимо подтверждает и
размер таблицы, и семантику указателей.
**Важно: сжатия в этом формате нет.** RLE/дельта-упаковка (`SngExpand`/
`DblExpand`/`DeltaExpPop`/`DeltaExpWipe` в `01 POP Source/Source/UNPACK.S`)
применяется только к полноэкранным изображениям (титры/пролог/катсцены), но
не к CHTAB/BGTAB — спрайты и фоновые тайлы хранятся как чистые упакованные
байты hi-res/double-hi-res экрана Apple II, без какого-либо RLE или дельты.
### 2.2 Параметры отрисовки (не часть файла ресурса)
При выводе спрайта (`LAY`/`FASTLAY`/`PEEL` и т.д., `HIRES.S:658-1740`)
используются zero-page параметры `PAGE/XCO/YCO/OFFSET/IMAGE/OPACITY/TABLE/
BANK` (описаны в `HIRES.S:155-178`): `OFFSET` (0–6) — горизontальный сдвиг на
под-байтовый пиксель, `OPACITY` выбирает режим совмещения (AND/OR/STA/XOR/
маска-OR) плюс отдельный бит горизонтального зеркалирования (бит 7). Это
чисто рантайм-параметры отрисовки, не хранящиеся в файле ресурса. Точный
механизм барабанного сдвига для `OFFSET` (таблицы `HRTABLES.S`/`YLO`/`YHI`)
не прослежен до конца — при необходимости требует отдельного анализа.
### 2.3 Инструмент DRAZ (авторская утилита создания спрайтов)
В `04 Support/DRAZ` нет исходников самой утилиты DRAZ — только файлы данных
(`PAC.*` — позы персонажей, и уже скомпилированные `IMG.*`), поэтому
внутренний пайплайн DRAZ (как позы превращаются в CHTAB) напрямую не виден.
Формат контейнера выше выведен полностью из кода движка-потребителя, что
является надёжным, но косвенным источником.
Отдельно: в игровой логике списков объектов (`ADDBACK`, `GRAFIX.S:191-214`)
встречается **рантайм-упаковка ссылки на фоновую картинку** в один байт: бит
7 выбирает `bgtable1` или `bgtable2`, биты 0-6 — номер картинки в таблице
(0-63). Это соглашение для внутриигровых списков объектов (`bgIMG` и т.п.), а
не свойство самих файлов CHTAB/BGTAB на диске.
---
## 3. "Главного индекса ресурсов" не существует
В отличие от DOS-версии (см. `docs/MSDOS_RESOURCE_FORMAT.md`), в рантайм-коде
Apple II **нет обобщённого справочника "имя ресурса → расположение на
диске"**. Расположение каждого ресурса зашито напрямую как таблицы
дорожка/группа-секторов прямо в коде загрузчика:
- Уровни: `bluepTRKlst`/`bluepREGlst` (`MISC.S:776-787`), используются
`LOADLEVELX`/`LOADLEVEL` (`MISC.S:795-809`, `MASTER.S:467-481`).
- Альтернативные наборы фонов/персонажей: `bg1trk`/`bg2trk`/`ch4trk`/`ch4off`
(`MASTER.S:522-528`).
- Массовая загрузка при старте (chtable1-7, bgtable1-2, seqtable и т.д.):
прямые вызовы `rw18`/`RdGrp`/`RdSeq` с литеральными hex-списками
групп-секторов в `MASTER.S:1250-1360` и `BOOT.S:100-118`.
Весь дисковый ввод-вывод идёт через нестандартный низкоуровневый драйвер
`rw18` (`rw18 = $d000`, `EQ.S:11-12`; папка `02 POP Disk Routines/RW1835`),
реализующий нестандартный формат **18 секторов/дорожку** (вместо 16 у
стандартного DOS 3.3) — этим объясняется, почему регионы уровня (9×256Б)
идут парами на одной физической дорожке. Символические имена
`chtableN`/`bgtableN` в `GAMEEQ.S` — ближайший аналог "индекса ресурсов", но
они связывают ресурс с **фиксированным адресом в ОЗУ**, а не с положением на
диске; связь с диском — отдельная, вручную сопровождаемая таблица,
сопоставленная с ресурсом лишь порядком вызовов загрузчика.
---
## 4. Что ещё не восстановлено (открытые вопросы)
- Точное назначение бит `secmask` (%11000000) в `BLUETYPE` — не встречено
использование в доступном игровом коде (возможно, поле только для
редактора уровней, не читается движком).
- Механизм барабанного сдвига `OFFSET` для суб-байтового позиционирования
спрайта по X (`HRTABLES.S`) — не прослежен в деталях.
- Внутренний формат авторских файлов `PAC.*` инструмента DRAZ (как позы
скелетной анимации превращаются в растровые кадры CHTAB) — исходники DRAZ
отсутствуют в репозитории, можно только косвенно восстановить по
результату (уже скомпилированным `IMG.*`).