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:
@@ -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.*`).
|
||||
Reference in New Issue
Block a user