cd8d566d82
Порт 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>
284 lines
20 KiB
Markdown
284 lines
20 KiB
Markdown
# Формат ресурсов 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.*`).
|