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>
This commit is contained in:
@@ -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 Б | 0–719 | 24 экрана × 30 тайлов: тип объекта/тайла |
|
||||
| `BLUESPEC` | 720 Б | 720–1439 | 24 экрана × 30 тайлов: доп. байт состояния объекта |
|
||||
| `LINKLOC` | 256 Б | 1440–1695 | Таблица связей нажимных плит/дверей, часть 1 |
|
||||
| `LINKMAP` | 256 Б | 1696–1951 | Таблица связей, часть 2 |
|
||||
| `MAP` | 96 Б | 1952–2047 | 24 экрана × 4 байта: граф соседних экранов |
|
||||
| `INFO` | 256 Б | 2048–2303 | Метаданные уровня: старт Кида, стражников и т.д. |
|
||||
|
||||
Сумма: 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 |
|
||||
| 71–94 | `GdStartBlock[1..24]` — стартовый блок стражника на экране; **≥30 = "стражника нет"** (`AUTO.S:1832-1834`, `SUBS.S:1677-1679`) | 24 |
|
||||
| 95–118 | `GdStartFace[1..24]` (86 = "стражника нет", см. `ShadFace cmp #86` по всему `AUTO.S`) | 24 |
|
||||
| 119–142 | `GdStartX[1..24]` — пересчитывается заново из блока при старте уровня, значение на диске почти не используется (`SUBS.S:1674-1690`) | 24 |
|
||||
| 143–166 | `GdStartSeqL[1..24]` | 24 |
|
||||
| 167–190 | `GdStartProg[1..24]` — "программа"/поведение ИИ стражника | 24 |
|
||||
| 191–214 | `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.*`).
|
||||
@@ -0,0 +1,301 @@
|
||||
# Формат ресурсов Prince of Persia (MS-DOS, каталог `MSDOS/`)
|
||||
|
||||
Документ описывает бинарный формат `*.DAT`-файлов ресурсов DOS-версии PoP.
|
||||
Исходников для этой версии нет, поэтому всё, что ниже — результат
|
||||
структурного (эмпирического) анализа реальных файлов из `MSDOS/`, а не чтения
|
||||
кода. Уровень уверенности указан для каждого раздела. Все находки проверены
|
||||
скриптами (Python), которые разбирают файл и валидируют согласованность
|
||||
(например: смещение+размер последней записи таблицы точно совпадает с
|
||||
началом самой таблицы — то есть данные и каталог стыкуются без дыр).
|
||||
|
||||
**Основной источник спецификации формата — `POP-DAT-FormatSpecifications.pdf`**
|
||||
(и его текстовая конверсия `POP-DAT-FormatSpecifications.txt` в этой же папке,
|
||||
для grep/цитирования): *«Prince of Persia — Specifications of File Formats»*,
|
||||
Princed Development Team, 2008 — каноническая спецификация формата `DAT v1.0`,
|
||||
на которой построен и SDLPoP, и Princed Resources. Разбирает контейнер, индекс,
|
||||
чек-сумму, кодеки изображений (RLE / LZG), палитры, формат уровней (room
|
||||
mapping, wall-drawing, room-linking, guards, start position, door events),
|
||||
звук (digital waves / MIDI / PC speaker), бинарные файлы и Mac-варианты. При
|
||||
любом расхождении между эмпирическими находками ниже и этим документом —
|
||||
источником истины считать спецификацию (сверять §-номера: её §3.x).
|
||||
|
||||
Дополнительно как справка при реализации (порт на ZX Sprinter):
|
||||
|
||||
- **SDLPoP** (github.com/NagyD/SDLPoP, GPLv3) — open-source реализация
|
||||
DOS-версии на основе дизассемблирования оригинального `PRINCE.EXE`. Содержит
|
||||
рабочий код чтения `.DAT`-файлов и полный кодек изображений/уровней. Точные
|
||||
структуры (`dat_table_type` и т.п.), процитированные ниже, получены через
|
||||
автоматический пересказ содержимого файла третьей стороной, а не через
|
||||
прямое чтение исходника — поэтому такие детали помечены как "требует сверки
|
||||
при реализации", в отличие от эмпирически подтверждённых байтовых оффсетов.
|
||||
- **Princed Resources / PR** (github.com/NagyD/PR, princed.org, GPLv2) — это
|
||||
профильный инструмент именно для распаковки/запаковки `.DAT`-ресурсов PoP
|
||||
(версии DAT 1 и 2), сделанный тем же автором. В его документации
|
||||
(`doc/Dataformats.md`) официально описаны экспортные форматы ресурсов —
|
||||
это подтверждает и уточняет часть находок ниже (см. §3–4), и является более
|
||||
надёжным источником, чем самостоятельная догадка по байтам.
|
||||
|
||||
**Важная находка:** репозиторий SDLPoP в папке `data/` содержит не только
|
||||
код движка, но и **реальные ресурсы игры** — как сырые `.DAT`-контейнеры, так
|
||||
и уже распакованные поштучно файлы (PNG-кадры спрайтов, `.pal`-палитры,
|
||||
`.bin`-дампы уровней), см. §7. Это готовый источник ассетов и одновременно
|
||||
независимая проверка формата, описанного в этом документе.
|
||||
|
||||
---
|
||||
|
||||
## 1. Общий контейнер `.DAT` (уверенность: высокая, подтверждено на 28 файлах)
|
||||
|
||||
Каждый `*.DAT`-файл (кроме служебных `config.dat`/`setup.dat`, см. §5) — это
|
||||
простой архив-контейнер: блок данных + оглавление (каталог ресурсов) в конце
|
||||
файла.
|
||||
|
||||
### 1.1 Заголовок файла (6 байт, смещение 0x00)
|
||||
|
||||
| Смещение | Размер | Поле | Значение |
|
||||
|----------|--------|--------------|----------|
|
||||
| 0x00 | 4 | `tableOffset`| LE u32. Абсолютное смещение в файле, с которого начинается таблица оглавления. Совпадает с "концом данных". |
|
||||
| 0x04 | 2 | `tableSize` | LE u16. Размер таблицы оглавления в байтах. |
|
||||
|
||||
Инвариант, подтверждённый на всех 28 `.dat`-файлах в каталоге:
|
||||
|
||||
```
|
||||
tableOffset + tableSize == размер файла (без исключений)
|
||||
```
|
||||
|
||||
Данные ресурсов идут сразу после заголовка, начиная с байта 0x06, и
|
||||
заканчиваются на `tableOffset`.
|
||||
|
||||
### 1.2 Таблица оглавления (по смещению `tableOffset`, длиной `tableSize`)
|
||||
|
||||
Таблица — плоский массив записей по 8 байт. Количество записей:
|
||||
`tableSize / 8` (округление вниз; в файле почти всегда остаётся 2 "лишних"
|
||||
байта в хвосте таблицы — назначение не установлено, вероятно, служебное поле
|
||||
инструмента-упаковщика или паддинг; на итоговый разбор не влияет).
|
||||
|
||||
Запись (8 байт):
|
||||
|
||||
| Смещение в записи | Размер | Поле | Описание |
|
||||
|---|---|---|---|
|
||||
| 0 | 2 | `size` | LE u16 — размер данных ресурса в байтах |
|
||||
| 2 | 2 | `id` | LE u16 — идентификатор ресурса |
|
||||
| 4 | 2 | `offset` | LE u16 — **абсолютное** смещение данных ресурса в файле (не относительное!) |
|
||||
| 6 | 2 | `reserved` | во всех проверенных записях (сотни штук) всегда `0x0000` |
|
||||
|
||||
Проверено на `levels.dat`: 16 записей, `id`=2000..2015, и `offset[i] + size[i]
|
||||
== offset[i+1]` для всех соседних записей, а последняя запись заканчивается
|
||||
ровно на `tableOffset` — то есть данные абсолютно плотно упакованы, без
|
||||
пробелов, для этого файла. В других файлах (например `guard.dat`) между
|
||||
записями изредка есть небольшие зазоры в несколько байт (вероятно, выравнивание
|
||||
или "мёртвые" байты от инструмента-компоновщика) — не является нарушением
|
||||
формата.
|
||||
|
||||
### 1.3 Диапазоны `id` по типам файлов (собрано эмпирически)
|
||||
|
||||
Похоже, что числовые ID образуют условные "пространства имён" по типу
|
||||
контента — вероятно, глобальные константы в оригинальном коде:
|
||||
|
||||
| Файл(ы) | Диапазон `id` | Кол-во записей | Предполагаемое содержимое |
|
||||
|---|---|---|---|
|
||||
| `levels.dat` | 2000–2015 | 16 | id=2000 — служебный блок (16 байт, см. §3); id=2001..2015 — 15 уровней |
|
||||
| `guard.dat`, `fat.dat`, `skel.dat`, `shadow.dat` | 750–784 (варьируется) | ~30–35 | id=751(750) — служебный блок; остальные — кадры анимации спрайта |
|
||||
| `vizier.dat` | аналогично guard | — | кадры анимации визиря |
|
||||
| `kid.dat` | ~400+ | 220 | кадры анимации игрока (намного больше — герой умеет гораздо больше действий) |
|
||||
| `guard1.dat`, `guard2.dat` | 750 (1 запись) | 1 | вероятно, дополнительные/альтернативные кадры/варианты |
|
||||
| `title.dat` | 40–55 | 12 | картинки титульного экрана/логотипов |
|
||||
| `cpalace.dat`,`epalace.dat`,`vpalace.dat`,`cdungeon.dat`,`edungeon.dat`,`vdungeon.dat` | 200–1343 | 205–238 | фоновые тайлы дворца/подземелья, отдельно для CGA(`c*`)/EGA(`e*`)/VGA(`v*`) |
|
||||
| `pv.dat` | 800–981 | 103 | доп. графика (возможно, "Prince/Vizier" катсцены) |
|
||||
| `digisnd1/2/3.dat` | 10000+ | 20–44 | оцифрованный звук (Covox/Disney Sound Source) |
|
||||
| `midisnd1/2.dat` | 10024+ / аналог | 16 | General MIDI музыка |
|
||||
| `mt32snd1/2.dat` | 10000+ | 24/7 | музыка для Roland MT-32 |
|
||||
| `ibm_snd1/2.dat` | 10000+ | 44 | музыка/эффекты через PC-спикер |
|
||||
| `prince.dat` | — (1 крупный ресурс) | — | MIDI-тема (вероятно, финальная тема "Принц"/титры — см. текстовые события "The Princess awaits") |
|
||||
|
||||
Во всех файлах первая (наименьшая по `id`) запись — маленький "служебный"
|
||||
ресурс (6–44 байта), стоящий перед основным контентом. Скорее всего это
|
||||
локальная мини-таблица/палитра/список ссылок для данного набора ресурсов —
|
||||
по аналогии с тем, что у уровней id=2000 отдельно от самих уровней (см. §3).
|
||||
|
||||
---
|
||||
|
||||
## 2. Формат уровня (`levels.dat`, id=2001..2015) — уверенность: высокая
|
||||
|
||||
Каждая запись уровня имеет размер **2305 байт** и по данным полностью
|
||||
совпадает по объёму с уровнями из Apple II версии (`01 POP Source/Levels/LEVELn`
|
||||
— ровно **2304 байта** каждый, см. `docs/APPLEII_RESOURCE_FORMAT.md`).
|
||||
|
||||
Вывод: формат карты уровня в DOS-версии, судя по всему, **унаследован
|
||||
практически без изменений от оригинального Apple II формата** (Джордан
|
||||
Мехнер писал игру на 6502 и данные уровней переносились как есть), с добавлением
|
||||
одного лишнего байта в DOS-упаковке (2304+1=2305 — вероятно, контрольный байт/
|
||||
маркер конца, добавленный DOS-упаковщиком ресурсов, а не часть игровых данных).
|
||||
|
||||
**Практическое следствие:** байтовая структура самого уровня (тайлы 3×10 на
|
||||
экран, 24 экрана, таблицы стражников, дверей и т.д.) должна документироваться
|
||||
один раз — по исходникам Apple II (см. соответствующий раздел), и напрямую
|
||||
применяться к DOS `levels.dat`, отбросив 1 лишний байт в конце каждой записи.
|
||||
Байтовые значения тайлов в дампе (в основном 0x00–0x39) визуально согласуются
|
||||
с диапазоном небольших целых кодов тайлов, что для формата карты и ожидается.
|
||||
|
||||
Первая запись, id=2000, размер 16 байт — не уровень, а отдельный маленький
|
||||
блок (возможно: количество уровней, начальный уровень, версия формата,
|
||||
стартовые координаты игрока/охраны по умолчанию). Точное назначение не
|
||||
установлено — требует сопоставления с дизассемблированным кодом загрузчика
|
||||
уровней (в SDLPoP это, по всем признакам, отдельная процедура чтения
|
||||
`level` ресурса).
|
||||
|
||||
**Сверка с независимой распаковкой SDLPoP (`data/LEVELS/`):** там лежат файлы
|
||||
`res2000.bin`…`res2015.bin` (16 штук — количество совпадает). Байты
|
||||
`res2001.bin` содержательно совпадают с тайловыми данными нашей записи
|
||||
id=2001 (та же последовательность значений тайлов) — это подтверждает, что
|
||||
нумерация id верна. Но есть нестыковка по размеру: у SDLPoP `res2000.bin` —
|
||||
**2305 байт** (как и все остальные), тогда как в нашем локальном
|
||||
`levels.dat` запись id=2000 — всего **16 байт**. Скорее всего, это разные
|
||||
релизы/сборки игры (см. §7 — размеры некоторых `.dat` у SDLPoP и у нас уже
|
||||
отличались), и в версии SDLPoP маленький служебный блок либо отсутствует,
|
||||
либо пронумерован иначе. Это не меняет сам формат контейнера, но означает,
|
||||
что **точную семантику 16-байтного блока id=2000 в нашей копии игры пока
|
||||
нельзя проверить через данные SDLPoP** — открытый вопрос.
|
||||
|
||||
---
|
||||
|
||||
### 2.1 Кросс-подтверждение по исходникам Apple II
|
||||
|
||||
Фоновый анализ исходников Apple II (см. `docs/APPLEII_RESOURCE_FORMAT.md`)
|
||||
подтверждает и объясняет структуру уровня напрямую по коду. Уровень на Apple
|
||||
II — дамп структуры `blueprnt` (`EQ.S`): `BLUETYPE`(720Б, 24 экрана×30 тайлов)
|
||||
+ `BLUESPEC`(720Б) + `LINKLOC`(256Б) + `LINKMAP`(256Б) + `MAP`(96Б, граф
|
||||
соседних экранов) + `INFO`(256Б, метаданные/старт Кида/стражников) = ровно
|
||||
2304 байта. Учитывая, что DOS-запись уровня — это ровно 2304+1 байт с
|
||||
байтовыми значениями тайлов, укладывающимися в диапазон 0–29 (id тайла) плюс
|
||||
служебные биты (аналогично `idmask=%00011111`, `reqmask=%00100000` из
|
||||
`EQ.S:484-486`), можно с высокой уверенностью считать, что **DOS-версия
|
||||
использует ту же самую раскладку `blueprnt`**, лишь с добавлением одного
|
||||
байта (вероятно, контрольной суммы) в конце DOS-упаковки. Это снимает
|
||||
необходимость отдельно реверсить формат уровня для DOS — таблица тайлов,
|
||||
enum id (0=space...29=archtop4), формат `LINKLOC`/`LINKMAP` и `INFO` из
|
||||
Apple II документа применимы напрямую.
|
||||
|
||||
## 3. Графика (спрайты и фоновые тайлы) — уверенность: средняя/низкая
|
||||
|
||||
Файлы `kid.dat`, `guard.dat`, `fat.dat`, `shadow.dat`, `skel.dat`,
|
||||
`vizier.dat`, `title.dat`, `c/e/v-palace.dat`, `c/e/v-dungeon.dat`, `pv.dat`
|
||||
хранят по контейнерному формату (§1) множество мелких чанков (десятки—сотни
|
||||
байт каждый).
|
||||
|
||||
Что подтверждено:
|
||||
- Наборы `shadow.dat`/`kid.dat` и `fat.dat`/`vizier.dat` содержат **побайтово
|
||||
идентичные фрагменты** данных в начале файла — это ожидаемо: "Тень" (Shadow)
|
||||
визуально копирует анимацию Кида, а "Толстый страж" (Fat guard, пасхалка)
|
||||
переиспользует модель Визиря. Подтверждает, что персонажи одного "типа
|
||||
тела" используют общий набор геометрии/анимации.
|
||||
- Отдельные чанки *не* имеют очевидного унифицированного заголовка
|
||||
(высота/ширина/палитра) фиксированного размера — попытка интерпретировать
|
||||
первые байты чанка как `{height:u16, width:u16, flags:u16}` не подтвердилась
|
||||
на реальных данных (получаются нереалистичные размеры для маленьких чанков).
|
||||
Вероятно, как и в Apple II версии (см. `FRAMEDEF.S`/`SEQTABLE.S`), геометрия
|
||||
кадра (ширина, высота, точка привязки) хранится **отдельно от самих
|
||||
пиксельных данных** — в таблицах внутри `PRINCE.EXE`, а не в `.DAT`-чанке.
|
||||
Сам чанк, вероятно, содержит только упакованные пиксельные данные
|
||||
(RLE/дельта-упаковка, по аналогии с `UNPACK.S` в Apple II исходниках).
|
||||
- Точный алгоритм упаковки пикселей **не восстановлен** в рамках этого
|
||||
анализа по сырым байтам — байт-в-байт разбор распаковщика без
|
||||
дизассемблирования `PRINCE.EXE` надёжно не сделать. **Но для практических
|
||||
целей это не требуется**: см. §7 — в SDLPoP уже есть тот же самый набор
|
||||
изображений в готовом, распакованном виде (PNG), которым можно пользоваться
|
||||
напрямую как источником ассетов, не реализуя свой декодер `.DAT`-пикселей.
|
||||
Писать собственный декодер имеет смысл только если понадобится читать
|
||||
оригинальные `.DAT` "на лету" (например, для точной сверки контента именно
|
||||
нашей копии игры) — тогда ориентир — исходник SDLPoP (`src/seg009.c`).
|
||||
|
||||
---
|
||||
|
||||
## 4. Звук — уверенность: высокая (по структуре), низкая (по деталям кодека)
|
||||
|
||||
Обнаружено 4 параллельных набора звуковых ресурсов под разные звуковые
|
||||
устройства DOS-эпохи — типично для игр начала 1990-х с "звуковым меню":
|
||||
|
||||
| Файл | Устройство | Формат чанка |
|
||||
|---|---|---|
|
||||
| `midisnd1.dat`, `midisnd2.dat` | General MIDI / MPU-401 | каждый чанк = 2-байтовый LE-префикс длины + встроенный Standard MIDI File (`MThd`...`MTrk`...) |
|
||||
| `mt32snd1.dat`, `mt32snd2.dat` | Roland MT-32/CM-32L | тот же формат: префикс длины + `MThd`/`MTrk`, с MT-32-специфичными SysEx (видны строки `MT-32.mff`, текстовые мета-события вроде `"The Princess awaits"`) |
|
||||
| `prince.dat` | (аналогично MIDI) | отдельный крупный музыкальный ресурс, тот же MIDI-контейнер — вероятно, финальная тема |
|
||||
| `digisnd1/2/3.dat` | Covox / Disney Sound Source / Sound Blaster (оцифрованный звук) | чанк начинается с нескольких служебных байт, среди которых слово `0x2AF8` = 11000 — похоже на частоту дискретизации 11 кГц; далее — сырые 8-битные PCM-сэмплы (значения кластеризуются вокруг ~0x7A–0x90, типично для беззнакового 8-бит аудио, смещённого к середине шкалы) |
|
||||
| `ibm_snd1.dat`, `ibm_snd2.dat` | PC Speaker | чанк — последовательность троек байт похожих на (длительность, делитель_частоты) — простой формат "бипера", отличный от MIDI |
|
||||
|
||||
Подтверждено разбором первых чанков в каждом файле (см. байтовые дампы,
|
||||
проверялись скриптом). Точная семантика полей внутри `digisnd`/`ibm_snd`
|
||||
(разрядность, порядок байт служебного заголовка) не выведена до конца — при
|
||||
реализации порта достаточно распознавания по типу файла и (для MIDI-семейства)
|
||||
можно напрямую воспроизводить встроенный Standard MIDI File, пропустив
|
||||
2-байтовый префикс длины.
|
||||
|
||||
---
|
||||
|
||||
## 5. Готовые распакованные ассеты в SDLPoP (`data/`) — практический источник для порта
|
||||
|
||||
Репозиторий github.com/NagyD/SDLPoP содержит папку `data/`, где, помимо
|
||||
самих `.DAT`-контейнеров, каждый ресурс **продублирован в виде отдельно
|
||||
распакованного файла**, названного по его `id` из таблицы оглавления (§1.2).
|
||||
Проверено через GitHub API (`api.github.com/repos/NagyD/SDLPoP/contents/...`):
|
||||
|
||||
| Подпапка/файл в `data/` | Содержимое | Соответствие нашему разбору |
|
||||
|---|---|---|
|
||||
| `GUARD.DAT`, `GUARD1.DAT`, `GUARD2.DAT` | сырые `.DAT` | размер **побайтово совпадает** с нашими локальными `guard.dat`/`guard1.dat`/`guard2.dat` (6950 / 117 / 117 байт) |
|
||||
| `DIGISND1.DAT`, `MIDISND2.DAT` и др. | сырые `.DAT` | размер **не совпадает** с нашими локальными файлами (48545 vs 50101, 18408 vs 18958) — другой релиз/сборка игры |
|
||||
| `GUARD/res751.png` … `res784.png` | готовые PNG, по одному на кадр анимации, имя = `res<id>.png` | id-диапазон (751-784) точно совпадает с нашим разбором `guard.dat` |
|
||||
| `VPALACE/res200.pal`, `res201.png`, `res202.png`, … | палитра (JASC `.pal`) + PNG-кадры фонов дворца, **VGA-вариант (256 цветов)** | id-диапазон (200+) совпадает с `vpalace.dat` |
|
||||
| `LEVELS/res2000.bin` … `res2015.bin` | сырые дампы уровней по 2304-2305 байт | id совпадает с `levels.dat`; содержимое `res2001.bin` **сверено побайтово** с нашим id=2001 — тайловые данные совпадают |
|
||||
| `KID/`, `PRINCE/`, `SHADOW/`, `SKEL/`, `VIZIER/`, `FAT/`, `TITLE/`, `VDUNGEON/`, `PV/`, `IBM_SND1/`, `IBM_SND2/`, `font/`, `music/` | аналогичные наборы для остальных ресурсов | не проверялись по отдельности, но структура (папка на каждый `.dat`, файлы `res<id>.ext`) наблюдается одинаково |
|
||||
|
||||
**Вывод:** это данные из немного **другого релиза DOS-версии**, чем те, что
|
||||
лежат у нас в `MSDOS/` (см. расхождение в размере `digisnd`/`midisnd`), но
|
||||
формат контейнера и нумерация `id` — те же самые. Практически это значит:
|
||||
|
||||
1. Для получения играбельных PNG-спрайтов и VGA-фонов **не нужно
|
||||
реализовывать декодер сжатия пикселей** — можно взять готовые файлы
|
||||
`data/<ИМЯ>/res<id>.png` напрямую как исходный материал для конвертации
|
||||
под видеорежим ZX Sprinter (в т.ч. `VPALACE`/`VDUNGEON` — уже
|
||||
256-цветный VGA-арт, что прямо отвечает на вопрос про полноцветность).
|
||||
2. Если в проекте важно использовать именно ту версию контента, что в наших
|
||||
`MSDOS/*.dat` (а не версию из SDLPoP) — распаковку своих файлов всё же
|
||||
придётся делать (кодек пикселей по-прежнему не восстановлен для сырых
|
||||
`.DAT`, см. §3), либо принять решение работать с версией SDLPoP как
|
||||
мастер-источником ассетов вместо своей.
|
||||
|
||||
---
|
||||
|
||||
## 6. Служебные не-ресурсные файлы
|
||||
|
||||
- `config.dat`, `setup.dat` — 28 байт, не являются ресурсными контейнерами
|
||||
(не проходят проверку §1.1 — "размер" получается больше самого файла).
|
||||
Скорее всего простые бинарные структуры настроек (звук/видеорежим,
|
||||
выбранный на этапе `SETUP.EXE`/`INSTALL.EXE`), не связаны с игровым
|
||||
контентом.
|
||||
- `desktopd.cfg`, `setup.cfg` — текстовые/бинарные конфиги DOS-инсталлятора,
|
||||
вне скоупа игровых ресурсов.
|
||||
- `PRINCE.EXE` / `PRINCE.REM` — почти идентичны (отличие в единичных байтах
|
||||
в районе смещения ~0x4ED0), похоже на кряк/патч одного байта проверки —
|
||||
не относится к формату ресурсов.
|
||||
- `old-games.nfo` — ASCII-арт NFO релиз-группы (old-games.ru), не игровые
|
||||
данные.
|
||||
|
||||
---
|
||||
|
||||
## 7. Итоговая таблица уверенности
|
||||
|
||||
| Раздел | Уверенность | Как подтверждено |
|
||||
|---|---|---|
|
||||
| Контейнер `.DAT` (заголовок + таблица) | Высокая | Проверено скриптом на всех 28 файлах, инвариант offset+size выполняется без исключений; независимо подтверждено именованием `res<id>.*` в SDLPoP `data/` |
|
||||
| ID-пространства ресурсов | Средняя-высокая | Наблюдение по диапазонам + сверка с `res<id>` именами файлов SDLPoP и побайтовым содержимым `res2001.bin` |
|
||||
| Формат уровня = формату Apple II | Высокая (по размеру и содержимому), служебный блок id=2000 — открытый вопрос | Совпадение размера (2304 vs 2305), тайловые байты сходятся с `res2001.bin` из SDLPoP |
|
||||
| Формат изображений/спрайтов (сырой `.DAT`) | Низкая-средняя | Контейнер подтверждён, кодек пикселей — нет; но практически закрыто наличием готовых PNG в SDLPoP `data/` (§5) |
|
||||
| Формат звука (тип контейнера) | Высокая для MIDI-семейств, средняя для digisnd/ibm_snd | Явные MIDI-сигнатуры `MThd`/`MTrk` видны в байтах |
|
||||
|
||||
**Рекомендация для дальнейшей работы:** для получения арт-ассетов (спрайты,
|
||||
фоны, палитры) — использовать готовые распакованные файлы из
|
||||
`github.com/NagyD/SDLPoP/tree/master/data` (§5), это быстрее и надёжнее
|
||||
самостоятельной реализации декодера. Декодер сырого `.DAT`-формата
|
||||
изображений и точную семантику служебных полей `digisnd`/`ibm_snd`
|
||||
(§3, §4) стоит восстанавливать только если понадобится читать именно нашу
|
||||
локальную копию `MSDOS/*.dat` "как есть" — тогда ориентир прежний: исходник
|
||||
SDLPoP (`src/seg009.c`, `src/data.c`/`data.h`).
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user