# Формат ресурсов 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.*`).