Files
snark13 cd8d566d82 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>
2026-07-17 17:51:08 +03:00

20 KiB
Raw Permalink Blame History

Формат ресурсов Prince of Persia (Apple II, оригинальные исходники 1989)

Источник — официально опубликованные Джорданом Мехнером исходники (Prince-of-Persia-Apple-II/, 6502-ассемблер). В отличие от DOS-версии, здесь формат восстановлен напрямую по коду, а не по догадкам о байтах — уверенность высокая везде, где указана ссылка на конкретный файл/строки.


1. Формат уровня (01 POP Source/Levels/LEVEL0LEVEL14, 2304 байта)

Файлы уровня — это побайтовый дамп структуры blueprnt, которая грузится по фиксированному адресу $b700 (EQ.S:28) и объявлена как dum blueprnt в EQ.S:258-266. Никакого отдельного заголовка файла нет — это чистый образ структуры в памяти:

Поле Размер Смещение в файле Описание
BLUETYPE 720 Б 0719 24 экрана × 30 тайлов: тип объекта/тайла
BLUESPEC 720 Б 7201439 24 экрана × 30 тайлов: доп. байт состояния объекта
LINKLOC 256 Б 14401695 Таблица связей нажимных плит/дверей, часть 1
LINKMAP 256 Б 16961951 Таблица связей, часть 2
MAP 96 Б 19522047 24 экрана × 4 байта: граф соседних экранов
INFO 256 Б 20482303 Метаданные уровня: старт Кида, стражников и т.д.

Сумма: 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
163 резерв/не используется 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
7194 GdStartBlock[1..24] — стартовый блок стражника на экране; ≥30 = "стражника нет" (AUTO.S:1832-1834, SUBS.S:1677-1679) 24
95118 GdStartFace[1..24] (86 = "стражника нет", см. ShadFace cmp #86 по всему AUTO.S) 24
119142 GdStartX[1..24] — пересчитывается заново из блока при старте уровня, значение на диске почти не используется (SUBS.S:1674-1690) 24
143166 GdStartSeqL[1..24] 24
167190 GdStartProg[1..24] — "программа"/поведение ИИ стражника 24
191214 GdStartSeqH[1..24] — обнуляется при старте (SUBS.S:1685-1686) 24
215255 резерв/не используется 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.*).