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