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:
2026-07-17 17:51:08 +03:00
parent 484b18d10c
commit cd8d566d82
196 changed files with 6630 additions and 0 deletions
@@ -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 Б | 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 |
| 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 |
| 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 |
| 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.*`).
+199
View File
@@ -0,0 +1,199 @@
# Prince of Persia — Kid (персонаж): анализ и план
Статус: план (2026-07-16). Опирается на разбор `SDLPoP/src/seg006.c`
(ядро физики/управления Kid), `seqtbl.c` (таблицы последовательностей),
`types.h` (char_type, seq_*, SEQ_*, actions_*), `SDLPoP/data/KID` (спрайты).
Фон уже готов и проверен на MAME (`applications/PoP/roomtest`, см.
`memory/pop_background_strategy`) — Kid развиваем в том же `roomtest` как
новый PoC (решение пользователя: старый `poc/` не трогаем).
**Копирайт:** спрайты Kid (`SDLPoP/data/KID`) — Broderbund/Ubisoft.
Использование настоящей графики Kid — сознательное решение пользователя
(в отличие от плейсхолдера в старом `poc/`, см. PORT_PLAN §5.1).
---
## 1. Как устроен персонаж в оригинале (что портируем)
### 1.1 Состояние — `char_type` (14 полей, types.h)
```
frame текущий номер кадра (индекс во frame_table_kid)
x, y позиция (byte; x — с учётом direction)
direction -1 влево / 0 вправо
curr_col, логическая клетка (тайл), где персонаж
curr_row
action КАТЕГОРИЯ действия (actions_*, см. 1.2)
fall_x, скорость падения (fall_y<22 = 1 ряд, <33 = 2 ряда)
fall_y
room комната
repeat счётчик для удержания-ввода (напр. повторный прыжок)
sword есть ли меч (бой — вне Фазы 1)
alive жив/мёртв
curr_seq УКАЗАТЕЛЬ в seqtbl (байткод текущей последовательности)
```
Состояние крошечное — легко живёт в W2.
### 1.2 Категории действия — `actions_*` (9 шт)
`0 stand`, `1 run_jump`, `2 hang_climb`, `3 in_midair`, `4 in_freefall`,
`5 bumped`, `6 hang_straight`, `7 turn`, `99 hurt`. `action` определяет,
как `check_action()`/`play_kid()` реагируют на ввод и физику каждый тик.
### 1.3 Движок анимации/движения — ГЛАВНОЕ
**Движение НЕ физика, а байткод + per-frame смещения** (подтверждает
PORT_PLAN §6). Три уровня:
1. **`seqtbl`** — байткод-программа на действие. Опкоды (types.h):
`SEQ_DX`(0xFB) сдвиг x на amount×direction, `SEQ_DY`(0xFA) сдвиг y,
`SEQ_FLIP`(0xFE) разворот, `SEQ_JMP`(0xFF)/`SEQ_JMP_IF_FEATHER`(0xF7),
`SEQ_UP`/`SEQ_DOWN`(0xFD/0xFC) смена ряда, `SEQ_ACTION`(0xF9) задать
`Char.action`, `SEQ_SET_FALL`(0xF8), `SEQ_KNOCK_UP/DOWN`, `SEQ_SOUND`,
`SEQ_DIE`/`SEQ_END_LEVEL`/`SEQ_GET_ITEM`. **Байт < 0xF0 = НОМЕР КАДРА**
→ ставит `Char.frame` и play_seq возвращается (один кадр за тик).
2. **`play_seq()`** (seg006.c:570) — интерпретатор: крутит опкоды из
`seqtbl + Char.curr_seq`, пока не встретит кадр. ~15 case — портируется
1-в-1. **Квирк:** seqtbl использует АБСОЛЮТНЫЕ DOS-адреса в JMP;
`SEQTBL_0 = seqtbl - SEQTBL_BASE(0x196E)` — при порте пересчитать
базу (JMP-адреса в наших данных).
3. **`frame_table_kid[]`** (seg006.c:127, ~180 кадров) — на КАЖДЫЙ кадр:
`{image, sword_flags, dx, dy, flags}`. `image` — индекс спрайта Kid;
`dx/dy` — смещение позиции ЭТОГО кадра; `flags`: 0x1F weight_x, 0x20
thin, 0x40 needs_floor, 0x80 even/odd-pixel (влияет на x-рендер).
**Тик персонажа:** `play_kid()` (диспетчер по action+вводу) → `play_seq()`
(двигает curr_seq, ставит кадр, применяет seq-dx/dy) → `frame_table[frame]`
даёт image+собственные dx/dy → позиция и спрайт. У нас это ложится на
`sprite_frame`+`sprite_move` (НЕ `sprite_anim`/`sprite_moveto` — см.
PORT_PLAN §6: авторские таблицы, не автопрогрессия).
### 1.4 Управление — `control_kid()`/`read_user_control()` (seg006.c)
Читает ввод (у нас — held-state `kbd_raw`, уже готово, §2 PORT_PLAN) и по
`Char.action` выбирает последовательность (`seqtbl_offset_char(seq_id)`).
Логика «что можно из какого состояния» — ядро ощущения PoP.
### 1.5 Взаимодействие с картой — collision (seg006.c)
`check_on_floor()`/`start_fall()` — пол под ногами / падение в яму;
`in_wall()` — упор в стену (сдвиг наружу); `check_grab()`/
`can_grab_front_above()` — зацеп за уступ; `fell_out()` — вывалиться из
комнаты; `check_spiked()`/loose — ловушки; `fall_accel()`/`fall_speed()`
ускорение падения. Всё читает ТИП тайла (`get_tile`) — у нас это уже
разобранные `fg[]/bg[]` (level.h/room1_data.h).
---
## 2. Спрайты Kid (219 шт, 16 цветов, 177 КБ)
- 219 PNG (`data/KID`), 16-цветные (палитра `res400.pal`, 16×RGB как env/
wall), макс кадр **53×35** — влезает в лимит движка 64×64. 177 КБ в
8bpp.
- `frame_table_kid` отображает кадр→`image` (индекс спрайта). Число
РАЗЛИЧНЫХ image — уточнить (≤219); паковать те, что реально используются
платформинг-последовательностями Фазы 1 (не все 219 — бой/катсцены
отдельно).
- **Палитра:** Kid 16 цветов → слоты Sprinter `0x70-0x7F` (env 0x50, wall
0x60 уже заняты; Kid не пересекается). Пиксель i: 0→0xFF, i→0x70+i.
Тот же пайплайн, что `pop_pack_bg.py`.
- **Атлас:** прямая адресация по номеру image (как фон): `kid[img>>5]`,
idx `img&31`; ~7 EMM-страниц (или SHIFT=4). Свой пакер `pop_pack_kid.py`
(переиспользовать код `pop_pack_bg.py`).
### 2.1 РЕШЕНИЕ ДО СТАРТА: per-frame offset vs padding
Кадры Kid — РАЗНОГО размера, а `sprite_t` рисует от угла фикс. w/h. Два
пути (см. PORT_PLAN §6.1, `memory/png_strip_padding_tradeoff`):
- **Padding** (bottom-center) — просто, но 219×53×35 ≈ 406 КБ (раздув ×2.3).
- **Per-frame offset** — хранить XCO/YCO кадра (у оригинала он и есть,
`APPLEII_RESOURCE_FORMAT §2.2`), рисовать `blit(x+xco, y+yco)`; паддинг не
нужен, память по факту (177 КБ). Требует лёгкого расширения хранения
(offset рядом с кадром) ИЛИ ручного смещения в коде рендера Kid.
**Рекомендация:** per-frame offset — оригинал так и делает (frame_table dx/dy
+ image XCO/YCO), даёт точное позиционирование И экономию. Хранить xco/yco
в нашей копии frame_table (добавить 2 байта/кадр — ~360 Б). Не тянуть
расширение `sprite.h` — рисовать Kid прямым `gfx_blit(x+xco, y+yco, img)`
(как фон), НЕ через retained `sprite_t`, раз позиция и кадр всё равно
задаются вручную каждый тик.
---
## 3. Данные для порта (объём)
- `frame_table_kid` → C-массив ~180×(5+2 offset) ≈ 1.3 КБ (const, ROM).
- `seqtbl` (нужные последовательности) → C-массив байт. Весь seqtbl ~1-2 КБ;
для Фазы 1 можно взять только платформинг-последовательности (вырезать
бой/гардов 55-92) — оценить после разметки. JMP-адреса пересчитать под
свою базу.
- Спрайты — атласы (EMM, не W2).
---
## 4. Фазы работы (по твоему списку, порядок по зависимостям)
**Фаза K0 — конвейер спрайтов + отрисовка одного кадра**
- `pop_pack_kid.py`: 219 (или подмножество) → `kid*.atl` + `kid.pal`
(слоты 0x70), таблица кадр→image + xco/yco.
- Отрисовать Kid ОДНИМ кадром (stand) в roomtest поверх фона на верном
тайле — проверить палитру/позицию/прозрачность на MAME.
- Артефакт-цель: Kid стоит на уступе комнаты 1 как в `1.1-2.png`.
**Фаза K1 — движок анимации (play_seq + frame_table)**
- Портировать `play_seq()` (интерпретатор) + `frame_table_kid` + минимальный
`seqtbl` (stand/run/turn).
- Прогнать несколько последовательностей вручную (stand→run→stop) —
проверить, что кадры и смещения совпадают с оригиналом (сверять с
SDLPoP/скриншотами, тайминг 50 Гц).
**Фаза K2 — управление на месте + ходьба (твои а, б)**
- `control_kid` подмножество: stand (2), run (1/84/13), turn (5/6),
standing_jump (3), crouch (50/49), safe_step (29-44 — аккуратный шаг).
- Held-state через `kbd_raw` (готово).
**Фаза K3 — коллизия с картой (твой п.3)**
- `check_on_floor`/`start_fall` — падение в ямы (тип тайла под ногами из
`fg[]`); `in_wall`/стоп у стены; `fell_out` (край экрана — пока без
перехода комнат).
- Падения/приземления (seq 7/17/19/20) + `fall_accel/fall_speed`.
**Фаза K4 — прыжки и повисание (твои а-прыжок, в)**
- run_jump (4), jump_up (28/14), grab (8/16/24), climb_up (10)/down (68),
hang (25/6), release (11/23). Это самый «PoP-овый» кусок — сверять
дистанции/тайминг с оригиналом (не на глаз).
**Фаза K5 — прочее (твой г)**
- drink (78), level_door (70), crouch_hop (79), spiked/loose/chomped
(ловушки, если тайлы есть в комнате), death (71).
Бой (меч, seq 55-92, стражники — seg005) — ВНЕ этого плана (отдельная фаза
полного приложения, PORT_PLAN §7 Фаза 3).
---
## 5. Риски/решения ДО кода (правило defer_unexplained_quirks)
1. **Per-frame offset** (§2.1) — решить до K0 (влияет на формат данных).
Рекомендация: xco/yco в frame_table, прямой blit.
2. **seqtbl rebasing** — JMP-адреса абсолютные (SEQTBL_BASE 0x196E); при
порте пересчитать в оффсеты своего массива. Проверить на 1-2 seq.
3. **Тайминг** — оригинал (DOS) фиксированный тик; наш 50 Гц. Если
логическая частота кадров иная — пересчёт dx/dy (PORT_PLAN §8.4).
Сверять дистанцию бега/прыжка с эталоном.
4. **Число реально нужных кадров/последовательностей** для Фазы 1 —
разметить (вырезать бой/катсцены/гардов), чтобы не тянуть все 219
спрайта и весь seqtbl.
5. **Копирайт графики Kid** — подтверждено решение пользователя (§вводная).
---
## 6. Что переиспользуем (готово)
- Фон комнаты (`pop_bg.c`) — Kid рисуется ПОВЕРХ (сейчас — прямым blit;
heal против фона — когда/если понадобится через RAM-копию, фон её уже
заполняет, `GFX_BANK_TRANSPARENT`).
- `kbd_raw` held-state (§2 PORT_PLAN) — готов и проверен.
- Пакер спрайтов/палитра (`pop_pack_bg.py`) — шаблон для `pop_pack_kid.py`.
- Разобранная карта комнаты (`fg[]/bg[]`, level.h) — для коллизий.
- `gfx_blit`/`gfx_w0_map` из W0-атласа — проверенный путь (bgtest/roomtest).
@@ -0,0 +1,291 @@
# Формат ресурсов Prince of Persia (MS-DOS, каталог `MSDOS/`)
Документ описывает бинарный формат `*.DAT`-файлов ресурсов DOS-версии PoP.
Исходников для этой версии нет, поэтому всё, что ниже — результат
структурного (эмпирического) анализа реальных файлов из `MSDOS/`, а не чтения
кода. Уровень уверенности указан для каждого раздела. Все находки проверены
скриптами (Python), которые разбирают файл и валидируют согласованность
(например: смещение+размер последней записи таблицы точно совпадает с
началом самой таблицы — то есть данные и каталог стыкуются без дыр).
Для справки при последующей реализации (порт на ZX Sprinter) стоит держать в
уме два внешних проекта:
- **SDLPoP** (github.com/NagyD/SDLPoP, GPLv3) — open-source реализация
DOS-версии на основе дизассемблирования оригинального `PRINCE.EXE`. Содержит
рабочий код чтения `.DAT`-файлов и полный кодек изображений/уровней. Точные
структуры (`dat_table_type` и т.п.), процитированные ниже, получены через
автоматический пересказ содержимого файла третьей стороной, а не через
прямое чтение исходника — поэтому такие детали помечены как "требует сверки
при реализации", в отличие от эмпирически подтверждённых байтовых оффсетов.
- **Princed Resources / PR** (github.com/NagyD/PR, princed.org, GPLv2) — это
профильный инструмент именно для распаковки/запаковки `.DAT`-ресурсов PoP
(версии DAT 1 и 2), сделанный тем же автором. В его документации
(`doc/Dataformats.md`) официально описаны экспортные форматы ресурсов —
это подтверждает и уточняет часть находок ниже (см. §3–4), и является более
надёжным источником, чем самостоятельная догадка по байтам.
**Важная находка:** репозиторий SDLPoP в папке `data/` содержит не только
код движка, но и **реальные ресурсы игры** — как сырые `.DAT`-контейнеры, так
и уже распакованные поштучно файлы (PNG-кадры спрайтов, `.pal`-палитры,
`.bin`-дампы уровней), см. §7. Это готовый источник ассетов и одновременно
независимая проверка формата, описанного в этом документе.
---
## 1. Общий контейнер `.DAT` (уверенность: высокая, подтверждено на 28 файлах)
Каждый `*.DAT`-файл (кроме служебных `config.dat`/`setup.dat`, см. §5) — это
простой архив-контейнер: блок данных + оглавление (каталог ресурсов) в конце
файла.
### 1.1 Заголовок файла (6 байт, смещение 0x00)
| Смещение | Размер | Поле | Значение |
|----------|--------|--------------|----------|
| 0x00 | 4 | `tableOffset`| LE u32. Абсолютное смещение в файле, с которого начинается таблица оглавления. Совпадает с "концом данных". |
| 0x04 | 2 | `tableSize` | LE u16. Размер таблицы оглавления в байтах. |
Инвариант, подтверждённый на всех 28 `.dat`-файлах в каталоге:
```
tableOffset + tableSize == размер файла (без исключений)
```
Данные ресурсов идут сразу после заголовка, начиная с байта 0x06, и
заканчиваются на `tableOffset`.
### 1.2 Таблица оглавления (по смещению `tableOffset`, длиной `tableSize`)
Таблица — плоский массив записей по 8 байт. Количество записей:
`tableSize / 8` (округление вниз; в файле почти всегда остаётся 2 "лишних"
байта в хвосте таблицы — назначение не установлено, вероятно, служебное поле
инструмента-упаковщика или паддинг; на итоговый разбор не влияет).
Запись (8 байт):
| Смещение в записи | Размер | Поле | Описание |
|---|---|---|---|
| 0 | 2 | `size` | LE u16 — размер данных ресурса в байтах |
| 2 | 2 | `id` | LE u16 — идентификатор ресурса |
| 4 | 2 | `offset` | LE u16 — **абсолютное** смещение данных ресурса в файле (не относительное!) |
| 6 | 2 | `reserved` | во всех проверенных записях (сотни штук) всегда `0x0000` |
Проверено на `levels.dat`: 16 записей, `id`=2000..2015, и `offset[i] + size[i]
== offset[i+1]` для всех соседних записей, а последняя запись заканчивается
ровно на `tableOffset` — то есть данные абсолютно плотно упакованы, без
пробелов, для этого файла. В других файлах (например `guard.dat`) между
записями изредка есть небольшие зазоры в несколько байт (вероятно, выравнивание
или "мёртвые" байты от инструмента-компоновщика) — не является нарушением
формата.
### 1.3 Диапазоны `id` по типам файлов (собрано эмпирически)
Похоже, что числовые ID образуют условные "пространства имён" по типу
контента — вероятно, глобальные константы в оригинальном коде:
| Файл(ы) | Диапазон `id` | Кол-во записей | Предполагаемое содержимое |
|---|---|---|---|
| `levels.dat` | 20002015 | 16 | id=2000 — служебный блок (16 байт, см. §3); id=2001..2015 — 15 уровней |
| `guard.dat`, `fat.dat`, `skel.dat`, `shadow.dat` | 750–784 (варьируется) | ~3035 | id=751(750) — служебный блок; остальные — кадры анимации спрайта |
| `vizier.dat` | аналогично guard | — | кадры анимации визиря |
| `kid.dat` | ~400+ | 220 | кадры анимации игрока (намного больше — герой умеет гораздо больше действий) |
| `guard1.dat`, `guard2.dat` | 750 (1 запись) | 1 | вероятно, дополнительные/альтернативные кадры/варианты |
| `title.dat` | 40–55 | 12 | картинки титульного экрана/логотипов |
| `cpalace.dat`,`epalace.dat`,`vpalace.dat`,`cdungeon.dat`,`edungeon.dat`,`vdungeon.dat` | 2001343 | 205238 | фоновые тайлы дворца/подземелья, отдельно для CGA(`c*`)/EGA(`e*`)/VGA(`v*`) |
| `pv.dat` | 800981 | 103 | доп. графика (возможно, "Prince/Vizier" катсцены) |
| `digisnd1/2/3.dat` | 10000+ | 20–44 | оцифрованный звук (Covox/Disney Sound Source) |
| `midisnd1/2.dat` | 10024+ / аналог | 16 | General MIDI музыка |
| `mt32snd1/2.dat` | 10000+ | 24/7 | музыка для Roland MT-32 |
| `ibm_snd1/2.dat` | 10000+ | 44 | музыка/эффекты через PC-спикер |
| `prince.dat` | — (1 крупный ресурс) | — | MIDI-тема (вероятно, финальная тема "Принц"/титры — см. текстовые события "The Princess awaits") |
Во всех файлах первая (наименьшая по `id`) запись — маленький "служебный"
ресурс (6–44 байта), стоящий перед основным контентом. Скорее всего это
локальная мини-таблица/палитра/список ссылок для данного набора ресурсов —
по аналогии с тем, что у уровней id=2000 отдельно от самих уровней (см. §3).
---
## 2. Формат уровня (`levels.dat`, id=2001..2015) — уверенность: высокая
Каждая запись уровня имеет размер **2305 байт** и по данным полностью
совпадает по объёму с уровнями из Apple II версии (`01 POP Source/Levels/LEVELn`
— ровно **2304 байта** каждый, см. `docs/APPLEII_RESOURCE_FORMAT.md`).
Вывод: формат карты уровня в DOS-версии, судя по всему, **унаследован
практически без изменений от оригинального Apple II формата** (Джордан
Мехнер писал игру на 6502 и данные уровней переносились как есть), с добавлением
одного лишнего байта в DOS-упаковке (2304+1=2305 — вероятно, контрольный байт/
маркер конца, добавленный DOS-упаковщиком ресурсов, а не часть игровых данных).
**Практическое следствие:** байтовая структура самого уровня (тайлы 3×10 на
экран, 24 экрана, таблицы стражников, дверей и т.д.) должна документироваться
один раз — по исходникам Apple II (см. соответствующий раздел), и напрямую
применяться к DOS `levels.dat`, отбросив 1 лишний байт в конце каждой записи.
Байтовые значения тайлов в дампе (в основном 0x00–0x39) визуально согласуются
с диапазоном небольших целых кодов тайлов, что для формата карты и ожидается.
Первая запись, id=2000, размер 16 байт — не уровень, а отдельный маленький
блок (возможно: количество уровней, начальный уровень, версия формата,
стартовые координаты игрока/охраны по умолчанию). Точное назначение не
установлено — требует сопоставления с диз­ассемблированным кодом загрузчика
уровней (в SDLPoP это, по всем признакам, отдельная процедура чтения
`level` ресурса).
**Сверка с независимой распаковкой SDLPoP (`data/LEVELS/`):** там лежат файлы
`res2000.bin``res2015.bin` (16 штук — количество совпадает). Байты
`res2001.bin` содержательно совпадают с тайловыми данными нашей записи
id=2001 (та же последовательность значений тайлов) — это подтверждает, что
нумерация id верна. Но есть нестыковка по размеру: у SDLPoP `res2000.bin`
**2305 байт** (как и все остальные), тогда как в нашем локальном
`levels.dat` запись id=2000 — всего **16 байт**. Скорее всего, это разные
релизы/сборки игры (см. §7 — размеры некоторых `.dat` у SDLPoP и у нас уже
отличались), и в версии SDLPoP маленький служебный блок либо отсутствует,
либо пронумерован иначе. Это не меняет сам формат контейнера, но означает,
что **точную семантику 16-байтного блока id=2000 в нашей копии игры пока
нельзя проверить через данные SDLPoP** — открытый вопрос.
---
### 2.1 Кросс-подтверждение по исходникам Apple II
Фоновый анализ исходников Apple II (см. `docs/APPLEII_RESOURCE_FORMAT.md`)
подтверждает и объясняет структуру уровня напрямую по коду. Уровень на Apple
II — дамп структуры `blueprnt` (`EQ.S`): `BLUETYPE`(720Б, 24 экрана×30 тайлов)
+ `BLUESPEC`(720Б) + `LINKLOC`(256Б) + `LINKMAP`(256Б) + `MAP`(96Б, граф
соседних экранов) + `INFO`(256Б, метаданные/старт Кида/стражников) = ровно
2304 байта. Учитывая, что DOS-запись уровня — это ровно 2304+1 байт с
байтовыми значениями тайлов, укладывающимися в диапазон 0–29 (id тайла) плюс
служебные биты (аналогично `idmask=%00011111`, `reqmask=%00100000` из
`EQ.S:484-486`), можно с высокой уверенностью считать, что **DOS-версия
использует ту же самую раскладку `blueprnt`**, лишь с добавлением одного
байта (вероятно, контрольной суммы) в конце DOS-упаковки. Это снимает
необходимость отдельно реверсить формат уровня для DOS — таблица тайлов,
enum id (0=space...29=archtop4), формат `LINKLOC`/`LINKMAP` и `INFO` из
Apple II документа применимы напрямую.
## 3. Графика (спрайты и фоновые тайлы) — уверенность: средняя/низкая
Файлы `kid.dat`, `guard.dat`, `fat.dat`, `shadow.dat`, `skel.dat`,
`vizier.dat`, `title.dat`, `c/e/v-palace.dat`, `c/e/v-dungeon.dat`, `pv.dat`
хранят по контейнерному формату (§1) множество мелких чанков (десятки—сотни
байт каждый).
Что подтверждено:
- Наборы `shadow.dat`/`kid.dat` и `fat.dat`/`vizier.dat` содержат **побайтово
идентичные фрагменты** данных в начале файла — это ожидаемо: "Тень" (Shadow)
визуально копирует анимацию Кида, а "Толстый страж" (Fat guard, пасхалка)
переиспользует модель Визиря. Подтверждает, что персонажи одного "типа
тела" используют общий набор геометрии/анимации.
- Отдельные чанки *не* имеют очевидного унифицированного заголовка
(высота/ширина/палитра) фиксированного размера — попытка интерпретировать
первые байты чанка как `{height:u16, width:u16, flags:u16}` не подтвердилась
на реальных данных (получаются нереалистичные размеры для маленьких чанков).
Вероятно, как и в Apple II версии (см. `FRAMEDEF.S`/`SEQTABLE.S`), геометрия
кадра (ширина, высота, точка привязки) хранится **отдельно от самих
пиксельных данных** — в таблицах внутри `PRINCE.EXE`, а не в `.DAT`-чанке.
Сам чанк, вероятно, содержит только упакованные пиксельные данные
(RLE/дельта-упаковка, по аналогии с `UNPACK.S` в Apple II исходниках).
- Точный алгоритм упаковки пикселей **не восстановлен** в рамках этого
анализа по сырым байтам — байт-в-байт разбор распаковщика без
дизассемблирования `PRINCE.EXE` надёжно не сделать. **Но для практических
целей это не требуется**: см. §7 — в SDLPoP уже есть тот же самый набор
изображений в готовом, распакованном виде (PNG), которым можно пользоваться
напрямую как источником ассетов, не реализуя свой декодер `.DAT`-пикселей.
Писать собственный декодер имеет смысл только если понадобится читать
оригинальные `.DAT` "на лету" (например, для точной сверки контента именно
нашей копии игры) — тогда ориентир — исходник SDLPoP (`src/seg009.c`).
---
## 4. Звук — уверенность: высокая (по структуре), низкая (по деталям кодека)
Обнаружено 4 параллельных набора звуковых ресурсов под разные звуковые
устройства DOS-эпохи — типично для игр начала 1990-х с "звуковым меню":
| Файл | Устройство | Формат чанка |
|---|---|---|
| `midisnd1.dat`, `midisnd2.dat` | General MIDI / MPU-401 | каждый чанк = 2-байтовый LE-префикс длины + встроенный Standard MIDI File (`MThd`...`MTrk`...) |
| `mt32snd1.dat`, `mt32snd2.dat` | Roland MT-32/CM-32L | тот же формат: префикс длины + `MThd`/`MTrk`, с MT-32-специфичными SysEx (видны строки `MT-32.mff`, текстовые мета-события вроде `"The Princess awaits"`) |
| `prince.dat` | (аналогично MIDI) | отдельный крупный музыкальный ресурс, тот же MIDI-контейнер — вероятно, финальная тема |
| `digisnd1/2/3.dat` | Covox / Disney Sound Source / Sound Blaster (оцифрованный звук) | чанк начинается с нескольких служебных байт, среди которых слово `0x2AF8` = 11000 — похоже на частоту дискретизации 11 кГц; далее — сырые 8-битные PCM-сэмплы (значения кластеризуются вокруг ~0x7A–0x90, типично для беззнакового 8-бит аудио, смещённого к середине шкалы) |
| `ibm_snd1.dat`, `ibm_snd2.dat` | PC Speaker | чанк — последовательность троек байт похожих на (длительность, делитель_частоты) — простой формат "бипера", отличный от MIDI |
Подтверждено разбором первых чанков в каждом файле (см. байтовые дампы,
проверялись скриптом). Точная семантика полей внутри `digisnd`/`ibm_snd`
(разрядность, порядок байт служебного заголовка) не выведена до конца — при
реализации порта достаточно распознавания по типу файла и (для MIDI-семейства)
можно напрямую воспроизводить встроенный Standard MIDI File, пропустив
2-байтовый префикс длины.
---
## 5. Готовые распакованные ассеты в SDLPoP (`data/`) — практический источник для порта
Репозиторий github.com/NagyD/SDLPoP содержит папку `data/`, где, помимо
самих `.DAT`-контейнеров, каждый ресурс **продублирован в виде отдельно
распакованного файла**, названного по его `id` из таблицы оглавления (§1.2).
Проверено через GitHub API (`api.github.com/repos/NagyD/SDLPoP/contents/...`):
| Подпапка/файл в `data/` | Содержимое | Соответствие нашему разбору |
|---|---|---|
| `GUARD.DAT`, `GUARD1.DAT`, `GUARD2.DAT` | сырые `.DAT` | размер **побайтово совпадает** с нашими локальными `guard.dat`/`guard1.dat`/`guard2.dat` (6950 / 117 / 117 байт) |
| `DIGISND1.DAT`, `MIDISND2.DAT` и др. | сырые `.DAT` | размер **не совпадает** с нашими локальными файлами (48545 vs 50101, 18408 vs 18958) — другой релиз/сборка игры |
| `GUARD/res751.png``res784.png` | готовые PNG, по одному на кадр анимации, имя = `res<id>.png` | id-диапазон (751-784) точно совпадает с нашим разбором `guard.dat` |
| `VPALACE/res200.pal`, `res201.png`, `res202.png`, … | палитра (JASC `.pal`) + PNG-кадры фонов дворца, **VGA-вариант (256 цветов)** | id-диапазон (200+) совпадает с `vpalace.dat` |
| `LEVELS/res2000.bin``res2015.bin` | сырые дампы уровней по 2304-2305 байт | id совпадает с `levels.dat`; содержимое `res2001.bin` **сверено побайтово** с нашим id=2001 — тайловые данные совпадают |
| `KID/`, `PRINCE/`, `SHADOW/`, `SKEL/`, `VIZIER/`, `FAT/`, `TITLE/`, `VDUNGEON/`, `PV/`, `IBM_SND1/`, `IBM_SND2/`, `font/`, `music/` | аналогичные наборы для остальных ресурсов | не проверялись по отдельности, но структура (папка на каждый `.dat`, файлы `res<id>.ext`) наблюдается одинаково |
**Вывод:** это данные из немного **другого релиза DOS-версии**, чем те, что
лежат у нас в `MSDOS/` (см. расхождение в размере `digisnd`/`midisnd`), но
формат контейнера и нумерация `id` — те же самые. Практически это значит:
1. Для получения играбельных PNG-спрайтов и VGA-фонов **не нужно
реализовывать декодер сжатия пикселей** — можно взять готовые файлы
`data/<ИМЯ>/res<id>.png` напрямую как исходный материал для конвертации
под видеорежим ZX Sprinter (в т.ч. `VPALACE`/`VDUNGEON` — уже
256-цветный VGA-арт, что прямо отвечает на вопрос про полноцветность).
2. Если в проекте важно использовать именно ту версию контента, что в наших
`MSDOS/*.dat` (а не версию из SDLPoP) — распаковку своих файлов всё же
придётся делать (кодек пикселей по-прежнему не восстановлен для сырых
`.DAT`, см. §3), либо принять решение работать с версией SDLPoP как
мастер-источником ассетов вместо своей.
---
## 6. Служебные не-ресурсные файлы
- `config.dat`, `setup.dat` — 28 байт, не являются ресурсными контейнерами
(не проходят проверку §1.1 — "размер" получается больше самого файла).
Скорее всего простые бинарные структуры настроек (звук/видеорежим,
выбранный на этапе `SETUP.EXE`/`INSTALL.EXE`), не связаны с игровым
контентом.
- `desktopd.cfg`, `setup.cfg` — текстовые/бинарные конфиги DOS-инсталлятора,
вне скоупа игровых ресурсов.
- `PRINCE.EXE` / `PRINCE.REM` — почти идентичны (отличие в единичных байтах
в районе смещения ~0x4ED0), похоже на кряк/патч одного байта проверки —
не относится к формату ресурсов.
- `old-games.nfo` — ASCII-арт NFO релиз-группы (old-games.ru), не игровые
данные.
---
## 7. Итоговая таблица уверенности
| Раздел | Уверенность | Как подтверждено |
|---|---|---|
| Контейнер `.DAT` (заголовок + таблица) | Высокая | Проверено скриптом на всех 28 файлах, инвариант offset+size выполняется без исключений; независимо подтверждено именованием `res<id>.*` в SDLPoP `data/` |
| ID-пространства ресурсов | Средняя-высокая | Наблюдение по диапазонам + сверка с `res<id>` именами файлов SDLPoP и побайтовым содержимым `res2001.bin` |
| Формат уровня = формату Apple II | Высокая (по размеру и содержимому), служебный блок id=2000 — открытый вопрос | Совпадение размера (2304 vs 2305), тайловые байты сходятся с `res2001.bin` из SDLPoP |
| Формат изображений/спрайтов (сырой `.DAT`) | Низкая-средняя | Контейнер подтверждён, кодек пикселей — нет; но практически закрыто наличием готовых PNG в SDLPoP `data/` (§5) |
| Формат звука (тип контейнера) | Высокая для MIDI-семейств, средняя для digisnd/ibm_snd | Явные MIDI-сигнатуры `MThd`/`MTrk` видны в байтах |
**Рекомендация для дальнейшей работы:** для получения арт-ассетов (спрайты,
фоны, палитры) — использовать готовые распакованные файлы из
`github.com/NagyD/SDLPoP/tree/master/data` (§5), это быстрее и надёжнее
самостоятельной реализации декодера. Декодер сырого `.DAT`-формата
изображений и точную семантику служебных полей `digisnd`/`ibm_snd`
(§3, §4) стоит восстанавливать только если понадобится читать именно нашу
локальную копию `MSDOS/*.dat` "как есть" — тогда ориентир прежний: исходник
SDLPoP (`src/seg009.c`, `src/data.c`/`data.h`).
+509
View File
@@ -0,0 +1,509 @@
# Prince of Persia на ZX Sprinter — план порта
Статус: план (2026-07-15). §2 (A: kbd_mod_state / B: kbd_raw) —
РЕАЛИЗОВАНО и частично проверено в MAME (tests/kbdraw, 2026-07-15,
подробности в §2.2); PoC (§5) и остальные фазы — не начаты. Опирается на
`APPLEII_RESOURCE_FORMAT.md` / `MSDOS_RESOURCE_FORMAT.md` / `README.md` в
этой папке, на текущий sprinter-cc/libc/libbgi (см. §1) и на локальные копии
`applications/PoP/SDLPoP` (github.com/NagyD/SDLPoP, GPLv3) и
`applications/PoP/PR` (github.com/NagyD/PR, GPLv2) — используются только как
справочник по структурам/константам оригинального движка и как источник
готовых распакованных ассетов (`SDLPoP/data/`), не как код для копирования.
---
## 1. Что уже есть в sprinter-cc и библиотеках (используем как есть)
Собрано из `docs/TODO.md`, `docs/libc-reference.md`, `docs/sprite-api-design.md`,
`libbgi/include/{gfx.h,sprite.h,graphics.h}`, `examples/rpgwalk`.
- **Графика 320×256×256** (`GFX_MODE_320x256x256`, режим 0x81) — разрешение и
глубина цвета совпадают почти впрямую с VGA-ассетами оригинала
(`SDLPoP/data/VPALACE`, `VDUNGEON` — уже 256-цветные PNG). Не нужно ужимать
в EGA/CGA палитру.
- **BGI-слой** (`graphics.h`) — примитивы, палитра, текст, `getimage/putimage`
— Фазы 1-2d готовы и проверены в MAME.
- **Спрайтовый движок v2** (`sprite.h`, ветка `sprite-engine-v2`) — ровно то,
что нужно персонажам PoP:
- retained-модель (`sprite_update`/`sprite_flip`, double-buffer, dirty-биты,
heal+blit за один проход);
- кадровая анимация по ленте (`sprite_anim`, LOOP/PINGPONG/ONCE,
горизонтальная/вертикальная лента) и tween-перемещение
(`sprite_moveto`, DDA без knowledge-heavy арифметики);
- Y-сортировка слоями (`gfx_sprite_ysort`, `layer`) — то, что нужно для
«Кид перед/за стражником» без ручной пересортировки;
- атласы в EMM-страницах (`atlas_t`/`atlas_load`) — на восьмерых
персонажей в `rpgwalk` уже работает: прямой прецедент для Кида/стражника;
- ограничение кадра ≤ 64×64 — с запасом (см. §3: кадры Кида в оригинале
~12-30 × 39-42 px).
- **Frame pacing** (`gfx_set_fps_div`) + цепочка кадровых IRQ — стабильный
логический тик независимо от рендер-нагрузки экрана (проверено MAME).
- **EMM-бюджет**: ~3.3 МБ свободно на старте (`memory/sprinter_emm_budget`) —
с большим запасом на все спрайт-атласы и предрендеренные фоны комнат (см.
§4) даже без выгрузки неиспользуемых уровней.
- **Файловый ввод-вывод** (FILE* v2, `fopen/fread/...`) — для загрузки
уровней/атласов/палитр с дискеты, по образцу `rpgwalk` (`atlas_load`,
`gfx_pal_fload`).
- **Клавиатура (событийная)** — `kbhit/getch/getkey` (ASCII + `KEY_*` скан-код
для стрелок), см. §2 — это НЕ то, что нужно для управления Кидом один в
один (см. ниже).
- **Звук** — `cbl.h` (потоковый CBL/COVOX, callback-модель, verified MAME) —
подходит для оцифрованных эффектов (`digisnd*.dat` — PC-звук
~11 кГц 8-бит, см. `MSDOS_RESOURCE_FORMAT.md` §4).
Вывод: **движок отрисовки и анимации почти не требует нового кода**
самый близкий по духу пример (`rpgwalk`: атласы, анимация, tween, дабл-буфер,
FPS-делитель) переносится на PoP почти без изменений архитектуры.
---
## 2. Единственный принципиальный пробел: удержание клавиш
**Спайк проведён (2026-07-15), вопрос закрыт артефактами — не догадкой.**
`getch`/`getkey` — это события ESTEX WAITKEY/SCANKEY (по нажатию), без чёткой
информации о СОСТОЯНИИ (что зажато прямо сейчас, несколько клавиш
одновременно). Prince of Persia на управлении требует именно состояния:
держать направление (бег) + одновременно нажать вверх (прыжок вперёд), держать
Shift (модификатор) + направление и т.д.
### 2.1 Находки
1. **`docs/converted/ProgrammerManual.txt` документирует функцию, которую мы
раньше пропустили: `CTRLKEY` (ESTEX $33h)** — «Получить состояние
клавиатуры». Дословно: «данные берутся не из буфера клавиатуры (как в
остальных функциях), а непосредственно из результатов ПОСЛЕДНЕГО
сканирования» — то есть это НАСТОЯЩЕЕ live-state, не событие. Но
покрывает только модификаторы: Left/Right Shift, Ctrl, Alt,
Rus/Lat, Num/Scroll/Caps Lock, Insert (не обычные клавиши вроде стрелок).
Готовое решение для «держать Shift = бежать» — тривиальная обёртка,
без архитектурных рисков.
2. Для ОБЫЧНЫХ клавиш (стрелки, буквы) такого live-state нет нигде в ESTEX —
`WAITKEY`/`SCANKEY`/`TESTKEY` ($30/$31/$37h) — все три отдают ОДИНАКОВЫЙ
формат «очередное нажатие», без release. `TESTKEY` не удаляет событие из
буфера (полезно для «подсмотреть, не потребляя»), но это тоже разовое
нажатие, не состояние.
3. Автоповтор клавиатуры (typematic) не годится как замена held-state:
`MAME_MCP_GUIDE.md` фиксирует задержку до первого повтора ~1 секунда
(типично для PS/2) — на порядок медленнее кадра (20 мс), не подходит для
платформера.
4. **Решающий артефакт — `libc/irq/_irq_tramp.c` (сам трамплин прерывания,
не гипотеза):** вектор 0xFF общий для кадра/клавиатуры/CBL. Ветка
клавиатуры (бит 0 порта 0x19 = SIO-A RR0 «байт принят») делает буквально
`jp 0x0038` (прямиком в DSS) **до какого-либо чтения порта данных 0x18 И
до нашей кадровой цепочки (`_irq_chain`)** — наш `irq_chain_add`
вообще не видит клавиатурные прерывания, они физически не доходят до
цепочки (см. `tr_notkbd`/`tr_frame` разбор в файле). Значит текущая
инфраструктура (тот же механизм, что несёт FPS-делитель) НЕ дает
зацепки для клавиатуры без правки самого трамплина.
5. Регистр данных SIO (порт 0x18) — аппаратный приёмный буфer, чтение
деструктивно (дёргает байт из очереди); кто прочитал первым, тот и
владеет байтом. Значит «подглядеть, не мешая DSS» технически
невозможно — необходимо либо совсем не трогать этот путь (статус-кво),
либо взять его СЕБЕ полностью на время геймплея.
### 2.2 Рекомендация (конкретная, не три равнозначных варианта)
**A. Тривиально, почти без риска — обернуть `CTRLKEY` ($33h)** отдельной
функцией (например `kbd_mod_state()` в `<conio.h>`) — даёт настоящий
held-state для Shift/Ctrl/Alt. Можно делать хоть сейчас, не архитектурное
решение.
**B. Для обычных клавиш (стрелки и т.д.) — по прецеденту CBL.** В
`_irq_tramp.c` уже есть пример «приватного» пути на том же векторе 0xFF,
который сознательно НЕ чейнится к DSS (CBL: бит 7 порта 0xFE, свой
хук `_irq_cbl_hook`, полный сейв, свой `reti`). Предлагаемый новый
компонент `<kbd_raw.h>` — симметричный: ветка по биту 0 порта 0x19 читает
порт 0x18 САМА (декодирует PS/2 make/break, `0xF0`-префикс — протокол
уже задокументирован в `docs/samples/sprinterKeybLib.asm`), ведёт битовую
карту «клавиша N зажата», и НЕ прыгает в DSS, пока путь активен —
жизненный цикл `kbd_raw_open()`/`kbd_raw_close()` один в один как у
`cbl_open`/`cbl_close`.
**Важное следствие (сообщить пользователю явно, не прятать):** пока
`kbd_raw_open()` активен, DSS вообще не получает клавиатурных байт —
`kbhit/getch/getkey/CTRLKEY` заведомо не будут работать, ESC для выхода
в DSS-смысле тоже (нужно проверять raw-битовую карту самим). Это
нормально для активной фазы геймплея (у самой игры и так свой цикл
ввода), но означает: экраны/паузы, которым нужен ESTEX-ввод (например,
диалог сохранения через `fopen`, если тот когда-либо потребует ввода
с консоли), должны на это время `kbd_raw_close()`.
**Не рекомендую вариант «таймаут-эвристика поверх SCANKEY»** — after
находки о typematic-задержке ~1с он не даёт нужной задержки для игры;
рекомендация A+B закрывает потребность без компромиссов.
**Статус: A+B РЕАЛИЗОВАНЫ (2026-07-15, по согласованию с пользователем).**
- A: `kbd_mod_state()``libc/conio/kbd_mod_state.c` + `<conio.h>`
(`KBD_MOD_*`).
- B: `<kbd_raw.h>` (`libc/kbd/`) + правка `libc/irq/_irq_tramp.c`
(новая ветка на бите 0 порта 0x19: raw активен → сама читает порт
0x18, декодирует make/break, НЕ чейнится к DSS; raw выключен —
поведение как раньше, без изменений). Трамплин вырос со 150 до
220 байт — `_IRQ_TRAMP_BUF_SIZE` поднят с 224 до 288 (было 4 байта
запаса, стало ≥60). `make -C libc` (fast+safe) — чисто.
- **Верификация в MAME** (`tests/kbdraw`, полный цикл open→держать→
отпустить→ESC-выход→close): `KBD_LEFT` (0x16B, расширенный код
E0 6B) — down на нажатие, up на отпускание, ТОЧНО совпало с
константой из `<kbd_raw.h>`; `KBD_ESC` (0x76, обычный код) —
корректно закрыл raw-канал и вернул DSS (`IM` вернулся в 1).
Побочно найдено и задокументировано в `docs/libc-reference.md`
(`<kbd_raw.h>`): MAME-мостовой `press_key` дёргает ОБЕ клавиатуры
(PC+ZX) одновременно и через ZX-путь давал паразitный незатухающий
бит — не относится к реальному сценарию (пользователь подтвердил:
матрица на Sprinter давно не используется), но означает, что
будущие MAME-тесты этой функции надо гонять через `:kbd:ms_naturl:*`
напрямую, не через удобный `press_key`. UP/DOWN/RIGHT/SPACE/SHIFT
константы — НЕ перепроверены поштучно (тот же общеизвестный
стандарт PS/2 Set 2, что и подтверждённые LEFT/ESC — проверить перед
использованием в PoC, если управление будет ощущаться неверно).
- На реальном железе — не проверено (только MAME).
---
## 3. Формат данных — что напрямую переносим из docs/*RESOURCE_FORMAT.md
- **Уровень** (`BLUETYPE`/`BLUESPEC`/`LINKLOC`/`LINKMAP`/`MAP`/`INFO`,
2304 байта, 24 экрана × 30 тайлов) — читаем один раз при загрузке уровня
в свою C-структуру (прямой memcpy дампа файла, поля читаем по офсетам
из `APPLEII_RESOURCE_FORMAT.md` §1). DOS `levels.dat` даёт то же самое
+1 байт в конце записи — отбросить.
- **Графика фона/спрайтов** — кодек сжатия DOS `.DAT` не восстановлен и
восстанавливать не будем: используем уже распакованные PNG из
`SDLPoP/data/{KID,GUARD,VPALACE,VDUNGEON,...}` (см.
`MSDOS_RESOURCE_FORMAT.md` §5, §7 — тот же контейнерный формат/нумерация,
просто другой релиз сборки данных). Измерено локально: кадры Кида —
~12×39 .. 30×42 px (P-режим, 4-бит палитра), фоновые тайлы подземелья —
32 px по ширине (10 колонок × 32 = 320 — сходится с шириной экрана), высота
тайла 20/60/62 px (неоднородные ряды пола/потолка/арок) — укладывается в
лимит спрайтового движка (кадр ≤ 64×64) без всяких изменений движка.
- **Звук** — `digisnd*.dat` (PC-звук 8-бит ~11 кГц) — конвертация в сырой
PCM и проигрывание через `cbl_open`/`cbl_push_*`; `ibm_snd*.dat` (PC-спикер
тройки «частота×2Б + длительность») — тривиальный бипер, не требует CBL.
MIDI-семейство (`midisnd`, `mt32snd`, `prince.dat`) — вне скоупа (нет
синтеза MIDI на платформе; не блокирует геймплей).
---
## 4. Стратегия фона — ПЕРЕСМОТРЕНО 2026-07-15: тайловый рендерер В РАНТАЙМЕ
**Было** (первая версия плана): офлайн-склейка каждой комнаты в готовую
растровую картинку 320×~193, `gfx_blit` целиком при входе — обоснование
было «ноль нового кода в libbgi». Пересчёт по факту наличия структурных
данных комнаты (§3.4 формата, `level.h`) показал: 16 уровней × 24 комнаты ×
~60-80 КБ/картинка — это **30+ МБ**, при том что одна и та же картинка
тайла (пол/стена/колонна) переиспользуется в десятках комнат — офлайн-
склейка печёт её заново в каждую копию.
**Стало**: тайлы — переиспользуемый набор картинок ОДИН на визуальный
стиль (не на комнату), структурные данные комнаты — компактные (60 байт:
30×foretable+30×backtable, все 16 уровней ≈ 37 КБ, см. `level.h`).
`room_draw()` (applications/PoP/poc/room.c) проходит 30 тайлов комнаты и
зовёт `gfx_blit` для каждого, читая картинку из таблицы по типу тайла
(`tile_images[TILE_TYPE]`). Итог: десятки-сотни КБ переиспользуемых
тайл-картинок на весь визуальный стиль + ~37 КБ структуры уровней —
вместо 30+ МБ.
**Почему это НЕ бьёт по бюджету кадра**: `room_draw()` зовётся ОДИН РАЗ
при входе в комнату (смена комнаты — не every-frame событие), не в
игровом цикле — это не `sprite_update`, тактовый бюджет кадра не
затронут.
Анимированные тайлы (факел, шипы, дверь-плита) по-прежнему рисуются как
отдельные `sprite_t` поверх фона — движок это уже умеет (Y-order/layers,
dirty-биты, heal против фона через ОЗУ-копию); `room_draw()` кладёт в
ОЗУ-копию именно статичную геометрию (пол/стены/колонны Фазы 1 — §5.2),
поверх неё heal спрайтов работает как обычно.
`toolchain/room_compose.py` (генерик-компоновщик тайлов в одну картинку,
§6.1) остаётся полезным ИНСТРУМЕНТОМ конвертации отдельных тайл-картинок
(PNG → getimage raw), просто теперь его выход — 32 маленьких файла
`tileNN.raw` (по одному на тип тайла), а не один большой файл на комнату;
сама раскладка/повторное использование по комнатам — в C-коде
(`room_draw`), не в офлайн-склейке.
---
## 5. Proof-of-Concept — цель: доказать, что порт вообще ощущается как PoP
**Объём**: одна комната (например Level 1, экран старта Кида), без
переходов между экранами, без стражников (стретч-цель, не обязательна).
**Что показываем**:
1. Кид на экране, с закреплённым офлайн-конвертированным набором кадров
(подмножество: idle, walk L/R, jump-начало/дуга/приземление, стоп-на-краю,
возможно повисание на краю) — атлас в W0-странице, по образцу `rpgwalk`.
2. Управление: держать влево/вправо — идёт; отпустил — тормозит/стоит;
нажатие вверх во время бега — прыжок вперёд (дуга по авторским таблицам
смещений, не по gravity-физике «с нуля» — см. §6). Здесь же проверяется
решение по §2 (реальный held-state).
3. Столкновения: пол/край экрана/провал — по факту чтения тайла из
`BLUETYPE` под ногами (без LINKLOC-триггеров пока).
4. Стабильный кадр 50 Гц через уже готовый `gfx_wait_vsync`/дабл-буфер
(без FPS-делителя — Кид анимируется каждый видеокадр, как в оригинале).
**Критерий успеха**: субъективно «прыжок ощущается как в PoP» (дистанция и
тайминг прыжка сверены с оригинальными таблицами, не подобраны на глаз —
см. §6), управление отзывчивое (не событийное с задержкой), сцена не мерцает
на стыке спрайт/фон.
**Не входит в PoC**: стражники/бой, звук, HUD/таймер, переходы между
комнатами, ловушки/триггеры, титры/меню, сохранения.
**Расположение**: `applications/PoP/poc/` (свой sprinter-cc проект + Python
конвертер ассетов, по структуре `examples/rpgwalk`).
### 5.1 Статус (2026-07-15) — первая итерация: управление + коллизия края
Сделано и проверено в MAME (`applications/PoP/poc/`, `make run`):
держать LEFT/RIGHT (`kbd_raw_down`, raw-канал из §2) — идёт непрерывно,
отпустил — стоит на месте (не событийно, реальный held-state);
столкновение с краями экрана (клип по `MINX`/`MAXX`); анимация
ходьбы/разворота лицом по направлению (`sprite_anim` пинг-понг);
дабл-буфер + `gfx_wait_vsync` — без видимого мерцания. Сборка —
`--memory huge` без `--bank` (§10, подтверждено рабочим).
**Важное отступление от плана (осознанно, не молча):** персонаж —
ВРЕМЕННАЯ заглушка (лицензированный спрайт-пак
`third_party/16x16-RPG-characters` через `tools/gen_kid_placeholder.py`,
тот же источник, что уже использует `examples/rpgwalk`), а НЕ
конвертированная графика оригинальной Prince of Persia. Причина:
исходный набор кадров Кида (`SDLPoP/data/KID`) — копирайт
Broderbund/Ubisoft; автоматический конвейер, который систематически
извлекает и переупаковывает его в новый формат, — это на практике
внутрипроектное решение, которое стоит принимать пользователю явно
для каждого шага, а не проводить асинхронно агентом без лишнего
подтверждения. Сама графика — не то, что проверяет PoC (§5 явно:
цель — ощущение управления/коллизий, не визуальная точность). Замена
на настоящую графику Кида — отдельный шаг, на усмотрение пользователя.
**Ещё не сделано** (следующие итерации §5): авторские таблицы
смещений кадров (§6 — движение при ходьбе линейное, px/кадр),
реальный уровень/фон по `BLUETYPE`/`LEVEL1` (сейчас — плейсхолдер:
плоский пол на весь экран, без ямы/выступа), `kbd_mod_state`/
Shift-бег не подключены к игровому циклу (обёртка готова с Фазы A).
**Прыжок/присед добавлены и ПРОВЕРЕНЫ (2026-07-15)**: состояние
`jumping`/`jump_t`/`crouching`, своя приблизительная дуга прыжка
(`jump_height[]`, 40 кадров) — не авторская таблица, см. §6.1.
HUD-текст статуса (нет отдельной позы).
Живое тестирование пользователем нашло реальный баг: держа UP чуть
дольше 0.8 с (длительность дуги), получали ДВА прыжка подряд — код
проверял `kbd_raw_down(KBD_UP)` как уровень (держится, пока клавиша
физически зажата), а не как фронт нажатия, поэтому в момент
приземления «UP всё ещё зажат» тут же триггерил новый прыжок.
Исправлено edge-detect'ом (`up_prev` — предыдущее состояние UP,
триггер только на переход 0→1). Проверено брейкпоинтом в отладчике
MAME на адресе входа в код прыжка: за одно длинное удержание UP
брейкпоинт срабатывает РОВНО ОДИН РАЗ — фикс подтверждён на уровне
кода, не только «на глаз».
Побочный урок (см. `docs/libc-reference.md` `<kbd_raw.h>`): моя
более ранняя попытка проверить UP/DOWN/RIGHT по скриншотам после
`press_key` ошибочно решила, что скрипт их не нажимает вообще —
на самом деле нажимает исправно, просто скриншот ловил случайный
момент дуги. Брейкпоинт/watchpoint на конкретный адрес кода —
надёжнее скриншота для таких проверок.
---
## 6. Модель движения: авторские таблицы кадров, не физика с нуля
Оригинальный движок PoP не считает прыжок как непрерывную физику
(gravity/velocity каждый тик) — движение персонажа задано таблицами кадров
анимации, где у части кадров зашито фиксированное смещение (dx, dy) для
ЭТОГО конкретного кадра последовательности (структура видна и в
исходниках Apple II — `SEQTABLE.S`/`MOVER.S`, и в SDLPoP `seg003.c`/`seq*`
таблицах). Практическое следствие для нашего движка:
- **Не использовать** `sprite_anim`/`sprite_moveto` для основного
персонажа как есть (они лианейно тянут по таймеру/тянут к линейной
цели) — вместо этого приложение само на каждый логический тик:
переключает кадр (`sprite_frame`, атлас как лента поз, не «прогрессия
первый..последний» автоматом) и одновременно применяет dx,dy ЭТОГО
кадра к позиции (`sprite_move`).
- Готовая автоматика движка (`sprite_anim`/`sprite_moveto`/tween,
Y-сортировка) остаётся полезной для декоративных/фоновых элементов
(факелы, патрулирующий стражник вне боя — почти один в один паттерн
`rpgwalk`).
- Источник таблиц смещений: переснять из `Prince-of-Persia-Apple-II/01 POP
Source/Source/{MOVER.S,SEQTABLE.S,FRAMEADV.S}` и/или
`SDLPoP/src/seq*.c` — задача Фазы 1 полной реализации (§7), не PoC
(для PoC можно взять урезанный набор смещений вручную по количеству
пикселей на кадр, посчитанному по видео/скриншотам оригинала, и уточнить
позже).
### 6.1 Инструмент конвертации кадров разного размера (`toolchain/png_strip.py`)
Кадры персонажа в оригинале — РАЗНОГО размера каждый (bbox зависит от
позы; `sprite_t` нашего движка (`libbgi/include/sprite.h`) хранит ОДИН
фиксированный w/h на весь спрайт и рисует от угла, без per-frame
смещения — в отличие от оригинала, где на каждый кадр было своё XCO/YCO
(`APPLEII_RESOURCE_FORMAT.md` §2.2). `toolchain/png_strip.py` (генерик,
не завязан на PoP — принимает произвольный список PNG) закрывает это
ПАДДИНГОМ: канвас = макс. w/h среди кадров ленты, якорь по умолчанию
bottom-center («ноги на месте»), остальное — прозрачность.
**Компромисс, не полноценное решение**: один сильно выбивающийся по
размеру кадр в ленте раздувает канвас (и память) ВСЕХ кадров этой же
ленты. Смягчается группировкой по похожим размерам в отдельные атласы
(не одна лента на все позы актора — так уже сделано для ходьбы отдельно
от прыжка).
**Полноценное решение (кандидат в будущее расширение библиотеки, НЕ
делать без предложения и подтверждения пользователя)**: per-frame
смещение в `sprite_t` (аналог XCO/YCO оригинала) — тогда паддинг
не нужен вообще, экономия памяти по полной. Делать только если память
станет РЕАЛЬНОЙ проблемой (не гипотетической) — тогда предложить как
отдельную правку `sprite.h`/движка. Подробности компромисса —
memory/png_strip_padding_tradeoff.
---
## 7. Полноценное приложение — фазы (после PoC)
Порядок — по риску и зависимостям, не по геймплейной важности.
**Фаза 0 — инфраструктура порта** (расширяет PoC, не переписывает):
- Хелд-стейт клавиатуры — финальное решение и реализация по §2 (после
подтверждения пользователем).
- Полный конвертер уровней (все 15 файлов `levels.dat`/`LEVELn`) → бинарный
формат приложения (можно 1-в-1 raw dump, читать по офсетам в рантайме —
не обязательно разворачивать в C-struct с указателями).
- Полный конвертер фона (24 экрана × N уровней) в растры + конвертер
спрайт-лент Кид/стражник/скелет/тень/Джаффар в атласы `.atl` (расширение
`conv_sprites.py`/формата `.atl`, если частот кадров/атласов на актора не
хватит текущего лимита — см. риск в §8).
**Фаза 1 — Кид, полный набор действий**: стоять/идти/бежать/тормозить/
разворот/прыжок (на месте, вперёд, «прыжок с разбега»)/повисание на
краю/подтягивание/спуск по свисанию/приседание/питьё зелья/смерть от
провала. Переходы между экранами (`MAP`-граф, `INFO.KidStartScrn`).
**Фаза 2 — мир и ловушки**: нажимные плиты/двери через граф
`LINKLOC`/`LINKMAP` (см. `APPLEII_RESOURCE_FORMAT.md` §1.2), шипы
(выдвижение/втягивание/заклинивание), шаткие плиты (loose, обрушение),
зелья (эффект по `BLUESPEC×32`), стартовые позиции по `INFO`.
**Фаза 3 — бой**: подбор/выхватывание меча, состояние стойки, парирование/
удар, коллизия клинков — по логике `AUTO.S`/`seg003-006.c` (референс, не
копия). Стражник: базовое AI-поведение по `GdStartProg` (несколько
шаблонов программ), Y-сортировка слоями уже есть в движке для «кто
спереди/сзади».
**Фаза 4 — разнообразие противников**: скелет, тень (копия анимации Кида —
подтверждено побайтовым совпадением данных, см. `MSDOS_RESOURCE_FORMAT.md`
§3), толстый стражник/визирь (общая база анимации с визирем).
**Фаза 5 — звук**: CBL-эффекты (шаги, удары, двери, падение) из
`digisnd*.dat`→PCM; PC-спикер тройки (`ibm_snd*.dat`) как опциональный
дешёвый бипер без CBL, если формат подтвердится простым парсингом.
**Фаза 6 — оболочка**: титры, меню/выбор уровня, HUD (таймер/жизни),
сохранение прогресса (FILE*), финальные катсцены — по минимуму,
геймплейно не критично.
**Фаза 7 — стабилизация**: полный прогон всех 14 уровней в MAME
(`mame_interactive.py`), затем на реальном железе; профилирование бюджета
кадра по методике `sprite_engine_perf`/`sprite-api-design.md` §9д на самых
насыщенных экранах (несколько стражников + ловушки одновременно —
проверить лимит ~21 спрайт/кадр и Y-sort лимит 32); при необходимости —
банкинг (`--memory big/huge`) для кода/уровня, если размер вылезет за
tiny/small.
---
## 8. Риски, требующие спайка/артефакта до架构 решений
(по правилу `defer_unexplained_quirks` — не гадать, проверять)
1. **Held-state клавиатуры** (§2) — блокирует даже PoC, если решать
«правильно»; иначе PoC на компромиссном варианте 2 (таймаут-эвристика).
2. **Бюджет спрайтов на насыщенный экран** — сцена с 2+ стражниками +
несколько анимированных ловушек может приблизиться к лимиту
~21 спрайт/кадр (`sprite_engine_perf`) — нужна прикидка по реальным
уровням (сколько объектов одновременно активно в худшем экране).
3. **Ёмкость одного атласа/страницы EMM на актора** — у Кида ~220 кадров
(все действия) против 4×12 у `rpgwalk` — потребуется либо несколько
атласов на актора с переключением по фазе действия (стоять/идти отдельно
от боя), либо расширение формата `.atl`/загрузчика на мульти-страничные
атласы — оценить фактический байтовый вес конвертированных кадров Кида
прежде чем проектировать.
4. **Тайминг оригинала** — сверить логическую частоту кадров анимации
оригинала (Apple II ~60 Гц NTSC / DOS — фиксированный таймер) с 50 Гц
Sprinter; если оригинал считался на другой частоте — потребуется
коэффициент пересчёта смещений кадров (§6), иначе прыжки/бег будут
визуально быстрее/медленнее эталона.
---
## 10. Режим памяти сборки
Пользователь предложил `huge` (горячий код в W1, данные в W2, редко
вызываемая логика — банками в W3) как целевой режим. Согласен, с уточнением
по срокам принятия решения.
**`huge` — правильная цель для ПОЛНОГО приложения**, но не то, с чего надо
стартовать:
- Layout `huge` (см. `memory_modes_implemented`): CODE_LOC=0x4100 (W1),
DATA_LOC=0x8000 (W2), банки — W3 (порт 0xE2), `crt0_banked` +
автодетект W2 (как `small`). Состояние приложения (структуры Кида,
уровня, массив `sprite_t`) остаётся в обычном W2-heap ДАЖЕ если код,
который его трогает, забанкован — `malloc` из банка возвращает
W2-указатель (`bank_local_data_pattern`), так что данные не привязаны к
конкретному банку.
- Оверхед `__banked`-вызова (trampoline: +3 байта на стеке между ret и
аргументами, виртуальный 24-битный адрес, см. `sdcc_banking`) — фиксированная
небольшая цена ЗА ВЫЗОВ, не за такт. Это не страшно для функций, которые
вызываются РЕДКО за кадр (AI одного стражника, диалог, переход между
комнатами) — страшно было бы забанковать что-то, что дёргается ВНУТРИ
горячего цикла отрисовки (там уже и так основной бюджет уходит на
`sprite_update`/блиты — см. `sprite_engine_perf`, ~19.5К тактов/спрайт).
Правило простое: **не банковать код на пути "раз в кадр на объект",
банковать код на пути "раз в кадр на комнату/раз в переход/раз в
редкое событие"**: логика ИИ стражника целиком, диалоги/катсцены, меню/
титры/выбор уровня, парсинг уровня при входе в комнату, сериализация
сохранений — хорошие кандидаты в банки; тик Кида, чтение столкновений,
вызов `sprite_update`/`gfx_wait_vsync`, обработка ввода — должны остаться
небанкованными (W1/W2).
- Гранулярность банкования — целый файл (`--bank N=FILE.c`), это уже
системный паттерн проекта (тот же принцип, что и «1 файл = 1 юнит DCE» в
libc) — значит выгодно с САМОГО начала Фазы 1 (не задним числом) резать
исходники приложения по границе «горячее/холодное» файл-в-файл: например
`kid_tick.c`/`collision.c`/`room.c`/`input.c` — неизменно вне банков;
`guard_ai_*.c`/`dialogue.c`/`menu.c`/`levelload.c`/`combat.c` — кандидаты
под `--bank`. Тогда переход на `huge` позже — это правка Makefile/
sprinter-cc-вызова (`--memory huge --bank N=file.c ...`), а не рефакторинг
логики.
**Уточнение (проверено в `bin/sprinter-cc`, строки ~342-350): можно сразу
собирать PoC на `--memory huge` без единого `--bank`.** Скрипт сам
подставляет стаб `const unsigned char n_banks = 0;`, когда `--bank` не
передан ни один раз — `crt0_banked` линкуется и корректно пропускает цикл
загрузки банков при старте. Layout при этом byte-в-byte совпадает с тем,
что делает `crt0_small` для режима `small` (CODE 0x4100/W1, DATA 0x8000/W2,
автодетект W2) — разница только в том, что попутно линкуется сам
`bank.s` (таблица `_bank_pages` + trampoline-инфраструктура), это
незначительный довесок к размеру, не к рантайм-цене. Значит **PoC можно
сразу собирать вызовом `sprinter-cc --memory huge` без `--bank`-флагов** —
и когда в полном приложении появятся первые «холодные» файлы, переход на
банкование — это просто добавление `--bank N=file.c`, без смены
`--memory`/адресов/crt0. Сборочная конфигурация не потребует миграции
между PoC и полным приложением.
Единственное, что стоит сделать уже в Фазе 1 полного приложения (не в
PoC) — планировать структуру исходников с расчётом на будущий файл-в-файл
сплит под банки (см. выше), раз гранулярность банкования — целый файл.
---
## 9. Что нужно от пользователя, прежде чем двигаться дальше
- Подтверждение направления по §2 (какой из трёх вариантов held-state
клавиатуры пробовать первым, или сначала спайк-эксперимент в MAME).
- Подтверждение объёма PoC (§5) — устраивает ли «одна комната без
стражников», или сразу закладывать хотя бы одного патрулирующего
стражника (это не архитектурно сложнее — Y-order и tween уже есть,
просто больше конвертации ассетов).
+81
View File
@@ -0,0 +1,81 @@
# Форматы ресурсов Prince of Persia — сводка
Цель этих документов — подготовить почву для будущего порта Prince of Persia
на ZX Sprinter, разобрав, как устроены ресурсы игры в двух доступных нам
версиях:
- [`APPLEII_RESOURCE_FORMAT.md`](./APPLEII_RESOURCE_FORMAT.md) — формат
уровней и графики по официально опубликованным исходникам 1989 года
(6502-ассемблер). Уверенность высокая везде — восстановлено прямым чтением
кода движка, а не догадками.
- [`MSDOS_RESOURCE_FORMAT.md`](./MSDOS_RESOURCE_FORMAT.md) — формат `.DAT`
ресурсов DOS-версии (исходников нет). Восстановлено эмпирически (разбор
байтов + перепроверка скриптами) и сверено с документацией открытых
сторонних инструментов (SDLPoP, Princed Resources).
## Главный вывод
**Формат уровня практически идентичен в обеих версиях**: Apple II `LEVELn`
занимает ровно 2304 байта (структура `blueprnt` — тайлы, связи
плит/дверей, граф экранов, метаданные старта Кида/стражников), а запись
уровня в DOS `levels.dat` занимает 2305 байт с байтовыми значениями тайлов
того же диапазона. То есть Джордан Мехнер перенёс формат карты уровня в
DOS-порт практически без изменений (+1 байт, вероятно контрольная сумма от
DOS-упаковщика). Это значит: раскладку `BLUETYPE`/`BLUESPEC`/`LINKLOC`/
`LINKMAP`/`MAP`/`INFO`, задокументированную по Apple II исходникам, можно
применять напрямую и к DOS `levels.dat`.
Формат же **графики отличается принципиально**: на Apple II это простой
несжатый rowbyte-формат hi-res экрана с плоской таблицей указателей; в DOS —
контейнер с оглавлением ресурсов (id/size/offset), с отдельными вариантами
под CGA/EGA/VGA — точный кодек пикселей внутри сырого `.DAT`-чанка не
восстановлен ни для той, ни для другой версии до конца. **Но для DOS-графики
это не блокирует работу**: в репозитории github.com/NagyD/SDLPoP (папка
`data/`) уже лежат готовые распакованные PNG для каждого спрайта/фона
(включая VGA-256-цветный вариант `VPALACE`/`VDUNGEON` — то, что нужно под
320×256×256 Sprinter), см. §5 `MSDOS_RESOURCE_FORMAT.md`. Это другой
релиз/сборка данных, чем наш локальный `MSDOS/` (некоторые звуковые `.dat`
отличаются по размеру), но нумерация ресурсов и формат контейнера — те же,
что подтверждено побайтовой сверкой уровня `res2001.bin`.
## Общий контейнерный формат DOS `.DAT` (кратко)
```
[0x00] u32 LE tableOffset — смещение начала таблицы оглавления
[0x04] u16 LE tableSize — размер таблицы оглавления
[0x06..tableOffset) — данные ресурсов (конкатенация чанков)
[tableOffset..tableOffset+tableSize)
— массив записей по 8 байт:
u16 size, u16 id, u16 offset(абсолютный), u16 reserved(=0)
```
Инвариант `tableOffset + tableSize == размер файла` подтверждён на всех 28
`.dat`-файлах в `MSDOS/`, и независимо — именованием файлов `res<id>.*` в
`data/` репозитория SDLPoP.
## Готовые ассеты для порта (важно для практической работы)
`github.com/NagyD/SDLPoP/tree/master/data` содержит не только код движка, но
и сами ресурсы игры — как сырые `.DAT`, так и распакованные поштучно файлы
(`res<id>.png` для спрайтов/фонов, `res<id>.pal` для палитр, `res<id>.bin`
для уровней). Для арт-ассетов (в т.ч. нужного полноцветного VGA-варианта
дворца/подземелий) практичнее взять их оттуда напрямую, чем писать свой
декодер сжатия пикселей DOS `.DAT`.
## Что дальше (не сделано в этом заходе)
1. Точный кодек сжатия пикселей спрайтов в сыром DOS `.DAT` (нужен только
если понадобится читать именно нашу локальную копию `MSDOS/*.dat`
"как есть", а не ассеты из SDLPoP `data/`) — сверка с исходником SDLPoP,
`src/seg009.c`.
2. Семантика служебных полей `digisnd*.dat`/`ibm_snd*.dat` перед сырыми
сэмплами/нотами (частично прояснено документацией Princed Resources —
PC speaker: 1 байт заголовка + повторяющиеся тройки байт "2 байта частоты
+ 1 байт длительности"; WAV: 8 бит, моно, unsigned, 11025 Гц).
3. Назначение бит `secmask` в `BLUETYPE` (Apple II) и служебного блока
`id=2000` в начале DOS `levels.dat` (16 байт в нашей копии, но 2305 байт
в версии SDLPoP — расхождение между релизами, не разобрано).
4. Оценка, какие видеорежимы/цветовые палитры ZX Sprinter реалистично
покрывают исходную графику (CGA/EGA/VGA варианты в DOS-ресурсах против
hi-res Apple II) — отдельная архитектурная задача порта, не формат
ресурсов как таковой.