diff --git a/.gitignore b/.gitignore index 651e592..74a1045 100644 --- a/.gitignore +++ b/.gitignore @@ -40,6 +40,30 @@ tests/*/*.cdb tests/*/*.mem tests/*/*.rst +# applications/// — реальные приложения (та же схема, что +# examples/ и tests/, но на уровень глубже: applications/PoP/roomtest/...) +applications/*/*/*.exe +applications/*/*/*.asm +applications/*/*/*.lst +applications/*/*/*.lk +applications/*/*/*.ihx +applications/*/*/*.noi +applications/*/*/*.sym +applications/*/*/*.map +applications/*/*/*.rel +applications/*/*/*.cdb +applications/*/*/*.mem +applications/*/*/*.rst + +# PoP-порт: внешние референс-репозитории (собственные git-клоны — НЕ +# часть этого репозитория) + оригинальные game-данные (копирайт, только +# для реверса форматов на этой машине). +applications/PoP/SDLPoP/ +applications/PoP/mininim/ +applications/PoP/PR/ +applications/PoP/Prince-of-Persia-Apple-II/ +applications/PoP/MSDOS/ + # libc + libbgi archives (built by libc/Makefile + libbgi/Makefile) lib/*.lib diff --git a/applications/PoP/bgtest/Makefile b/applications/PoP/bgtest/Makefile new file mode 100644 index 0000000..cd2bee0 --- /dev/null +++ b/applications/PoP/bgtest/Makefile @@ -0,0 +1,27 @@ +# bgtest — мини-тест атласов статического фона PoP (Шаг 2, до порта room.c). +# Проверяет загрузку .atl + палитру + прямую адресацию + прозрачность. +# +# --memory huge (как poc): atlas_load/gfx_blit трогают W3; huge кладёт +# CODE в W1, DATA/BSS в W2 — без банков. --gfx 256 подлинкует bgi256. +# +# Данные фона генерит toolchain/pop_pack_bg.py в ../poc/res/bg/. + +PROJ_ROOT := $(abspath $(CURDIR)/../../..) +EXAMPLE := bgtest +MEMORY ?= huge +EXTRA_FLAGS ?= --gfx 256 + +BG_DIR := $(CURDIR)/../poc/res/bg +BG_DATA := $(BG_DIR)/pop_env0.atl $(BG_DIR)/pop_env1.atl $(BG_DIR)/pop_env2.atl \ + $(BG_DIR)/pop_env3.atl $(BG_DIR)/pop_env4.atl \ + $(BG_DIR)/pop_wall.atl $(BG_DIR)/pop_fore.atl $(BG_DIR)/pop_bg.pal + +EXTRA_DATA := $(BG_DATA) +include $(PROJ_ROOT)/app.mk + +# Ассеты фона: пересобрать пакером, если исходники поменялись. +$(BG_DATA): $(PROJ_ROOT)/applications/PoP/toolchain/pop_pack_bg.py \ + $(PROJ_ROOT)/applications/PoP/toolchain/render_room.py + cd $(PROJ_ROOT)/applications/PoP/toolchain && python3 pop_pack_bg.py + +$(EXAMPLE).exe: $(BG_DATA) diff --git a/applications/PoP/bgtest/bgtest.c b/applications/PoP/bgtest/bgtest.c new file mode 100644 index 0000000..6d76751 --- /dev/null +++ b/applications/PoP/bgtest/bgtest.c @@ -0,0 +1,112 @@ +/* + * bgtest.c — мини-тест атласов статического фона PoP (Шаг 2 порта, ДО + * порта room.c). Проверяет на MAME: загрузку 7 .atl, палитру pop_bg.pal, + * ПРЯМУЮ адресацию (id -> страница/idx без remap-таблиц) и прозрачность + * (индекс 0xFF). Рисует сетку репрезентативных спрайтов на цветной + * заливке — прозрачные области должны показать фон. + * + * Раскладка из toolchain/pop_pack_bg.py (см. pop_bg_atlas.h): + * ENV фон id N -> env[N>>5], idx N&31 ; WALL/FORE idx = id. + */ +#include +#include +#include +#include +#include + +/* Имена файлов — плоская ФС диска (make_disk кладёт по basename). */ +static const char *const ENV_ATL[5] = { + "pop_env0.atl", "pop_env1.atl", "pop_env2.atl", + "pop_env3.atl", "pop_env4.atl" +}; + +static atlas_t env[5]; +static atlas_t wall_a; +static atlas_t fore_a; + +/* Блит одного спрайта ленты idx атласа a в (x,y): страница атласа в W0, + * gfx_blit читает w/h из getimage-заголовка ленты (в W0). */ +static void put(atlas_t *a, unsigned char idx, int x, int y) +{ + const void *img = atlas_image(a, idx); + gfx_w0_map(a->page); + gfx_blit(x, y, img); + gfx_w0_unmap(); +} + +static void put_env(unsigned char id, int x, int y) +{ + put(&env[id >> 5], (unsigned char)(id & 31), x, y); +} + +int main(void) +{ + int i; + + /* Загрузка всех 7 атласов (env0..4 + wall + fore). */ + for (i = 0; i < 5; i++) { + if (atlas_load(&env[i], ENV_ATL[i]) != 0) { + printf("atlas_load %s failed\n", ENV_ATL[i]); + return 1; + } + } + if (atlas_load(&wall_a, "pop_wall.atl") != 0) { puts("wall atl fail"); return 1; } + if (atlas_load(&fore_a, "pop_fore.atl") != 0) { puts("fore atl fail"); return 1; } + + initgraph(); + gfx_set_draw_page(0); + gfx_set_visible_page(0); + if (gfx_pal_fload(0, "pop_bg.pal") < 0) + gfx_pal_fload(0, "a:\\pop_bg.pal"); + /* Свой яркий фон-индекс (вне 0x50..0x6F, занятых графикой) — чтобы + * прозрачные (0xFF) области спрайтов были ЯВНО видны. */ +#define BG_IDX 0x20 + gfx_pal_set(0, BG_IDX, 120, 0, 90); /* r,g,b — тёмно-пурпурный */ + gfx_pal_sync(); + + setfillstyle(SOLID_FILL, BG_IDX); + bar(0, 0, 319, 255); + setcolor(WHITE); + outtextxy(2, 2, "PoP bg atlas test"); + + /* Прозрачный блит: банк не пишет 0xFF (фон проступает). */ + gfx_set_bank(GFX_BANK_TRANSPARENT); + + /* --- Стены (chtab_7): нижние грани 7/9/5/3, основные 8/10/6/4 --- */ + put(&wall_a, 3, 4, 20); put(&wall_a, 5, 40, 20); + put(&wall_a, 7, 76, 20); put(&wall_a, 9, 112, 20); + put(&wall_a, 4, 4, 90); put(&wall_a, 6, 40, 90); + put(&wall_a, 8, 76, 90); put(&wall_a, 10, 112, 90); + /* декали-марки стен 14..17 */ + put(&wall_a, 14, 150, 20); put(&wall_a, 15, 168, 20); + put(&wall_a, 16, 186, 20); put(&wall_a, 17, 204, 20); + + /* --- Столб: база 92, боковая грань 93, фронт 95 (fore) --- */ + put_env(92, 4, 160); + put_env(93, 40, 160); + put(&fore_a, 95, 76, 160); + + /* --- Пол: база 41, правый треуг. 42, низ 43; силуэт 44/45 --- */ + put_env(41, 150, 120); + put_env(42, 190, 120); + put_env(44, 230, 120); + put_env(45, 260, 120); + + /* --- Решётка-окно 126, дебрис 97/98, слайсы ворот 52/53 --- */ + put_env(126, 150, 160); + put_env(97, 190, 160); + put_env(52, 230, 160); + put_env(53, 250, 160); + + gfx_set_bank(GFX_BANK_NORMAL); + /* Держим картинку на экране до нажатия (не полагаемся на блокирующий + * getch — в автотесте клавиш нет, крутимся до таймаута MAME). */ + while (!kbhit()) + gfx_wait_vsync(); + + closegraph(); + for (i = 0; i < 5; i++) atlas_free(&env[i]); + atlas_free(&wall_a); + atlas_free(&fore_a); + return 0; +} diff --git a/applications/PoP/coltest/Makefile b/applications/PoP/coltest/Makefile new file mode 100644 index 0000000..4ee59f6 --- /dev/null +++ b/applications/PoP/coltest/Makefile @@ -0,0 +1,6 @@ +# coltest — прототип вертикального accel-copy (gfx_blit_cols) + флип. +PROJ_ROOT := $(abspath $(CURDIR)/../../..) +EXAMPLE := coltest +MEMORY ?= huge +EXTRA_FLAGS ?= --gfx 256 +include $(PROJ_ROOT)/app.mk diff --git a/applications/PoP/coltest/coltest.c b/applications/PoP/coltest/coltest.c new file mode 100644 index 0000000..87a6ce6 --- /dev/null +++ b/applications/PoP/coltest/coltest.c @@ -0,0 +1,64 @@ +/* + * coltest.c — прототип K2a: проверка вертикального accel-COPY + * (gfx_blit_cols) + прозрачности 0xFF + горизонтального флипа. + * + * Спрайт 16x24 column-major, АСИММЕТРИЧНЫЙ: + * левые 8 колонок: верх (row<12) = ПРОЗРАЧНО (0xFF), низ = БЕЛЫЙ (1) + * правые 8 колонок: КРАСНЫЙ (2) + * На синем фоне (3). Ожидаем на MAME: + * normal @ (50,100): слева бело-снизу+прозрачно-сверху, справа красный; + * flip @(120,100): ЗЕРКАЛО — слева красный, справа бело+прозрачно-сверху; + * в прозрачных местах виден СИНИЙ фон. + */ +#include +#include +#include + +#define W 16 +#define H 24 +static unsigned char spr[4 + W * H]; + +int main(void) +{ + int col, row; + + spr[0] = W; spr[1] = 0; + spr[2] = H; spr[3] = 0; + for (col = 0; col < W; col++) + for (row = 0; row < H; row++) { + /* ФИНАЛ: АСИММЕТРИЧНЫЙ — лево верх прозрачно / низ белый, + * право красное. normal: лево бело+прозрач-верх, право красн; + * flip: ЗЕРКАЛО (лево красн, право бело+прозрач-верх). */ + unsigned char v; + if (col < 8) + v = (row < 12) ? 0xFF : 1; + else + v = 2; + spr[4 + col * H + row] = v; + } + + initgraph(); + gfx_set_draw_page(0); + gfx_set_visible_page(0); + gfx_pal_set(0, 1, 255, 255, 255); /* белый */ + gfx_pal_set(0, 2, 224, 32, 32); /* красный */ + gfx_pal_set(0, 3, 32, 48, 200); /* синий фон */ + gfx_pal_set(0, 255, 0, 224, 0); /* ДИАГ: 0xFF как ЗЕЛЁНЫЙ цвет (bank NORMAL) */ + gfx_pal_sync(); + + setfillstyle(SOLID_FILL, 3); + bar(0, 0, 319, 255); + setcolor(1); + outtextxy(40, 80, "normal"); + outtextxy(110, 80, "flip"); + + gfx_set_bank(GFX_BANK_TRANSPARENT); /* ДИАГ: 0x58 подавление 0xFF (с тенью) */ + gfx_blit_cols(50, 100, spr, 0); /* обычный */ + gfx_blit_cols(120, 100, spr, 1); /* зеркало */ + gfx_set_bank(GFX_BANK_NORMAL); + + while (!kbhit()) + gfx_wait_vsync(); + closegraph(); + return 0; +} diff --git a/applications/PoP/docs/APPLEII_RESOURCE_FORMAT.md b/applications/PoP/docs/APPLEII_RESOURCE_FORMAT.md new file mode 100644 index 0000000..a08fed1 --- /dev/null +++ b/applications/PoP/docs/APPLEII_RESOURCE_FORMAT.md @@ -0,0 +1,283 @@ +# Формат ресурсов Prince of Persia (Apple II, оригинальные исходники 1989) + +Источник — официально опубликованные Джорданом Мехнером исходники +(`Prince-of-Persia-Apple-II/`, 6502-ассемблер). В отличие от DOS-версии, здесь +формат восстановлен **напрямую по коду**, а не по догадкам о байтах — +уверенность высокая везде, где указана ссылка на конкретный файл/строки. + +--- + +## 1. Формат уровня (`01 POP Source/Levels/LEVEL0`…`LEVEL14`, 2304 байта) + +Файлы уровня — это побайтовый дамп структуры `blueprnt`, которая грузится по +фиксированному адресу `$b700` (`EQ.S:28`) и объявлена как `dum blueprnt` в +`EQ.S:258-266`. Никакого отдельного заголовка файла нет — это чистый образ +структуры в памяти: + +| Поле | Размер | Смещение в файле | Описание | +|---|---|---|---| +| `BLUETYPE` | 720 Б | 0–719 | 24 экрана × 30 тайлов: тип объекта/тайла | +| `BLUESPEC` | 720 Б | 720–1439 | 24 экрана × 30 тайлов: доп. байт состояния объекта | +| `LINKLOC` | 256 Б | 1440–1695 | Таблица связей нажимных плит/дверей, часть 1 | +| `LINKMAP` | 256 Б | 1696–1951 | Таблица связей, часть 2 | +| `MAP` | 96 Б | 1952–2047 | 24 экрана × 4 байта: граф соседних экранов | +| `INFO` | 256 Б | 2048–2303 | Метаданные уровня: старт Кида, стражников и т.д. | + +Сумма: 720+720+256+256+96+256 = **2304** — точно совпадает с размером файла, +что подтверждает: это чистый дамп структуры, без обёртки. + +### 1.1 Сетка тайлов (`BLUETYPE` / `BLUESPEC`) + +Каждый экран — ровно **30 тайлов** (10 столбцов × 3 ряда): подтверждено +таблицами `BlockTable`/`BlockEdge` (`TABLES.S:74-154`) и логикой перехода +между экранами в `CTRLSUBS.S:218-234` (при переходе через край экрана +`tempblockx` меняется на ±10, `tempblocky` — на ±3). + +Функция `CALCBLUE` (`GRAFIX.S:1757-1784`) вычисляет для экрана 1–24: +`BlueType = blueprnt + (screen-1)*30`, `BlueSpec = BlueType + 24*30`, +используя таблицу `Mult30` (`TABLES.S:131-140`). + +Байт `BLUETYPE` упакован битовыми полями (`EQ.S:484-486`): + +``` +бит 7-6: secmask (%11000000) — назначение не установлено по доступному коду + (возможно, служебное поле редактора) +бит 5: reqmask (%00100000) — флаг "необходимая опорная плитка" + (проверяется в BREAKLOOSE, MOVER.S:395-397) +бит 4-0: idmask (%00011111) — тип тайла/объекта, 0-29 +``` + +Перечень 30 типов объектов (`MOVEDATA.S:8-37`): + +``` +0 space 8 pillarbottom 16 exit 24 window2 +1 floor 9 pillartop 17 exit2 25 archbot +2 spikes 10 flask 18 slicer 26 archtop1 +3 posts 11 loose 19 torch 27 archtop2 +4 gate 12 panelwof 20 block 28 archtop3 +5 dpressplate 13 mirror 21 bones 29 archtop4 +6 pressplate 14 rubble 22 sword +7 panelwif 15 upressplate 23 window +``` + +Проверено вручную на дампе начала `LEVEL1` (`00 00 00 21 01 21 21 21 34 34 +33 33 21 23 00 34 14 14 14 34 14 34 34 2e 23 0b 01 21 34`) — например, +`0x33 → id=0x13=19 (torch)`+reqmask, `0x34 → id=20 (block)`+reqmask — +декодирование по таблице сходится чисто. + +`BLUESPEC` — доп. байт, чья семантика зависит от типа тайла (единой схемы +нет, разбирается объект-специфичным кодом): + +- **gate** (дверь, `FRAMEADV.S:2222-2234`): на диске — маленький enum (1 = + начинает открытой сверху, 2 = снизу, …), который через `initsettings` + (`FRAMEADV.S:22-23`, диапазон `gminval=0`..`gmaxval=188`, из + `MOVEDATA.S:56-57`) при инициализации уровня превращается в живой счётчик + "высоты двери" 0–188. +- **loose** (шаткая плитка, `FRAMEADV.S:2224-2237`): при инициализации всегда + принудительно обнуляется, независимо от значения на диске. +- **flask** (зелье, `FRAMEADV.S:2226,2239-2246`): значение×32 выбирает + цвет/тип зелья. +- **spikes** (шипы, `MOVER.S:365-382`, константы `spikeExt=5, spikeRet=9` в + `MOVEDATA.S:45-46`): 0 = безопасно/убраны, 1–8 — кадр анимации + выдвижения/втягивания, `$FF` = навсегда заклинило (тело наколото). +- **pressplate/upressplate** (нажимные плиты, `MOVER.S:425-464`, + `FRAMEADV.S:2059-2098`): значение — это **индекс в цепочке связей** + `LINKLOC`/`LINKMAP` (см. ниже); младшие 5 бит `LINKMAP` по этому индексу + одновременно служат счётчиком таймера плиты (0–31), определяющим + состояние "поднято/опущено". + +### 1.2 `LINKLOC` / `LINKMAP` — граф триггеров (нажимные плиты → двери и т.п.) + +Два параллельных массива по 256 байт кодируют цепочки "нажатие плиты X → +сработать объект на экране S, блок B". Восстановлено из `MOVER.S:506-537` +(цикл `trigger`) и `MOVER.S:1549-1581` (`gettimer/chgtimer/getloc/ +getlastflag/getscrn`): + +``` +LINKLOC[i]: бит 7 = флаг "последнее звено цепочки" + биты 6-5 = младшие 2 бита номера целевого экрана + биты 4-0 = номер целевого блока (0-29); $FF = "никуда не привязано" + +LINKMAP[i]: биты 7-5 = старшие 3 бита номера целевого экрана + (вместе с LINKLOC биты 6-5 → полный номер экрана 0-31) + биты 4-0 = таймер обратного отсчёта плиты (0-31, значим только + по индексу самой плиты) +``` + +`BLUESPEC` плиты хранит индекс `i` её *первого* звена; `getlastflag` идёт +вперёд (`inc linkindex`), пока не встретит бит 7 в `LINKLOC`. Сверено на +`LEVEL1`: байт по смещению 1440 (`0x89 = 10001001` → флаг конца цепочки, +целевой блок 9) и параллельно байт по смещению 1696 (`0x60 = 01100000` → +старшие биты номера экрана) — согласуется с этой раскладкой. Заполнены +реально используемые уровнем звенья, остальное — "мусорные" повторяющиеся +байты-заполнители. + +### 1.3 `MAP` — граф соседних экранов + +24 записи × 4 байта = 96 байт: для каждого экрана (1–24) +`MAP[(scrn-1)*4 + 0..3] = левый, правый, верхний, нижний соседние экраны`, +читается через `GETLEFT/GETRIGHT/GETUP/GETDOWN` (`CTRLSUBS.S:244-274`, +индексация `MAP-4..MAP-1,x` при `x = scrn*4`). Экран `0` зарезервирован как +"нет экрана" (проверка `beq ]rts` в этих же процедурах). + +### 1.4 `INFO` — метаданные уровня (256 байт, база = смещение файла 2048) + +Объявлено как `dum INFO` в `EQ.S:272-288`: + +| Смещение (от начала INFO) | Поле | Размер | +|---|---|---| +| 0 | "число экранов + 1" (используется в `SETINITIALS`, `SUBS.S:1441-1445`) | 1 | +| 1–63 | резерв/не используется | 63 | +| 64 | `KidStartScrn` | 1 | +| 65 | `KidStartBlock` | 1 | +| 66 | `KidStartFace` (направление; при загрузке инвертируется XOR `$ff`, `SUBS.S:1516-1518`) | 1 | +| 67 | заполнитель | 1 | +| 68 | `SwStartScrn` (стартовый экран меча) | 1 | +| 69 | `SwStartBlock` | 1 | +| 70 | заполнитель | 1 | +| 71–94 | `GdStartBlock[1..24]` — стартовый блок стражника на экране; **≥30 = "стражника нет"** (`AUTO.S:1832-1834`, `SUBS.S:1677-1679`) | 24 | +| 95–118 | `GdStartFace[1..24]` (86 = "стражника нет", см. `ShadFace cmp #86` по всему `AUTO.S`) | 24 | +| 119–142 | `GdStartX[1..24]` — пересчитывается заново из блока при старте уровня, значение на диске почти не используется (`SUBS.S:1674-1690`) | 24 | +| 143–166 | `GdStartSeqL[1..24]` | 24 | +| 167–190 | `GdStartProg[1..24]` — "программа"/поведение ИИ стражника | 24 | +| 191–214 | `GdStartSeqH[1..24]` — обнуляется при старте (`SUBS.S:1685-1686`) | 24 | +| 215–255 | резерв/не используется | 41 | + +Проверено на `LEVEL1`: байт по смещению файла 0x800 = `0x18`=24 (число +активных экранов = 23+1); по смещению 0x840 — `01 00 ff 00 00 00 ff 1e 1e +11 1e 1e ...` → `KidStartScrn=1, KidStartBlock=0, KidStartFace=$FF, +SwStartScrn=0, SwStartBlock=0`, далее 24 байта `GdStartBlock`, в основном +`0x1e`(30, "нет стражника"), с реальной расстановкой только на экране 3 +(`0x11`=17) и экране 23 (`0x06`) — согласуется с уровнем, где всего два +стражника. + +### 1.5 Как уровень попадает с диска (важно: имя файла — не игровой механизм) + +В рантайме нет чтения "по имени файла LEVELn" — это чисто утилита для +экспорта в этом репозитории. Реально `LOADLEVELX` (`MISC.S:795-809`) +использует фиксированные таблицы по номеру уровня `bluepTRKlst`/ +`bluepREGlst` (`MISC.S:776-787`), дающие физическую **дорожку (1-33)** и +**регион (0/1)**, затем `rdbluep` (`MASTER.S:598-616`) вызывает +низкоуровневое чтение `rw18` (`RdGrpErr`) 9 физических групп по 256 байт +(`$b7-$bf`) — 9×256=2304 байта — прямо в буфер blueprint; регион 0/1 выбирает +половину 18-секторной дорожки (два уровня делят одну дорожку). Файлы +`LEVELn` в этом репозитории — реконструкция этого сырого блока для удобства +работы с инструментами. + +--- + +## 2. Формат изображений/спрайтов (`IMG.CHTAB1-7`, `IMG.BGTAB1/2.DUN/.PAL`) + +Каждый такой файл грузится целиком по **фиксированному адресу**, заданному +константами `chtableN`/`bgtableN` (`GAMEEQ.S:9-18`): + +``` +chtable1=$6000 chtable2=$8400 chtable3=$0800 chtable4=$9600 +chtable5=$a800 chtable6=$6000 chtable7=$9f00 +bgtable1=$6000 bgtable2=$8400 +``` +— то есть смещения внутри файла один-в-один совпадают с адресами в памяти +после загрузки. + +### 2.1 Раскладка контейнера + +Восстановлено из заголовка-комментария "Image table format" в `HIRES.S:181-186`, +процедуры разрешения указателя `setimage` (`HIRES.S:263-277`) и +`GETWIDTH`/`PREPREP` (`HIRES.S:283-339`): + +``` +Смещение 0 : 1 байт — число изображений в таблице (максимум 127, + в образцах встречается 0x7f) +Смещение 1..254 : 127 × 2-байтных little-endian указателей + (указатель на изображение N — по смещению 1+(N-1)*2, + N=1..127) — АБСОЛЮТНЫЕ адреса в адресном пространстве + фиксированной загрузки этой таблицы, указывающие на + запись данных этого изображения +Смещение 255 : заполнитель (таблица указателей занимает ровно 256 байт) +Смещение 256 (база+0x100) и далее: + последовательно идущие записи данных изображений: + байт 0: ширина (в байтах на строку) + байт 1: высота (число строк) + байты 2..(2+ширина*высота-1): сырые байты пикселей, + слева направо, сверху вниз, БЕЗ сжатия +``` + +Проверено вручную на `IMG.BGTAB1.DUN`: с точной арифметикой индексов из +`setimage` (`Y = image*2 - 1`, `HIRES.S:264-267`) первые ~30 записей дают +строго возрастающую последовательность указателей `0x6101, 0x6133, 0x6159, +0x618b, 0x61c9, 0x61fb, 0x6221, 0x6313, 0x63c9, ...` — указатель +изображения #1 приходится ровно на `bgtable1 ($6000) + 0x100`, то есть точно +на конец 256-байтной таблицы указателей. Это независимо подтверждает и +размер таблицы, и семантику указателей. + +**Важно: сжатия в этом формате нет.** RLE/дельта-упаковка (`SngExpand`/ +`DblExpand`/`DeltaExpPop`/`DeltaExpWipe` в `01 POP Source/Source/UNPACK.S`) +применяется только к полноэкранным изображениям (титры/пролог/катсцены), но +не к CHTAB/BGTAB — спрайты и фоновые тайлы хранятся как чистые упакованные +байты hi-res/double-hi-res экрана Apple II, без какого-либо RLE или дельты. + +### 2.2 Параметры отрисовки (не часть файла ресурса) + +При выводе спрайта (`LAY`/`FASTLAY`/`PEEL` и т.д., `HIRES.S:658-1740`) +используются zero-page параметры `PAGE/XCO/YCO/OFFSET/IMAGE/OPACITY/TABLE/ +BANK` (описаны в `HIRES.S:155-178`): `OFFSET` (0–6) — горизontальный сдвиг на +под-байтовый пиксель, `OPACITY` выбирает режим совмещения (AND/OR/STA/XOR/ +маска-OR) плюс отдельный бит горизонтального зеркалирования (бит 7). Это +чисто рантайм-параметры отрисовки, не хранящиеся в файле ресурса. Точный +механизм барабанного сдвига для `OFFSET` (таблицы `HRTABLES.S`/`YLO`/`YHI`) +не прослежен до конца — при необходимости требует отдельного анализа. + +### 2.3 Инструмент DRAZ (авторская утилита создания спрайтов) + +В `04 Support/DRAZ` нет исходников самой утилиты DRAZ — только файлы данных +(`PAC.*` — позы персонажей, и уже скомпилированные `IMG.*`), поэтому +внутренний пайплайн DRAZ (как позы превращаются в CHTAB) напрямую не виден. +Формат контейнера выше выведен полностью из кода движка-потребителя, что +является надёжным, но косвенным источником. + +Отдельно: в игровой логике списков объектов (`ADDBACK`, `GRAFIX.S:191-214`) +встречается **рантайм-упаковка ссылки на фоновую картинку** в один байт: бит +7 выбирает `bgtable1` или `bgtable2`, биты 0-6 — номер картинки в таблице +(0-63). Это соглашение для внутриигровых списков объектов (`bgIMG` и т.п.), а +не свойство самих файлов CHTAB/BGTAB на диске. + +--- + +## 3. "Главного индекса ресурсов" не существует + +В отличие от DOS-версии (см. `docs/MSDOS_RESOURCE_FORMAT.md`), в рантайм-коде +Apple II **нет обобщённого справочника "имя ресурса → расположение на +диске"**. Расположение каждого ресурса зашито напрямую как таблицы +дорожка/группа-секторов прямо в коде загрузчика: + +- Уровни: `bluepTRKlst`/`bluepREGlst` (`MISC.S:776-787`), используются + `LOADLEVELX`/`LOADLEVEL` (`MISC.S:795-809`, `MASTER.S:467-481`). +- Альтернативные наборы фонов/персонажей: `bg1trk`/`bg2trk`/`ch4trk`/`ch4off` + (`MASTER.S:522-528`). +- Массовая загрузка при старте (chtable1-7, bgtable1-2, seqtable и т.д.): + прямые вызовы `rw18`/`RdGrp`/`RdSeq` с литеральными hex-списками + групп-секторов в `MASTER.S:1250-1360` и `BOOT.S:100-118`. + +Весь дисковый ввод-вывод идёт через нестандартный низкоуровневый драйвер +`rw18` (`rw18 = $d000`, `EQ.S:11-12`; папка `02 POP Disk Routines/RW1835`), +реализующий нестандартный формат **18 секторов/дорожку** (вместо 16 у +стандартного DOS 3.3) — этим объясняется, почему регионы уровня (9×256Б) +идут парами на одной физической дорожке. Символические имена +`chtableN`/`bgtableN` в `GAMEEQ.S` — ближайший аналог "индекса ресурсов", но +они связывают ресурс с **фиксированным адресом в ОЗУ**, а не с положением на +диске; связь с диском — отдельная, вручную сопровождаемая таблица, +сопоставленная с ресурсом лишь порядком вызовов загрузчика. + +--- + +## 4. Что ещё не восстановлено (открытые вопросы) + +- Точное назначение бит `secmask` (%11000000) в `BLUETYPE` — не встречено + использование в доступном игровом коде (возможно, поле только для + редактора уровней, не читается движком). +- Механизм барабанного сдвига `OFFSET` для суб-байтового позиционирования + спрайта по X (`HRTABLES.S`) — не прослежен в деталях. +- Внутренний формат авторских файлов `PAC.*` инструмента DRAZ (как позы + скелетной анимации превращаются в растровые кадры CHTAB) — исходники DRAZ + отсутствуют в репозитории, можно только косвенно восстановить по + результату (уже скомпилированным `IMG.*`). diff --git a/applications/PoP/docs/KID_PLAN.md b/applications/PoP/docs/KID_PLAN.md new file mode 100644 index 0000000..f0756f6 --- /dev/null +++ b/applications/PoP/docs/KID_PLAN.md @@ -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). diff --git a/applications/PoP/docs/MSDOS_RESOURCE_FORMAT.md b/applications/PoP/docs/MSDOS_RESOURCE_FORMAT.md new file mode 100644 index 0000000..5034e56 --- /dev/null +++ b/applications/PoP/docs/MSDOS_RESOURCE_FORMAT.md @@ -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` | 2000–2015 | 16 | id=2000 — служебный блок (16 байт, см. §3); id=2001..2015 — 15 уровней | +| `guard.dat`, `fat.dat`, `skel.dat`, `shadow.dat` | 750–784 (варьируется) | ~30–35 | 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` | 200–1343 | 205–238 | фоновые тайлы дворца/подземелья, отдельно для CGA(`c*`)/EGA(`e*`)/VGA(`v*`) | +| `pv.dat` | 800–981 | 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.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.ext`) наблюдается одинаково | + +**Вывод:** это данные из немного **другого релиза DOS-версии**, чем те, что +лежат у нас в `MSDOS/` (см. расхождение в размере `digisnd`/`midisnd`), но +формат контейнера и нумерация `id` — те же самые. Практически это значит: + +1. Для получения играбельных PNG-спрайтов и VGA-фонов **не нужно + реализовывать декодер сжатия пикселей** — можно взять готовые файлы + `data/<ИМЯ>/res.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.*` в SDLPoP `data/` | +| ID-пространства ресурсов | Средняя-высокая | Наблюдение по диапазонам + сверка с `res` именами файлов 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`). diff --git a/applications/PoP/docs/POP-DAT-FormatSpecifications.pdf b/applications/PoP/docs/POP-DAT-FormatSpecifications.pdf new file mode 100644 index 0000000..0d5c219 Binary files /dev/null and b/applications/PoP/docs/POP-DAT-FormatSpecifications.pdf differ diff --git a/applications/PoP/docs/PORT_PLAN.md b/applications/PoP/docs/PORT_PLAN.md new file mode 100644 index 0000000..6249770 --- /dev/null +++ b/applications/PoP/docs/PORT_PLAN.md @@ -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()` в ``) — даёт настоящий +held-state для Shift/Ctrl/Alt. Можно делать хоть сейчас, не архитектурное +решение. + +**B. Для обычных клавиш (стрелки и т.д.) — по прецеденту CBL.** В +`_irq_tramp.c` уже есть пример «приватного» пути на том же векторе 0xFF, +который сознательно НЕ чейнится к DSS (CBL: бит 7 порта 0xFE, свой +хук `_irq_cbl_hook`, полный сейв, свой `reti`). Предлагаемый новый +компонент `` — симметричный: ветка по биту 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` + `` + (`KBD_MOD_*`). +- B: `` (`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_ESC` (0x76, обычный код) — + корректно закрыл raw-канал и вернул DSS (`IM` вернулся в 1). + Побочно найдено и задокументировано в `docs/libc-reference.md` + (``): 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` ``): моя +более ранняя попытка проверить 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 уже есть, + просто больше конвертации ассетов). diff --git a/applications/PoP/docs/README.md b/applications/PoP/docs/README.md new file mode 100644 index 0000000..c645b33 --- /dev/null +++ b/applications/PoP/docs/README.md @@ -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.*` в +`data/` репозитория SDLPoP. + +## Готовые ассеты для порта (важно для практической работы) + +`github.com/NagyD/SDLPoP/tree/master/data` содержит не только код движка, но +и сами ресурсы игры — как сырые `.DAT`, так и распакованные поштучно файлы +(`res.png` для спрайтов/фонов, `res.pal` для палитр, `res.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) — отдельная архитектурная задача порта, не формат + ресурсов как таковой. diff --git a/applications/PoP/poc/Makefile b/applications/PoP/poc/Makefile new file mode 100644 index 0000000..a2e46ad --- /dev/null +++ b/applications/PoP/poc/Makefile @@ -0,0 +1,53 @@ +# PoC порта Prince of Persia (applications/PoP/docs/PORT_PLAN.md §5). +# --memory huge, БЕЗ --bank. +# +# ПОЧЕМУ huge, а НЕ small (исправлено 2026-07-16): poc использует +# raw-клавиатуру (kbd_raw_open → IM2-таблица). Буферы IM2 (_irq_vec_buf, +# BSS) ОБЯЗАНЫ жить в W2 (0x8000-0xBFFF) — во время прерывания W1/W3 +# могут быть перемаплены DSS (см. libc/irq/_irq_table.c, memory/ +# fps_divider: «verified tiny/big/huge, small=EINVAL»). --memory small +# пулит W1+W2 в плоские ~32КБ и чейнит DATA за CODE — при небольшом CODE +# BSS уезжает в W1 (<0x8000), и _irq_table_ref отдаёт EINVAL → +# kbd_raw_open молча возвращал -1, poc печатал ошибку УЖЕ в графическом +# режиме (невидимо) и выходил в prompt. huge кладёт CODE в W1, а +# DATA/BSS/STACK/HEAP жёстко в W2 → IM2 работает. Цена: раздел 16КБ +# CODE / 16КБ DATA вместо общего 32КБ-пула small — сейчас влезает с +# запасом; при росте настоящего PoP CODE>16КБ понадобится банк под код. +# +# --bank НЕ нужен: gfx_blit_part()/atlas_load() сами временно трогают W3 +# (видеобанк / чтение атласа) — банк room.c в W3 давал вероятностный +# «снег»; банк в W1 несовместим с CODE=W1 (трамплин переключения W1 сам +# бы уехал). sprintf() заменён на ручное hex-форматирование пути в +# tile_atlas_load() (единственный потребитель printf, ~2.9КБ). + +PROJ_ROOT := $(abspath $(CURDIR)/../../..) +EXAMPLE := poc +MEMORY ?= huge +EXTRA_FLAGS ?= --gfx 256 +EXTRA_SRCS := room.c +TILE_ATLASES := res/tiles/tile01.atl res/tiles/tile14.atl res/tiles/tile03.atl \ + res/tiles/tile13.atl res/tiles/tile0e.atl res/tiles/tile0b.atl +EXTRA_DATA := tools/kid.atl tools/room.pal res/room1.dat $(TILE_ATLASES) +include $(PROJ_ROOT)/app.mk + +LEVEL1_BIN := $(PROJ_ROOT)/applications/PoP/SDLPoP/data/LEVELS/res2001.bin + +tools/kid.raw tools/kid.pal: tools/gen_kid_placeholder.py + cd tools && python3 gen_kid_placeholder.py + +tools/kid.atl: tools/kid.raw + python3 $(PROJ_ROOT)/toolchain/mkatlas.py $@ tools/kid.raw:16x16:1x12 + +res/room1.dat: tools/extract_room.py $(LEVEL1_BIN) + python3 tools/extract_room.py $(LEVEL1_BIN) 1 res/room1.dat + +res/tiles/1F-0.png: tools/gen_tile_placeholders.py + cd tools && python3 gen_tile_placeholders.py + +tools/room.pal $(TILE_ATLASES): tools/kid.pal res/tiles/1F-0.png tools/build_room_palette.py + cd tools && python3 build_room_palette.py + +# make_disk.py упаковывает EXTRA_DATA на диск ПОД БАЗОВЫМ ИМЕНЕМ +# (плоская ФС) — tileNN.atl/room1.dat оказываются в корне рядом с +# kid.atl/room.pal, room.c/poc.c грузят их без пути. +$(EXAMPLE).exe: room.c tools/kid.atl tools/room.pal res/room1.dat $(TILE_ATLASES) diff --git a/applications/PoP/poc/level.h b/applications/PoP/poc/level.h new file mode 100644 index 0000000..fe119df --- /dev/null +++ b/applications/PoP/poc/level.h @@ -0,0 +1,153 @@ +/* + * level.h — структуры уровня PoP под наш движок. + * + * Формат — POP-DAT-FormatSpecifications.pdf §3.4 (DAT 1.0): комната = + * 30 тайлов (10 колонок x 3 ряда), foretable даёт тип тайла, + * backtable — модификатор/состояние (семантика зависит от типа). + * Адресация тайла: tile = (room-1)*30 + tileOffset, tileOffset 0-9 = + * верхний ряд, 10-19 = средний, 20-29 = нижний, слева направо. + * + * Фаза 1 (applications/PoP/docs/PORT_PLAN.md §5, сузили объём): + * только геометрия (пол/стены/провалы) для коллизий — двери, факелы, + * ловушки, гарды НЕ используются (структуры под них здесь тоже + * упрощены/оставлены как заготовка на потом, см. §7 плана Фаза 2). + */ +#ifndef LEVEL_H +#define LEVEL_H + +#include + +/* 1 (не 24) — PoC грузит и рисует только комнату 1; расширить, когда + * появится настоящий level_load() на несколько комнат. */ +#define LEVEL_ROOMS 1 /* комнаты нумеруются 1..24 в файле, + * здесь индекс 0..23 = комната N+1 */ +#define ROOM_COLS 10 +#define ROOM_ROWS 3 +#define ROOM_TILES (ROOM_COLS * ROOM_ROWS) /* 30 */ + +/* --- Типы тайлов (Table 7 спецификации) --- */ +#define TILE_TYPE(byte) ((uint8_t)((byte) & 0x1F)) +#define TILE_MODIFIER(byte) ((uint8_t)(((byte) >> 5) & 1)) + +enum { + TILE_EMPTY = 0x00, + TILE_FLOOR = 0x01, + TILE_SPIKES = 0x02, + TILE_PILLAR = 0x03, + TILE_GATE = 0x04, + TILE_STUCK_BUTTON = 0x05, + TILE_DROP_BUTTON = 0x06, + TILE_TAPESTRY = 0x07, + TILE_PILLAR_BOTTOM = 0x08, + TILE_PILLAR_TOP = 0x09, + TILE_POTION = 0x0A, + TILE_LOOSE = 0x0B, + TILE_TAPESTRY_TOP = 0x0C, + TILE_MIRROR = 0x0D, + TILE_DEBRIS = 0x0E, + TILE_RAISE_BUTTON = 0x0F, + TILE_EXIT_LEFT = 0x10, + TILE_EXIT_RIGHT = 0x11, + TILE_CHOPPER = 0x12, + TILE_TORCH = 0x13, + TILE_WALL = 0x14, + TILE_SKELETON = 0x15, + TILE_SWORD = 0x16, + TILE_BALCONY_LEFT = 0x17, + TILE_BALCONY_RIGHT = 0x18, + TILE_LATTICE_PILLAR = 0x19, + TILE_LATTICE_SUPPORT= 0x1A, + TILE_LATTICE_SMALL = 0x1B, + TILE_LATTICE_LEFT = 0x1C, + TILE_LATTICE_RIGHT = 0x1D, + TILE_TORCH_DEBRIS = 0x1E, + TILE_NULL = 0x1F +}; + +/* Твёрдые тайлы — Фаза 1 (только геометрия); классификация наша, для + * коллизий движка, не часть исходного формата. Двери/шипы/дробилки и + * т.п. сознательно исключены из объёма Фазы 1 (см. §5 плана) — при + * встрече в реальных данных трактовать как проходимые до Фазы 2. + * + * tile_is_solid() — "есть опора сверху" (вертикальный смысл: можно + * стоять НА этом тайле) — Floor ТОЖЕ solid в этом смысле! Для + * горизонтальной коллизии (можно ли ВОЙТИ в эту клетку сбоку) нужен + * ОТДЕЛЬНЫЙ предикат — см. tile_blocks_side ниже. Баг 2026-07-16: + * col_blocked() в poc.c ошибочно звал tile_is_solid() для бокового + * упора — Floor блокировал сам себя, Кид не мог сдвинуться с места + * стоя на полу. */ +static inline uint8_t tile_is_solid(uint8_t byte) +{ + switch (TILE_TYPE(byte)) { + case TILE_FLOOR: + case TILE_PILLAR: + case TILE_PILLAR_BOTTOM: + case TILE_PILLAR_TOP: + case TILE_WALL: + case TILE_BALCONY_LEFT: + case TILE_BALCONY_RIGHT: + return 1; + default: + return 0; + } +} + +/* tile_blocks_side() — настоящая преграда СБОКУ (нельзя войти в + * клетку по горизонтали): Wall/Pillar-семейство. Floor/Balcony НЕ + * блокируют — по ним идёшь (тайл под ногами, не впереди). Lattice- + * колонны (0x19-0x1D) — узкие, Кид физически проходит мимо (см. + * gen_tile_placeholders.py draw_lattice_like) — тоже НЕ блокируют. */ +static inline uint8_t tile_blocks_side(uint8_t byte) +{ + switch (TILE_TYPE(byte)) { + case TILE_PILLAR: + case TILE_PILLAR_BOTTOM: + case TILE_PILLAR_TOP: + case TILE_WALL: + return 1; + default: + return 0; + } +} + +/* --- Тайл и комната --- */ +typedef struct { + uint8_t type; /* foretable byte (rrmccccc — см. TILE_TYPE/MODIFIER) */ + uint8_t state; /* backtable byte — модификатор/состояние */ +} tile_t; + +typedef struct { + tile_t tiles[ROOM_TILES]; /* индекс = tileOffset 0..29 */ + uint8_t link_left, link_right; /* links-блок: 0 = нет соседа */ + uint8_t link_up, link_down; + uint8_t guard_location; /* 0..29; 30 (и выше) = нет гарда */ + int8_t guard_direction; /* 0 = вправо, -1 = влево */ + uint8_t guard_skill; /* 0..9 */ + uint8_t guard_colour; /* индекс палитры, Table 11 */ + /* door I/II (событийные цепочки) — Фаза 2, не здесь */ +} room_t; + +typedef struct { + room_t rooms[LEVEL_ROOMS]; /* индекс 0 = комната 1 (файл 1-based) */ + uint8_t start_room; /* 1..24 */ + uint8_t start_location; /* 0..29 */ + int8_t start_direction; /* 0 = вправо, -1 = влево */ +} level_t; + +/* Тайл по (room 1-based, col 0-9, row 0-2). */ +static inline tile_t *level_tile(level_t *lv, uint8_t room, uint8_t col, uint8_t row) +{ + return &lv->rooms[room - 1].tiles[row * ROOM_COLS + col]; +} + +/* Загружает ОДНУ комнату + стартовую позицию уровня из файла в формате + * tools/extract_room.py (63 Б: foretable[30]+backtable[30] той комнаты + * + start_room+start_pos+start_dir, вырезанные из res20NN.bin — layout + * подтверждён декодом байт 2026-07-15/16: файл начинается СРАЗУ с + * foretable[720], потом backtable[720], без заголовка; start_position + * — смещение 2112, сверено со структурой level_type в SDLPoP/src/ + * types.h). Заполняет lv->start_room/start_location/start_direction. + * 0 — OK, -1 — файл не найден/короче 63 Б. */ +int level_load_room(level_t *lv, uint8_t room, const char *path); + +#endif diff --git a/applications/PoP/poc/poc.c b/applications/PoP/poc/poc.c new file mode 100644 index 0000000..5c03ad5 --- /dev/null +++ b/applications/PoP/poc/poc.c @@ -0,0 +1,264 @@ +/* + * poc.c — PoC порта Prince of Persia (applications/PoP/docs/PORT_PLAN.md + * §5): проверяем управление (raw-клавиатура, held-state) + коллизию + * по краям экрана + анимацию ходьбы + прыжок/присед поверх готового + * спрайтового движка (sprite.h). + * + * ВАЖНО: персонаж — ВРЕМЕННАЯ ЗАГЛУШКА (лицензированный спрайт-пак + * third_party/16x16-RPG-characters через tools/gen_kid_placeholder.py, + * тот же источник, что уже использует examples/rpgwalk), НЕ графика + * оригинальной Prince of Persia — см. §5 и §8.5 плана. У заглушки нет + * отдельных поз прыжка/приседа — механика (тайминг дуги, состояние, + * коллизия с полом) проверяется на том же спрайте без смены позы; + * визуально это упрощение, не финальный вид. + * + * Дуга прыжка (jump_height[]) — СВОЯ, приблизительная (не таблица + * смещений оригинала — см. §6 плана: авторские таблицы кадров решено + * не переносить, только код/структуры). + * + * Нет ещё (следующие итерации): реальный уровень/фон по BLUETYPE, + * рывок вбок при прыжке с разбега, зацепление за уступ. + */ +#include +#include +#include +#include +#include +#include +#include "level.h" +#include "room.h" + +/* Комната 1 уровня 1 — РЕАЛЬНАЯ геометрия (foretable/backtable), + * вырезана tools/extract_room.py из applications/PoP/SDLPoP/data/ + * LEVELS/res2001.bin (level_load_room(), см. level.h/room.c) — не + * плейсхолдер. Верхний ряд (row0) — floor-уступ на cols 3-7 (там + * реально стоит персонаж в оригинале), стены по cols 8-9; ряды 1-2 — + * ниже уступа (торч/колонны/пол — Фаза 1 просто их отрисовывает по + * тем же типам, без многоуровневой физики падения). */ +static level_t test_level; + +#define CHAR_ROW 0 /* ряд, где стоит персонаж (floor-уступ room1) */ +#define ROW_Y(r) ((r) * 64) /* room_row_h все по 64 */ +#define GROUND_Y ROW_Y(CHAR_ROW + 1) /* низ ряда CHAR_ROW = верх пола */ +#define KIDY (GROUND_Y - 16) /* y спрайта (16 px высотой) */ +#define SPEED 2 /* px/кадр — заглушка, не авторский темп */ + +/* Настоящая преграда (Wall/Pillar) слева/справа от кандидата x в + * CHAR_ROW — блокирует движение (грубая проверка по краям хитбокса + * 16px, без под-тайловой подгонки — для PoC достаточно, см. + * PORT_PLAN.md §6). tile_blocks_side(), НЕ tile_is_solid(): Floor — + * тайл, на котором Кид СТОИТ (тот же CHAR_ROW), tile_is_solid() его + * тоже считает "твёрдым" (можно стоять сверху) — если проверять им же + * боковую преграду, Кид не мог сдвинуться с собственного пола (баг, + * найден 2026-07-16). */ +static uint8_t col_blocked(int x) +{ + uint8_t c0, c1; + + if (x < 0 || x + 15 >= ROOM_COLS * ROOM_TILE_W) + return 1; + c0 = (uint8_t)(x / ROOM_TILE_W); + c1 = (uint8_t)((x + 15) / ROOM_TILE_W); + if (tile_blocks_side(level_tile(&test_level, 1, c0, CHAR_ROW)->type)) + return 1; + if (tile_blocks_side(level_tile(&test_level, 1, c1, CHAR_ROW)->type)) + return 1; + return 0; +} + +/* Ленты атласа: dir*3+frame, dir 0=вниз/1=влево/2=вправо/3=вверх, + * 3 кадра маятника на направление (см. tools/gen_kid_placeholder.py). */ +#define DIR_DOWN 0 +#define DIR_LEFT 1 +#define DIR_RIGHT 2 + +/* Дуга прыжка: своя, приблизительная (не авторская таблица, см. шапку + * файла) — высота над полом (px) по кадрам 0..19, УЖЕ ПОЛНЫЙ горб + * (подъём 2→22 к элементу 10, спуск обратно к 2 к элементу 19) — БЕЗ + * зеркалирования в коде, массив читается один раз целиком. БАГ, + * найденный пользователем 2026-07-15: раньше код ЕЩЁ РАЗ зеркалил + * этот уже полный горб на 40 кадров — получалось два полных прыжка + * подряд от одного триггера (не проблема клавиатуры/декодера, чистая + * рассинхронизация данных и комментария). 20 кадров @ 50 Гц ~= 0.4 с. */ +static const uint8_t jump_height[20] = { + 2, 4, 7, 10, 13, 16, 18, 20, 21, 22, + 22, 21, 20, 18, 16, 13, 10, 7, 4, 2 +}; +#define JUMP_FRAMES 20 + +static atlas_t at; +static sprite_t kid; +static uint8_t facing = DIR_DOWN; /* текущее направление анимации */ +static uint8_t jumping = 0; /* 0 = на земле */ +static uint8_t jump_t = 0; /* кадр дуги, 0..JUMP_FRAMES-1 */ +static uint8_t crouching = 0; +/* Прыжок — level-triggered НАМЕРЕННО (не edge-detect): если UP всё ещё + * зажат к моменту приземления — следующий прыжок стартует СРАЗУ (цепочка + * прыжков, пока держишь); отпустил раньше — второй прыжок не начнётся + * сам, только по следующему нажатию. Раньше здесь были up_prev/ + * jump_cooldown — попытка "починить" ровно ЭТО поведение, приняв его + * за баг; убрано 2026-07-15 после уточнения желаемого поведения. */ + +static void draw_room(void) +{ + setfillstyle(SOLID_FILL, BLACK); + bar(0, 0, 319, 255); + room_draw(&test_level, 1); + setcolor(LIGHTGRAY); + outtextxy(60, 4, "PoP PoC: hold LEFT/RIGHT to walk, ESC to quit"); + outtextxy(4, 14, "(placeholder tiles -- not original PoP art)"); +} + +/* HUD-плашка состояния (нет отдельной позы прыжка/приседа — статус + * текстом, рисуется банком 0x50, heal спрайтового движка её не + * трогает — как fps-плашка в examples/rpgwalk). */ +static void show_state(uint8_t jump, uint8_t crouch) +{ + setfillstyle(SOLID_FILL, BLACK); + bar(0, 24, 60, 32); + setcolor(YELLOW); + if (jump) + outtextxy(0, 24, "JUMP"); + else if (crouch) + outtextxy(0, 24, "CROUCH"); +} + +static void set_facing(uint8_t dir) +{ + if (facing == dir) + return; + facing = dir; + sprite_anim(&kid, (uint8_t)(dir * 3), (uint8_t)(dir * 3 + 2), + 6, ANIM_PINGPONG); +} + +int main(void) +{ + uint8_t page, hidden; + int x; + + if (level_load_room(&test_level, 1, "room1.dat") != 0 && + level_load_room(&test_level, 1, "a:\\room1.dat") != 0) { + puts("room1.dat not found"); + return 1; + } + /* Стартовая позиция. В данных room1 start_location = tileOffset 0 + * (row0/col0) — а там EMPTY (провал, без пола); в Фазе 1 нет физики + * падения, поэтому для PoC ставим Кида на floor-уступ (col 3, где он + * реально стоит в оригинале). Когда появится многоуровневая физика — + * брать col из start_location. start_direction: -1 = влево, 0 = + * вправо (level.h). */ + x = 3 * ROOM_TILE_W; + facing = (test_level.start_direction < 0) ? DIR_LEFT : DIR_RIGHT; + + if (atlas_load(&at, "kid.atl") != 0 && + atlas_load(&at, "a:\\kid.atl") != 0) { + puts("kid.atl not found"); + return 1; + } + if (tile_atlas_load("") == 0 && tile_atlas_load("a:\\") == 0) { + puts("tile atlases not found"); + atlas_free(&at); + return 1; + } + + initgraph(); + /* room.pal = EGA16 + Kid + Floor + Wall — ОДНА палитра на всё, + * собрана tools/build_room_palette.py (см. --seed-pal в + * toolchain/png_strip.py) — Kid и тайлы на экране одновременно, + * их "свои" цвета обязаны жить в одной таблице. */ + if (gfx_pal_fload(0, "room.pal") < 0) + gfx_pal_fload(0, "a:\\room.pal"); + gfx_pal_sync(); + gfx_sprite_clip(0); /* коллизия по краям гарантирует границы */ + + /* kid.atl — ОДНА лента (12 кадров вертикально: dir*3+кадр, см. шапку + * файла и examples/rpgwalk); индекс atlas_sprite_init — это НОМЕР + * ЛЕНТЫ (персонажа), а не кадра. Лента одна → всегда 0. Стартовый + * кадр направления выставляем sprite_frame (вертикальная лента, fh=16: + * кадр N на sy=N*16). Баг Соннета (найден 2026-07-16): здесь стоял + * facing*3 как индекс ЛЕНТЫ — при старте лицом влево (idx 3) читался + * мусор за каталогом атласа → мусорные w/h/src → блит спрайта заливал + * пол-экрана «снегом». */ + atlas_sprite_init(&kid, &at, 0); + sprite_frame(&kid, 0, (int)(facing * 3) * 16); + kid.x = x; + kid.y = KIDY; + sprite_show(&kid); + + for (page = 0; page < 2; page++) { /* фон + спрайт на обе страницы */ + gfx_set_draw_page(page); + draw_room(); + sprite_update(&kid, 1); + } + gfx_set_visible_page(0); + + /* kbd_raw требует BSS в W2 (IM2-таблица) — недоступно в --memory small + * (там BSS может уехать в W1 → EINVAL); poc собирается --memory huge + * (CODE в W1, DATA/BSS в W2). closegraph ДО puts — иначе сообщение + * ушло бы в графический режим (невидимо), а программа молча вышла бы + * в prompt (баг Соннета, найден 2026-07-16). */ + if (kbd_raw_open() != 0) { + closegraph(); + puts("kbd_raw_open failed (need memory mode with BSS in W2)"); + atlas_free(&at); + return 1; + } + + for (;;) { + uint8_t moving = 0; + int y = KIDY; + + if (jumping) { + /* дуга идёт сама; направлением можно скользить вбок, + * поза не меняется (нет отдельного кадра прыжка) */ + if (kbd_raw_down(KBD_LEFT)) { + if (!col_blocked(x - SPEED)) x -= SPEED; + } else if (kbd_raw_down(KBD_RIGHT)) { + if (!col_blocked(x + SPEED)) x += SPEED; + } + y = KIDY - jump_height[jump_t]; + jump_t++; + if (jump_t >= JUMP_FRAMES) { + jumping = 0; + y = KIDY; + } + } else if (crouching) { + if (!kbd_raw_down(KBD_DOWN)) + crouching = 0; + } else if (kbd_raw_down(KBD_UP)) { + jumping = 1; + jump_t = 0; + } else if (kbd_raw_down(KBD_DOWN)) { + crouching = 1; + } else if (kbd_raw_down(KBD_LEFT)) { + if (!col_blocked(x - SPEED)) x -= SPEED; + set_facing(DIR_LEFT); + moving = 1; + } else if (kbd_raw_down(KBD_RIGHT)) { + if (!col_blocked(x + SPEED)) x += SPEED; + set_facing(DIR_RIGHT); + moving = 1; + } + if (!moving && !jumping && (sprite_anim_status(&kid) & SPR_ANIM_ON)) + sprite_anim_stop(&kid, (int8_t)(facing * 3)); + + sprite_move(&kid, x, y); + + if (kbd_raw_down(KBD_ESC)) + break; + + hidden = gfx_get_visible_page() ^ 1; + gfx_set_draw_page(hidden); + show_state(jumping, crouching); + sprite_update(&kid, 1); + gfx_wait_vsync(); + gfx_set_visible_page(hidden); + } + + kbd_raw_close(); + closegraph(); + tile_atlas_free(); + atlas_free(&at); + return 0; +} diff --git a/applications/PoP/poc/res/bg/pop_bg.pal b/applications/PoP/poc/res/bg/pop_bg.pal new file mode 100644 index 0000000..ed5e328 Binary files /dev/null and b/applications/PoP/poc/res/bg/pop_bg.pal differ diff --git a/applications/PoP/poc/res/bg/pop_bg_atlas.h b/applications/PoP/poc/res/bg/pop_bg_atlas.h new file mode 100644 index 0000000..9fb8247 --- /dev/null +++ b/applications/PoP/poc/res/bg/pop_bg_atlas.h @@ -0,0 +1,32 @@ +/* pop_bg_atlas.h — раскладка атласов статического фона PoP. + * Сгенерировано toolchain/pop_pack_bg.py — НЕ править вручную. + * + * Прямая адресация (ноль remap-таблиц в W2): + * ENV фон id N -> atlas env_bg[N>>5], idx N&31 + * WALL id N -> atlas wall, idx N + * FORE id N -> atlas fore, idx N + */ +#ifndef POP_BG_ATLAS_H +#define POP_BG_ATLAS_H + +#define POP_ENV_SHIFT 5 +#define POP_ENV_MASK 31 +#define POP_ENV_PAGES 5 + +/* Палитра: env-слоты, wall-слоты (сприйт-пиксель i -> база+i). */ +#define POP_PAL_ENV 0x50 +#define POP_PAL_WALL 0x60 + +/* Имена файлов атласов (грузятся atlas_load). */ +static const char *const pop_env_atl[POP_ENV_PAGES] = { + "pop_env0.atl", + "pop_env1.atl", + "pop_env2.atl", + "pop_env3.atl", + "pop_env4.atl", +}; +#define POP_WALL_ATL "pop_wall.atl" +#define POP_FORE_ATL "pop_fore.atl" +#define POP_BG_PAL "pop_bg.pal" + +#endif diff --git a/applications/PoP/poc/res/bg/pop_env0.atl b/applications/PoP/poc/res/bg/pop_env0.atl new file mode 100644 index 0000000..91f08be Binary files /dev/null and b/applications/PoP/poc/res/bg/pop_env0.atl differ diff --git a/applications/PoP/poc/res/bg/pop_env1.atl b/applications/PoP/poc/res/bg/pop_env1.atl new file mode 100644 index 0000000..bfc986b Binary files /dev/null and b/applications/PoP/poc/res/bg/pop_env1.atl differ diff --git a/applications/PoP/poc/res/bg/pop_env2.atl b/applications/PoP/poc/res/bg/pop_env2.atl new file mode 100644 index 0000000..c204ff5 Binary files /dev/null and b/applications/PoP/poc/res/bg/pop_env2.atl differ diff --git a/applications/PoP/poc/res/bg/pop_env3.atl b/applications/PoP/poc/res/bg/pop_env3.atl new file mode 100644 index 0000000..61e7276 Binary files /dev/null and b/applications/PoP/poc/res/bg/pop_env3.atl differ diff --git a/applications/PoP/poc/res/bg/pop_env4.atl b/applications/PoP/poc/res/bg/pop_env4.atl new file mode 100644 index 0000000..9b98786 Binary files /dev/null and b/applications/PoP/poc/res/bg/pop_env4.atl differ diff --git a/applications/PoP/poc/res/bg/pop_fore.atl b/applications/PoP/poc/res/bg/pop_fore.atl new file mode 100644 index 0000000..15df455 Binary files /dev/null and b/applications/PoP/poc/res/bg/pop_fore.atl differ diff --git a/applications/PoP/poc/res/bg/pop_wall.atl b/applications/PoP/poc/res/bg/pop_wall.atl new file mode 100644 index 0000000..9556592 Binary files /dev/null and b/applications/PoP/poc/res/bg/pop_wall.atl differ diff --git a/applications/PoP/poc/res/kid/kid.pal b/applications/PoP/poc/res/kid/kid.pal new file mode 100644 index 0000000..4a5d9ab Binary files /dev/null and b/applications/PoP/poc/res/kid/kid.pal differ diff --git a/applications/PoP/poc/res/kid/kid0.atl b/applications/PoP/poc/res/kid/kid0.atl new file mode 100644 index 0000000..865a068 Binary files /dev/null and b/applications/PoP/poc/res/kid/kid0.atl differ diff --git a/applications/PoP/poc/res/kid/kid1.atl b/applications/PoP/poc/res/kid/kid1.atl new file mode 100644 index 0000000..2bb6b45 Binary files /dev/null and b/applications/PoP/poc/res/kid/kid1.atl differ diff --git a/applications/PoP/poc/res/kid/kid10.atl b/applications/PoP/poc/res/kid/kid10.atl new file mode 100644 index 0000000..fa95752 Binary files /dev/null and b/applications/PoP/poc/res/kid/kid10.atl differ diff --git a/applications/PoP/poc/res/kid/kid11.atl b/applications/PoP/poc/res/kid/kid11.atl new file mode 100644 index 0000000..f9ad644 Binary files /dev/null and b/applications/PoP/poc/res/kid/kid11.atl differ diff --git a/applications/PoP/poc/res/kid/kid12.atl b/applications/PoP/poc/res/kid/kid12.atl new file mode 100644 index 0000000..64e27f5 Binary files /dev/null and b/applications/PoP/poc/res/kid/kid12.atl differ diff --git a/applications/PoP/poc/res/kid/kid13.atl b/applications/PoP/poc/res/kid/kid13.atl new file mode 100644 index 0000000..8d3440d Binary files /dev/null and b/applications/PoP/poc/res/kid/kid13.atl differ diff --git a/applications/PoP/poc/res/kid/kid14.atl b/applications/PoP/poc/res/kid/kid14.atl new file mode 100644 index 0000000..a63711b Binary files /dev/null and b/applications/PoP/poc/res/kid/kid14.atl differ diff --git a/applications/PoP/poc/res/kid/kid15.atl b/applications/PoP/poc/res/kid/kid15.atl new file mode 100644 index 0000000..2365673 Binary files /dev/null and b/applications/PoP/poc/res/kid/kid15.atl differ diff --git a/applications/PoP/poc/res/kid/kid16.atl b/applications/PoP/poc/res/kid/kid16.atl new file mode 100644 index 0000000..d7c351b Binary files /dev/null and b/applications/PoP/poc/res/kid/kid16.atl differ diff --git a/applications/PoP/poc/res/kid/kid17.atl b/applications/PoP/poc/res/kid/kid17.atl new file mode 100644 index 0000000..940aa54 Binary files /dev/null and b/applications/PoP/poc/res/kid/kid17.atl differ diff --git a/applications/PoP/poc/res/kid/kid18.atl b/applications/PoP/poc/res/kid/kid18.atl new file mode 100644 index 0000000..d964cdc Binary files /dev/null and b/applications/PoP/poc/res/kid/kid18.atl differ diff --git a/applications/PoP/poc/res/kid/kid19.atl b/applications/PoP/poc/res/kid/kid19.atl new file mode 100644 index 0000000..cd127b9 Binary files /dev/null and b/applications/PoP/poc/res/kid/kid19.atl differ diff --git a/applications/PoP/poc/res/kid/kid2.atl b/applications/PoP/poc/res/kid/kid2.atl new file mode 100644 index 0000000..8e2f3a1 Binary files /dev/null and b/applications/PoP/poc/res/kid/kid2.atl differ diff --git a/applications/PoP/poc/res/kid/kid20.atl b/applications/PoP/poc/res/kid/kid20.atl new file mode 100644 index 0000000..32fd208 Binary files /dev/null and b/applications/PoP/poc/res/kid/kid20.atl differ diff --git a/applications/PoP/poc/res/kid/kid21.atl b/applications/PoP/poc/res/kid/kid21.atl new file mode 100644 index 0000000..22cbda0 Binary files /dev/null and b/applications/PoP/poc/res/kid/kid21.atl differ diff --git a/applications/PoP/poc/res/kid/kid22.atl b/applications/PoP/poc/res/kid/kid22.atl new file mode 100644 index 0000000..a5fd553 Binary files /dev/null and b/applications/PoP/poc/res/kid/kid22.atl differ diff --git a/applications/PoP/poc/res/kid/kid23.atl b/applications/PoP/poc/res/kid/kid23.atl new file mode 100644 index 0000000..b61dd25 Binary files /dev/null and b/applications/PoP/poc/res/kid/kid23.atl differ diff --git a/applications/PoP/poc/res/kid/kid24.atl b/applications/PoP/poc/res/kid/kid24.atl new file mode 100644 index 0000000..57623df Binary files /dev/null and b/applications/PoP/poc/res/kid/kid24.atl differ diff --git a/applications/PoP/poc/res/kid/kid25.atl b/applications/PoP/poc/res/kid/kid25.atl new file mode 100644 index 0000000..e543d74 Binary files /dev/null and b/applications/PoP/poc/res/kid/kid25.atl differ diff --git a/applications/PoP/poc/res/kid/kid26.atl b/applications/PoP/poc/res/kid/kid26.atl new file mode 100644 index 0000000..39ee166 Binary files /dev/null and b/applications/PoP/poc/res/kid/kid26.atl differ diff --git a/applications/PoP/poc/res/kid/kid27.atl b/applications/PoP/poc/res/kid/kid27.atl new file mode 100644 index 0000000..03c1cff Binary files /dev/null and b/applications/PoP/poc/res/kid/kid27.atl differ diff --git a/applications/PoP/poc/res/kid/kid3.atl b/applications/PoP/poc/res/kid/kid3.atl new file mode 100644 index 0000000..39a3f9f Binary files /dev/null and b/applications/PoP/poc/res/kid/kid3.atl differ diff --git a/applications/PoP/poc/res/kid/kid4.atl b/applications/PoP/poc/res/kid/kid4.atl new file mode 100644 index 0000000..fe74474 Binary files /dev/null and b/applications/PoP/poc/res/kid/kid4.atl differ diff --git a/applications/PoP/poc/res/kid/kid5.atl b/applications/PoP/poc/res/kid/kid5.atl new file mode 100644 index 0000000..bcc0463 Binary files /dev/null and b/applications/PoP/poc/res/kid/kid5.atl differ diff --git a/applications/PoP/poc/res/kid/kid6.atl b/applications/PoP/poc/res/kid/kid6.atl new file mode 100644 index 0000000..1b2fd42 Binary files /dev/null and b/applications/PoP/poc/res/kid/kid6.atl differ diff --git a/applications/PoP/poc/res/kid/kid7.atl b/applications/PoP/poc/res/kid/kid7.atl new file mode 100644 index 0000000..a95a4f0 Binary files /dev/null and b/applications/PoP/poc/res/kid/kid7.atl differ diff --git a/applications/PoP/poc/res/kid/kid8.atl b/applications/PoP/poc/res/kid/kid8.atl new file mode 100644 index 0000000..04de057 Binary files /dev/null and b/applications/PoP/poc/res/kid/kid8.atl differ diff --git a/applications/PoP/poc/res/kid/kid9.atl b/applications/PoP/poc/res/kid/kid9.atl new file mode 100644 index 0000000..ed13db5 Binary files /dev/null and b/applications/PoP/poc/res/kid/kid9.atl differ diff --git a/applications/PoP/poc/res/kid/kid_atlas.h b/applications/PoP/poc/res/kid/kid_atlas.h new file mode 100644 index 0000000..067cbd1 --- /dev/null +++ b/applications/PoP/poc/res/kid/kid_atlas.h @@ -0,0 +1,39 @@ +/* kid_atlas.h — раскладка атласов Kid. Сгенерировано pop_pack_kid.py. */ +#ifndef KID_ATLAS_H +#define KID_ATLAS_H +#define KID_SHIFT 3 +#define KID_MASK 7 +#define KID_PAGES 28 +#define KID_PAL 0x70 +static const char *const kid_atl[KID_PAGES] = { + "kid0.atl", + "kid1.atl", + "kid2.atl", + "kid3.atl", + "kid4.atl", + "kid5.atl", + "kid6.atl", + "kid7.atl", + "kid8.atl", + "kid9.atl", + "kid10.atl", + "kid11.atl", + "kid12.atl", + "kid13.atl", + "kid14.atl", + "kid15.atl", + "kid16.atl", + "kid17.atl", + "kid18.atl", + "kid19.atl", + "kid20.atl", + "kid21.atl", + "kid22.atl", + "kid23.atl", + "kid24.atl", + "kid25.atl", + "kid26.atl", + "kid27.atl", +}; +#define KID_PAL_FILE "kid.pal" +#endif diff --git a/applications/PoP/poc/res/room1.dat b/applications/PoP/poc/res/room1.dat new file mode 100644 index 0000000..71beaa2 Binary files /dev/null and b/applications/PoP/poc/res/room1.dat differ diff --git a/applications/PoP/poc/res/tiles/0-0.png b/applications/PoP/poc/res/tiles/0-0.png new file mode 100644 index 0000000..e0ed9f1 Binary files /dev/null and b/applications/PoP/poc/res/tiles/0-0.png differ diff --git a/applications/PoP/poc/res/tiles/1-0.png b/applications/PoP/poc/res/tiles/1-0.png new file mode 100644 index 0000000..87ac84a Binary files /dev/null and b/applications/PoP/poc/res/tiles/1-0.png differ diff --git a/applications/PoP/poc/res/tiles/10-0.png b/applications/PoP/poc/res/tiles/10-0.png new file mode 100644 index 0000000..9592e3d Binary files /dev/null and b/applications/PoP/poc/res/tiles/10-0.png differ diff --git a/applications/PoP/poc/res/tiles/11-0.png b/applications/PoP/poc/res/tiles/11-0.png new file mode 100644 index 0000000..4285c0c Binary files /dev/null and b/applications/PoP/poc/res/tiles/11-0.png differ diff --git a/applications/PoP/poc/res/tiles/12-0.png b/applications/PoP/poc/res/tiles/12-0.png new file mode 100644 index 0000000..95f277e Binary files /dev/null and b/applications/PoP/poc/res/tiles/12-0.png differ diff --git a/applications/PoP/poc/res/tiles/13-0.png b/applications/PoP/poc/res/tiles/13-0.png new file mode 100644 index 0000000..2451799 Binary files /dev/null and b/applications/PoP/poc/res/tiles/13-0.png differ diff --git a/applications/PoP/poc/res/tiles/14-0.png b/applications/PoP/poc/res/tiles/14-0.png new file mode 100644 index 0000000..0cc421c Binary files /dev/null and b/applications/PoP/poc/res/tiles/14-0.png differ diff --git a/applications/PoP/poc/res/tiles/15-0.png b/applications/PoP/poc/res/tiles/15-0.png new file mode 100644 index 0000000..9bf0709 Binary files /dev/null and b/applications/PoP/poc/res/tiles/15-0.png differ diff --git a/applications/PoP/poc/res/tiles/16-0.png b/applications/PoP/poc/res/tiles/16-0.png new file mode 100644 index 0000000..72eca2b Binary files /dev/null and b/applications/PoP/poc/res/tiles/16-0.png differ diff --git a/applications/PoP/poc/res/tiles/17-0.png b/applications/PoP/poc/res/tiles/17-0.png new file mode 100644 index 0000000..0cc421c Binary files /dev/null and b/applications/PoP/poc/res/tiles/17-0.png differ diff --git a/applications/PoP/poc/res/tiles/18-0.png b/applications/PoP/poc/res/tiles/18-0.png new file mode 100644 index 0000000..0cc421c Binary files /dev/null and b/applications/PoP/poc/res/tiles/18-0.png differ diff --git a/applications/PoP/poc/res/tiles/19-0.png b/applications/PoP/poc/res/tiles/19-0.png new file mode 100644 index 0000000..c7dc664 Binary files /dev/null and b/applications/PoP/poc/res/tiles/19-0.png differ diff --git a/applications/PoP/poc/res/tiles/1A-0.png b/applications/PoP/poc/res/tiles/1A-0.png new file mode 100644 index 0000000..7fdc838 Binary files /dev/null and b/applications/PoP/poc/res/tiles/1A-0.png differ diff --git a/applications/PoP/poc/res/tiles/1B-0.png b/applications/PoP/poc/res/tiles/1B-0.png new file mode 100644 index 0000000..adae927 Binary files /dev/null and b/applications/PoP/poc/res/tiles/1B-0.png differ diff --git a/applications/PoP/poc/res/tiles/1C-0.png b/applications/PoP/poc/res/tiles/1C-0.png new file mode 100644 index 0000000..4f4b779 Binary files /dev/null and b/applications/PoP/poc/res/tiles/1C-0.png differ diff --git a/applications/PoP/poc/res/tiles/1D-0.png b/applications/PoP/poc/res/tiles/1D-0.png new file mode 100644 index 0000000..a1e1fdf Binary files /dev/null and b/applications/PoP/poc/res/tiles/1D-0.png differ diff --git a/applications/PoP/poc/res/tiles/1E-0.png b/applications/PoP/poc/res/tiles/1E-0.png new file mode 100644 index 0000000..1a18c88 Binary files /dev/null and b/applications/PoP/poc/res/tiles/1E-0.png differ diff --git a/applications/PoP/poc/res/tiles/1F-0.png b/applications/PoP/poc/res/tiles/1F-0.png new file mode 100644 index 0000000..92f75ea Binary files /dev/null and b/applications/PoP/poc/res/tiles/1F-0.png differ diff --git a/applications/PoP/poc/res/tiles/2-0.png b/applications/PoP/poc/res/tiles/2-0.png new file mode 100644 index 0000000..4a75f0d Binary files /dev/null and b/applications/PoP/poc/res/tiles/2-0.png differ diff --git a/applications/PoP/poc/res/tiles/3-0.png b/applications/PoP/poc/res/tiles/3-0.png new file mode 100644 index 0000000..0cc421c Binary files /dev/null and b/applications/PoP/poc/res/tiles/3-0.png differ diff --git a/applications/PoP/poc/res/tiles/4-0.png b/applications/PoP/poc/res/tiles/4-0.png new file mode 100644 index 0000000..49356c3 Binary files /dev/null and b/applications/PoP/poc/res/tiles/4-0.png differ diff --git a/applications/PoP/poc/res/tiles/5-0.png b/applications/PoP/poc/res/tiles/5-0.png new file mode 100644 index 0000000..ce25026 Binary files /dev/null and b/applications/PoP/poc/res/tiles/5-0.png differ diff --git a/applications/PoP/poc/res/tiles/6-0.png b/applications/PoP/poc/res/tiles/6-0.png new file mode 100644 index 0000000..ae5ae7f Binary files /dev/null and b/applications/PoP/poc/res/tiles/6-0.png differ diff --git a/applications/PoP/poc/res/tiles/7-0.png b/applications/PoP/poc/res/tiles/7-0.png new file mode 100644 index 0000000..e090d76 Binary files /dev/null and b/applications/PoP/poc/res/tiles/7-0.png differ diff --git a/applications/PoP/poc/res/tiles/8-0.png b/applications/PoP/poc/res/tiles/8-0.png new file mode 100644 index 0000000..0cc421c Binary files /dev/null and b/applications/PoP/poc/res/tiles/8-0.png differ diff --git a/applications/PoP/poc/res/tiles/9-0.png b/applications/PoP/poc/res/tiles/9-0.png new file mode 100644 index 0000000..0cc421c Binary files /dev/null and b/applications/PoP/poc/res/tiles/9-0.png differ diff --git a/applications/PoP/poc/res/tiles/A-0.png b/applications/PoP/poc/res/tiles/A-0.png new file mode 100644 index 0000000..85e7a55 Binary files /dev/null and b/applications/PoP/poc/res/tiles/A-0.png differ diff --git a/applications/PoP/poc/res/tiles/B-0.png b/applications/PoP/poc/res/tiles/B-0.png new file mode 100644 index 0000000..1c698b4 Binary files /dev/null and b/applications/PoP/poc/res/tiles/B-0.png differ diff --git a/applications/PoP/poc/res/tiles/C-0.png b/applications/PoP/poc/res/tiles/C-0.png new file mode 100644 index 0000000..15824da Binary files /dev/null and b/applications/PoP/poc/res/tiles/C-0.png differ diff --git a/applications/PoP/poc/res/tiles/D-0.png b/applications/PoP/poc/res/tiles/D-0.png new file mode 100644 index 0000000..ae6fc2f Binary files /dev/null and b/applications/PoP/poc/res/tiles/D-0.png differ diff --git a/applications/PoP/poc/res/tiles/E-0.png b/applications/PoP/poc/res/tiles/E-0.png new file mode 100644 index 0000000..a468fe6 Binary files /dev/null and b/applications/PoP/poc/res/tiles/E-0.png differ diff --git a/applications/PoP/poc/res/tiles/F-0.png b/applications/PoP/poc/res/tiles/F-0.png new file mode 100644 index 0000000..5251ccb Binary files /dev/null and b/applications/PoP/poc/res/tiles/F-0.png differ diff --git a/applications/PoP/poc/res/tiles/tile00.pal b/applications/PoP/poc/res/tiles/tile00.pal new file mode 100644 index 0000000..1ca5537 Binary files /dev/null and b/applications/PoP/poc/res/tiles/tile00.pal differ diff --git a/applications/PoP/poc/res/tiles/tile01.atl b/applications/PoP/poc/res/tiles/tile01.atl new file mode 100644 index 0000000..854bd57 Binary files /dev/null and b/applications/PoP/poc/res/tiles/tile01.atl differ diff --git a/applications/PoP/poc/res/tiles/tile01.pal b/applications/PoP/poc/res/tiles/tile01.pal new file mode 100644 index 0000000..d15dee2 Binary files /dev/null and b/applications/PoP/poc/res/tiles/tile01.pal differ diff --git a/applications/PoP/poc/res/tiles/tile01.raw b/applications/PoP/poc/res/tiles/tile01.raw new file mode 100644 index 0000000..f406d72 --- /dev/null +++ b/applications/PoP/poc/res/tiles/tile01.raw @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/applications/PoP/poc/res/tiles/tile01_merged.pal b/applications/PoP/poc/res/tiles/tile01_merged.pal new file mode 100644 index 0000000..350006d Binary files /dev/null and b/applications/PoP/poc/res/tiles/tile01_merged.pal differ diff --git a/applications/PoP/poc/res/tiles/tile02.pal b/applications/PoP/poc/res/tiles/tile02.pal new file mode 100644 index 0000000..ac7c0f1 Binary files /dev/null and b/applications/PoP/poc/res/tiles/tile02.pal differ diff --git a/applications/PoP/poc/res/tiles/tile03.atl b/applications/PoP/poc/res/tiles/tile03.atl new file mode 100644 index 0000000..e91004b Binary files /dev/null and b/applications/PoP/poc/res/tiles/tile03.atl differ diff --git a/applications/PoP/poc/res/tiles/tile03.raw b/applications/PoP/poc/res/tiles/tile03.raw new file mode 100644 index 0000000..040d79b --- /dev/null +++ b/applications/PoP/poc/res/tiles/tile03.raw @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/applications/PoP/poc/res/tiles/tile03_merged.pal b/applications/PoP/poc/res/tiles/tile03_merged.pal new file mode 100644 index 0000000..e792994 Binary files /dev/null and b/applications/PoP/poc/res/tiles/tile03_merged.pal differ diff --git a/applications/PoP/poc/res/tiles/tile04.pal b/applications/PoP/poc/res/tiles/tile04.pal new file mode 100644 index 0000000..c631629 Binary files /dev/null and b/applications/PoP/poc/res/tiles/tile04.pal differ diff --git a/applications/PoP/poc/res/tiles/tile05.pal b/applications/PoP/poc/res/tiles/tile05.pal new file mode 100644 index 0000000..3383048 Binary files /dev/null and b/applications/PoP/poc/res/tiles/tile05.pal differ diff --git a/applications/PoP/poc/res/tiles/tile06.pal b/applications/PoP/poc/res/tiles/tile06.pal new file mode 100644 index 0000000..1dd634a Binary files /dev/null and b/applications/PoP/poc/res/tiles/tile06.pal differ diff --git a/applications/PoP/poc/res/tiles/tile07.pal b/applications/PoP/poc/res/tiles/tile07.pal new file mode 100644 index 0000000..d3ba12e Binary files /dev/null and b/applications/PoP/poc/res/tiles/tile07.pal differ diff --git a/applications/PoP/poc/res/tiles/tile08.pal b/applications/PoP/poc/res/tiles/tile08.pal new file mode 100644 index 0000000..3358611 Binary files /dev/null and b/applications/PoP/poc/res/tiles/tile08.pal differ diff --git a/applications/PoP/poc/res/tiles/tile09.pal b/applications/PoP/poc/res/tiles/tile09.pal new file mode 100644 index 0000000..3358611 Binary files /dev/null and b/applications/PoP/poc/res/tiles/tile09.pal differ diff --git a/applications/PoP/poc/res/tiles/tile0a.pal b/applications/PoP/poc/res/tiles/tile0a.pal new file mode 100644 index 0000000..1100030 Binary files /dev/null and b/applications/PoP/poc/res/tiles/tile0a.pal differ diff --git a/applications/PoP/poc/res/tiles/tile0b.atl b/applications/PoP/poc/res/tiles/tile0b.atl new file mode 100644 index 0000000..cd4f242 Binary files /dev/null and b/applications/PoP/poc/res/tiles/tile0b.atl differ diff --git a/applications/PoP/poc/res/tiles/tile0b.raw b/applications/PoP/poc/res/tiles/tile0b.raw new file mode 100644 index 0000000..251fc9d --- /dev/null +++ b/applications/PoP/poc/res/tiles/tile0b.raw @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/applications/PoP/poc/res/tiles/tile0b_merged.pal b/applications/PoP/poc/res/tiles/tile0b_merged.pal new file mode 100644 index 0000000..b965cf3 Binary files /dev/null and b/applications/PoP/poc/res/tiles/tile0b_merged.pal differ diff --git a/applications/PoP/poc/res/tiles/tile0c.pal b/applications/PoP/poc/res/tiles/tile0c.pal new file mode 100644 index 0000000..4ea9a75 Binary files /dev/null and b/applications/PoP/poc/res/tiles/tile0c.pal differ diff --git a/applications/PoP/poc/res/tiles/tile0d.pal b/applications/PoP/poc/res/tiles/tile0d.pal new file mode 100644 index 0000000..a6144a1 Binary files /dev/null and b/applications/PoP/poc/res/tiles/tile0d.pal differ diff --git a/applications/PoP/poc/res/tiles/tile0e.atl b/applications/PoP/poc/res/tiles/tile0e.atl new file mode 100644 index 0000000..114a61f Binary files /dev/null and b/applications/PoP/poc/res/tiles/tile0e.atl differ diff --git a/applications/PoP/poc/res/tiles/tile0e.raw b/applications/PoP/poc/res/tiles/tile0e.raw new file mode 100644 index 0000000..10116ff --- /dev/null +++ b/applications/PoP/poc/res/tiles/tile0e.raw @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/applications/PoP/poc/res/tiles/tile0e_merged.pal b/applications/PoP/poc/res/tiles/tile0e_merged.pal new file mode 100644 index 0000000..1dcf99d Binary files /dev/null and b/applications/PoP/poc/res/tiles/tile0e_merged.pal differ diff --git a/applications/PoP/poc/res/tiles/tile0f.pal b/applications/PoP/poc/res/tiles/tile0f.pal new file mode 100644 index 0000000..32fde77 Binary files /dev/null and b/applications/PoP/poc/res/tiles/tile0f.pal differ diff --git a/applications/PoP/poc/res/tiles/tile10.pal b/applications/PoP/poc/res/tiles/tile10.pal new file mode 100644 index 0000000..36cf0d8 Binary files /dev/null and b/applications/PoP/poc/res/tiles/tile10.pal differ diff --git a/applications/PoP/poc/res/tiles/tile11.pal b/applications/PoP/poc/res/tiles/tile11.pal new file mode 100644 index 0000000..6c1b705 Binary files /dev/null and b/applications/PoP/poc/res/tiles/tile11.pal differ diff --git a/applications/PoP/poc/res/tiles/tile12.pal b/applications/PoP/poc/res/tiles/tile12.pal new file mode 100644 index 0000000..7ed7e8e Binary files /dev/null and b/applications/PoP/poc/res/tiles/tile12.pal differ diff --git a/applications/PoP/poc/res/tiles/tile13.atl b/applications/PoP/poc/res/tiles/tile13.atl new file mode 100644 index 0000000..0b8d6af Binary files /dev/null and b/applications/PoP/poc/res/tiles/tile13.atl differ diff --git a/applications/PoP/poc/res/tiles/tile13.raw b/applications/PoP/poc/res/tiles/tile13.raw new file mode 100644 index 0000000..69c1670 --- /dev/null +++ b/applications/PoP/poc/res/tiles/tile13.raw @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/applications/PoP/poc/res/tiles/tile13_merged.pal b/applications/PoP/poc/res/tiles/tile13_merged.pal new file mode 100644 index 0000000..83541b0 Binary files /dev/null and b/applications/PoP/poc/res/tiles/tile13_merged.pal differ diff --git a/applications/PoP/poc/res/tiles/tile14.atl b/applications/PoP/poc/res/tiles/tile14.atl new file mode 100644 index 0000000..e91004b Binary files /dev/null and b/applications/PoP/poc/res/tiles/tile14.atl differ diff --git a/applications/PoP/poc/res/tiles/tile14.pal b/applications/PoP/poc/res/tiles/tile14.pal new file mode 100644 index 0000000..3358611 Binary files /dev/null and b/applications/PoP/poc/res/tiles/tile14.pal differ diff --git a/applications/PoP/poc/res/tiles/tile14.raw b/applications/PoP/poc/res/tiles/tile14.raw new file mode 100644 index 0000000..040d79b --- /dev/null +++ b/applications/PoP/poc/res/tiles/tile14.raw @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/applications/PoP/poc/res/tiles/tile14_merged.pal b/applications/PoP/poc/res/tiles/tile14_merged.pal new file mode 100644 index 0000000..e792994 Binary files /dev/null and b/applications/PoP/poc/res/tiles/tile14_merged.pal differ diff --git a/applications/PoP/poc/res/tiles/tile15.pal b/applications/PoP/poc/res/tiles/tile15.pal new file mode 100644 index 0000000..cb4ec0a Binary files /dev/null and b/applications/PoP/poc/res/tiles/tile15.pal differ diff --git a/applications/PoP/poc/res/tiles/tile16.pal b/applications/PoP/poc/res/tiles/tile16.pal new file mode 100644 index 0000000..4f0b536 Binary files /dev/null and b/applications/PoP/poc/res/tiles/tile16.pal differ diff --git a/applications/PoP/poc/res/tiles/tile17.pal b/applications/PoP/poc/res/tiles/tile17.pal new file mode 100644 index 0000000..3358611 Binary files /dev/null and b/applications/PoP/poc/res/tiles/tile17.pal differ diff --git a/applications/PoP/poc/res/tiles/tile18.pal b/applications/PoP/poc/res/tiles/tile18.pal new file mode 100644 index 0000000..3358611 Binary files /dev/null and b/applications/PoP/poc/res/tiles/tile18.pal differ diff --git a/applications/PoP/poc/res/tiles/tile19.pal b/applications/PoP/poc/res/tiles/tile19.pal new file mode 100644 index 0000000..9f81666 Binary files /dev/null and b/applications/PoP/poc/res/tiles/tile19.pal differ diff --git a/applications/PoP/poc/res/tiles/tile1a.pal b/applications/PoP/poc/res/tiles/tile1a.pal new file mode 100644 index 0000000..f7a99d7 Binary files /dev/null and b/applications/PoP/poc/res/tiles/tile1a.pal differ diff --git a/applications/PoP/poc/res/tiles/tile1b.pal b/applications/PoP/poc/res/tiles/tile1b.pal new file mode 100644 index 0000000..a2b647d Binary files /dev/null and b/applications/PoP/poc/res/tiles/tile1b.pal differ diff --git a/applications/PoP/poc/res/tiles/tile1c.pal b/applications/PoP/poc/res/tiles/tile1c.pal new file mode 100644 index 0000000..4823657 Binary files /dev/null and b/applications/PoP/poc/res/tiles/tile1c.pal differ diff --git a/applications/PoP/poc/res/tiles/tile1d.pal b/applications/PoP/poc/res/tiles/tile1d.pal new file mode 100644 index 0000000..1a1f7dc Binary files /dev/null and b/applications/PoP/poc/res/tiles/tile1d.pal differ diff --git a/applications/PoP/poc/res/tiles/tile1e.pal b/applications/PoP/poc/res/tiles/tile1e.pal new file mode 100644 index 0000000..121acae Binary files /dev/null and b/applications/PoP/poc/res/tiles/tile1e.pal differ diff --git a/applications/PoP/poc/res/tiles/tile1f.pal b/applications/PoP/poc/res/tiles/tile1f.pal new file mode 100644 index 0000000..185a9ed Binary files /dev/null and b/applications/PoP/poc/res/tiles/tile1f.pal differ diff --git a/applications/PoP/poc/room.c b/applications/PoP/poc/room.c new file mode 100644 index 0000000..798728a --- /dev/null +++ b/applications/PoP/poc/room.c @@ -0,0 +1,158 @@ +/* + * room.c — вся логика комнаты, ОБЫЧНЫЙ HOME-файл (без --bank). + * + * 2026-07-15/16: изначально была банковская (--bank 1), сначала в W3 + * (huge-дефолт), потом пробовали W1 (--memory big) — оба варианта + * упирались в проблемы (W3 конфликтует с gfx_blit_part/atlas_load, + * которые сами трогают W3; W1-банк несовместим с CODE=W1 — трамплин + * сам бы уехал из W1; --memory big тесно — CODE+DATA+BSS ~20КБ, не + * лезет в один 16КБ). После замены sprintf() на ручное hex-форматирование + * пути (экономия ~2.9КБ — sprintf/vsprintf тянули за собой заметный + * кусок форматирующего кода, единственный потребитель в HOME) всё + * снова влезает в W1 обычным HOME-кодом — банк ОКАЗАЛСЯ НЕ НУЖЕН. + * Так проще: gfx_blit_part()/atlas_load() (трогают W3 транзитивно) + * теперь просто обычные HOME-функции, без всяких W1/W3-танцев. + * + * gfx_blit_part пишет ПРЯМО в текущий банк (видео-ОЗУ + теневая копия + * при GFX_BANK_NORMAL) — перманентно, не через sprite_t. Вызывающий + * код обязан позвать room_draw() на КАЖДУЮ из двух страниц дабл-буфера + * при входе в комнату, иначе heal спрайтов увидит фон только на одной + * странице. + */ +#include +#include +#include "room.h" + +/* 32x64 на тайл: 3*64+8=200 — сходится с разрешением DOS 320x200 + * (8px внизу — под полоску жизней/строку текста, не часть игрового + * поля). Уточнить точнее, когда будут готовы настоящие ассеты. */ +uint8_t room_row_h[ROOM_ROWS] = {64, 64, 64}; +tile_atlas_t tile_atlas[32]; + +int tile_atlas_load(const char *dir) +{ + static const char HEXD[] = "0123456789abcdef"; + char path[64]; + int i, count = 0; + + for (i = 0; i < 32; i++) { + /* Вручную, не sprintf("%stile%02x.atl",...) — единственный + * потребитель printf-семейства в этом приложении, тянул за + * собой ~2.9КБ форматирующего кода; формат тут фиксированный + * (2 hex-цифры). */ + char *p = path; + const char *s = dir; + while (*s) *p++ = *s++; + s = "tile"; + while (*s) *p++ = *s++; + *p++ = HEXD[(i >> 4) & 0xF]; + *p++ = HEXD[i & 0xF]; + s = ".atl"; + while (*s) *p++ = *s++; + *p = '\0'; + if (atlas_load(&tile_atlas[i].at, path) == 0) { + tile_atlas[i].loaded = 1; + count++; + } else { + tile_atlas[i].loaded = 0; + } + } + return count; +} + +void tile_atlas_free(void) +{ + int i; + + for (i = 0; i < 32; i++) { + if (tile_atlas[i].loaded) { + atlas_free(&tile_atlas[i].at); + tile_atlas[i].loaded = 0; + } + } +} + +int level_load_room(level_t *lv, uint8_t room, const char *path) +{ + FILE *f = fopen(path, "rb"); + uint8_t buf[ROOM_TILES * 2 + 3]; /* fore[30]+back[30]+start_room/pos/dir */ + int i; + + if (!f) + return -1; + if (fread(buf, 1, sizeof(buf), f) != sizeof(buf)) { + fclose(f); + return -1; + } + fclose(f); + + for (i = 0; i < ROOM_TILES; i++) { + tile_t *t = level_tile(lv, room, (uint8_t)(i % ROOM_COLS), + (uint8_t)(i / ROOM_COLS)); + t->type = buf[i]; + t->state = buf[ROOM_TILES + i]; + } + lv->start_room = buf[ROOM_TILES * 2]; + lv->start_location = buf[ROOM_TILES * 2 + 1]; + lv->start_direction = (int8_t)buf[ROOM_TILES * 2 + 2]; + return 0; +} + +/* _bgi_blit_rows_raw держит ОДИН DI на весь блит (см. её шапку) — + * вызывающий обязан резать блиты выше 16 строк на чанки, иначе окно + * DI слишком длинное (риск сорвать кадровую цепочку IRQ/CBL). Наши + * тайлы 64px высотой — режем на чанки по CHUNK_H строк. */ +#define CHUNK_H 16 + +void room_draw(level_t *lv, uint8_t room) +{ + int row, col, y = 0; + + for (row = 0; row < ROOM_ROWS; row++) { + int x = 0; + uint8_t fh = room_row_h[row]; + + for (col = 0; col < ROOM_COLS; col++) { + tile_t *t = level_tile(lv, room, (uint8_t)col, (uint8_t)row); + uint8_t type = TILE_TYPE(t->type); + + if (tile_atlas[type].loaded) { + uint8_t count = tile_atlas[type].at.count; + uint8_t variant = 0; + uint8_t is_var; + + if (count != 0) { + switch (type) { + case TILE_SPIKES: + case TILE_GATE: + case TILE_TAPESTRY: + case TILE_TAPESTRY_TOP: + case TILE_CHOPPER: + case TILE_POTION: + is_var = 1; + break; + default: + is_var = 0; + } + variant = is_var + ? (uint8_t)(t->state % count) + : (uint8_t)((room * 31u + col * 7u + row * 13u) % count); + } + const void *img = atlas_image(&tile_atlas[type].at, 0); + uint8_t sub_y; + + gfx_w0_map(tile_atlas[type].at.page); + for (sub_y = 0; sub_y < fh; sub_y = (uint8_t)(sub_y + CHUNK_H)) { + uint8_t chunk = (uint8_t)(fh - sub_y); + if (chunk > CHUNK_H) + chunk = CHUNK_H; + gfx_blit_part(x, y + sub_y, img, variant * ROOM_TILE_W, + sub_y, ROOM_TILE_W, chunk); + } + gfx_w0_unmap(); + } + x += ROOM_TILE_W; + } + y += fh; + } +} diff --git a/applications/PoP/poc/room.h b/applications/PoP/poc/room.h new file mode 100644 index 0000000..ee26e91 --- /dev/null +++ b/applications/PoP/poc/room.h @@ -0,0 +1,68 @@ +/* + * room.h — отрисовка комнаты из тайлов В РАНТАЙМЕ, через спрайтовые + * атласы (см. пересмотр PORT_PLAN.md §4, 2026-07-15). + * + * Один атлас на ТИП тайла (0x00..0x1F) — лента из N вариантов + * (кадр fw x fh, fw=ROOM_TILE_W, fh=высота ряда, где тип реально + * стоит). Для типов, где backtable — настоящее состояние + * (gate/tapestry/chomp/spike) — вариант = state; для остальных — + * простой псевдослучайный выбор по позиции (свой приём, не алгоритм + * оригинала — просто чтобы плитка не повторялась один-в-один). + * + * Загрузка "всей лентой — копии одной картинки" на старте, до + * появления настоящих вариантов — нормальная стратегия: движок/API + * не меняются, когда картинки заменятся на настоящие. + * + * Фаза 1 (PORT_PLAN.md §5): анимированные типы (факел/дверь/шипы/ + * дробилка) НЕ рисуются вообще — просто не грузим для них атлас, + * room_draw пропускает клетки без атласа. Анимация через отдельные + * sprite_t поверх фона — Фаза 2, см. §7 плана. + * + * room_draw() зовётся ОДИН РАЗ при входе в комнату, не в игровом + * цикле — бюджет кадра не затронут (gfx_w0_map/unmap на тайл — тоже + * разовая цена, не per-frame). + */ +#ifndef ROOM_H +#define ROOM_H + +#include +#include /* atlas_t, atlas_load/free, atlas_image, gfx_w0_map/unmap */ +#include "level.h" + +#define ROOM_TILE_W 32 /* ширина колонки, px (10 колонок = 320) */ + +/* Высоты рядов, px — измерено по реальным тайлам подземелья + * (PORT_PLAN.md §3: ~20/60/62) — ПОДТВЕРДИТЬ при подготовке настоящих + * ассетов, значения приблизительные. */ +extern uint8_t room_row_h[ROOM_ROWS]; + +typedef struct { + atlas_t at; + uint8_t loaded; /* 0 = атласа нет — тип не рисуется */ +} tile_atlas_t; + +/* Индекс — TILE_TYPE (0..31). */ +extern tile_atlas_t tile_atlas[32]; + +/* Грузит "{dir}tileNN.atl" (hex TILE_TYPE, 2 знака, напр. "tile01.atl" + * — Floor) — какие есть, те и грузятся. dir — префикс ПОДКЛЕИВАЕТСЯ + * как есть (без добавления разделителя) — файлы на диске лежат ПЛОСКО + * (см. соглашение других examples: "name.ext" либо "a:\\name.ext"), так что + * dir обычно "" или "a:\\". Каждый файл — ОДНА лента (idx 0) из N + * кадров fw=ROOM_TILE_W x fh=room_row_h[row] (row, где этот тип + * реально стоит) — готовится toolchain/png_strip.py + + * toolchain/mkatlas.py. Возврат: число загруженных (0..32). + * + * room.c — обычный HOME-файл, БЕЗ --bank (см. шапку room.c за + * историей: банк в W3 конфликтовал с gfx_blit_part/atlas_load, + * банк в W1 невозможен при CODE=W1 — трамплин сам уехал бы из W1; + * после чистки sprintf() всё влезает в HOME без банка вообще). */ +int tile_atlas_load(const char *dir); +void tile_atlas_free(void); + +/* Нарисовать комнату (room 1..24) — только типы с загруженным + * атласом; остальные клетки пропускаются (в т.ч. все анимированные + * типы в Фазе 1 — атлас для них просто не грузится). */ +void room_draw(level_t *lv, uint8_t room); + +#endif diff --git a/applications/PoP/poc/tools/build_room_palette.py b/applications/PoP/poc/tools/build_room_palette.py new file mode 100644 index 0000000..6ffe9e7 --- /dev/null +++ b/applications/PoP/poc/tools/build_room_palette.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +""" +build_room_palette.py — сводит kid.pal (уже собран +gen_kid_placeholder.py) + Floor + Wall тайлы в ОДНУ merged-палитру +room.pal, грузимую ОДИН раз через gfx_pal_fload (см. poc.c) — экран +показывает Kid и тайлы ОДНОВРЕМЕННО, значит их "свои" цвета обязаны +жить в одной палитре (см. --seed-pal в toolchain/png_strip.py). + +На вход — только типы, которые Фаза 1 реально рисует (room_draw +рисует только загруженные атласы; Floor/Wall — единственные твёрдые +типы с нетривиальной картинкой заглушки, см. gen_tile_placeholders.py +draw_floor_like/draw_wall_like). Остальные 30 типов из Фазы 1 +исключены полностью (не грузятся, не рисуются, PORT_PLAN.md §5) — +добавить сюда, когда понадобятся. + +Перезаписывает res/tiles/tile01.raw/.atl (Floor) и tile14.raw/.atl +(Wall, TILE_WALL=0x14 — room.c ищет "tile%02x.atl" ОБЯЗАТЕЛЬНО в hex, +не decimal!) с индексами палитры, согласованными с kid.pal; пишет +kid+floor+wall.pal рядом с kid.pal (poc.c грузит её вместо отдельной +kid.pal). +""" +import subprocess +import sys +from pathlib import Path + +HERE = Path(__file__).resolve().parent +PROJ_ROOT = HERE.parents[3] +TILES_DIR = HERE.parent / "res" / "tiles" +PNG_STRIP = PROJ_ROOT / "toolchain" / "png_strip.py" +MKATLAS = PROJ_ROOT / "toolchain" / "mkatlas.py" + +ROOM_TILE_W = 32 +ROOM_ROW_H = 64 + +KID_PAL = HERE / "kid.pal" +OUT_PAL = HERE / "room.pal" + +TYPES = [0x01, 0x14, 0x03, 0x13, 0x0E, 0x0B] # Floor, Wall, Pillar, Torch, Debris, Loose +# (2026-07-16: временное ограничение до 2 типов снято — корень +# вероятностного "снега" найден: tile_atlas_load()/room_draw() трогали +# W3 транзитивно (через atlas_load/gfx_blit_part) ИЗ БАНКА, который сам +# живёт в W3 — конфликт. Обе функции переехали в room_draw.c (HOME), +# см. её шапку.) + + +def run(cmd): + print("+", " ".join(str(c) for c in cmd)) + subprocess.run([str(c) for c in cmd], check=True) + + +def main(): + if not KID_PAL.exists(): + sys.exit(f"build_room_palette: нет {KID_PAL} — сначала gen_kid_placeholder.py") + + seed = KID_PAL + for t in TYPES: + base = f"tile{t:02x}" # room.c ищет "tile%02x.atl" — hex, не decimal + png = TILES_DIR / f"{t:X}-0.png" + raw = TILES_DIR / f"{base}.raw" + pal = TILES_DIR / f"{base}_merged.pal" + atl = TILES_DIR / f"{base}.atl" + + run([sys.executable, PNG_STRIP, raw, pal, png, + "--canvas", f"{ROOM_TILE_W}x{ROOM_ROW_H}", + "--anchor", "top-left", "--seed-pal", seed]) + run([sys.executable, MKATLAS, atl, f"{raw}:{ROOM_TILE_W}x{ROOM_ROW_H}:1x1"]) + seed = pal + + import shutil + shutil.copyfile(seed, OUT_PAL) + print(f"build_room_palette: итоговая палитра -> {OUT_PAL}") + + +if __name__ == "__main__": + main() diff --git a/applications/PoP/poc/tools/build_tile_atlases.py b/applications/PoP/poc/tools/build_tile_atlases.py new file mode 100644 index 0000000..cb12893 --- /dev/null +++ b/applications/PoP/poc/tools/build_tile_atlases.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +""" +build_tile_atlases.py — собирает tileNN.atl для всех 32 типов тайлов +из applications/PoP/poc/res/tiles/{TYPE:X}-{N}.png (см. +gen_tile_placeholders.py). Для каждого типа находит ВСЕ файлы +{TYPE:X}-*.png (сколько бы вариантов ни было — 1 заглушка или много +настоящих), сортирует по номеру варианта и собирает через +png_strip.py + mkatlas.py. Типы без ни одного файла — пропускаются +(room_draw их и так не рисует). + +Запускать заново каждый раз, когда добавили/заменили картинки в +res/tiles/ — атласы просто пересоберутся. +""" +import re +import subprocess +import sys +from pathlib import Path + +HERE = Path(__file__).resolve().parent +PROJ_ROOT = HERE.parents[3] # .../C-Compiler +TILES_DIR = HERE.parent / "res" / "tiles" +OUT_DIR = HERE.parent / "res" / "tiles" +PNG_STRIP = PROJ_ROOT / "toolchain" / "png_strip.py" +MKATLAS = PROJ_ROOT / "toolchain" / "mkatlas.py" + +ROOM_TILE_W = 32 +ROOM_ROW_H = 64 # см. room.c room_row_h — держать в синхроне + +PATTERN = re.compile(r"^([0-9A-Fa-f]+)-(\d+)\.png$") + + +def run(cmd): + print("+", " ".join(str(c) for c in cmd)) + subprocess.run([str(c) for c in cmd], check=True) + + +def main(): + by_type = {} + for f in TILES_DIR.glob("*.png"): + m = PATTERN.match(f.name) + if not m: + continue + t = int(m.group(1), 16) + n = int(m.group(2)) + by_type.setdefault(t, {})[n] = f + + if not by_type: + sys.exit(f"build_tile_atlases: нет файлов TYPE-N.png в {TILES_DIR}") + + built = 0 + for t in sorted(by_type): + variants = by_type[t] + files = [variants[n] for n in sorted(variants)] + strip_raw = OUT_DIR / f"tile{t:02x}.raw" + strip_pal = OUT_DIR / f"tile{t:02x}.pal" + atl = OUT_DIR / f"tile{t:02x}.atl" + + run([sys.executable, PNG_STRIP, strip_raw, strip_pal, + *files, "--canvas", f"{ROOM_TILE_W}x{ROOM_ROW_H}", + "--anchor", "top-left"]) + run([sys.executable, MKATLAS, atl, + f"{strip_raw}:{ROOM_TILE_W}x{ROOM_ROW_H}:{len(files)}x1"]) + built += 1 + + print(f"build_tile_atlases: собрано {built} атласов " + f"(типы: {', '.join('%X' % t for t in sorted(by_type))})") + + +if __name__ == "__main__": + main() diff --git a/applications/PoP/poc/tools/extract_room.py b/applications/PoP/poc/tools/extract_room.py new file mode 100644 index 0000000..0027782 --- /dev/null +++ b/applications/PoP/poc/tools/extract_room.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +""" +extract_room.py — вырезает ОДНУ комнату (foretable+backtable, 30+30 +байт) + стартовую позицию уровня (start_room/start_pos/start_dir, 3 +байта) из level-ресурса формата "res20NN.bin" (SDLPoP data/LEVELS/). + +Layout подтверждён структурным разбором: файл начинается СРАЗУ с +foretable[720] (24 комнаты * 30 тайлов), затем backtable[720] — без +заголовка (applications/PoP/docs/POP-DAT-FormatSpecifications.pdf §3.4 ++ декод байт 2026-07-15). Тайл tileOffset 0-9 = верхний ряд, 10-19 = +средний, 20-29 = нижний (см. level.h level_tile()). + +Смещение start_room/start_pos/start_dir НЕ сразу после links (как +предполагалось изначально) — сверено со структурой level_type в +applications/PoP/SDLPoP/src/types.h (маленький структурный факт, +порядок полей, не объём кода): fg[720]+bg[720]+doorlinks1[256]+ +doorlinks2[256]+roomlinks[96]+used_rooms[1]+roomxs[24]+roomys[24]+ +fill_1[15] = 2112 -> start_room(u8), start_pos(u8), start_dir(s8) +(2026-07-16, подтверждено на res2001.bin: start_room=1, start_pos=0, +start_dir=-1 — согласуется с links[room1] и решулось эмпирически +через таблицу room-links, которая идеально легла на смещение 1952). + +Использование: + extract_room.py LEVEL.bin ROOM OUT.dat + +Выход: OUT.dat — 63 байта (fore[30]+back[30]+start_room+start_pos+ +start_dir), формат level_load_room() (room.c). ROOM — 1-based (как в +файле/level.h); start_* — общеуровневые (не зависят от ROOM). +""" +import sys + +FORETABLE_OFF = 0 +BACKTABLE_OFF = 720 +ROOM_TILES = 30 +START_OFF = 720 + 720 + 256 + 256 + 96 + 1 + 24 + 24 + 15 # 2112 + + +def main(): + if len(sys.argv) != 4: + sys.exit(__doc__.strip()) + src, room_s, out = sys.argv[1], sys.argv[2], sys.argv[3] + room = int(room_s) + + data = open(src, "rb").read() + if len(data) < START_OFF + 3: + sys.exit(f"extract_room: {src}: {len(data)} Б — меньше ожидаемых " + f"{START_OFF + 3} (foretable+backtable+...+start_position)") + + base = (room - 1) * ROOM_TILES + fore = data[FORETABLE_OFF + base:FORETABLE_OFF + base + ROOM_TILES] + back = data[BACKTABLE_OFF + base:BACKTABLE_OFF + base + ROOM_TILES] + start = data[START_OFF:START_OFF + 3] + open(out, "wb").write(fore + back + start) + print(f"extract_room: комната {room} из {src} -> {out} " + f"({len(fore)}+{len(back)}+{len(start)} Б); " + f"start_room={start[0]} start_pos={start[1]} " + f"start_dir={start[2] if start[2] < 128 else start[2] - 256}") + + +if __name__ == "__main__": + main() diff --git a/applications/PoP/poc/tools/gen_kid_placeholder.py b/applications/PoP/poc/tools/gen_kid_placeholder.py new file mode 100644 index 0000000..2f6363e --- /dev/null +++ b/applications/PoP/poc/tools/gen_kid_placeholder.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +""" +gen_kid_placeholder.py — временная ЗАГЛУШКА персонажа для PoC. + +НЕ используем графику оригинальной Prince of Persia (авторские права +Broderbund/Ubisoft) — PoC проверяет управление/коллизии/движок, не +визуальную точность (applications/PoP/docs/PORT_PLAN.md §5). Вместо +этого берём уже лицензированный спрайт-пак, который в проекте и так +используется (examples/rpgwalk/conv_sprites.py) — +third_party/16x16-RPG-characters, один персонаж (индекс 0) той же +раскладкой: 12 кадров (dir*3+frame, dir 0=вниз/1=влево/2=вправо/3=вверх, +3 кадра маятника на направление). + +Замену на настоящую графику Кида (если/когда появится легально чистый +источник) пользователь делает сам — вне этого скрипта. + +Выход: kid.raw (16×192, 12 кадров) + kid.pal (EGA 16 + цвета спрайта). +""" +from PIL import Image + +SRC = ("../../../../third_party/16x16-RPG-characters/sprites/" + "old-style/02-bard.png") +CHAR = 0 # какой из 8 персонажей листа берём + +EGA = [ # B, G, R, pad — как в libbgi/bgi256/_bgi_mode_set.c + (0, 0, 0), (168, 0, 0), (0, 168, 0), (168, 168, 0), + (0, 0, 168), (168, 0, 168), (0, 84, 168), (168, 168, 168), + (84, 84, 84), (255, 84, 84), (84, 255, 84), (255, 255, 84), + (84, 84, 255), (255, 84, 255), (84, 255, 255), (255, 255, 255), +] +BASE = 16 + +im = Image.open(SRC).convert("RGBA") +assert im.size == (192, 128), im.size + +colors = {} + + +def px(x, y): + r, g, b, a = im.getpixel((x, y)) + if a < 128: + return 0xFF + key = (r, g, b) + if key not in colors: + assert BASE + len(colors) < 0xFF + colors[key] = BASE + len(colors) + return colors[key] + + +bx, by = (CHAR % 4) * 48, (CHAR // 4) * 64 +data = bytearray() +for d in range(4): + for f in range(3): + for y in range(16): + for x in range(16): + data.append(px(bx + f * 16 + x, by + d * 16 + y)) +open("kid.raw", "wb").write(data) + +pal = bytearray() +for b, g, r in EGA: + pal += bytes((b, g, r, 0)) +for (r, g, b) in colors: + pal += bytes((b, g, r, 0)) +open("kid.pal", "wb").write(pal) + +print(f"gen_kid_placeholder: kid.raw 16x192 (12 кадров), " + f"kid.pal EGA+{len(colors)} цветов ({len(pal)} Б)") diff --git a/applications/PoP/poc/tools/gen_tile_placeholders.py b/applications/PoP/poc/tools/gen_tile_placeholders.py new file mode 100644 index 0000000..54ac170 --- /dev/null +++ b/applications/PoP/poc/tools/gen_tile_placeholders.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python3 +""" +gen_tile_placeholders.py — ЗАГЛУШКИ для 32 типов тайлов (не графика +оригинала!) — простая процедурная геометрия (кирпичная кладка линиями, +плоская поверхность и т.п.), чтобы визуально читалось "это стена"/ +"это пол", пока не готовы настоящие ассеты (см. room.h/room.c: +tile_atlas[TYPE], вариант 0 — "index"-картинка на тип). + +Выход: applications/PoP/poc/res/tiles/{TYPE:X}-0.png — 32 файла, +32x64 (ROOM_TILE_W x room_row_h), RGBA. + +Для твёрдых типов (Фаза 1: Floor/Wall/Pillar/PillarBot/PillarTop/ +BalconyL/R) — функциональная заглушка (см. draw_wall_like/draw_floor +ниже); Lattice-типы (0x19-0x1D) — узкая проходимая колонна той же +кладки (draw_lattice_like, см. reference-скриншот оригинала); для +остальных (декоративные/интерактивные, вне объёма Фазы 1) — просто +цвет + подпись, этого достаточно для отладки конвейера. Empty (0x00) +— полностью прозрачно (ничего не рисуется). + +По мере готовности настоящих ассетов — рядом кладутся 0-1.png, +0-2.png и т.д. (следующие варианты той же ленты), см. room.h. +""" +import colorsys +from pathlib import Path +from PIL import Image, ImageDraw, ImageFont + +W, H = 32, 64 + +NAMES = [ + "Empty", "Floor", "Spikes", "Pillar", "Gate", "StuckBtn", "DropBtn", + "Tapestry", "PillarBot", "PillarTop", "Potion", "Loose", "TapestryTop", + "Mirror", "Debris", "RaiseBtn", "ExitL", "ExitR", "Chopper", "Torch", + "Wall", "Skeleton", "Sword", "BalconyL", "BalconyR", "LatticePillar", + "LatticeSupport", "LatticeSmall", "LatticeL", "LatticeR", "TorchDebris", + "Null", +] +assert len(NAMES) == 32 + +WALL_LIKE = {0x03, 0x08, 0x09, 0x14, 0x17, 0x18} # Pillar/PillarBot/Top/Wall/BalconyL/R +FLOOR_LIKE = {0x01} # Floor +# Lattice — узкие декоративные каменные колонны (см. reference-скриншот +# оригинала 2026-07-16): Кид физически проходит МИМО них (не полноценная +# стена на всю ширину тайла) — колонна той же STONE/MORTAR кладки, но +# ýже тайла, с прозрачными полями по бокам. (col_w, align): +# align: "center"/"left"/"right". +LATTICE_LIKE = { + 0x19: (14, "center"), # LatticePillar + 0x1A: (18, "center"), # LatticeSupport — потолще + 0x1B: (10, "center"), # LatticeSmall — потоньше + 0x1C: (14, "left"), # LatticeLeft + 0x1D: (14, "right"), # LatticeRight +} +# Плоский однотонный цвет БЕЗ текста (не через антиалиасинг шрифта) — +# держит "своих" цветов в палитре по 1 на тип, иначе объединённая +# палитра (--seed-pal) быстро упирается в лимит 0xFF (см. +# pop_tile_atlas_palette_merge в памяти). +FLAT_LIKE = {0x13: (200, 90, 30, 255), # Torch — тёплый оранжевый + 0x0E: (110, 90, 70, 255), # Debris — серо-бурый + 0x0B: (140, 120, 60, 255)} # Loose — тускло-жёлтый (плита) + +STONE = (150, 150, 158, 255) +MORTAR = (90, 90, 98, 255) +GROUND = (120, 100, 80, 255) +GROUND_TOP = (170, 150, 120, 255) + +out_dir = Path(__file__).resolve().parent.parent / "res" / "tiles" +out_dir.mkdir(parents=True, exist_ok=True) + +try: + font = ImageFont.load_default() +except Exception: + font = None + + +def draw_wall_like(im): + """Кирпичная кладка — просто ряды прямоугольников со смещением + в шахматном порядке и линиями раствора между ними (обобщённый + геометрический приём, не стиль конкретной игры).""" + draw = ImageDraw.Draw(im) + draw.rectangle([0, 0, W - 1, H - 1], fill=STONE) + brick_h = 12 + brick_w = 16 + row = 0 + y = 0 + while y < H: + offset = (brick_w // 2) if (row % 2) else 0 + x = -brick_w + offset + while x < W: + draw.rectangle([x, y, x + brick_w - 2, y + brick_h - 2], + outline=MORTAR, width=1) + x += brick_w + y += brick_h + row += 1 + + +def draw_lattice_like(im, col_w, align): + """Узкая колонна кирпичной кладки шириной col_w {out_dir}") diff --git a/applications/PoP/poc/tools/kid.atl b/applications/PoP/poc/tools/kid.atl new file mode 100644 index 0000000..6eff625 Binary files /dev/null and b/applications/PoP/poc/tools/kid.atl differ diff --git a/applications/PoP/poc/tools/kid.pal b/applications/PoP/poc/tools/kid.pal new file mode 100644 index 0000000..0eeb432 Binary files /dev/null and b/applications/PoP/poc/tools/kid.pal differ diff --git a/applications/PoP/poc/tools/kid.raw b/applications/PoP/poc/tools/kid.raw new file mode 100644 index 0000000..7c51c15 --- /dev/null +++ b/applications/PoP/poc/tools/kid.raw @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/applications/PoP/poc/tools/room.pal b/applications/PoP/poc/tools/room.pal new file mode 100644 index 0000000..b965cf3 Binary files /dev/null and b/applications/PoP/poc/tools/room.pal differ diff --git a/applications/PoP/roomtest/Makefile b/applications/PoP/roomtest/Makefile new file mode 100644 index 0000000..ddaeea4 --- /dev/null +++ b/applications/PoP/roomtest/Makefile @@ -0,0 +1,34 @@ +# roomtest — проверка порта статического фона PoP (Шаг 3): реальная +# композиция слоёв (pop_bg.c) для комнаты 1. --memory huge --gfx 256. + +PROJ_ROOT := $(abspath $(CURDIR)/../../..) +EXAMPLE := roomtest +# small: W1+W2 как единое 32КБ-пространство, DATA сразу за CODE (иначе +# CODE ~23КБ переполняет W1 и лезет в фиксированный DATA@0x8000 в huge). +MEMORY ?= small +EXTRA_FLAGS ?= --gfx 256 +EXTRA_SRCS := pop_bg.c pop_kid.c pop_ctrl.c pop_map.c + +BG_DIR := $(CURDIR)/../poc/res/bg +KID_DIR := $(CURDIR)/../poc/res/kid +BG_DATA := $(BG_DIR)/pop_env0.atl $(BG_DIR)/pop_env1.atl $(BG_DIR)/pop_env2.atl \ + $(BG_DIR)/pop_env3.atl $(BG_DIR)/pop_env4.atl \ + $(BG_DIR)/pop_wall.atl $(BG_DIR)/pop_fore.atl +# ВСЕ 28 страниц Kid — иначе кадры торможения/разворота/приседа (image>23) +# не рисуются (kid_draw пропускает незагруженную страницу). Палитра — +# ЕДИНАЯ игровая (env+wall+kid). +KID_PAGES := $(foreach n,0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27,$(KID_DIR)/kid$(n).atl) +KID_DATA := $(KID_PAGES) $(KID_DIR)/kid.pal + +EXTRA_DATA := $(BG_DATA) $(KID_DATA) +include $(PROJ_ROOT)/app.mk + +TC := $(PROJ_ROOT)/applications/PoP/toolchain +$(BG_DATA): $(TC)/pop_pack_bg.py $(TC)/render_room.py + cd $(TC) && python3 pop_pack_bg.py +$(KID_DATA): $(TC)/pop_pack_kid.py $(TC)/pop_pack_bg.py + cd $(TC) && python3 pop_pack_kid.py +kid_data.h: $(TC)/pop_extract_kid_data.py + cd $(TC) && python3 pop_extract_kid_data.py + +$(EXAMPLE).exe: pop_bg.c pop_bg.h pop_kid.c pop_kid.h pop_ctrl.c pop_ctrl.h pop_map.c pop_map.h kid_data.h room1_data.h $(BG_DATA) $(KID_DATA) diff --git a/applications/PoP/roomtest/kid_data.h b/applications/PoP/roomtest/kid_data.h new file mode 100644 index 0000000..8f01566 --- /dev/null +++ b/applications/PoP/roomtest/kid_data.h @@ -0,0 +1,474 @@ +/* kid_data.h — данные анимации Kid (frame_table + seqtbl + + * offsets), извлечено toolchain/pop_extract_kid_data.py из + * SDLPoP. НЕ править вручную. seqtbl адресуется базой + * SEQTBL_BASE: kid_seqtbl[addr - SEQTBL_BASE]. */ +#ifndef KID_DATA_H +#define KID_DATA_H +#include + +#define SEQTBL_BASE 0x196Eu + +/* {image, dx, dy, flags, sword}; chtab = sword>>6 (0=kid). */ +typedef struct { uint8_t image; int8_t dx, dy; uint8_t flags, sword; } kframe; +#define KID_NFRAMES 241 +static const kframe kid_frames[KID_NFRAMES] = { + {255, 0, 0,0x00,0x00}, + { 0, 1, 0,0xC4,0x00}, + { 1, 1, 0,0x44,0x00}, + { 2, 3, 0,0x47,0x00}, + { 3, 4, 0,0x48,0x00}, + { 4, 0, 0,0xE6,0x00}, + { 5, 0, 0,0x49,0x00}, + { 6, 0, 0,0x4A,0x00}, + { 7, 0, 0,0xC5,0x00}, + { 8, 0, 0,0x44,0x00}, + { 9, 0, 0,0x47,0x00}, + { 10, 0, 0,0x4B,0x00}, + { 11, 0, 0,0x43,0x00}, + { 12, 0, 0,0xC3,0x00}, + { 13, 0, 0,0x47,0x00}, + { 14, 0, 0,0x43,0x09}, + { 15, 0, 0,0xC3,0x00}, + { 16, 0, 0,0x44,0x00}, + { 17, 0, 0,0x46,0x00}, + { 18, 0, 0,0x48,0x00}, + { 19, 0, 0,0x89,0x00}, + { 20, 0, 0,0x0B,0x00}, + { 21, 0, 0,0x8B,0x00}, + { 22, 0, 0,0x11,0x00}, + { 23, 0, 0,0x07,0x00}, + { 24, 0, 0,0x05,0x00}, + { 25, 0, 0,0xC1,0x00}, + { 26, 0, 0,0xC6,0x00}, + { 27, 0, 0,0x43,0x00}, + { 28, 0, 0,0x48,0x00}, + { 29, 0, 0,0x42,0x00}, + { 30, 0, 0,0x42,0x00}, + { 31, 0, 0,0xC2,0x00}, + { 32, 0, 0,0xC2,0x00}, + { 33, 0, 0,0x43,0x00}, + { 34, 0, 0,0x48,0x00}, + { 35, 0, 0,0xCE,0x00}, + { 36, 0, 0,0xC1,0x00}, + { 37, 0, 0,0x45,0x00}, + { 38, 0, 0,0x8E,0x00}, + { 39, 0, 0,0x0B,0x00}, + { 40, 0, 0,0x8B,0x00}, + { 41, 0, 0,0x8A,0x00}, + { 42, 0, 0,0x01,0x00}, + { 43, 0, 0,0xC4,0x00}, + { 44, 0, 0,0xC3,0x00}, + { 45, 0, 0,0xC3,0x00}, + { 46, 0, 0,0xA5,0x00}, + { 47, 0, 0,0xA4,0x00}, + { 48, 0, 0,0x66,0x00}, + { 49, 4, 0,0x67,0x00}, + { 50, 3, 0,0x66,0x00}, + { 51, 1, 0,0x44,0x00}, + { 64, 0, 0,0xC2,0x00}, + { 65, 0, 0,0x41,0x00}, + { 66, 0, 0,0x42,0x00}, + { 67, 0, 0,0x00,0x00}, + { 68, 0, 0,0x00,0x00}, + { 69, 0, 0,0x80,0x00}, + { 70, 0, 0,0x00,0x00}, + { 71, 0, 0,0x80,0x00}, + { 72, 0, 0,0x00,0x00}, + { 73, 0, 0,0x80,0x00}, + { 74, 0, 0,0x00,0x00}, + { 75, 0, 0,0x00,0x00}, + { 76, 0, 0,0x80,0x00}, + {255, 0, 0,0x00,0x00}, + { 80, -2, 0,0x41,0x00}, + { 81, -2, 0,0x41,0x00}, + { 82, -1, 0,0xC2,0x00}, + { 83, -2, 0,0x42,0x00}, + { 84, -2, 0,0x41,0x00}, + { 85, -2, 0,0x41,0x00}, + { 86, -2, 0,0x41,0x00}, + { 87, -1, 0,0x07,0x00}, + { 88, -1, 0,0x05,0x00}, + { 89, 2, 0,0x07,0x00}, + { 90, 2, 0,0x07,0x00}, + { 91, 2, -3,0x00,0x00}, + { 92, 2, -10,0x00,0x00}, + { 93, 2, -11,0x80,0x00}, + { 94, 3, -2,0x43,0x00}, + { 95, 3, 0,0xC3,0x00}, + { 96, 3, 0,0xC3,0x00}, + { 97, 3, 0,0x63,0x00}, + { 98, 4, 0,0xE3,0x00}, + { 28, 0, 0,0x00,0x00}, + { 99, 7, -14,0x80,0x00}, + {100, 7, -12,0x80,0x00}, + {101, 4, -12,0x00,0x00}, + {102, 3, -10,0x80,0x00}, + {103, 2, -10,0x80,0x00}, + {104, 1, -10,0x80,0x00}, + {105, 0, -11,0x00,0x00}, + {106, -1, -12,0x00,0x00}, + {107, -1, -14,0x00,0x00}, + {108, -1, -14,0x00,0x00}, + {109, -1, -15,0x80,0x00}, + {110, -1, -15,0x80,0x00}, + {111, 0, -15,0x00,0x00}, + {255, 0, 0,0x00,0x00}, + {255, 0, 0,0x00,0x00}, + {112, 0, 0,0xC6,0x00}, + {113, 0, 0,0x46,0x00}, + {114, 0, 0,0xC5,0x00}, + {115, 0, 0,0x45,0x00}, + {116, 0, 0,0xC2,0x00}, + {117, 0, 0,0xC4,0x00}, + {118, 0, 0,0xC5,0x00}, + {119, 0, 0,0x46,0x00}, + {120, 0, 0,0x47,0x00}, + {121, 0, 0,0x47,0x00}, + {122, 0, 0,0x49,0x00}, + {123, 0, 0,0xC8,0x00}, + {124, 0, 0,0xC9,0x00}, + {125, 0, 0,0x49,0x00}, + {126, 0, 0,0x45,0x00}, + {127, 2, 0,0x45,0x00}, + {128, 2, 0,0xC5,0x00}, + {129, 0, 0,0xC3,0x00}, + {255, 0, 0,0x00,0x00}, + {133, 0, 0,0x43,0x00}, + {134, 0, 0,0xC4,0x00}, + {135, 0, 0,0xC5,0x00}, + {136, 0, 0,0x48,0x00}, + {137, 0, 0,0x6C,0x00}, + {138, 0, 0,0xEF,0x00}, + {139, 0, 0,0x63,0x00}, + {140, 0, 0,0xC3,0x00}, + {141, 0, 0,0x43,0x00}, + {142, 0, 0,0x43,0x00}, + {143, 0, 0,0x44,0x00}, + {144, 0, 0,0x44,0x00}, + {172, 0, 1,0xC1,0x00}, + {173, 0, 1,0xC7,0x00}, + {145, 0, -12,0x01,0x00}, + {146, 0, -21,0x00,0x00}, + {147, 1, -26,0x80,0x00}, + {148, 4, -32,0x80,0x00}, + {149, 6, -36,0x81,0x00}, + {150, 7, -41,0x82,0x00}, + {151, 2, 17,0x42,0x00}, + {152, 4, 9,0xC4,0x00}, + {153, 4, 5,0xC9,0x00}, + {154, 4, 4,0xC8,0x00}, + {155, 5, 0,0x69,0x00}, + {156, 5, 0,0xE9,0x00}, + {157, 5, 0,0xE8,0x00}, + {158, 5, 0,0x69,0x00}, + {159, 5, 0,0x69,0x00}, + {184, 0, 2,0x80,0x10}, + {174, 0, 2,0x80,0x1A}, + {175, 3, 2,0x00,0x12}, + {176, 7, 2,0xC4,0x16}, + {177, 10, 2,0x00,0x15}, + {178, 7, 2,0x80,0x17}, + {179, 4, 2,0x80,0x19}, + {180, 0, 2,0xCE,0x18}, + {181, 0, 2,0xCD,0x0F}, + {182, 3, 2,0x00,0x14}, + {183, 3, 2,0x00,0x1F}, + {184, 0, 2,0x80,0x10}, + {185, 0, 2,0x80,0x11}, + {186, 0, 2,0x00,0x20}, + {187, 0, 2,0x80,0x21}, + {188, 2, 2,0xC3,0x22}, + { 14, 0, 0,0x43,0x00}, + {189, 7, 2,0x80,0x13}, + {190, 1, 2,0x80,0x0E}, + {191, 0, 2,0x80,0x1B}, + {181, 0, 2,0xCD,0x0F}, + {181, 0, 2,0xCD,0x0F}, + {112, 0, 0,0xC6,0x2B}, + {113, 0, 0,0x46,0x2C}, + {114, 0, 0,0xC5,0x2D}, + {115, 0, 0,0x45,0x2E}, + {114, 0, 0,0xC5,0x00}, + { 78, 0, 3,0x8A,0x00}, + { 77, 4, 3,0x87,0x00}, + {211, 0, 1,0x44,0x00}, + {212, 0, 1,0x44,0x00}, + {213, 0, 1,0x44,0x00}, + {214, 0, 1,0x47,0x00}, + {215, 0, 7,0x4B,0x00}, + {255, 0, 0,0x00,0x00}, + { 79, 4, 7,0x49,0x00}, + {130, 0, 0,0x44,0x00}, + {131, 0, 0,0x44,0x00}, + {132, 0, 2,0x44,0x00}, + {255, 0, 0,0x00,0x00}, + {255, 0, 0,0x00,0x00}, + {192, 0, 0,0x00,0x00}, + {193, 0, 1,0x00,0x00}, + {194, 0, 0,0x80,0x00}, + {195, 0, 0,0x00,0x00}, + {196, -1, 0,0x00,0x00}, + {197, -1, 0,0x00,0x00}, + {198, -1, 0,0x00,0x00}, + {199, -4, 0,0x00,0x00}, + {200, -4, 0,0x80,0x00}, + {201, -4, 0,0x00,0x00}, + {202, -4, 0,0x00,0x00}, + {203, -4, 0,0x00,0x00}, + {204, -4, 0,0x00,0x00}, + {205, -5, 0,0x00,0x00}, + {206, -5, 0,0x00,0x00}, + {255, 0, 0,0x00,0x00}, + {207, 0, 1,0x46,0x00}, + {208, 0, 1,0xC6,0x00}, + {209, 0, 1,0xC8,0x00}, + {210, 0, 1,0x4A,0x00}, + {255, 0, 0,0x00,0x00}, + {255, 0, 0,0x00,0x00}, + {255, 0, 0,0x00,0x00}, + {255, 0, 0,0x00,0x00}, + {255, 0, 0,0x00,0x00}, + {255, 0, 0,0x00,0x00}, + { 52, 0, 0,0x80,0x00}, + { 53, 0, 0,0x00,0x00}, + { 54, 0, 0,0x00,0x00}, + { 55, 0, 0,0x00,0x00}, + { 56, 0, 0,0x80,0x00}, + { 57, 0, 0,0x00,0x00}, + { 58, 0, 0,0x00,0x00}, + { 59, 0, 0,0x00,0x00}, + { 60, 0, 0,0x80,0x00}, + { 61, 0, 0,0x00,0x00}, + { 62, 0, 0,0x80,0x00}, + { 63, 0, 0,0x00,0x00}, + {160, 1, 1,0xC3,0x23}, + {161, 0, 1,0x49,0x24}, + {162, 0, 1,0xC3,0x25}, + {163, 0, 1,0x49,0x26}, + {164, 0, 1,0xC3,0x27}, + {165, 1, 1,0x49,0x28}, + {166, 1, 1,0x43,0x29}, + {167, 1, 1,0xC9,0x2A}, + {168, 4, 1,0xC6,0x00}, + {169, 3, 1,0xCA,0x00}, + {170, 1, 1,0x43,0x00}, + {171, 1, 1,0xC8,0x00}, +}; + +#define KID_SEQTBL_LEN 2310 +static const uint8_t kid_seqtbl[KID_SEQTBL_LEN] = { + 0xF9,0x01,0xFF,0x81,0x19,0xF9,0x01,0x01,0x02,0x03,0x04,0xFB, + 0x08,0x05,0xFB,0x03,0x06,0xFB,0x03,0x07,0xFB,0x05,0x08,0xFB, + 0x01,0xF2,0x01,0x09,0xFB,0x02,0x0A,0xFB,0x04,0x0B,0xFB,0x05, + 0x0C,0xFB,0x02,0xF2,0x01,0x0D,0xFB,0x03,0x0E,0xFB,0x04,0xFF, + 0x81,0x19,0xF9,0x00,0x0F,0xFF,0xA0,0x19,0xF9,0x01,0xA6,0xFF, + 0xA8,0x19,0xF9,0x05,0xFB,0x0A,0xB1,0xB1,0xFB,0xF9,0xFA,0xFE, + 0xB2,0xFB,0x05,0xFA,0x02,0xA6,0xFB,0xFF,0xFF,0xD2,0x19,0xFF, + 0xD2,0x19,0xF9,0x01,0xFB,0x02,0xCF,0xD0,0xFB,0x02,0xD1,0xFB, + 0x02,0xD2,0xFB,0x03,0xF9,0x01,0xF2,0x00,0x9E,0xAA,0xAB,0xFF, + 0xD8,0x19,0xF9,0x05,0xF8,0xFF,0x00,0xAC,0xFB,0xFF,0xFA,0x01, + 0xAD,0xFB,0xFF,0xAE,0xFB,0xFF,0xFA,0x02,0xFB,0xFE,0xFA,0x01, + 0xFB,0xFB,0xFA,0xFC,0xFF,0x4D,0x1A,0xF9,0x01,0xF8,0x01,0x00, + 0x9B,0xFB,0x02,0xA5,0xFB,0xFE,0xFF,0xD2,0x19,0xF9,0x01,0xF8, + 0xFF,0x00,0x9B,0x9C,0x9D,0x9E,0xFF,0x2E,0x1A,0xF9,0x01,0xF8, + 0x01,0x00,0xFB,0x02,0xA3,0xFB,0x04,0xA4,0xA5,0xFF,0xD2,0x19, + 0xF9,0x01,0xF8,0x01,0x00,0xFB,0x06,0xA4,0xA5,0xFF,0xD2,0x19, + 0xF9,0x01,0xF8,0xFF,0x00,0xFB,0xFD,0xA0,0xFB,0xFE,0x9D,0xFF, + 0xD2,0x19,0xF9,0x01,0xF8,0xFF,0x00,0xA8,0xF9,0x01,0x97,0xF9, + 0x01,0x98,0x99,0x9A,0xF9,0x05,0x9B,0xF9,0x01,0x9C,0x9D,0xFF, + 0xD2,0x19,0xF9,0x01,0xA7,0xFF,0x4A,0x1A,0xA2,0xFF,0x45,0x1A, + 0xA9,0x96,0xFF,0xD2,0x19,0x9F,0xA0,0xFF,0x5F,0x1A,0xF9,0x01, + 0xF4,0xFF,0xD2,0x19,0xF9,0x05,0xFB,0xF8,0xFF,0xD2,0x19,0xF9, + 0x05,0xA0,0x9D,0xFF,0xD2,0x19,0xF9,0x07,0xFB,0xF8,0xFF,0x39, + 0x1B,0xF9,0x05,0xFE,0xFB,0x05,0xFF,0x2E,0x1A,0xF9,0x05,0xFE, + 0xFB,0x12,0xFF,0xA6,0x19,0xF9,0x01,0x10,0x11,0xFB,0x02,0x12, + 0xFB,0x02,0x13,0xFB,0x02,0x14,0xFB,0x02,0x15,0xFB,0x02,0x16, + 0xFB,0x07,0x17,0xFB,0x09,0x18,0xFB,0x05,0xFA,0xFA,0x19,0xFB, + 0x01,0xFA,0x06,0x1A,0xFB,0x04,0xF4,0xF2,0x01,0x1B,0xFB,0xFD, + 0x1C,0xFB,0x05,0x1D,0xF2,0x01,0x1E,0x1F,0x20,0x21,0xFB,0x01, + 0xFF,0xA0,0x19,0xF9,0x01,0xF2,0x01,0x22,0xFB,0x05,0x23,0xFB, + 0x06,0x24,0xFB,0x03,0x25,0xFB,0x05,0xF2,0x01,0x26,0xFB,0x07, + 0x27,0xFB,0x0C,0xFA,0xFD,0x28,0xFB,0x08,0xFA,0xF7,0x29,0xFB, + 0x08,0xFA,0xFE,0x2A,0xFB,0x04,0xFA,0x0B,0x2B,0xFB,0x04,0xFA, + 0x03,0x2C,0xFB,0x05,0xF4,0xF2,0x01,0xFF,0x81,0x19,0xF9,0x01, + 0xFB,0x01,0x6B,0xFB,0x02,0xFB,0x02,0x6C,0xFB,0x02,0x6D,0xFB, + 0x02,0x6D,0xFB,0x02,0x6D,0xFF,0x16,0x1B,0x00,0xF9,0x01,0xFB, + 0x01,0x6E,0x6F,0xFB,0x02,0x70,0xFB,0x02,0x6C,0xFB,0x02,0x6D, + 0xFF,0x29,0x1B,0xF9,0x07,0xFE,0xFB,0x06,0x2D,0xFB,0x01,0x2E, + 0xFF,0xC4,0x19,0xF9,0x07,0xFE,0xFB,0x06,0x2D,0xFB,0x01,0x2E, + 0xFB,0x02,0x2F,0xFB,0xFF,0x30,0xFB,0x01,0x31,0xFB,0xFE,0x32, + 0x33,0x34,0xFF,0xA0,0x19,0xF9,0x01,0xFB,0xFF,0xFF,0x75,0x19, + 0xF9,0x01,0xFB,0x01,0x35,0xFB,0x01,0xF2,0x01,0x36,0xFB,0x08, + 0x37,0xF2,0x01,0x38,0xFB,0x07,0x39,0xFB,0x03,0x3A,0xFB,0x01, + 0x3B,0x3C,0xFB,0x02,0x3D,0xFB,0xFF,0x3E,0x3F,0x40,0xFB,0xFF, + 0x41,0xFB,0xF2,0xFE,0xFF,0x95,0x19,0xF9,0x03,0xFA,0xFF,0x66, + 0xFB,0xFE,0xFA,0x06,0x67,0xFB,0xFE,0xFA,0x09,0x68,0xFB,0xFF, + 0xFA,0x0C,0x69,0xFB,0xFD,0xF8,0x00,0x0F,0xFF,0x49,0x1D,0xF9, + 0x03,0xFA,0xFF,0xFB,0xFE,0x66,0xFB,0xFD,0xFA,0x06,0x67,0xFB, + 0xFD,0xFA,0x09,0x68,0xFB,0xFE,0xFA,0x0C,0x69,0xFB,0xFD,0xF8, + 0x00,0x0F,0xFF,0x49,0x1D,0xF9,0x03,0xFB,0x01,0xFA,0xFF,0x66, + 0xFB,0x02,0xFA,0x06,0x67,0xFB,0xFF,0xFA,0x09,0x68,0xFA,0x0C, + 0x69,0xFB,0xFE,0xF8,0x01,0x0F,0xFF,0x49,0x1D,0xF9,0x03,0xFB, + 0x01,0xFA,0x03,0xF7,0x06,0x1C,0x66,0xFB,0x02,0xFA,0x06,0x67, + 0xFB,0xFF,0xFA,0x09,0x68,0xFA,0x0C,0x69,0xFB,0xFE,0xF8,0x01, + 0x0F,0xFF,0x49,0x1D,0xFB,0xFF,0xFA,0xFD,0xFF,0xE4,0x1B,0xFB, + 0x01,0xFF,0xDB,0x1B,0x66,0xFB,0x02,0xFA,0x03,0x67,0xFB,0xFF, + 0xFA,0x04,0x68,0xFA,0x05,0x69,0xFB,0xFE,0xF8,0x01,0x06,0xFF, + 0x49,0x1D,0xF9,0x03,0xFB,0x01,0xFA,0x03,0x66,0xFB,0x02,0xFA, + 0x06,0x67,0xFB,0x01,0xFA,0x09,0x68,0xFB,0x02,0xFA,0x0C,0x69, + 0xF8,0x02,0x0F,0xFF,0x49,0x1D,0xF9,0x03,0xFB,0x01,0xFA,0x03, + 0x66,0xFB,0x03,0xFA,0x06,0x67,0xFB,0x02,0xFA,0x09,0x68,0xFB, + 0x03,0xFA,0x0C,0x69,0xF8,0x03,0x0F,0xFF,0x49,0x1D,0xF9,0x01, + 0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4A,0x4B,0x4C,0x4D,0xF9, + 0x02,0x4E,0x4F,0x50,0xFF,0xA1,0x1C,0xF9,0x01,0x43,0x44,0x45, + 0x46,0x47,0x48,0x49,0x4A,0x4B,0x4C,0x4D,0xF9,0x02,0xFB,0x01, + 0x4E,0xFB,0x02,0x4F,0xFB,0x01,0x50,0xFF,0xA1,0x1C,0xF9,0x01, + 0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4A,0x4B,0x4C,0xFB,0xFF, + 0x4D,0xF9,0x02,0xFB,0xFE,0x4E,0xFB,0xFF,0x4F,0xFB,0xFF,0x50, + 0xFF,0xA1,0x1C,0xF9,0x02,0x5B,0x5A,0x59,0x58,0x57,0x57,0x57, + 0x58,0x59,0x5A,0x5B,0x5C,0x5D,0x5E,0x5F,0x60,0x61,0x62,0x63, + 0x61,0x60,0x5F,0x5E,0x5D,0x5C,0x5B,0x5A,0x59,0x58,0x57,0x58, + 0x59,0x5A,0x5B,0x5C,0x5D,0x5E,0x5F,0x60,0x5F,0x5E,0x5D,0x5C, + 0xFF,0x25,0x1D,0xF9,0x06,0x5C,0x5D,0x5D,0x5C,0x5C,0x5B,0xFF, + 0xD8,0x1C,0x87,0x88,0x89,0x89,0x8A,0x8A,0x8A,0x8A,0x89,0x88, + 0x87,0xFB,0xF9,0xFF,0x25,0x1D,0xF9,0x01,0x94,0x91,0x90,0x8F, + 0x8E,0x8D,0xFB,0xFB,0xFA,0x3F,0xFC,0xF9,0x03,0x8C,0x8A,0x88, + 0x5B,0xF9,0x02,0xFF,0xA4,0x1C,0xF9,0x01,0x87,0x88,0x89,0x8A, + 0x8B,0x8C,0xFB,0x05,0xFA,0xC1,0xFD,0x8D,0x8E,0x8F,0x90,0x91, + 0x92,0x93,0x94,0xF9,0x05,0x95,0xF9,0x01,0x76,0x77,0xFB,0x01, + 0xFF,0xA0,0x19,0x51,0x52,0xF9,0x05,0x53,0xF9,0x01,0xF4,0xF2, + 0x00,0x54,0x55,0xFB,0x03,0xFF,0xA0,0x19,0xF9,0x03,0x51,0xFA, + 0x06,0x51,0xFA,0x09,0x51,0xFA,0x0C,0xFB,0x02,0xF8,0x00,0x0C, + 0xFF,0x49,0x1D,0xF9,0x04,0x6A,0xFF,0x4B,0x1D,0xF9,0x01,0x35, + 0xFB,0x02,0xF2,0x01,0x36,0xFB,0x07,0x37,0xF2,0x01,0x38,0xFB, + 0x02,0x31,0xFB,0xFE,0x32,0x33,0x34,0xFF,0xA0,0x19,0xF9,0x01, + 0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4A,0x4B,0x4C,0x4D,0x4E, + 0xF9,0x00,0xF5,0x4F,0xFF,0x25,0x1D,0xF9,0x01,0x43,0x44,0x45, + 0x46,0x47,0x48,0x49,0x4A,0x4B,0x4C,0x4D,0x4E,0x4F,0xFA,0xFC, + 0x4F,0xFA,0xFE,0x4F,0x4F,0xFA,0x02,0x4F,0xFA,0x04,0xFF,0x25, + 0x1D,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4A,0x4B,0x4C,0xFA, + 0xFF,0x4D,0xFA,0xFD,0x4E,0xFA,0xFC,0x4F,0xFA,0xF6,0x4F,0xFA, + 0xF7,0x4F,0xFA,0xF8,0x4F,0xFA,0xF9,0x4F,0xFA,0xFA,0x4F,0xFA, + 0xFB,0x4F,0xFA,0xFC,0x4F,0xFA,0xFD,0x4F,0xFA,0xFE,0x4F,0xFA, + 0xFE,0x4F,0xFA,0xFF,0x4F,0xFA,0xFF,0x4F,0xFA,0xFF,0x4F,0x4F, + 0x4F,0x4F,0xFA,0x01,0x4F,0xFA,0x01,0x4F,0xFA,0x02,0x4F,0xFA, + 0x02,0x4F,0xFA,0x03,0x4F,0xFA,0x04,0x4F,0xFA,0x05,0x4F,0xFA, + 0x06,0x4F,0xF8,0x00,0x06,0xFF,0x49,0x1D,0xF9,0x03,0x50,0xFF, + 0xA1,0x1C,0xF9,0x05,0xFB,0xFC,0x32,0x33,0x34,0xFF,0xA0,0x19, + 0xF9,0x05,0xFB,0x01,0xFA,0x03,0xF7,0x25,0x1E,0x66,0xFB,0x02, + 0xFA,0x06,0x67,0xFB,0xFF,0xFA,0x09,0x68,0xFA,0x0C,0x69,0xFB, + 0xFE,0xF8,0x00,0x0F,0xFF,0x49,0x1D,0x66,0xFB,0x02,0xFA,0x03, + 0x67,0xFB,0xFF,0xFA,0x04,0x68,0xFA,0x05,0x69,0xFB,0xFE,0xF8, + 0x00,0x06,0xFF,0x49,0x1D,0xF9,0x05,0xFB,0xFF,0xFA,0xFC,0x66, + 0xFB,0xFF,0xFA,0x03,0xFB,0xFD,0xFA,0x01,0xF4,0xFB,0x01,0xF2, + 0x01,0x6B,0xFB,0x02,0x6C,0xF2,0x01,0x6D,0xFF,0xB3,0x1F,0x79, + 0xFB,0x01,0x7A,0x7B,0xFB,0x02,0x7C,0xFB,0x04,0x7D,0xFB,0x03, + 0x7E,0xFB,0xFC,0x56,0xF2,0x01,0xF4,0xFB,0xFC,0x74,0xFB,0xFE, + 0x75,0x76,0x77,0xFF,0xA0,0x19,0xFB,0xFB,0xFF,0xA0,0x19,0xF9, + 0x01,0x79,0xFB,0x01,0x7A,0xFB,0x01,0x7B,0xFB,0x03,0x7C,0xFB, + 0x04,0x7D,0xFB,0x03,0x7E,0xFB,0xFF,0xFB,0x03,0x7F,0x80,0x81, + 0x82,0x83,0x84,0xFF,0xA0,0x19,0xF9,0x01,0x79,0xFB,0x01,0x7A, + 0xFB,0x01,0x7B,0xFB,0x03,0x7C,0xFB,0x04,0x7D,0xFB,0x03,0x7E, + 0xFB,0xFF,0xFB,0x02,0x7F,0x80,0x81,0x82,0x83,0x84,0xFF,0xA0, + 0x19,0xF9,0x01,0x79,0xFB,0x01,0x7A,0xFB,0x01,0x7B,0xFB,0x03, + 0x7C,0xFB,0x04,0x7D,0xFB,0x03,0x7E,0xFB,0xFF,0xFB,0x01,0x7F, + 0x80,0x81,0x82,0x83,0x84,0xFF,0xA0,0x19,0xF9,0x01,0x79,0xFB, + 0x01,0x7A,0xFB,0x01,0x7B,0xFB,0x03,0x7C,0xFB,0x04,0x7D,0xFB, + 0x03,0x7E,0xFB,0xFF,0x7F,0x80,0x81,0x82,0x83,0x84,0xFF,0xA0, + 0x19,0xF9,0x01,0x79,0xFB,0x01,0x7A,0xFB,0x01,0x7B,0xFB,0x03, + 0x7C,0xFB,0x04,0x7D,0xFB,0x03,0x7E,0xFB,0xFE,0x80,0x81,0x82, + 0x83,0x84,0xFF,0xA0,0x19,0xF9,0x01,0x79,0xFF,0xFC,0x1E,0xF9, + 0x01,0x79,0xFB,0x01,0x7A,0xFB,0x01,0x7B,0xFB,0x03,0x7C,0xFB, + 0x04,0x7D,0xFB,0xFF,0x7F,0x80,0x81,0x82,0x83,0x84,0xFF,0xA0, + 0x19,0xF9,0x01,0x79,0xFB,0x01,0x7A,0xFB,0x01,0x7B,0xFB,0x03, + 0x7C,0xFB,0x02,0x81,0x82,0x83,0x84,0xFF,0xA0,0x19,0xF9,0x01, + 0x79,0xFB,0x01,0x7A,0xFB,0x01,0x7B,0xFB,0x02,0x7C,0xFB,0x02, + 0x81,0x82,0x83,0x84,0xFF,0xA0,0x19,0xF9,0x01,0x79,0xFB,0x01, + 0x7A,0xFB,0x01,0x7B,0xFB,0x02,0x7C,0xFB,0x01,0x81,0x82,0x83, + 0x84,0xFF,0xA0,0x19,0xF9,0x01,0x79,0xFB,0x01,0x7A,0xFB,0x01, + 0x7B,0xFB,0x02,0x83,0x84,0xFF,0xA0,0x19,0xF9,0x01,0x79,0xFB, + 0x01,0x7A,0xFB,0x01,0x7B,0xFB,0x01,0x83,0x84,0xFF,0xA0,0x19, + 0xF9,0x01,0x79,0xFB,0x01,0x7A,0xFB,0x01,0x84,0xFF,0xA0,0x19, + 0xF9,0x01,0x79,0xFB,0x01,0x84,0xFF,0xA0,0x19,0xF9,0x01,0xFB, + 0x01,0x6B,0xFB,0x02,0x6C,0x6D,0xFF,0xAF,0x1F,0xF9,0x05,0xFB, + 0x01,0x6E,0x6F,0xFB,0x02,0x70,0x71,0xFB,0x01,0x72,0x73,0x74, + 0xFB,0xFC,0x75,0x76,0x77,0xFF,0xA0,0x19,0xF9,0x01,0xF3,0x01, + 0xE5,0xE5,0xE5,0xE5,0xE5,0xE5,0xE6,0xE7,0xE8,0xFF,0xDA,0x1F, + 0xF9,0x01,0xFB,0xFB,0xE9,0xEA,0xEB,0xEC,0xED,0xEE,0xEF,0xF0, + 0x85,0x85,0x86,0x86,0x86,0x30,0xFB,0x01,0x31,0xFB,0xFE,0xF9, + 0x05,0x32,0xF9,0x01,0x33,0x34,0xFF,0xA0,0x19,0xF9,0x01,0xFB, + 0xFB,0xEA,0xEC,0xEE,0xF0,0x86,0xFB,0xFF,0xFF,0xA0,0x19,0xF9, + 0x01,0xFB,0x04,0xBF,0xC0,0xC1,0xC2,0xC3,0xC4,0xC5,0xF2,0x03, + 0xC6,0xC7,0xC8,0xC9,0xCA,0xCB,0xCC,0xCD,0xCD,0xCD,0xF3,0x01, + 0xCD,0xCD,0xC9,0xC6,0xFB,0xFC,0xFF,0xA0,0x19,0xF9,0x05,0xF4, + 0xFB,0x01,0x6B,0xFB,0x02,0x6C,0xF9,0x01,0x6D,0xFF,0x36,0x20, + 0xF9,0x01,0xFA,0xFE,0xFB,0x01,0x6B,0xFB,0x02,0x6C,0x6D,0xFB, + 0x01,0x6E,0x6F,0xFB,0x02,0x70,0x71,0xFB,0x01,0xFA,0x01,0x72, + 0xFA,0x01,0x73,0xFB,0xFE,0xFF,0x78,0x19,0xF9,0x05,0xF4,0xFA, + 0xFE,0xFB,0x01,0xFB,0x02,0x6C,0x6D,0x6D,0x6D,0x6D,0x6D,0x6D, + 0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,0x6D, + 0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,0x6D,0xFB, + 0x01,0x6E,0x6E,0x6E,0x6F,0xFB,0x02,0x70,0x71,0xFB,0x01,0xFA, + 0x01,0x72,0xFA,0x01,0x73,0x74,0xFB,0xFC,0x75,0x76,0x77,0xFF, + 0xA0,0x19,0xF9,0x05,0xF4,0xFA,0xFE,0xFB,0x03,0xB9,0xF6,0xB9, + 0xFF,0xA5,0x20,0xF9,0x05,0xFF,0xAE,0x20,0xF9,0x01,0xF6,0xB3, + 0xB4,0xB5,0xB6,0xFB,0x01,0xB7,0xFB,0xFC,0xB9,0xFF,0xBA,0x20, + 0xF9,0x01,0xF4,0xFB,0x04,0xB1,0xF6,0xB1,0xFF,0xC5,0x20,0xF9, + 0x01,0xB2,0xF6,0xB2,0xFF,0xCD,0x20,0xFF,0x5A,0x20,0xF8,0x00, + 0x00,0xF9,0x04,0xB9,0xFF,0xD9,0x20,0xF9,0x05,0xFB,0xFB,0xFA, + 0xFF,0xF2,0x01,0xD9,0xDA,0xDB,0xFB,0x01,0xDC,0xFB,0xFC,0xFA, + 0xFD,0xF2,0x01,0xDD,0xFB,0xFC,0xFA,0xFE,0xDE,0xFB,0xFE,0xFA, + 0xFD,0xDF,0xFB,0xFD,0xFA,0xF8,0xF2,0x04,0xF2,0x01,0xE0,0xFB, + 0xFF,0xFA,0xFF,0xE1,0xFB,0xFD,0xFA,0xFC,0xE2,0xFB,0xFF,0xFA, + 0xFB,0xF2,0x01,0xE3,0xFB,0xFE,0xFA,0xFF,0xE4,0x00,0xF2,0x01, + 0x00,0x00,0x00,0xF2,0x01,0x00,0x00,0x00,0xF2,0x01,0x00,0x00, + 0x00,0xF2,0x01,0xF1,0x00,0xFF,0x2E,0x21,0x36,0xFF,0x32,0x21, + 0x55,0x43,0x43,0x43,0x43,0x43,0x43,0x43,0x43,0x43,0x43,0x44, + 0x45,0x46,0x47,0x48,0x49,0x4A,0x4B,0x53,0x54,0x4C,0xFF,0x4B, + 0x21,0xFB,0x01,0x30,0xFB,0x02,0x31,0xFB,0x06,0x32,0xFB,0x01, + 0x33,0xFB,0xFF,0x34,0xFB,0x01,0x35,0xFB,0x01,0xFF,0x51,0x21, + 0xFB,0x01,0x37,0x38,0xFF,0x32,0x21,0x4D,0x4E,0x4F,0x50,0x51, + 0x52,0xFB,0x01,0x36,0x36,0x36,0x36,0x36,0x36,0x39,0x3A,0x3B, + 0x3C,0x3D,0xFB,0x02,0x3E,0xFB,0xFF,0x3F,0xFB,0xFD,0x40,0x41, + 0xFB,0xFF,0x42,0xFE,0xFB,0x10,0xFB,0x03,0xFF,0x54,0x21,0x0B, + 0xFF,0x95,0x21,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0xFE, + 0xFB,0x08,0x0B,0xFF,0x95,0x21,0xFE,0xFB,0x0B,0x0C,0xFB,0x01, + 0x0D,0xFB,0x01,0x0E,0xFB,0x03,0x0F,0xFB,0x01,0x10,0x11,0xFF, + 0xB8,0x21,0x13,0xFF,0xBC,0x21,0x14,0xFF,0xC0,0x21,0x15,0xFB, + 0x01,0x16,0x17,0x18,0xFB,0x01,0x19,0xFB,0xFD,0x1A,0xFB,0xFE, + 0x1B,0xFB,0xFC,0x1C,0xFB,0xFD,0x1D,0xFB,0xFE,0x1E,0xFB,0xFD, + 0x1F,0xFB,0xFF,0x20,0x21,0xFF,0xE2,0x21,0x25,0xFF,0xE6,0x21, + 0x25,0x26,0x27,0x28,0x29,0x2A,0x2B,0x2C,0x2D,0x2E,0x2F,0xFE, + 0xFB,0x0C,0x0B,0xFF,0xF8,0x21,0x0B,0x0B,0xFE,0xFB,0x0D,0x2F, + 0x2E,0x2D,0x2C,0x2B,0x2A,0x29,0x28,0x27,0x26,0x25,0x24,0x24, + 0x24,0x23,0x23,0x23,0x22,0x22,0x22,0x22,0x22,0x22,0x22,0x23, + 0x23,0x24,0x24,0x24,0x23,0x23,0x23,0x22,0x22,0x22,0x22,0x22, + 0x22,0x22,0x23,0x23,0x24,0x24,0x24,0x23,0x23,0x23,0x22,0x22, + 0x22,0x22,0x22,0x22,0x22,0x22,0x22,0x23,0x23,0x23,0x24,0xFF, + 0x3C,0x22,0x01,0x12,0xFF,0x41,0x22,0xF9,0x01,0xBA,0xFB,0x05, + 0xBA,0xFB,0x03,0xBB,0xFB,0x04,0xFF,0x47,0x22,0xBA,0xFF,0x53, + 0x22,0xBC,0xFF,0x57,0x22,0xF9,0x00,0xBA,0xBA,0xBA,0xBC,0xBC, + 0xBC,0xBC,0xBC,0xBC,0xBC,0xBC,0xFE,0xFB,0x08,0xFF,0x47,0x22, + 0xBA,0xBA,0xBC,0xFF,0x70,0x22, +}; + +#define KID_NSEQ 115 +static const uint16_t kid_seq_off[KID_NSEQ] = { + 0x0000,0x1973,0x19A0,0x1A93,0x1ACD,0x1B39,0x1B5A,0x1BDB, + 0x1C54,0x1CA1,0x1D04,0x1D25,0x1D49,0x1D4F,0x1D68,0x1DF6, + 0x1C84,0x202B,0x1C1C,0x1C01,0x205A,0x1C38,0x209C,0x1D36, + 0x1C69,0x1CD1,0x1B04,0x1B1A,0x1D7D,0x1F9E,0x1F92,0x1F82, + 0x1F72,0x1F5D,0x1F48,0x1F33,0x1F19,0x1F13,0x1EF7,0x1EDA, + 0x1EBB,0x1E9C,0x1E7D,0x1B53,0x1E59,0x1E06,0x1E3B,0x1DFC, + 0x1D9B,0x1FB3,0x1FA7,0x20BE,0x20D1,0x20D4,0x20C9,0x19C4, + 0x1A13,0x1A2E,0x1A3C,0x1A7C,0x1A83,0x1A63,0x1A5E,0x1A68, + 0x1A6E,0x1A75,0x1A5A,0x19F9,0x1CEC,0x1A54,0x20DD,0x20AE, + 0x1E78,0x1CDC,0x19DC,0x1A42,0x1A07,0x19A8,0x2009,0x1B1B, + 0x1A8B,0x1B85,0x1BA1,0x1BBF,0x196E,0x20A9,0x1A22,0x19A6, + 0x19AC,0x1B2D,0x19C1,0x1FCA,0x1FDA,0x1FFB,0x2195,0x2132, + 0x214F,0x2166,0x2199,0x21A8,0x216D,0x226E,0x2136,0x21BC, + 0x1BFA,0x2245,0x2253,0x225B,0x21C4,0x21C0,0x21E6,0x21EA, + 0x21FC,0x2240,0x2257, +}; + +#endif diff --git a/applications/PoP/roomtest/pop_bg.c b/applications/PoP/roomtest/pop_bg.c new file mode 100644 index 0000000..91cd722 --- /dev/null +++ b/applications/PoP/roomtest/pop_bg.c @@ -0,0 +1,394 @@ +/* + * pop_bg.c — статический фон комнаты PoP композицией слоёв (порт + * applications/PoP/toolchain/render_room.py -> seg008.c draw_tile/ + * wall_pattern). Подробности алгоритма — в render_room.py и + * memory/pop_background_strategy. + * + * Только DUNGEON-стиль (Фаза 1). Анимация (пламя факелов, шипы, дверь- + * плита, сама решётка ворот) НЕ рисуется — отдельными sprite_t поверх + * (Фаза 2). Решётка ворот из ЛЕВОЙ комнаты рисуется статичным кадром. + */ +#include +#include +#include +#include +#include +#include "pop_bg.h" + +/* ---- Атласы (прямая адресация) ------------------------------------- */ +static atlas_t env[5]; +static atlas_t wall_at; +static atlas_t fore_at; + +static const char *const ENV_ATL[5] = { + "pop_env0.atl", "pop_env1.atl", "pop_env2.atl", + "pop_env3.atl", "pop_env4.atl" +}; + +int pop_bg_load(void) +{ + int i; + for (i = 0; i < 5; i++) + if (atlas_load(&env[i], ENV_ATL[i]) != 0) return -1; + if (atlas_load(&wall_at, "pop_wall.atl") != 0) return -1; + if (atlas_load(&fore_at, "pop_fore.atl") != 0) return -1; + return 0; +} + +void pop_bg_free(void) +{ + int i; + for (i = 0; i < 5; i++) + atlas_free(&env[i]); + atlas_free(&wall_at); + atlas_free(&fore_at); +} + +/* Блит ленты idx атласа a: НИЗ спрайта на ybottom, левый край x, + * прозрачно (0xFF). w/h читаются из getimage-заголовка ленты (в W0). */ +static void blit_b(atlas_t *a, uint8_t idx, int x, int ybottom) +{ + const uint8_t *img; + uint16_t w, h; + if (idx >= a->count) + return; + img = (const uint8_t *)atlas_image(a, idx); + gfx_w0_map(a->page); + w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8)); + h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8)); + if (w && h) + gfx_blit(x, ybottom - (int)h + 1, img); + gfx_w0_unmap(); +} + +static void env_b(uint8_t id, int x, int yb) { blit_b(&env[id >> 5], (uint8_t)(id & 31), x, yb); } +static void wall_b(uint8_t id, int x, int yb) { blit_b(&wall_at, id, x, yb); } +static void fore_b(uint8_t id, int x, int yb) { blit_b(&fore_at, id, x, yb); } + +/* ---- Таблица тайлов (piece, seg008.c:27) --------------------------- */ +typedef struct { + uint8_t base_id, floor_left; int8_t base_y; + uint8_t right_id, floor_right; int8_t right_y; + uint8_t stripe_id, topright_id, bottom_id, fore_id, fore_x; int8_t fore_y; +} piece; + +static const piece tile_table[31] = { + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, /* 00 empty */ + { 41, 1, 0, 42, 1, 2, 145, 0, 43, 0, 0, 0}, /* 01 floor */ + {127, 1, 0, 133, 1, 2, 145, 0, 43, 0, 0, 0}, /* 02 spike */ + { 92, 1, 0, 93, 1, 2, 0, 94, 43, 95, 1, 0}, /* 03 pillar */ + { 46, 1, 0, 47, 1, 2, 0, 48, 43, 49, 3, 0}, /* 04 gate */ + { 41, 1, 1, 35, 1, 3, 145, 0, 36, 0, 0, 0}, /* 05 stuck floor */ + { 41, 1, 0, 42, 1, 2, 145, 0, 96, 0, 0, 0}, /* 06 close button */ + { 46, 1, 0, 0, 0, 2, 0, 0, 43, 49, 3, 0}, /* 07 door top w/floor */ + { 86, 1, 0, 87, 1, 2, 0, 0, 43, 88, 1, 0}, /* 08 big pillar bottom */ + { 0, 0, 0, 89, 0, 3, 0, 90, 0, 91, 1, 3}, /* 09 big pillar top */ + { 41, 1, 0, 42, 1, 2, 145, 0, 43, 12, 2, -3}, /* 0A potion */ + { 0, 1, 0, 0, 0, 0, 145, 0, 0, 0, 0, 0}, /* 0B loose floor */ + { 0, 0, 0, 0, 0, 2, 0, 0, 85, 49, 3, 0}, /* 0C door top */ + { 75, 1, 0, 42, 1, 2, 0, 0, 43, 77, 0, 0}, /* 0D mirror */ + { 97, 1, 0, 98, 1, 2, 145, 0, 43,100, 0, 0}, /* 0E debris */ + {147, 1, 0, 42, 1, 1, 145, 0,149, 0, 0, 0}, /* 0F open button */ + { 41, 1, 0, 37, 0, 0, 0, 38, 43, 0, 0, 0}, /* 10 leveldoor left */ + { 0, 0, 0, 39, 1, 2, 0, 40, 43, 0, 0, 0}, /* 11 leveldoor right */ + { 0, 0, 0, 42, 1, 2, 145, 0, 43, 0, 0, 0}, /* 12 chomper */ + { 41, 1, 0, 42, 1, 2, 0, 0, 43, 0, 0, 0}, /* 13 torch */ + { 0, 0, 0, 1, 1, 2, 0, 2, 0, 0, 0, 0}, /* 14 wall */ + { 30, 1, 0, 31, 1, 2, 0, 0, 43, 0, 0, 0}, /* 15 skeleton */ + { 41, 1, 0, 42, 1, 2, 145, 0, 43, 0, 0, 0}, /* 16 sword */ + { 41, 1, 0, 10, 0, 0, 0, 11, 43, 0, 0, 0}, /* 17 balcony left */ + { 0, 0, 0, 12, 1, 2, 0, 13, 43, 0, 0, 0}, /* 18 balcony right */ + { 92, 1, 0, 42, 1, 2, 145, 0, 43, 95, 1, 0}, /* 19 lattice pillar */ + { 1, 0, 0, 0, 0, 0, 0, 0, 2, 9, 0,-53}, /* 1A lattice down */ + { 3, 0,-10, 0, 0, 0, 0, 0, 0, 9, 0,-53}, /* 1B lattice small */ + { 4, 0,-10, 0, 0, 0, 0, 0, 0, 9, 0,-53}, /* 1C lattice left */ + { 5, 0,-10, 0, 0, 0, 0, 0, 0, 9, 0,-53}, /* 1D lattice right */ + { 97, 1, 0, 98, 1, 2, 0, 0, 43,100, 0, 0}, /* 1E debris w/torch */ +}; + +/* ---- Константные таблицы (render_room.py) -------------------------- */ +static const uint8_t COL_XH[10] = {0, 4, 8, 12, 16, 20, 24, 28, 32, 36}; +static const uint8_t TBL_LINE[3] = {0, 10, 20}; +static const uint8_t WALL_FRAM_BOTTOM[4] = {7, 9, 5, 3}; +static const uint8_t WALL_FRAM_MAIN[4] = {8, 10, 6, 4}; +static const uint8_t DOOR_FRAM_SLICE[9] = {67, 59, 58, 57, 56, 55, 54, 53, 52}; +static const uint8_t SPIKES_FRAM_RIGHT[10] = {0, 134, 135, 136, 137, 138, 137, 135, 134, 0}; +static const uint8_t BLUELINE_FRAM1[4] = {0, 124, 125, 126}; +static const int8_t BLUELINE_FRAM_Y[4] = {0, -20, -20, 0}; +static const uint8_t BLUELINE_FRAM3[4] = {44, 44, 45, 45}; +static const uint8_t LPOS[5] = {58, 41, 37, 20, 16}; +static const uint8_t RPOS[4] = {52, 42, 31, 21}; + +/* Wall (chtab_7) id: делители/блок/марки. */ +#define RES_WALL_RNDBLOCK 13 +#define RES_WALL_DIVIDER1 11 +#define RES_WALL_MARK_TL 14 +#define RES_WALL_MARK_BL 15 +#define RES_WALL_MARK_TR 16 +#define RES_WALL_MARK_BR 17 + +/* ---- Текущая комната (globals для tile_code/mod) ------------------- */ +static const uint8_t *g_fg, *g_bg, *g_lfg, *g_lbg; +static uint8_t g_room; + +static uint8_t tile_code(int row, int col) +{ + if (col < 0) { + if (g_lfg && row >= 0 && row < 3) return g_lfg[row]; + return 20; /* уровневая кромка = стена */ + } + if (row < 0) return 1; /* верхняя кромка = пол */ + if (row > 2 || col > 9) return 20; + return g_fg[row * 10 + col]; +} + +static uint8_t tile_mod(int row, int col) +{ + if (col < 0) { + if (g_lbg && row >= 0 && row < 3) return g_lbg[row]; + return 0; + } + if (row < 0 || row > 2 || col > 9) return 0; + return g_bg[row * 10 + col]; +} + +/* seg008.c:1255 (без FAKE_TILES): сосед — стена, если его тип == 20. + * На кромке col0 сосед по умолчанию = стена (как render_room). */ +static uint8_t wall_modifier(int row, int col) +{ + uint8_t lw = (col > 0) ? (tile_code(row, col - 1) == 20) : 1; + uint8_t rw = (col < 9) ? (tile_code(row, col + 1) == 20) : 1; + if (lw && rw) return 3; /* WWW */ + if (lw) return 2; /* WWS */ + if (rw) return 1; /* SWW */ + return 0; /* SWS */ +} + +/* ---- PRNG (seg009.c:321, 32-бит LCG) — только на ВХОД в комнату ---- */ +static unsigned long rnd_seed; + +static uint16_t prandom(uint16_t maxv) +{ + rnd_seed = rnd_seed * 214013UL + 2531011UL; + return (uint16_t)((uint16_t)(rnd_seed >> 16) % (uint16_t)(maxv + 1)); +} + +/* ---- Декали-марки стен (seg008.c:2041/2056) ------------------------ */ +static void draw_left_mark(uint16_t dv, int arg2, int arg1, int row, int col) +{ + uint8_t image_id = (dv & 1) ? RES_WALL_MARK_BL : RES_WALL_MARK_TL; + int lv2 = 0; + int xh = COL_XH[col]; + int dby = 63 * row + 65; + if (dv > 3) lv2 = arg1 + 6; + else if (dv > 1) lv2 = arg2 + 6; + wall_b(image_id, (xh + ((dv == 2 || dv == 3) ? 1 : 0)) * 8 + lv2, dby - LPOS[dv]); +} + +static void draw_right_mark(uint16_t dv, int arg1, int row, int col) +{ + uint8_t image_id = (dv & 1) ? RES_WALL_MARK_BR : RES_WALL_MARK_TR; + int a = (dv < 2) ? 24 : arg1 - 3; + int xh = COL_XH[col]; + int dby = 63 * row + 65; + wall_b(image_id, (xh + ((dv > 1) ? 1 : 0)) * 8 + a, dby - RPOS[dv]); +} + +/* ---- wall_pattern (seg008.c:1928, dungeon-ветка) ------------------- */ +static void wall_pattern(int row, int col, int which_part) +{ + int xh = COL_XH[col]; + int dby = 63 * row + 65; + unsigned long saved = rnd_seed; + uint16_t mid_div, mid_off, bot_div, bot_off; + uint8_t bg; + + rnd_seed = (unsigned long)(g_room + TBL_LINE[row] + col); + prandom(1); /* сброс */ + mid_div = prandom(1); + mid_off = prandom(4); + bot_div = prandom(1); + bot_off = prandom(4); + bg = wall_modifier(row, col); + + if (bg == 3) { /* WWW */ + if (which_part) { + if (prandom(4) == 0) + wall_b(RES_WALL_RNDBLOCK, xh * 8, dby - 42); + wall_b((uint8_t)(RES_WALL_DIVIDER1 + mid_div), (xh + 1) * 8 + mid_off, dby - 21); + } + wall_b((uint8_t)(RES_WALL_DIVIDER1 + bot_div), xh * 8 + bot_off, dby); + if (which_part) { + if (prandom(4) == 0) + draw_right_mark(prandom(3), mid_off, row, col); + if (prandom(4) == 0) + draw_left_mark(prandom(4), mid_off - mid_div, bot_off - bot_div, row, col); + } + } else if (bg == 0) { /* SWS */ + if (which_part) { + if (prandom(6) == 0) + draw_left_mark(prandom(1), mid_off - mid_div, bot_off - bot_div, row, col); + } + } else if (bg == 1) { /* SWW */ + if (which_part) { + if (prandom(4) == 0) + wall_b(RES_WALL_RNDBLOCK, xh * 8, dby - 42); + wall_b((uint8_t)(RES_WALL_DIVIDER1 + mid_div), (xh + 1) * 8 + mid_off, dby - 21); + if (prandom(4) == 0) + draw_right_mark(prandom(3), mid_off, row, col); + if (prandom(4) == 0) + draw_left_mark(prandom(3), mid_off - mid_div, bot_off - bot_div, row, col); + } + } else { /* WWS (bg==2) */ + if (which_part) + wall_b((uint8_t)(RES_WALL_DIVIDER1 + mid_div), (xh + 1) * 8 + mid_off, dby - 21); + wall_b((uint8_t)(RES_WALL_DIVIDER1 + bot_div), xh * 8 + bot_off, dby); + if (which_part) { + if (prandom(4) == 0) + draw_right_mark((uint16_t)(prandom(1) + 2), mid_off, row, col); + if (prandom(4) == 0) + draw_left_mark(prandom(4), mid_off - mid_div, bot_off - bot_div, row, col); + } + } + rnd_seed = saved; +} + +/* topright тайла снизу-слева (seg008.c:410). */ +static void draw_topright(uint8_t tt, int x, int dby) +{ + if (tt == 7 || tt == 12) return; + if (tt == 20) { wall_b(2, x, dby); return; } + if (tile_table[tt].topright_id) + env_b(tile_table[tt].topright_id, x, dby); +} + +/* Решётка ворот (portcullis) из ЛЕВОЙ комнаты (seg008.c:1110). */ +static void draw_gate_back(uint8_t modl, int xh, int dby, int dmy) +{ + int x = xh * 8; + int gate_top_y = dby - 62; + int openness = (((modl > 188) ? 188 : modl) >> 2) + 1; + int gate_bot_y = dmy - openness; + int yb, gate_frame; + + if (gate_bot_y + 12 < dmy) + env_b(50, x, gate_bot_y); + else + env_b(51, x, gate_bot_y - 2); + yb = gate_bot_y - 12; + if (yb < 192) { + while (yb >= 0 && yb > 7 && (yb - 7) > gate_top_y) { + env_b(52, x, yb); + yb -= 8; + } + } + gate_frame = yb - gate_top_y + 1; + if (gate_frame > 0 && gate_frame < 9) + env_b(DOOR_FRAM_SLICE[gate_frame], x, yb); +} + +/* ---- draw_tile (seg008.c:155) — одна клетка ------------------------ */ +static void draw_tile(int row, int col) +{ + uint8_t code = tile_code(row, col); + uint8_t lcode = tile_code(row, col - 1); + uint8_t lmod = tile_mod(row, col - 1); + int xh = COL_XH[col]; + int x = xh * 8; + int dby = 63 * row + 65; + int dmy = dby - 3; + const piece *t = &tile_table[code]; + const piece *lt = &tile_table[lcode]; + uint8_t rbl_code; + uint8_t wmod; + + /* тайл снизу-слева (для topright) */ + if (row + 1 > 2) rbl_code = 1; /* ниже нижнего ряда = пол */ + else rbl_code = tile_code(row + 1, col - 1); + + /* --- floorright (seg008.c:392) — только can_see_bottomleft --- */ + if (code == 0 || code == 9 || code == 12 || code == 26) { + draw_topright(rbl_code, x, dby); + /* floor-right тень (blitters_9_black) на ЧЁРНОМ фоне = no-op + * (чёрный силуэт по чёрному) — не рисуем. Если tile_left имеет + * floor_right: lt->floor_right. */ + } + + /* --- draw_tile_right (seg008.c:456) --- */ + if (code != 20) { + if (lcode == 20) { + wall_b(1, x, tile_table[20].right_y + dmy); /* грань стены */ + } else if (lcode == 0) { + if (lmod <= 3 && BLUELINE_FRAM1[lmod]) + env_b(BLUELINE_FRAM1[lmod], x, BLUELINE_FRAM_Y[lmod] + dmy); + } else if (lcode == 1) { + uint8_t num; + env_b(42, x, tile_table[1].right_y + dmy); /* правый треуг. пола */ + num = (lmod <= 3) ? lmod : 0; + if (num != 0) /* dungeon: level_type=0 */ + env_b(BLUELINE_FRAM3[num], x, dmy - 20); /* силуэт кладки */ + } else { + if (lt->right_id) + env_b(lt->right_id, x, lt->right_y + dmy); + /* stripe — только палас, dungeon пропускает */ + } + } + + /* --- draw_tile_anim_right (seg008.c:530) — статичные части --- */ + if (lcode == 4) { /* gate: решётка */ + draw_gate_back(lmod, xh, dby, dmy); + } else if (lcode == 11) { /* loose: правый край */ + env_b(42, x, dby - 1); + } else if (lcode == 2) { /* spike: статич. кадр */ + uint8_t sf = (lmod & 0x80) ? 5 : lmod; + if (sf < 10 && SPIKES_FRAM_RIGHT[sf]) + env_b(SPIKES_FRAM_RIGHT[sf], x, dmy - 7); + } + + /* --- draw_tile_bottom (seg008.c:570) --- */ + if (code == 20) { + wmod = wall_modifier(row, col); + wall_b(WALL_FRAM_BOTTOM[wmod], x, dby); /* сплошная нижняя грань */ + wall_pattern(row, col, 0); + } else if (t->bottom_id) { + env_b(t->bottom_id, x, dby); + } + + /* --- draw_loose (seg008.c:600) — статич. кадр --- */ + if (code == 11) + env_b(43, x, dby); /* loose_fram_bottom[0] */ + + /* --- draw_tile_base (seg008.c:611) --- */ + { + uint8_t base_id = t->base_id; + if (code == 11) base_id = 41; /* loose_fram_left[0] */ + if (base_id) + env_b(base_id, x, t->base_y + dmy); + } + + /* --- draw_tile_fore (seg008.c:690) --- */ + if (code == 20) { + wmod = wall_modifier(row, col); + wall_b(WALL_FRAM_MAIN[wmod], x, dmy); /* основная грань */ + wall_pattern(row, col, 1); + } + if (t->fore_id) + fore_b(t->fore_id, t->fore_x * 8 + x, t->fore_y + dmy); +} + +void pop_room_draw(uint8_t room_num, + const uint8_t *fg, const uint8_t *bg, + const uint8_t *lcol_fg, const uint8_t *lcol_bg) +{ + int row, col; + g_room = room_num; + g_fg = fg; g_bg = bg; + g_lfg = lcol_fg; g_lbg = lcol_bg; + + /* прозрачный блит: 0xFF не пишется (чёрный фон/нижние слои проступают) */ + gfx_set_bank(GFX_BANK_TRANSPARENT); + for (row = 2; row >= 0; row--) /* снизу вверх (2,1,0) */ + for (col = 0; col < 10; col++) + draw_tile(row, col); + gfx_set_bank(GFX_BANK_NORMAL); +} diff --git a/applications/PoP/roomtest/pop_bg.h b/applications/PoP/roomtest/pop_bg.h new file mode 100644 index 0000000..464f17a --- /dev/null +++ b/applications/PoP/roomtest/pop_bg.h @@ -0,0 +1,32 @@ +/* + * pop_bg.h — отрисовка СТАТИЧЕСКОГО фона комнаты PoP композицией слоёв + * (порт seg008.c draw_tile/wall_pattern, Шаг 3). Рисует прямо в текущую + * страницу (banked GFX_BANK_TRANSPARENT — в видео-ОЗУ + теневую копию, + * 0xFF прозрачен), ОДИН раз при входе в комнату. + * + * Данные: getimage-спрайты в атласах pop_env0..4/pop_wall/pop_fore + * (toolchain/pop_pack_bg.py), палитра pop_bg.pal. Прямая адресация + * id: env[id>>5] idx id&31, wall idx=id, fore idx=id — ноль remap-таблиц. + */ +#ifndef POP_BG_H +#define POP_BG_H + +#include + +/* Загрузить 7 атласов фона. Звать ДО initgraph (как poc.c — atlas_load + * трогает W3; проверенный порядок). Палитру pop_bg.pal грузит ПРИЛОЖЕНИЕ + * после initgraph (gfx_pal_fload). 0 — OK, -1 — ошибка. */ +int pop_bg_load(void); + +/* Освободить EMM-страницы атласов. */ +void pop_bg_free(void); + +/* Нарисовать комнату: fg[30]/bg[30] — foretable-коды/backtable-модификаторы + * (по строкам, row*10+col). lcol_fg/lcol_bg — 3 тайла правого столбца + * ЛЕВОЙ соседней комнаты (для границы, напр. решётки ворот), или 0 — + * тогда левая кромка = стена. room_num — номер комнаты (зерно стен). */ +void pop_room_draw(uint8_t room_num, + const uint8_t *fg, const uint8_t *bg, + const uint8_t *lcol_fg, const uint8_t *lcol_bg); + +#endif diff --git a/applications/PoP/roomtest/pop_ctrl.c b/applications/PoP/roomtest/pop_ctrl.c new file mode 100644 index 0000000..5a6d2a1 --- /dev/null +++ b/applications/PoP/roomtest/pop_ctrl.c @@ -0,0 +1,368 @@ +/* + * pop_ctrl.c — управление Kid (Фаза K2b). Порт SDLPoP: + * вход — read_user_control/user_control/rest_ctrl_1/save_ctrl_1 + * (seg006.c) поверх held-state ; + * диспетчер — control() + control_standing/turning/startrun/running/ + * crouched (seg005.c), Kid-only ветки (без меча/стражи); + * действия — forward/back/up/down_pressed, crouch, standing_jump, + * run_jump, safe_step (seg005.c). + * + * Map-запросы (стена/край/захват) ЗАСТАБЛЕНЫ «открытый пол» (map_* ниже) — + * реальная коллизия в K3/K4. Поэтому: forward -> всегда бег, down -> присед, + * back -> разворот, up+forward -> прыжок с места; osторожный шаг и climb + * помечены TODO(K3/K4). + * + * Значения CONTROL_* (types.h): RELEASED=0, IGNORE=1, HELD=-1; + * X: LEFT/FORWARD=-1, RIGHT/BACKWARD=+1; Y: UP=-1, DOWN=+1. + */ +#include +#include +#include "pop_kid.h" +#include "pop_map.h" + +/* ---- CONTROL_* и seq/frame id (подмножество types.h) --------------- */ +#define CONTROL_RELEASED 0 +#define CONTROL_IGNORE 1 +#define CONTROL_HELD (-1) +#define CONTROL_HELD_LEFT (-1) +#define CONTROL_HELD_RIGHT 1 +#define CONTROL_HELD_FORWARD (-1) +#define CONTROL_HELD_BACKWARD 1 +#define CONTROL_HELD_UP (-1) +#define CONTROL_HELD_DOWN 1 + +#define DIR_RIGHT 0 /* dir_0_right */ + +#define SEQ_1_START_RUN 1 +#define SEQ_2_STAND 2 +#define SEQ_3_STANDING_JUMP 3 +#define SEQ_4_RUN_JUMP 4 +#define SEQ_5_TURN 5 +#define SEQ_6_RUN_TURN 6 +#define SEQ_13_STOP_RUN 13 +#define SEQ_26_CROUCH_WHILE_RUNNING 26 +#define SEQ_43_START_RUN_AFTER_TURN 43 +#define SEQ_49_STAND_UP_FROM_CROUCH 49 +#define SEQ_50_CROUCH 50 +#define SEQ_79_CROUCH_HOP 79 +#define SEQ_STEP_BASE 28 /* +distance(1..14) = seq 29..42 шаг-N */ +#define SEQ_39_UNSAFE_STEP 39 /* шаг за кромку (distance 11) */ +#define SEQ_44_STEP_ON_EDGE 44 +#define SEQ_14_JUMP_INTO_CEILING 14 /* прыжок вверх: сверху стена/пол */ +#define SEQ_28_JUMP_NOTHING_ABOVE 28 /* прыжок вверх: сверху пусто */ + +#define FRAME_7_RUN 7 +#define FRAME_11_RUN 11 +#define FRAME_15_STAND 15 +#define FRAME_48_TURN 48 +#define FRAME_50_TURN 50 +#define FRAME_67_JUMPUP1 67 +#define FRAME_70_JUMPHANG 70 +#define FRAME_87_HANG1 87 +#define FRAME_109_CROUCH 109 + +/* ---- Состояние управления (seg006 globals) ------------------------ * + * control_x/y/shift — «сырое» состояние осей ЭТОГО кадра (абсолютно: + * X LEFT/-1..RIGHT/+1); *_forward/backward/up/down/shift2 — конечный + * автомат held/released/ignore (авто-повтор); ctrl1_* — их память между + * кадрами (rest/save). */ +static int8_t control_x, control_y, control_shift; +static int8_t control_forward, control_backward, control_up, control_down, control_shift2; +static int8_t ctrl1_forward, ctrl1_backward, ctrl1_up, ctrl1_down, ctrl1_shift2; + +/* ---- seqtbl_offset_char: сменить последовательность Kid ------------ * + * Обёртка над kid_set_seq (единая точка в pop_kid — kid_seq_off/kid_seqtbl + * не дублируются в этот TU). */ +#define seqtbl_offset_char(seq) kid_set_seq(seq) + +static int8_t release_arrows(void) +{ + control_backward = control_forward = control_up = control_down = CONTROL_RELEASED; + return CONTROL_IGNORE; /* seg006: return 1 */ +} + +/* Край/стена впереди — из pop_map (get_edge_distance): pop_edge_distance() + * возвращает дистанцию и ставит pop_edge_type() = EDGE_WALL/FLOOR/CLOSER. */ + +/* ---- Действия (seg005) -------------------------------------------- */ +static void crouch(void) +{ + seqtbl_offset_char(SEQ_50_CROUCH); + control_down = release_arrows(); +} + +static void standing_jump(void) +{ + control_up = control_forward = CONTROL_IGNORE; + seqtbl_offset_char(SEQ_3_STANDING_JUMP); +} + +static void run_jump(void) +{ + /* Оригинал выравнивает Kid по краю пола (нужны tile-запросы) — это + * полировка K3; K2b просто запускает run-jump. */ + if (Kid.frame >= FRAME_7_RUN) { + control_up = release_arrows(); + seqtbl_offset_char(SEQ_4_RUN_JUMP); + } +} + +static void safe_step(void); /* fwd */ + +static void forward_pressed(void) +{ + int distance = pop_edge_distance(); + if (pop_edge_type() == EDGE_WALL && distance < 8) { + /* рядом стена — вместо бега осторожный шаг (в стену не разбежится) */ + if (control_forward == CONTROL_HELD) + safe_step(); + } else { + seqtbl_offset_char(SEQ_1_START_RUN); + } +} + +static void back_pressed(void) +{ + control_backward = release_arrows(); + /* Kid без меча -> просто разворот. */ + seqtbl_offset_char(SEQ_5_TURN); +} + +static void down_pressed(void) +{ + control_down = CONTROL_IGNORE; /* без авто-повтора */ + /* climb-down у края (seq_68) — K4 (зацеп/спуск); пока просто присед. */ + crouch(); +} + +/* Вертикальный прыжок вверх (seg005 jump_up, упрощённо): сверху сплошное + * (стена/пол) → seq_14 (в потолок), иначе seq_28 (пусто сверху). Зацеп за + * уступ сверху-впереди (can_grab) — K4.2, пока чистый вертикальный прыжок. */ +static void jump_up(void) +{ + control_up = release_arrows(); + if (pop_above_solid()) + seqtbl_offset_char(SEQ_14_JUMP_INTO_CEILING); + else + seqtbl_offset_char(SEQ_28_JUMP_NOTHING_ABOVE); +} + +static void up_pressed(void) +{ + /* (дверь уровня/телепорт — нет в тесте.) ↑+forward = прыжок с места; + * иначе вертикальный прыжок вверх. */ + if (control_x == CONTROL_HELD_FORWARD) + standing_jump(); + else + jump_up(); +} + +/* Осторожный шаг (Shift+forward): шаг РОВНО до кромки/стены. distance>0 — + * шаг длины distance (seq 29..42 = step1..step14); ==0 у края — шаг на + * кромку (seq_44) либо неосторожный шаг за край (seq_39). Порт seg005. */ +static void safe_step(void) +{ + int distance; + control_shift2 = CONTROL_IGNORE; /* без авто-повтора */ + control_forward = CONTROL_IGNORE; + distance = pop_edge_distance(); + if (distance) { + Kid.repeat = 1; + seqtbl_offset_char((uint8_t)(distance + SEQ_STEP_BASE)); + } else if (pop_edge_type() == EDGE_WALL) { + /* вплотную к стене: маленький шаг-1 — check_bumped отскочит назад + * (шаг назад со звуком, как оригинал), НЕ seq_39 (шаг за край=падение). */ + Kid.repeat = 1; + seqtbl_offset_char((uint8_t)(1 + SEQ_STEP_BASE)); + } else if (Kid.repeat != 0) { + Kid.repeat = 0; + seqtbl_offset_char(SEQ_44_STEP_ON_EDGE); + } else { + seqtbl_offset_char(SEQ_39_UNSAFE_STEP); /* реальный обрыв (edge=CLOSER) */ + } +} + +/* ---- control_* обработчики состояний ------------------------------ */ +static void control_crouched(void) +{ + if (control_y != CONTROL_HELD_DOWN) { + seqtbl_offset_char(SEQ_49_STAND_UP_FROM_CROUCH); + } else if (control_forward == CONTROL_HELD) { + control_forward = CONTROL_IGNORE; + seqtbl_offset_char(SEQ_79_CROUCH_HOP); + } +} + +static void control_standing(void) +{ + /* Kid charid_0, без меча: остаётся нижняя часть control_standing. */ + if (control_shift == CONTROL_HELD) { + if (control_backward == CONTROL_HELD) back_pressed(); + else if (control_up == CONTROL_HELD) up_pressed(); + else if (control_down == CONTROL_HELD) down_pressed(); + else if (control_x == CONTROL_HELD_FORWARD && control_forward == CONTROL_HELD) + safe_step(); + } else if (control_forward == CONTROL_HELD) { + if (control_up == CONTROL_HELD) standing_jump(); /* keyboard mode */ + else forward_pressed(); + } else if (control_backward == CONTROL_HELD) { + back_pressed(); + } else if (control_up == CONTROL_HELD) { + if (control_forward == CONTROL_HELD) standing_jump(); + else up_pressed(); + } else if (control_down == CONTROL_HELD) { + down_pressed(); + } else if (control_x == CONTROL_HELD_FORWARD) { + forward_pressed(); + } +} + +static void control_turning(void) +{ + if (control_shift >= CONTROL_RELEASED && + control_x == CONTROL_HELD_FORWARD && control_y >= CONTROL_RELEASED) { + seqtbl_offset_char(SEQ_43_START_RUN_AFTER_TURN); + } +} + +static void control_startrun(void) +{ + if (control_y == CONTROL_HELD_UP && control_x == CONTROL_HELD_FORWARD) + standing_jump(); +} + +static void control_running(void) +{ + /* Стену НЕ обрабатываем тут — check_bumped (pop_phys_tick) даёт отскок + * seq_47 при беге в стену (как оригинал), с корректной позицией. */ + if (control_x == CONTROL_RELEASED && + (Kid.frame == FRAME_7_RUN || Kid.frame == FRAME_11_RUN)) { + control_forward = release_arrows(); + seqtbl_offset_char(SEQ_13_STOP_RUN); + } else if (control_x == CONTROL_HELD_BACKWARD) { + control_backward = release_arrows(); + seqtbl_offset_char(SEQ_6_RUN_TURN); + } else if (control_y == CONTROL_HELD_UP && control_up == CONTROL_HELD) { + run_jump(); + } else if (control_down == CONTROL_HELD) { + control_down = CONTROL_IGNORE; + seqtbl_offset_char(SEQ_26_CROUCH_WHILE_RUNNING); + } +} + +/* control() — диспетчер по кадру (seg005.c:252), Kid жив, без меча. */ +static void control(void) +{ + uint8_t f = Kid.frame; + if (f == FRAME_15_STAND || (f >= FRAME_50_TURN && f < 53)) + control_standing(); + else if (f == FRAME_48_TURN) + control_turning(); + else if (f < 4) + control_startrun(); + else if (f >= FRAME_67_JUMPUP1 && f < FRAME_70_JUMPHANG) + ; /* control_jumpup — K4 */ + else if (f < FRAME_15_STAND) + control_running(); + else if (f >= FRAME_87_HANG1 && f < 100) + ; /* control_hanging — K4 */ + else if (f == FRAME_109_CROUCH) + control_crouched(); +} + +/* ---- Модель ввода (seg006) ---------------------------------------- */ +static void read_input(void) +{ + uint8_t l = kbd_raw_down(KBD_LEFT); + uint8_t r = kbd_raw_down(KBD_RIGHT); + uint8_t u = kbd_raw_down(KBD_UP); + uint8_t d = kbd_raw_down(KBD_DOWN); + control_x = r ? CONTROL_HELD_RIGHT : (l ? CONTROL_HELD_LEFT : CONTROL_RELEASED); + control_y = d ? CONTROL_HELD_DOWN : (u ? CONTROL_HELD_UP : CONTROL_RELEASED); + control_shift = (kbd_raw_down(KBD_LSHIFT) || kbd_raw_down(KBD_RSHIFT)) + ? CONTROL_HELD : CONTROL_RELEASED; +} + +/* read_user_control (seg006:1557): «свежее нажатие» (RELEASED->HELD) с + * подавлением авто-повтора (IGNORE держится, пока клавишу не отпустят). */ +static void read_user_control(void) +{ + if (control_forward >= CONTROL_RELEASED) { + if (control_x == CONTROL_HELD_FORWARD) { + if (control_forward == CONTROL_RELEASED) control_forward = CONTROL_HELD; + } else control_forward = CONTROL_RELEASED; + } + if (control_backward >= CONTROL_RELEASED) { + if (control_x == CONTROL_HELD_BACKWARD) { + if (control_backward == CONTROL_RELEASED) control_backward = CONTROL_HELD; + } else control_backward = CONTROL_RELEASED; + } + if (control_up >= CONTROL_RELEASED) { + if (control_y == CONTROL_HELD_UP) { + if (control_up == CONTROL_RELEASED) control_up = CONTROL_HELD; + } else control_up = CONTROL_RELEASED; + } + if (control_down >= CONTROL_RELEASED) { + if (control_y == CONTROL_HELD_DOWN) { + if (control_down == CONTROL_RELEASED) control_down = CONTROL_HELD; + } else control_down = CONTROL_RELEASED; + } + if (control_shift2 >= CONTROL_RELEASED) { + if (control_shift == CONTROL_HELD) { + if (control_shift2 == CONTROL_RELEASED) control_shift2 = CONTROL_HELD; + } else control_shift2 = CONTROL_RELEASED; + } +} + +static void flip_control_x(void) +{ + int8_t t; + control_x = (int8_t)-control_x; + t = control_forward; control_forward = control_backward; control_backward = t; +} + +/* user_control (seg006:0DC0): «вперёд» относительно направления Kid. */ +static void user_control(void) +{ + if (Kid.direction >= DIR_RIGHT) { + flip_control_x(); + control(); + flip_control_x(); + } else { + control(); + } +} + +static void rest_ctrl_1(void) +{ + control_forward = ctrl1_forward; control_backward = ctrl1_backward; + control_up = ctrl1_up; control_down = ctrl1_down; control_shift2 = ctrl1_shift2; +} +static void save_ctrl_1(void) +{ + ctrl1_forward = control_forward; ctrl1_backward = control_backward; + ctrl1_up = control_up; ctrl1_down = control_down; ctrl1_shift2 = control_shift2; +} + +/* ---- Публичный API ------------------------------------------------ */ +int pop_ctrl_init(void) +{ + control_forward = control_backward = control_up = control_down = CONTROL_RELEASED; + control_shift2 = CONTROL_RELEASED; + ctrl1_forward = ctrl1_backward = ctrl1_up = ctrl1_down = ctrl1_shift2 = CONTROL_RELEASED; + return kbd_raw_open(); +} + +void pop_ctrl_close(void) +{ + kbd_raw_close(); +} + +void pop_ctrl_tick(void) +{ + rest_ctrl_1(); + read_input(); + read_user_control(); + user_control(); /* -> control(): может сменить Kid.curr_seq */ + save_ctrl_1(); +} diff --git a/applications/PoP/roomtest/pop_ctrl.h b/applications/PoP/roomtest/pop_ctrl.h new file mode 100644 index 0000000..727cfc8 --- /dev/null +++ b/applications/PoP/roomtest/pop_ctrl.h @@ -0,0 +1,31 @@ +/* + * pop_ctrl.h — управление Kid (Фаза K2b): порт диспетчера control() + * (SDLPoP seg005.c) + модель ввода read_user_control/user_control + * (seg006.c) поверх held-state клавиатуры . + * + * Kid-only подмножество: без меча и стражи. Map-запросы (стена/край/ + * захват) ЗАСТАБЛЕНЫ «открытый пол» — реальная коллизия (стоп у стены, + * падение в ямы, повисание) приходит в K3/K4. Даёт отклик на клавиши: + * бег влево/вправо, разворот, стоп, присед, прыжок с места, прыжок в + * беге, присед-прыжок. + * + * Раскладка (kbd_raw, PS/2 Set 2): ←/→ = движение, ↓ = присед, ↑ = + * прыжок вверх (с →), Shift = осторожный шаг (K3). ESC — выход (проверяет + * приложение сам, канал raw забирает весь ввод у DSS). + */ +#ifndef POP_CTRL_H +#define POP_CTRL_H + +#include + +/* Открыть raw-канал клавиатуры и обнулить память авто-повтора. + * 0 — OK, -1 — ошибка (EBUSY и пр., см. kbd_raw_open). */ +int pop_ctrl_init(void); +void pop_ctrl_close(void); + +/* Один кадр ввода: прочитать клавиши -> обновить control_* -> прогнать + * диспетчер control() (может сменить Kid.curr_seq). Звать ПЕРЕД + * kid_tick() (play_seq продолжит с новой последовательности). */ +void pop_ctrl_tick(void); + +#endif diff --git a/applications/PoP/roomtest/pop_kid.c b/applications/PoP/roomtest/pop_kid.c new file mode 100644 index 0000000..ec345a2 --- /dev/null +++ b/applications/PoP/roomtest/pop_kid.c @@ -0,0 +1,220 @@ +/* + * pop_kid.c — движок Kid (Фаза K1): порт seg006.c play_seq/load_frame + + * seqtbl-интерпретатор. Данные — kid_data.h. + */ +#include +#include +#include +#include "pop_kid.h" +#include "kid_data.h" + +kid_t Kid; + +/* Координатные таблицы (data.h). */ +static const uint8_t x_bump[20] = { + (uint8_t)-12, 2, 16, 30, 44, 58, 72, 86, 100, 114, + 128, 142, 156, 170, 184, 198, 212, 226, 240, 254 +}; +static const int16_t y_land[5] = {-8, 55, 118, 181, 244}; +#define FIRST_ONSCREEN_COLUMN 5 +#define TILE_SIZEX 14 + +/* ---- Атласы Kid (прямая адресация image>>3) ------------------------ */ +#define KID_MAXPAGES 28 +static atlas_t kidp[KID_MAXPAGES]; +static uint8_t kid_npages; + +int pop_kid_load(uint8_t npages) +{ + static const char *const names[KID_MAXPAGES] = { + "kid0.atl","kid1.atl","kid2.atl","kid3.atl","kid4.atl","kid5.atl", + "kid6.atl","kid7.atl","kid8.atl","kid9.atl","kid10.atl","kid11.atl", + "kid12.atl","kid13.atl","kid14.atl","kid15.atl","kid16.atl","kid17.atl", + "kid18.atl","kid19.atl","kid20.atl","kid21.atl","kid22.atl","kid23.atl", + "kid24.atl","kid25.atl","kid26.atl","kid27.atl" + }; + uint8_t i; + if (npages > KID_MAXPAGES) npages = KID_MAXPAGES; + for (i = 0; i < npages; i++) + if (atlas_load(&kidp[i], names[i]) != 0) + return -1; + kid_npages = npages; + return 0; +} + +void pop_kid_free(void) +{ + uint8_t i; + for (i = 0; i < kid_npages; i++) + atlas_free(&kidp[i]); + kid_npages = 0; +} + +/* ---- seqtbl-доступ (база SEQTBL_BASE) ------------------------------ */ +#define SEQ(cs) kid_seqtbl[(uint16_t)((cs) - SEQTBL_BASE)] + +static uint8_t char_dx_forward(int8_t dx) +{ + if (Kid.direction < 0) dx = (int8_t)-dx; + return (uint8_t)(Kid.x + dx); +} + +/* Аксессоры к текущему кадру (для pop_map — без дублирования kid_frames). */ +int8_t kid_cur_dx(void) { return kid_frames[Kid.frame].dx; } +uint8_t kid_cur_flags(void) { return kid_frames[Kid.frame].flags; } + +/* seqtbl_offset_char: сменить последовательность Kid (единая точка — + * pop_ctrl/pop_map зовут ЕЁ, а не дублируют kid_seq_off/kid_seqtbl). */ +void kid_set_seq(uint8_t seq_id) { Kid.curr_seq = kid_seq_off[seq_id]; } + +/* play_seq (seg006.c:570): крутит опкоды до КАДРА (байт < 0xF0). */ +void play_seq(void) +{ + for (;;) { + uint8_t cmd = SEQ(Kid.curr_seq); + Kid.curr_seq++; + switch (cmd) { + case 0xFB: /* DX */ + Kid.x = char_dx_forward((int8_t)SEQ(Kid.curr_seq)); + Kid.curr_seq++; + break; + case 0xFA: /* DY */ + Kid.y = (uint8_t)(Kid.y + (int8_t)SEQ(Kid.curr_seq)); + Kid.curr_seq++; + break; + case 0xFE: /* FLIP */ + Kid.direction = (int8_t)~Kid.direction; + break; + case 0xF7: /* JMP_IF_FEATHER: нет пера -> пропустить адрес */ + Kid.curr_seq += 2; + break; + case 0xFF: { /* JMP */ + uint16_t a = (uint16_t)(SEQ(Kid.curr_seq) | + ((uint16_t)SEQ((uint16_t)(Kid.curr_seq + 1)) << 8)); + Kid.curr_seq = a; + break; + } + case 0xFD: /* UP */ + Kid.curr_row--; + break; + case 0xFC: /* DOWN */ + Kid.curr_row++; + break; + case 0xF9: /* ACTION */ + Kid.action = SEQ(Kid.curr_seq); + Kid.curr_seq++; + break; + case 0xF8: /* SET_FALL */ + Kid.fall_x = (int8_t)SEQ(Kid.curr_seq); Kid.curr_seq++; + Kid.fall_y = (int8_t)SEQ(Kid.curr_seq); Kid.curr_seq++; + break; + case 0xF5: case 0xF4: /* KNOCK up/down */ + break; + case 0xF2: /* SOUND — потребить 1 байт */ + Kid.curr_seq++; + break; + case 0xF1: /* END_LEVEL */ + break; + case 0xF3: /* GET_ITEM — потребить 1 байт */ + Kid.curr_seq++; + break; + case 0xF6: /* DIE nop */ + break; + default: /* КАДР */ + Kid.frame = cmd; + return; + } + } +} + +void kid_init(uint8_t seq_id, int8_t col, int8_t row, int8_t dir) +{ + Kid.curr_col = col; + Kid.curr_row = row; + Kid.direction = dir; + Kid.x = (uint8_t)(x_bump[col + FIRST_ONSCREEN_COLUMN] + TILE_SIZEX); + Kid.y = (uint8_t)y_land[row + 1]; + Kid.fall_x = Kid.fall_y = 0; + Kid.repeat = 0; + Kid.curr_seq = kid_seq_off[seq_id]; + play_seq(); /* до первого кадра */ +} + +void kid_tick(void) +{ + play_seq(); +} + +/* Отрисовка текущего кадра: obj_x=(char_x+dx)*2-116, obj_y=char_y+dy + * (seg008.c:1728 load_frame_to_obj). НИЗ спрайта на obj_y, левый край + * на obj_x. Калибровка XOFF/YOFF — сверить с эталоном. + * + * Зеркалирование (K2a): спрайты Kid хранятся ЛИЦОМ ВЛЕВО (как в оригинале). + * seg008.c:864 — флип, когда персонаж смотрит ВПРАВО (direction==dir_0_right + * ==0), и тогда xpos -= image->w (draw_mid). Реализуем бесплатным + * горизонтальным флипом column-major спрайта (gfx_blit_cols flip=1): + * зеркало происходит ВНУТРИ [x,x+w), поэтому при флипе подаём x=obj_x-w, + * чтобы «hot-point» кадра остался на месте. + * + * Kid рисуется банком GFX_BANK_SPRITE (только видео-ОЗУ, НЕ теневая копия + * — 0xFF прозрачен), чтобы kid_heal() мог восстановить чистый фон из + * теневой копии (фон запёкся в неё при pop_room_draw). Прямоугольник + * последней отрисовки запоминается для heal. */ +#define KID_XOFF 0 +#define KID_YOFF 0 + +static int kid_lx, kid_ly; +static uint16_t kid_lw, kid_lh; +static uint8_t kid_lvalid; + +/* Восстановить фон из теневой копии на месте ПРЕДЫДУЩЕГО кадра Kid. */ +void kid_heal(void) +{ + if (kid_lvalid) + gfx_heal(kid_lx, kid_ly, kid_lw, kid_lh); + kid_lvalid = 0; +} + +void kid_draw(void) +{ + kframe fr = kid_frames[Kid.frame]; + int fwd, obj_x, obj_y, top; + uint8_t page, idx; + const uint8_t *img; + uint16_t w, h; + + if (fr.image == 255) + return; /* пустой кадр */ + fwd = (int)Kid.x + ((Kid.direction < 0) ? -fr.dx : fr.dx); + obj_x = (fwd << 1) - 116; + obj_y = (int)Kid.y + fr.dy; + if ((int8_t)(fr.flags ^ Kid.direction) >= 0) + obj_x++; /* even/odd pixel (в ЛОГИЧЕСКОЙ координате) */ + /* seg008 calc_screen_x_coord: логическая X (280 шир = 28px/тайл) -> + * экран (320 шир = 32px/тайл), ×320/280 = ×8/7. БЕЗ этого Kid уезжает + * влево от тайлов тем сильнее, чем правее (col9 ~36px). Коллизия — в + * логической системе (не масштабируется). */ + obj_x = obj_x * 8 / 7; + + page = (uint8_t)(fr.image >> 3); + idx = (uint8_t)(fr.image & 7); + if (page >= kid_npages) + return; /* страница не загружена */ + img = (const uint8_t *)atlas_image(&kidp[page], idx); + gfx_w0_map(kidp[page].page); + w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8)); + h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8)); + if (w && h) { + uint8_t flip = (Kid.direction >= 0); /* dir_0_right -> зеркало */ + int bx = obj_x + KID_XOFF; + if (flip) + bx -= (int)w; /* seg008: xpos -= image->w */ + top = obj_y - (int)h + 1 + KID_YOFF; + gfx_set_bank(GFX_BANK_SPRITE); /* видео-ОЗУ, без теневой копии */ + gfx_blit_cols(bx, top, img, flip); + gfx_set_bank(GFX_BANK_NORMAL); + kid_lx = bx; kid_ly = top; + kid_lw = w; kid_lh = h; kid_lvalid = 1; + } + gfx_w0_unmap(); +} diff --git a/applications/PoP/roomtest/pop_kid.h b/applications/PoP/roomtest/pop_kid.h new file mode 100644 index 0000000..3ae3e32 --- /dev/null +++ b/applications/PoP/roomtest/pop_kid.h @@ -0,0 +1,63 @@ +/* + * pop_kid.h — движок анимации/движения Kid (Фаза K1): интерпретатор + * seqtbl (play_seq) + frame_table. Порт SDLPoP seg006.c. Данные — + * kid_data.h (pop_extract_kid_data.py). + * + * K1: только проигрывание последовательностей кадров с авторскими + * смещениями (движение = SEQ_DX/DY + frame.dx/dy). Управление (K2) и + * коллизия карты (K3) — отдельно. + */ +#ifndef POP_KID_H +#define POP_KID_H + +#include + +/* Состояние персонажа (подмножество char_type, types.h). */ +typedef struct { + uint8_t frame; + uint8_t x, y; + int8_t direction; /* -1 влево, 0 вправо */ + int8_t curr_col, curr_row; + uint8_t action; + int8_t fall_x, fall_y; + uint8_t repeat; + uint16_t curr_seq; /* адрес в seqtbl (база SEQTBL_BASE) */ +} kid_t; + +extern kid_t Kid; + +/* Загрузить npages атласов Kid (kid0..kid{npages-1}.atl). ДО initgraph. + * 0 — OK, -1 — ошибка. */ +int pop_kid_load(uint8_t npages); +void pop_kid_free(void); + +/* Инициализировать Kid: последовательность seq_id, клетка col/row, + * направление dir (-1/0). Ставит x/y по x_bump/y_land. */ +void kid_init(uint8_t seq_id, int8_t col, int8_t row, int8_t dir); + +/* Один логический тик: play_seq() — продвинуть последовательность до + * следующего кадра (применить SEQ_DX/DY/ACTION/...). */ +void kid_tick(void); + +/* play_seq напрямую (для pop_map start_fall — переход в fall-seq в тот же + * physics-шаг, как оригинал). */ +void play_seq(void); + +/* Поля текущего кадра kid_frames[Kid.frame] (pop_map, без дублирования + * таблицы во втором TU). */ +int8_t kid_cur_dx(void); +uint8_t kid_cur_flags(void); + +/* Сменить последовательность (Kid.curr_seq = kid_seq_off[seq_id]) — единая + * точка для pop_ctrl/pop_map (иначе каждый дублировал бы kid_seqtbl ~2.3КБ). */ +void kid_set_seq(uint8_t seq_id); + +/* Нарисовать текущий кадр Kid (банк GFX_BANK_SPRITE — видео-ОЗУ, без + * теневой копии; запоминает прямоугольник для kid_heal). */ +void kid_draw(void); + +/* Восстановить фон (из теневой копии) на месте ПРЕДЫДУЩЕГО кадра Kid — + * вместо перерисовки всего экрана. Звать ПЕРЕД kid_tick/kid_draw. */ +void kid_heal(void); + +#endif diff --git a/applications/PoP/roomtest/pop_map.c b/applications/PoP/roomtest/pop_map.c new file mode 100644 index 0000000..54bf482 --- /dev/null +++ b/applications/PoP/roomtest/pop_map.c @@ -0,0 +1,382 @@ +/* + * pop_map.c — коллизия Kid с картой (Фаза K3.2: падение). Порт SDLPoP + * seg005/seg006. Пер-кадровый physics-диспетч check_action() по Char.action: + * grounded (stand/run/step/crouch) -> check_on_floor(): кадр с флагом + * FRAME_NEEDS_FLOOR (0x40), под персонажем не floor -> start_fall(); + * стена -> in_wall(); + * freefall (action 4) -> do_fall(): fall_y-ускорение (в fall_accel/speed), + * достиг y_land[curr_row+1] -> land()/inc_curr_row. + * + * Координатная система PoP: экран.left = 58, тайл = 14 ед. (TILE_SIZEX); + * col = (xpos-58)/14 (округл. вниз), xl = остаток; get_tile_div_mod_m7 = + * div_mod(xpos-7). Карта: fg[row*10+col] & 0x1F; col<0/>9,row<0/>2 = стена. + * + * MVP: без spike/HP/grab/gate — падение + мягкое/среднее приземление. + * Бег В стену (check_bumped) и осторожный шаг у края (get_edge_distance) — + * следующим шагом. + */ +#include +#include "pop_kid.h" +#include "pop_map.h" + +/* ---- Тайлы / действия / seq id (подмножество PoP) ------------------ */ +#define TILE_EMPTY 0 +#define TILE_WALL 20 +#define ACT_STAND 0 +#define ACT_RUNJUMP 1 +#define ACT_HANGCLIMB 2 +#define ACT_MIDAIR 3 +#define ACT_FREEFALL 4 +#define ACT_BUMPED 5 +#define ACT_HANGSTRAIGHT 6 + +#define SEQ_7_FALL 7 /* stepfall (stand/run/step/crouch) */ +#define SEQ_17_SOFT_LAND 17 +#define SEQ_18_FALL_STANDJUMP 18 +#define SEQ_19_FALL 19 /* stepfall2 (run frames 9/13) */ +#define SEQ_20_MEDIUM_LAND 20 +#define SEQ_21_FALL_RUNJUMP 21 +#define SEQ_47_BUMP 47 /* удар в стену: dx(-4) отскок + 50-52 + stand */ + +#define FRAME_NEEDS_FLOOR 0x40 +#define FRAME_WEIGHT_X 0x1F +#define TILE_SIZEX 14 +#define TILE_RIGHTX 13 +#define SCREENSPACE_X 58 +#define FALL_ACCEL 3 +#define FALL_MAX 33 + +#define TILE_MIDX 7 +#define FIRST_ONSCREEN_COLUMN 5 +#define FRAME_THIN 0x20 + +static const int16_t y_land[5] = {-8, 55, 118, 181, 244}; +static const int8_t dir_front[2] = {-1, 1}; /* [dir+1]: dir=-1->-1, 0->+1 */ +static const int16_t x_bump[20] = { + -12, 2, 16, 30, 44, 58, 72, 86, 100, 114, + 128, 142, 156, 170, 184, 198, 212, 226, 240, 254 +}; +/* wall_type(tile): gate/doortop=1, mirror=2, chomper=3, wall=4, иначе 0. */ +static const int8_t wall_dl[6] = {0, 10, 0, -1, 0, 0}; /* wall_dist_from_left */ +static const int8_t wall_dr[6] = {0, 0, 10, 13, 0, 0}; /* wall_dist_from_right */ +static int8_t g_infrontx; /* колонка «перед персонажем» (set в get_tile_infrontof) */ + +/* ---- Карта комнаты ------------------------------------------------- */ +uint8_t pop_fell_out; /* 1 = Kid упал за низ комнаты (сброс приложением) */ +static const uint8_t *g_fg; /* fg[30], row*10+col */ +static uint8_t g_curr_tile; /* = curr_tile2: тайл последнего get_tile */ +static uint8_t g_obj_xl; /* позиция внутри тайла (get_tile_div_mod) */ + +void pop_map_set(const uint8_t *fg) { g_fg = fg; } + +static uint8_t get_tile(int8_t col, int8_t row) +{ + if (col < 0 || col > 9 || row < 0 || row > 2) + g_curr_tile = TILE_WALL; /* край уровня = стена */ + else + g_curr_tile = g_fg[row * 10 + col] & 0x1F; + return g_curr_tile; +} + +static uint8_t tile_is_floor(uint8_t t) +{ + switch (t) { + case 0: case 9: case 12: case 20: + case 26: case 27: case 28: case 29: + return 0; + default: + return 1; + } +} + +static uint8_t get_tile_at_char(void) +{ + return get_tile(Kid.curr_col, Kid.curr_row); +} + +static uint8_t get_tile_infrontof_char(void) +{ + g_infrontx = (int8_t)(dir_front[Kid.direction + 1] + Kid.curr_col); + return get_tile(g_infrontx, Kid.curr_row); +} + +static uint8_t wall_type(uint8_t t) +{ + switch (t) { + case 4: case 7: case 12: return 1; /* стена справа (gate/doortop) */ + case 13: return 2; /* стена слева (mirror) */ + case 18: return 3; /* chomper */ + case 20: return 4; /* стена с обеих сторон */ + default: return 0; + } +} + +/* ---- Координаты ---------------------------------------------------- */ +static int char_dx_forward(int8_t dx) +{ + int d = dx; + if (Kid.direction < 0) d = -d; + return (int)Kid.x + d; +} + +/* col = (xpos-58)/14 округл. вниз; g_obj_xl = позиция в тайле (0..13). */ +static int8_t get_tile_div_mod(int xpos) +{ + int x = xpos - SCREENSPACE_X; + int xh = x / TILE_SIZEX; + int xl = x % TILE_SIZEX; + if (xl < 0) { --xh; xl += TILE_SIZEX; } /* округление вниз */ + g_obj_xl = (uint8_t)xl; + return (int8_t)xh; +} + +static int8_t get_tile_div_mod_m7(int xpos) { return get_tile_div_mod(xpos - 7); } + +/* «весовая» точка кадра (dx с вычетом weight-битов флагов), в направлении. */ +static int dx_weight(void) +{ + int8_t offset = (int8_t)(kid_cur_dx() - (kid_cur_flags() & FRAME_WEIGHT_X)); + return char_dx_forward(offset); +} + +static void determine_col(void) +{ + Kid.curr_col = get_tile_div_mod_m7(dx_weight()); +} + +/* расстояние до края тайла (для in_wall). */ +static int distance_to_edge(int xpos) +{ + int d; + get_tile_div_mod_m7(xpos); /* -> g_obj_xl */ + d = g_obj_xl; + if (Kid.direction == 0) d = TILE_RIGHTX - d; /* dir_0_right */ + return d; +} +static int distance_to_edge_weight(void) { return distance_to_edge(dx_weight()); } + +/* ---- ТОЧНАЯ коллизия со стеной (seg004) --------------------------- * + * Передний край персонажа char_x_*_coll = char_dx_forward(cur_frame.dx) + * (obj_x/2+58 сокращается ровно к этому; ширина спрайта НЕ нужна), ∓4 для + * THIN-кадров. dist_from_wall_forward — расстояние переднего края до грани + * стены tiletype в колонке tcol: >0 зазор, <=0 достал/зашёл (насколько). */ +static int char_front_coll(void) +{ + int cx = char_dx_forward(kid_cur_dx()); + if (kid_cur_flags() & FRAME_THIN) + cx += (Kid.direction < 0) ? 4 : -4; /* THIN: край внутрь */ + return cx; +} + +static int dist_from_wall_forward(uint8_t tiletype, int8_t tcol) +{ + uint8_t type = wall_type(tiletype); + int coll_left, cx; + if (type == 0) return 127; + coll_left = x_bump[tcol + FIRST_ONSCREEN_COLUMN] + TILE_MIDX; + cx = char_front_coll(); + if (Kid.direction < 0) /* лицом влево */ + return cx - (coll_left + TILE_RIGHTX - wall_dr[type]); + else /* лицом вправо */ + return wall_dl[type] + coll_left - cx; +} + +/* Выровнять передний край РОВНО к грани стены, если он зашёл за неё (bumped: + * Char.x += dist; char_dx_forward отражает знак для обоих направлений). + * Стена ищется под персонажем, иначе перед ним. */ +static void in_wall(void) +{ + uint8_t t; int8_t tcol; int d; + t = get_tile_at_char(); tcol = Kid.curr_col; + if (!wall_type(t)) { t = get_tile_infrontof_char(); tcol = g_infrontx; } + if (!wall_type(t)) return; + d = dist_from_wall_forward(t, tcol); + if (d < 0) { + Kid.x = (uint8_t)char_dx_forward((int8_t)d); /* край -> грань */ + determine_col(); + get_tile_at_char(); + } +} + +/* ---- Падение ------------------------------------------------------- */ +static void inc_curr_row(void) { Kid.curr_row++; } + +static void land(void) +{ + uint8_t seq; + Kid.y = (uint8_t)y_land[Kid.curr_row + 1]; + /* к краю пола — чуть назад (как оригинал) */ + if (!tile_is_floor(get_tile_infrontof_char()) && distance_to_edge_weight() < 3) + Kid.x = (uint8_t)char_dx_forward(-3); + if (Kid.fall_y < 22) + seq = SEQ_17_SOFT_LAND; /* 1 этаж — мягко */ + else + seq = SEQ_20_MEDIUM_LAND; /* 2+ этажа — присесть (HP/смерть — позже) */ + Kid.fall_x = Kid.fall_y = 0; + kid_set_seq(seq); + play_seq(); + determine_col(); +} + +static void start_fall(void) +{ + uint8_t frame = Kid.frame, seq; + inc_curr_row(); + if (frame == 9 || frame == 13) seq = SEQ_19_FALL; /* падение из бега */ + else if (frame == 26) seq = SEQ_18_FALL_STANDJUMP; + else if (frame == 44) seq = SEQ_21_FALL_RUNJUMP; + else seq = SEQ_7_FALL; /* stand/step/crouch */ + kid_set_seq(seq); + play_seq(); + determine_col(); + if (get_tile_at_char() == TILE_WALL) + in_wall(); +} + +static void do_fall(void) +{ + uint8_t nrow = (uint8_t)(Kid.curr_row + 1); + if (nrow > 4) nrow = 4; /* защита y_land[] от выхода */ + if ((uint16_t)y_land[nrow] > (uint16_t)Kid.y) { + /* ещё летит (check_grab — K4) */ + } else { + if (get_tile_at_char() == TILE_WALL) + in_wall(); + if (tile_is_floor(g_curr_tile)) + land(); + else if (Kid.curr_row < 2) + inc_curr_row(); /* следующий ряд ВНУТРИ комнаты */ + else + pop_fell_out = 1; /* упал за низ комнаты (нет пола, + комнаты снизу нет) — приложение + сбросит Kid (K-later: переход) */ + } +} + +static void fall_accel(void) +{ + if (Kid.action == ACT_FREEFALL) { + int fy = (uint8_t)Kid.fall_y + FALL_ACCEL; + if (fy > FALL_MAX) fy = FALL_MAX; + Kid.fall_y = (int8_t)fy; + } +} + +static void fall_speed(void) +{ + Kid.y = (uint8_t)(Kid.y + (uint8_t)Kid.fall_y); + if (Kid.action == ACT_FREEFALL) { + Kid.x = (uint8_t)char_dx_forward(Kid.fall_x); + determine_col(); + } +} + +/* ---- get_edge_distance (seg004:067C) — ТОЧНО ---------------------- * + * Расстояние переднего края до стены/края впереди + тип g_edge_type: WALL + * (стена в пределах тайла), FLOOR (пол — далеко, 11), CLOSER (край уступа + * над ямой — distance_to_edge_weight). Стена сперва под персонажем, потом + * перед ним; dist<=TILE_RIGHTX -> WALL, иначе FLOOR. */ +static uint8_t g_edge_type; + +static int edge_classify(int d) +{ + if (d <= TILE_RIGHTX) g_edge_type = EDGE_WALL; + else { g_edge_type = EDGE_FLOOR; d = 11; } + return d; +} + +int pop_edge_distance(void) +{ + uint8_t t; int d; + determine_col(); + t = get_tile_at_char(); + if (wall_type(t)) { + d = dist_from_wall_forward(t, Kid.curr_col); + if (d >= 0) return edge_classify(d); + } + t = get_tile_infrontof_char(); + if (wall_type(t)) { + d = dist_from_wall_forward(t, g_infrontx); + if (d >= 0) return edge_classify(d); + } + if (tile_is_floor(t)) { g_edge_type = EDGE_FLOOR; return 11; } + g_edge_type = EDGE_CLOSER; /* яма впереди — до кромки уступа */ + return distance_to_edge_weight(); +} + +uint8_t pop_edge_type(void) { return g_edge_type; } + +/* Тайл НАД персонажем (curr_row-1, curr_col) сплошной (стена/пол)? — выбор + * seq прыжка вверх: solid -> seq_14 (в потолок), иначе seq_28 (пусто сверху). + * K4.1 упрощение: без grab-логики (уступ сверху-впереди) — та в K4.2. */ +uint8_t pop_above_solid(void) +{ + uint8_t t = get_tile(Kid.curr_col, (int8_t)(Kid.curr_row - 1)); + return (t == TILE_WALL || tile_is_floor(t)); +} + +/* Стена впереди достаточно близко для стопа бега? Детект (без сдвига — + * позицию держит check_bumped=in_wall). d<=2: передний край почти у грани. */ +int pop_wall_ahead(void) +{ + int d = pop_edge_distance(); + return (g_edge_type == EDGE_WALL && d <= 2); +} + +/* ---- Диспетчер ----------------------------------------------------- */ +static void check_on_floor(void) +{ + if (kid_cur_flags() & FRAME_NEEDS_FLOOR) { + if (get_tile_at_char() == TILE_WALL) + in_wall(); /* забежал в стену -> выровнять */ + if (!tile_is_floor(g_curr_tile)) + start_fall(); + } +} + +static void check_action(void) +{ + uint8_t action = Kid.action, frame = Kid.frame; + if (action == ACT_BUMPED || action == ACT_HANGSTRAIGHT) { + if (frame == 109 || (frame >= 110 && frame <= 119)) + check_on_floor(); + } else if (action == ACT_FREEFALL) { + do_fall(); + } else if (action == ACT_MIDAIR) { + /* frames 102..105: check_grab — K4 */ + } else if (action != ACT_HANGCLIMB) { + check_on_floor(); + } +} + +/* check_bumped (seg004): если передний край ЗАШЁЛ за грань стены — выровнять + * к грани И проиграть отскок seq_47_bump (dx−4 назад + кадры 50-52 + stand), + * как в оригинале (bumped_floor). Это даёт «шаг назад со звуком» при беге/ + * шаге в стену вместо падения. Уже в отскоке (action bumped) — только + * выравниваем, без рестарта. В падении/висе не трогаем. */ +static void check_bumped(void) +{ + uint8_t t; int8_t tcol; int d; + if (Kid.action == ACT_FREEFALL || Kid.action == ACT_HANGCLIMB) return; + t = get_tile_at_char(); tcol = Kid.curr_col; + if (!wall_type(t)) { t = get_tile_infrontof_char(); tcol = g_infrontx; } + if (!wall_type(t)) return; + d = dist_from_wall_forward(t, tcol); + if (d >= 0) return; /* ещё не зашёл за грань */ + Kid.x = (uint8_t)char_dx_forward((int8_t)d); /* край -> грань */ + determine_col(); + if (Kid.action == ACT_BUMPED) return; /* уже в отскоке — не рестартить */ + Kid.fall_y = 0; + kid_set_seq(SEQ_47_BUMP); /* отскок (TODO: bumped_sound) */ + play_seq(); + determine_col(); +} + +void pop_phys_tick(void) +{ + fall_accel(); + fall_speed(); + determine_col(); + check_bumped(); /* удержать у стены до check_action (порядок PoP) */ + check_action(); +} diff --git a/applications/PoP/roomtest/pop_map.h b/applications/PoP/roomtest/pop_map.h new file mode 100644 index 0000000..bf804b2 --- /dev/null +++ b/applications/PoP/roomtest/pop_map.h @@ -0,0 +1,48 @@ +/* + * pop_map.h — коллизия Kid с картой комнаты (Фаза K3): тайловые запросы + * + физика падения + приземление. Порт SDLPoP seg005/seg006 + * (check_on_floor/start_fall/do_fall/fall_accel/fall_speed/land/in_wall). + * + * K3.2 (падение): Kid, сойдя/сбежав с края пола (тайл под ним не floor), + * начинает падать (fall_y-ускорение, Char.y растёт), приземляется на пол + * ниже (soft/medium land) или продолжает вниз. Стена под персонажем — + * in_wall (выталкивание). Бег В стену (check_bumped) — отдельным шагом. + */ +#ifndef POP_MAP_H +#define POP_MAP_H + +#include + +/* Задать карту текущей комнаты: fg[30] = коды тайлов (row*10+col, 3×10). + * col<0/>9 или row<0/>2 трактуются как стена (край уровня). */ +void pop_map_set(const uint8_t *fg); + +/* Установлен в 1, когда Kid упал ниже комнаты (комнаты снизу нет в этом + * тесте). Приложение должно сбросить Kid (kid_init) и снять флаг. */ +extern uint8_t pop_fell_out; + +/* Пер-кадровая физика ПОСЛЕ kid_tick(play_seq): fall_accel + fall_speed + + * determine_col + check_action (триггер падения / do_fall / приземление). + * Порядок как в оригинале (seg000 play_kid_frame). */ +void pop_phys_tick(void); + +/* Тип препятствия/края ВПЕРЕДИ по направлению (pop_edge_type после + * pop_edge_distance): для control (бежать / осторожный шаг / стоп у стены). */ +#define EDGE_CLOSER 0 /* край уступа над ямой (дистанция = до кромки) */ +#define EDGE_WALL 1 /* стена впереди/под персонажем */ +#define EDGE_FLOOR 2 /* пол (нет препятствия рядом, дистанция = 11) */ + +/* Расстояние до края/стены впереди + побочно g_edge_type (pop_edge_type). */ +int pop_edge_distance(void); +uint8_t pop_edge_type(void); + +/* Стена впереди в пределах стопа? Если да — прижимает Kid.x к корректной + * позиции (передний край у стены, симметрично) и возвращает 1. Для + * control_running (чистый стоп у стены). */ +int pop_wall_ahead(void); + +/* Тайл над персонажем сплошной (стена/пол)? — выбор seq вертикального + * прыжка вверх (K4.1). */ +uint8_t pop_above_solid(void); + +#endif diff --git a/applications/PoP/roomtest/room1_data.h b/applications/PoP/roomtest/room1_data.h new file mode 100644 index 0000000..d0cc8de --- /dev/null +++ b/applications/PoP/roomtest/room1_data.h @@ -0,0 +1,10 @@ +/* room1_data.h — данные комнаты 1 ур.1 (res2001) + левый столбец + * соседа (room 5 col9) для границы-решётки. Сгенерировано + * gen из render_room; fg=foretable code, bg=backtable modifier. */ +#ifndef ROOM1_DATA_H +#define ROOM1_DATA_H +static const unsigned char room1_fg[30] = {0x00,0x00,0x00,0x01,0x01,0x01,0x01,0x01,0x14,0x14,0x13,0x13,0x01,0x03,0x00,0x14,0x14,0x14,0x14,0x14,0x14,0x14,0x14,0x14,0x0E,0x03,0x0B,0x01,0x01,0x14}; +static const unsigned char room1_bg[30] = {0xFF,0xFF,0xFF,0x00,0x01,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x00}; +static const unsigned char leftcol_fg[3] = {0x04,0x14,0x14}; +static const unsigned char leftcol_bg[3] = {0x01,0x00,0x00}; +#endif diff --git a/applications/PoP/roomtest/roomtest.c b/applications/PoP/roomtest/roomtest.c new file mode 100644 index 0000000..b9ab979 --- /dev/null +++ b/applications/PoP/roomtest/roomtest.c @@ -0,0 +1,79 @@ +/* + * roomtest.c — проверка порта статического фона (Шаг 3): рисует комнату 1 + * уровня 1 реальной композицией слоёв (pop_bg.c) и держит на экране. + * Сверять на MAME с эталоном applications/PoP/toolchain/1.1-2.png. + */ +#include +#include +#include +#include +#include +#include +#include "pop_bg.h" +#include "pop_kid.h" +#include "pop_ctrl.h" +#include "pop_map.h" +#include "room1_data.h" + +/* seq_2_stand (types.h seqids) — Kid стоит, ждёт ввода. */ +#define SEQ_STAND 2 + +int main(void) +{ + if (pop_bg_load() != 0) { /* атласы — ДО initgraph */ + puts("pop_bg_load failed"); + return 1; + } + if (pop_kid_load(28) != 0) { /* ВСЕ страницы: бег+стоп+разворот+присед+прыжок */ + puts("kid atlas load failed"); + return 1; + } + + initgraph(); + gfx_set_draw_page(0); + gfx_set_visible_page(0); + if (gfx_pal_fload(0, "kid.pal") < 0) /* единая игровая палитра */ + gfx_pal_fload(0, "a:\\kid.pal"); + gfx_pal_sync(); + + /* Фон комнаты рисуется ОДИН РАЗ — в видео-ОЗУ И теневую копию + * (GFX_BANK_TRANSPARENT внутри pop_room_draw). Дальше каждый тик + * восстанавливаем ТОЛЬКО прямоугольник Кида из теневой копии (heal), + * а не весь экран. */ + setfillstyle(SOLID_FILL, BLACK); + bar(0, 0, 319, 255); + pop_room_draw(1, room1_fg, room1_bg, leftcol_fg, leftcol_bg); + + /* K2b: Kid стоит на уступе (row1), управляется с клавиатуры. + * ←/→ бег, ↓ присед, ↑+→ прыжок с места, Shift шаг (K3). ESC — выход. */ + pop_map_set(room1_fg); /* карта для коллизии/падения (K3) */ + kid_init(SEQ_STAND, 1, 1, 0); + kid_draw(); + + if (pop_ctrl_init() != 0) { /* raw-канал клавиатуры (забирает ввод у DSS) */ + closegraph(); + puts("kbd_raw open failed"); + return 1; + } + + while (!kbd_raw_down(KBD_ESC)) { + gfx_wait_vsync(); + gfx_wait_vsync(); + gfx_wait_vsync(); + pop_ctrl_tick(); /* ввод -> control(): смена seq */ + kid_heal(); /* стереть прошлый кадр (фон из тени) */ + kid_tick(); /* play_seq: следующий кадр */ + pop_phys_tick(); /* K3: падение/приземление/стена */ + if (pop_fell_out) { /* упал за низ комнаты — сброс на старт */ + pop_fell_out = 0; + kid_init(SEQ_STAND, 1, 1, 0); + } + kid_draw(); /* нарисовать новый кадр */ + } + + pop_ctrl_close(); + closegraph(); + pop_bg_free(); + pop_kid_free(); + return 0; +} diff --git a/applications/PoP/toolchain/1.1 b/applications/PoP/toolchain/1.1 new file mode 100644 index 0000000..b71abdc Binary files /dev/null and b/applications/PoP/toolchain/1.1 differ diff --git a/applications/PoP/toolchain/1.1-2.png b/applications/PoP/toolchain/1.1-2.png new file mode 100644 index 0000000..9c8a8e9 Binary files /dev/null and b/applications/PoP/toolchain/1.1-2.png differ diff --git a/applications/PoP/toolchain/pop_extract_kid_data.py b/applications/PoP/toolchain/pop_extract_kid_data.py new file mode 100644 index 0000000..b4f93a8 --- /dev/null +++ b/applications/PoP/toolchain/pop_extract_kid_data.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +""" +pop_extract_kid_data.py — извлекает данные анимации Kid из исходников +SDLPoP в C-заголовок roomtest/kid_data.h (Фаза K1). + +Извлекает: + frame_table_kid[] (seg006.c) -> kframe kid_frames[] {image,dx,dy,flags} + original_seqtbl[] (seqtbl.c) -> byte kid_seqtbl[] (2310 Б, база 0x196E) + original_seqtbl_offsets[] (seqtbl.c) -> word kid_seq_off[] (seq_id -> адрес) + +seqtbl адресуется базой SEQTBL_BASE=0x196E: элемент = kid_seqtbl[addr-0x196E]. +Автогенерация — чтобы не переписывать ~180 кадров + 2КБ байткода руками. +""" +import os +import re + +SRC = "/Users/alex/Projects/DIY/Z80/Sprinter/C-Compiler/applications/PoP/SDLPoP/src" +OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), + "..", "roomtest", "kid_data.h") +SEQTBL_BASE = 0x196E + + +def read(name): + return open(os.path.join(SRC, name)).read() + + +def parse_frame_table(txt): + """frame_table_kid[] -> список (image, dx, dy, flags). Записи вида + { IMG, 0xC0| 4, DX, DY, 0x40| 4 } — sword/flags как OR-выражения.""" + m = re.search(r'const frame_type frame_table_kid\[\]\s*=\s*\{(.*?)\n\};', txt, re.S) + body = m.group(1) + frames = [] + for row in re.findall(r'\{([^}]*)\}', body): + parts = [p.strip() for p in row.split(',')] + image = int(parts[0], 0) + sword = eval(parts[1], {}, {}) # напр. "0x00| 9" + dx = int(parts[2], 0) + dy = int(parts[3], 0) + flags = eval(parts[4], {}, {}) + frames.append((image & 0xFF, dx & 0xFF, dy & 0xFF, flags & 0xFF, sword & 0xFF)) + return frames + + +def parse_byte_array(txt, name): + m = re.search(r'const byte %s\[\]\s*=\s*\{(.*?)\}' % name, txt, re.S) + return [int(x, 0) for x in re.findall(r'0x[0-9A-Fa-f]+|\d+', m.group(1))] + + +def parse_word_array(txt, name): + m = re.search(r'const word %s\[\]\s*=\s*\{(.*?)\}' % name, txt, re.S) + return [int(x, 0) for x in re.findall(r'0x[0-9A-Fa-f]+|\d+', m.group(1))] + + +def emit_c(vals, per_line=12, fmt="0x%02X"): + out = [] + for i in range(0, len(vals), per_line): + out.append(" " + ",".join(fmt % v for v in vals[i:i + per_line]) + ",") + return "\n".join(out) + + +def main(): + seg006 = read("seg006.c") + seqc = read("seqtbl.c") + + frames = parse_frame_table(seg006) + seqtbl = parse_byte_array(seqc, "original_seqtbl") + offs = parse_word_array(seqc, "original_seqtbl_offsets") + + with open(OUT, "w") as f: + f.write("/* kid_data.h — данные анимации Kid (frame_table + seqtbl +\n" + " * offsets), извлечено toolchain/pop_extract_kid_data.py из\n" + " * SDLPoP. НЕ править вручную. seqtbl адресуется базой\n" + " * SEQTBL_BASE: kid_seqtbl[addr - SEQTBL_BASE]. */\n") + f.write("#ifndef KID_DATA_H\n#define KID_DATA_H\n#include \n\n") + f.write("#define SEQTBL_BASE 0x%04Xu\n\n" % SEQTBL_BASE) + + # frame table + f.write("/* {image, dx, dy, flags, sword}; chtab = sword>>6 (0=kid). */\n") + f.write("typedef struct { uint8_t image; int8_t dx, dy; " + "uint8_t flags, sword; } kframe;\n") + f.write("#define KID_NFRAMES %d\n" % len(frames)) + f.write("static const kframe kid_frames[KID_NFRAMES] = {\n") + for (img, dx, dy, fl, sw) in frames: + dxs = dx - 256 if dx > 127 else dx + dys = dy - 256 if dy > 127 else dy + f.write(" {%3d,%4d,%4d,0x%02X,0x%02X},\n" % (img, dxs, dys, fl, sw)) + f.write("};\n\n") + + # seqtbl bytes + f.write("#define KID_SEQTBL_LEN %d\n" % len(seqtbl)) + f.write("static const uint8_t kid_seqtbl[KID_SEQTBL_LEN] = {\n") + f.write(emit_c(seqtbl) + "\n};\n\n") + + # seq offsets + f.write("#define KID_NSEQ %d\n" % len(offs)) + f.write("static const uint16_t kid_seq_off[KID_NSEQ] = {\n") + f.write(emit_c(offs, 8, "0x%04X") + "\n};\n\n") + f.write("#endif\n") + + print(f"kid_data.h: {len(frames)} кадров, seqtbl {len(seqtbl)} Б, " + f"{len(offs)} seq-оффсетов -> {OUT}") + + +if __name__ == "__main__": + main() diff --git a/applications/PoP/toolchain/pop_pack_bg.py b/applications/PoP/toolchain/pop_pack_bg.py new file mode 100644 index 0000000..27aea11 --- /dev/null +++ b/applications/PoP/toolchain/pop_pack_bg.py @@ -0,0 +1,288 @@ +#!/usr/bin/env python3 +""" +pop_pack_bg.py — упаковщик спрайтов СТАТИЧЕСКОГО ФОНА PoP (Шаг 2 порта). + +Что делает: + 1. Определяет РЕАЛЬНО используемый набор спрайтов фона (прогоняет + render_room.py по всем комнатам всех уровней, собирает (chtab,id)). + 2. Грузит исходные PNG (VDUNGEON→VPALACE каскад, как в игре), переводит + 16-цветный индексированный пиксель в 8bpp getimage-блоб: + индекс 0 -> 0xFF (прозрачность getimage) + индекс i -> 0x50+i (env, chtab_6) / 0x60+i (wall, chtab_7) + 3. Пакует в атласы .atl с ПРЯМОЙ адресацией (idx = id), каталог любого + размера (блобы кладутся ПОСЛЕ каталога — загрузчик atlas_image читает + offset явно, лимит 19 из libbgi/mkatlas.py не действует). libbgi НЕ + трогаем. Дырки в каталоге (неиспользуемые id) = фейковые записи, + ссылающиеся на общий пустой 0×0-блоб (осознанная трата места в EMM + ради нулевых remap-таблиц в W2 — требование пользователя). + 4. Собирает палитру Sprinter (.pal, записи B,G,R,0) из res200.pal (env + -> слоты 0x50..0x5F) и res360.pal (wall -> 0x60..0x6F). VGA 6-бит + каналы масштабируются в 8-бит. + 5. Пишет C-заголовок с раскладкой (имена файлов, SHIFT, число страниц, + набор fore-id). + +Раскладка (прямая адресация, SHIFT=5): + ENV фон : 5 страниц, страница = id>>5, idx = id&31 + WALL : 1 страница, idx = id + FORE : 1 страница (спрайты «перед персонажем»: фронты столбов/ + ворот/дебриса), idx = id — отдельный атлас (жизненный цикл + другой: рисуются каждый кадр как sprite_t, не запекаются). +W2-цена: 7 хэндлов atlas_t (~21 Б), ноль таблиц id->позиция. + +Выход: в applications/PoP/poc/res/bg/ — *.atl + pop_bg.pal + pop_bg_atlas.h +""" +import os +import sys +from PIL import Image + +HERE = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, HERE) +import render_room as R # noqa: E402 (экстрактор + каскад загрузки) + +OUT_DIR = os.path.join(HERE, "..", "poc", "res", "bg") + +# Спрайты FORE-слоя (add_foretable в seg008 — «перед персонажем»): фронты +# столбов, ворот, дебриса. См. tile_table.fore_id + анализ pop_background_strategy. +FORE_ENV_IDS = {9, 12, 49, 88, 91, 95, 100} + +ENV_SHIFT = 5 # env фон: страница = id>>5, idx = id&31 +ENV_MASK = (1 << ENV_SHIFT) - 1 + +# ---- .atl формат (== libbgi, но каталог не ограничен 19) -------------- +ATL_MAGIC = b"SPA1" +ATL_DIR_OFF = 0x68 # _ATL_DIR_OFF +ATL_MIN_SIZE = 0x100 # atlas_load требует n >= _ATL_DATA_OFF +PAGE_SIZE = 16384 + + +def die(msg): + sys.exit(f"pop_pack_bg: {msg}") + + +# ---- 1. Набор используемых спрайтов ---------------------------------- +def collect_used(): + """Прогон render_room по всем комнатам -> множество (chtab, id).""" + used = set() + orig = R.load_sprite + + def spy(id, chtab=R.CHTAB_ENV, reason=""): + img = orig(id, chtab, reason) + if img is not None and id > 0: + used.add((chtab, id)) + return img + + R.load_sprite = spy + for lvl in range(1, 15): + fn = os.path.join(R.LEVEL_DIR, f"res{2000 + lvl}.bin") + if not os.path.exists(fn): + continue + lv = R.Level(open(fn, "rb").read()) + for room in range(1, 25): + rm = R.Room(lv, room) + L = lv.links[room][0] + if L: + rm.left_room = R.Room(lv, L) + canvas = Image.new("RGBA", (320, 192), (0, 0, 0, 0)) + for dr in (2, 1, 0): + for c in range(10): + R.draw_tile(canvas, rm, dr, c) + R.load_sprite = orig + return used + + +# ---- 2. Загрузка спрайта в 8bpp-блоб --------------------------------- +def load_indexed(chtab, id): + """PNG (mode P, 16 цветов) -> (w, h, bytes 8bpp). Каскад VDUNGEON->VPALACE. + Пиксель 0 -> 0xFF (прозрач); i -> base+i (env 0x50 / wall 0x60).""" + base_res = 360 if chtab == R.CHTAB_WALL else 200 + pal_base = 0x60 if chtab == R.CHTAB_WALL else 0x50 + fname = f"res{base_res + id}.png" + path = None + for d in (R.VDUNGEON_DIR, R.VPALACE_DIR): + p = os.path.join(d, fname) + if os.path.exists(p): + path = p + break + if path is None: + die(f"нет файла {fname}") + im = Image.open(path) + if im.mode != "P": + die(f"{fname}: mode {im.mode}, ожидался P (16-цветный)") + src = im.tobytes() + out = bytes((0xFF if v == 0 else pal_base + v) for v in src) + return im.width, im.height, out + + +def blob(w, h, pix): + """getimage-блоб: u16 w, u16 h (LE) + пиксели.""" + return w.to_bytes(2, "little") + h.to_bytes(2, "little") + pix + + +# ---- 3. Пакер .atl (каталог любого размера) -------------------------- +def pack_atlas(path, entries): + """entries: dict idx -> (w, h, pix). Пустые idx (0..maxidx) заполняются + фейковой ссылкой на общий 0×0-блоб. Каталог count записей с 0x68, + блобы — сразу после каталога.""" + if not entries: + die(f"{path}: пустой атлас") + count = max(entries) + 1 + if count > 255: + die(f"{path}: count {count} > 255") + data_off = ATL_DIR_OFF + count * 8 + + blobs = bytearray() + empty_off = data_off + len(blobs) # общий 0×0-блоб для дырок + blobs += blob(0, 0, b"") + dir_bytes = bytearray(count * 8) + for idx in range(count): + e = entries.get(idx) + if e is None: + off = empty_off + fw = fh = 0 + else: + w, h, pix = e + off = data_off + len(blobs) + blobs += blob(w, h, pix) + fw = min(w, 255) + fh = min(h, 255) + d = idx * 8 + dir_bytes[d:d + 2] = off.to_bytes(2, "little") + dir_bytes[d + 2] = fw + dir_bytes[d + 3] = fh + dir_bytes[d + 4] = 1 # nx + dir_bytes[d + 5] = 1 # ny + # d+6,d+7 резерв + + out = bytearray(data_off) + out[0:4] = ATL_MAGIC + out[4] = count + out[ATL_DIR_OFF:ATL_DIR_OFF + len(dir_bytes)] = dir_bytes + out += blobs + if len(out) < ATL_MIN_SIZE: + out += bytes(ATL_MIN_SIZE - len(out)) # atlas_load: n >= 0x100 + if len(out) > PAGE_SIZE: + die(f"{path}: {len(out)} Б > {PAGE_SIZE} (одна EMM-страница)") + with open(path, "wb") as f: + f.write(out) + return count, len(out) + + +# ---- 4. Палитра ------------------------------------------------------ +def scale6to8(v): + return (v * 255) // 63 + + +def build_palette(out_path): + """256-записная палитра B,G,R,0: env@0x50..0x5F, wall@0x60..0x6F.""" + def read_pal(res, d): + b = open(os.path.join(d, res), "rb").read() + return [(b[4 + i * 3], b[4 + i * 3 + 1], b[4 + i * 3 + 2]) for i in range(16)] + + env = read_pal("res200.pal", R.VDUNGEON_DIR) + wall = read_pal("res360.pal", R.VDUNGEON_DIR) + pal = bytearray(256 * 4) + for i, (r, g, b) in enumerate(env): + o = (0x50 + i) * 4 + pal[o:o + 4] = bytes((scale6to8(b), scale6to8(g), scale6to8(r), 0)) + for i, (r, g, b) in enumerate(wall): + o = (0x60 + i) * 4 + pal[o:o + 4] = bytes((scale6to8(b), scale6to8(g), scale6to8(r), 0)) + with open(out_path, "wb") as f: + f.write(pal) + + +# ---- 5. C-заголовок -------------------------------------------------- +def write_header(out_path, env_pages, wall_name, fore_name): + lines = [ + "/* pop_bg_atlas.h — раскладка атласов статического фона PoP.", + " * Сгенерировано toolchain/pop_pack_bg.py — НЕ править вручную.", + " *", + " * Прямая адресация (ноль remap-таблиц в W2):", + " * ENV фон id N -> atlas env_bg[N>>%d], idx N&%d" % (ENV_SHIFT, ENV_MASK), + " * WALL id N -> atlas wall, idx N", + " * FORE id N -> atlas fore, idx N", + " */", + "#ifndef POP_BG_ATLAS_H", + "#define POP_BG_ATLAS_H", + "", + "#define POP_ENV_SHIFT %d" % ENV_SHIFT, + "#define POP_ENV_MASK %d" % ENV_MASK, + "#define POP_ENV_PAGES %d" % len(env_pages), + "", + "/* Палитра: env-слоты, wall-слоты (сприйт-пиксель i -> база+i). */", + "#define POP_PAL_ENV 0x50", + "#define POP_PAL_WALL 0x60", + "", + "/* Имена файлов атласов (грузятся atlas_load). */", + "static const char *const pop_env_atl[POP_ENV_PAGES] = {", + ] + for pg in range(len(env_pages)): + nm = env_pages[pg] + lines.append(' %s,' % ('"%s"' % nm if nm else "0 /* пусто */")) + lines += [ + "};", + '#define POP_WALL_ATL "%s"' % wall_name, + '#define POP_FORE_ATL "%s"' % fore_name, + '#define POP_BG_PAL "pop_bg.pal"', + "", + "#endif", + "", + ] + with open(out_path, "w") as f: + f.write("\n".join(lines)) + + +def main(): + os.makedirs(OUT_DIR, exist_ok=True) + used = collect_used() + env_ids = sorted(id for ch, id in used if ch != R.CHTAB_WALL) + wall_ids = sorted(id for ch, id in used if ch == R.CHTAB_WALL) + + env_bg = [i for i in env_ids if i not in FORE_ENV_IDS] + env_fore = [i for i in env_ids if i in FORE_ENV_IDS] + + # ENV фон -> страницы по id>>SHIFT + pages = {} + for id in env_bg: + w, h, pix = load_indexed(R.CHTAB_ENV, id) + pages.setdefault(id >> ENV_SHIFT, {})[id & ENV_MASK] = (w, h, pix) + max_pg = max(pages) if pages else -1 + env_page_names = [] + total_bytes = 0 + for pg in range(max_pg + 1): + if pg in pages: + nm = f"pop_env{pg}.atl" + cnt, sz = pack_atlas(os.path.join(OUT_DIR, nm), pages[pg]) + total_bytes += sz + env_page_names.append(nm) + print(f" {nm}: {len(pages[pg])} спрайтов, каталог {cnt}, {sz} Б") + else: + env_page_names.append(None) + print(f" (env стр {pg}: пусто)") + + # WALL -> idx=id + wall_entries = {id: load_indexed(R.CHTAB_WALL, id) for id in wall_ids} + cnt, sz = pack_atlas(os.path.join(OUT_DIR, "pop_wall.atl"), wall_entries) + total_bytes += sz + print(f" pop_wall.atl: {len(wall_ids)} спрайтов, каталог {cnt}, {sz} Б") + + # FORE -> idx=id + fore_entries = {id: load_indexed(R.CHTAB_ENV, id) for id in env_fore} + cnt, sz = pack_atlas(os.path.join(OUT_DIR, "pop_fore.atl"), fore_entries) + total_bytes += sz + print(f" pop_fore.atl: {len(env_fore)} спрайтов, каталог {cnt}, {sz} Б") + + build_palette(os.path.join(OUT_DIR, "pop_bg.pal")) + write_header(os.path.join(OUT_DIR, "pop_bg_atlas.h"), + env_page_names, "pop_wall.atl", "pop_fore.atl") + + n_pages = sum(1 for n in env_page_names if n) + 2 + print(f"ИТОГО: {len(env_bg)} env-фон + {len(wall_ids)} wall + " + f"{len(env_fore)} fore = {len(env_bg)+len(wall_ids)+len(env_fore)} спрайтов") + print(f" {n_pages} EMM-страниц, {total_bytes} Б на диске " + f"(+1КБ палитра), W2: {n_pages}×atlas_t = {n_pages*3} Б хэндлов") + print(f" -> {OUT_DIR}") + + +if __name__ == "__main__": + main() diff --git a/applications/PoP/toolchain/pop_pack_kid.py b/applications/PoP/toolchain/pop_pack_kid.py new file mode 100644 index 0000000..4baf7ef --- /dev/null +++ b/applications/PoP/toolchain/pop_pack_kid.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +""" +pop_pack_kid.py — упаковщик спрайтов персонажа Kid (Фаза K0 порта). + +Аналог pop_pack_bg.py, но для chtab_2_kid (база ресурса 400): image id N += res(400+N).png (data/KID), палитра res400.pal (16 цветов) -> слоты +Sprinter 0x70-0x7F (env 0x50 / wall 0x60 заняты фоном). Пиксель i: +0->0xFF (прозрач), i->0x70+i. + +Прямая адресация (ноль remap в W2): kid[img>>5], idx img&31. Дырки — +фейковые записи (как в фоне). Переиспользует pack_atlas/blob/scale6to8 +из pop_pack_bg. + +Выход: poc/res/kid/kid0..N.atl + kid.pal + kid_atlas.h +""" +import os +import sys +from PIL import Image + +HERE = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, HERE) +import pop_pack_bg as B # pack_atlas, blob, scale6to8, ENV_SHIFT + +KID_DIR = "/Users/alex/Projects/DIY/Z80/Sprinter/C-Compiler/applications/PoP/SDLPoP/data/KID" +OUT_DIR = os.path.join(HERE, "..", "poc", "res", "kid") +PAL_BASE = 0x70 # слоты палитры Sprinter под Kid +SHIFT = 3 # kid[img>>3], idx img&7 (Kid-кадры крупные: 8/стр ≤16КБ) +MASK = (1 << SHIFT) - 1 + + +def transpose_cols(w, h, rowmajor): + """row-major (pix[row*w+col]) -> column-major (out[col*h+row]). + Спрайты Kid хранятся КОЛОНКАМИ: gfx_blit_cols рисует вертикальным accel- + проходом (даёт бесплатный горизонтальный флип реверсом порядка колонок; + см. memory/accel_vertical_copy). Заголовок getimage (w,h) не меняется.""" + out = bytearray(w * h) + for col in range(w): + base = col * h + for row in range(h): + out[base + row] = rowmajor[row * w + col] + return bytes(out) + + +def load_kid(aidx): + """Атлас-индекс aidx (== frame.image) -> res(401+aidx).png -> (w, h, 8bpp + COLUMN-MAJOR). 0->0xFF, i->0x70+i. Пиксели транспонированы в колонки. + + МАППИНГ (seg008.c:1589): персонаж рисуется add_midtable(chtab, obj_id+1), + внутри get_image(chtab, id-1)=get_image(chtab, obj_id) -> chtab-индекс = + frame.image; первая картинка chtab_2_kid = res401 (res400 = палитра). + Значит frame.image=N -> res(401+N). Раньше грузили res(400+N) — off-by-one: + стойка frame_15 (image 14) рисовалась res414 (беговой наклон) вместо res415 + (прямая стойка).""" + path = os.path.join(KID_DIR, f"res{401 + aidx}.png") + if not os.path.exists(path): + return None + im = Image.open(path) + if im.mode != "P": + B.die(f"res{401+aidx}: mode {im.mode}, ожидался P") + src = im.tobytes() + rowmajor = bytes((0xFF if v == 0 else PAL_BASE + v) for v in src) + return im.width, im.height, transpose_cols(im.width, im.height, rowmajor) + + +def _read16(path): + b = open(path, "rb").read() + return [(b[4 + i * 3], b[4 + i * 3 + 1], b[4 + i * 3 + 2]) for i in range(16)] + + +def build_palette(out_path): + """ЕДИНАЯ игровая палитра (gfx_pal_fload пишет с индекса 0, перезатирая + всё — bg и Kid обязаны жить в одном файле): env@0x50, wall@0x60, kid@0x70. + """ + pal = bytearray(256 * 4) + groups = [ + (0x50, os.path.join(B.R.VDUNGEON_DIR, "res200.pal")), # env + (0x60, os.path.join(B.R.VDUNGEON_DIR, "res360.pal")), # wall + (PAL_BASE, os.path.join(KID_DIR, "res400.pal")), # kid 0x70 + ] + for base, path in groups: + for i, (r, g, bl) in enumerate(_read16(path)): + o = (base + i) * 4 + pal[o:o + 4] = bytes((B.scale6to8(bl), B.scale6to8(g), B.scale6to8(r), 0)) + with open(out_path, "wb") as f: + f.write(pal) + + +def main(): + os.makedirs(OUT_DIR, exist_ok=True) + # атлас-индексы aidx (== frame.image), для которых есть res(401+aidx).png + ids = [] + for aidx in range(0, 255): + if os.path.exists(os.path.join(KID_DIR, f"res{401 + aidx}.png")): + ids.append(aidx) + if not ids: + B.die("нет спрайтов KID") + + pages = {} + for id in ids: + w, h, pix = load_kid(id) + pages.setdefault(id >> SHIFT, {})[id & MASK] = (w, h, pix) + + max_pg = max(pages) + names = [] + total = 0 + for pg in range(max_pg + 1): + if pg in pages: + nm = f"kid{pg}.atl" + cnt, sz = B.pack_atlas(os.path.join(OUT_DIR, nm), pages[pg]) + total += sz + names.append(nm) + print(f" {nm}: {len(pages[pg])} спрайтов, каталог {cnt}, {sz} Б") + else: + names.append(None) + print(f" (kid стр {pg}: пусто)") + + build_palette(os.path.join(OUT_DIR, "kid.pal")) + + # C-заголовок + hdr = os.path.join(OUT_DIR, "kid_atlas.h") + with open(hdr, "w") as f: + f.write("/* kid_atlas.h — раскладка атласов Kid. Сгенерировано " + "pop_pack_kid.py. */\n#ifndef KID_ATLAS_H\n#define KID_ATLAS_H\n") + f.write(f"#define KID_SHIFT {SHIFT}\n#define KID_MASK {MASK}\n") + f.write(f"#define KID_PAGES {len(names)}\n#define KID_PAL 0x{PAL_BASE:02X}\n") + f.write("static const char *const kid_atl[KID_PAGES] = {\n") + for nm in names: + f.write(" %s,\n" % ('"%s"' % nm if nm else "0")) + f.write('};\n#define KID_PAL_FILE "kid.pal"\n#endif\n') + + print(f"ИТОГО: {len(ids)} спрайтов Kid, {len(names)} EMM-страниц, " + f"{total} Б диск (+1КБ палитра) -> {OUT_DIR}") + + +if __name__ == "__main__": + main() diff --git a/applications/PoP/toolchain/render_room.py b/applications/PoP/toolchain/render_room.py new file mode 100755 index 0000000..93cd8bb --- /dev/null +++ b/applications/PoP/toolchain/render_room.py @@ -0,0 +1,842 @@ +#!/usr/bin/env python3 +""" +Parse SDLPoP level file (res2001.bin) and render the first room by +faithfully replaying the tile-drawing logic of seg008.c (draw_room/draw_tile). + +Pixel model (from seg008.c): + - screen is 320px wide, 10 columns. + - col_xh[col] = {0,4,8,12,16,20,24,28,32,36}; x_px = xh*8 -> 0,32,64,...,288 + (tile column is 32px; the 4px offset handles overlap of floor/wall edges) + - rows drawn 2,1,0 (bottom..top). For each row: + draw_bottom_y = 63*drawn_row + 65 + draw_main_y = draw_bottom_y - 3 + row2 -> bottom_y=191, row1 -> 128, row0 -> 65 + - a tile sprite of height h drawn at ybottom is placed with its BOTTOM at ybottom: + y = ybottom - h + 1 + +We replay draw_tile() which composites (in order): + floorright, anim_topright, right, anim_right, bottom, loose, base, anim, fore +Dependencies on NEIGHBOURS: + - tile_left = tile in the column to the left (same room, same row) + - row_below_left_[col] = tile in (room_below, col-1, row_below) -> for topright/floorright + - For a single room with no links we treat out-of-range neighbours as walls + (level edges), matching SDLPoP's custom->drawn_tile_*_level_edge defaults + (left = wall, top = floor). + +We ignore animated variants (spike frames, chomper, torch flames, gate +animation) for this first static render; they use curr_modifier/backtable. +""" + +import os +from PIL import Image + +LEVEL_DIR = "/Users/alex/Projects/DIY/Z80/Sprinter/C-Compiler/applications/PoP/SDLPoP/data/LEVELS" +VPALACE_DIR = "/Users/alex/Projects/DIY/Z80/Sprinter/C-Compiler/applications/PoP/SDLPoP/data/VPALACE" +VDUNGEON_DIR = "/Users/alex/Projects/DIY/Z80/Sprinter/C-Compiler/applications/PoP/SDLPoP/data/VDUNGEON" + +# For dungeon levels (tbl_level_type == 0) the environment sprites come from +# VDUNGEON where present, otherwise fall back to the shared VPALACE set +# (PRINCE.DAT common images). For palace levels use VPALACE only. +IS_DUNGEON = True # room 1 (res2001) is the dungeon + +# tile_table[code] = (base_id, floor_left, base_y, right_id, floor_right, +# right_y, stripe_id, topright_id, bottom_id, fore_id, +# fore_x, fore_y) +TILE_TABLE = [ + ( 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), # 0x00 empty + ( 41, 1, 0, 42, 1, 2, 145, 0, 43, 0, 0, 0), # 0x01 floor + (127, 1, 0, 133, 1, 2, 145, 0, 43, 0, 0, 0), # 0x02 spike + ( 92, 1, 0, 93, 1, 2, 0, 94, 43, 95, 1, 0), # 0x03 pillar + ( 46, 1, 0, 47, 1, 2, 0, 48, 43, 49, 3, 0), # 0x04 gate + ( 41, 1, 1, 35, 1, 3, 145, 0, 36, 0, 0, 0), # 0x05 stuck floor + ( 41, 1, 0, 42, 1, 2, 145, 0, 96, 0, 0, 0), # 0x06 close button + ( 46, 1, 0, 0, 0, 2, 0, 0, 43, 49, 3, 0), # 0x07 door top with floor + ( 86, 1, 0, 87, 1, 2, 0, 0, 43, 88, 1, 0), # 0x08 big pillar bottom + ( 0, 0, 0, 89, 0, 3, 0, 90, 0, 91, 1, 3), # 0x09 big pillar top + ( 41, 1, 0, 42, 1, 2, 145, 0, 43, 12, 2, -3), # 0x0A potion + ( 0, 1, 0, 0, 0, 0, 145, 0, 0, 0, 0, 0), # 0x0B loose floor + ( 0, 0, 0, 0, 0, 2, 0, 0, 85, 49, 3, 0), # 0x0C door top + ( 75, 1, 0, 42, 1, 2, 0, 0, 43, 77, 0, 0), # 0x0D mirror + ( 97, 1, 0, 98, 1, 2, 145, 0, 43, 100, 0, 0), # 0x0E debris + (147, 1, 0, 42, 1, 1, 145, 0, 149, 0, 0, 0), # 0x0F open button + ( 41, 1, 0, 37, 0, 0, 0, 38, 43, 0, 0, 0), # 0x10 leveldoor left + ( 0, 0, 0, 39, 1, 2, 0, 40, 43, 0, 0, 0), # 0x11 leveldoor right + ( 0, 0, 0, 42, 1, 2, 145, 0, 43, 0, 0, 0), # 0x12 chomper + ( 41, 1, 0, 42, 1, 2, 0, 0, 43, 0, 0, 0), # 0x13 torch + ( 0, 0, 0, 1, 1, 2, 0, 2, 0, 0, 0, 0), # 0x14 wall + ( 30, 1, 0, 31, 1, 2, 0, 0, 43, 0, 0, 0), # 0x15 skeleton + ( 41, 1, 0, 42, 1, 2, 145, 0, 43, 0, 0, 0), # 0x16 sword + ( 41, 1, 0, 10, 0, 0, 0, 11, 43, 0, 0, 0), # 0x17 balcony left + ( 0, 0, 0, 12, 1, 2, 0, 13, 43, 0, 0, 0), # 0x18 balcony right + ( 92, 1, 0, 42, 1, 2, 145, 0, 43, 95, 1, 0), # 0x19 lattice pillar + ( 1, 0, 0, 0, 0, 0, 0, 0, 2, 9, 0, -53), # 0x1A lattice down + ( 3, 0, -10, 0, 0, 0, 0, 0, 0, 9, 0, -53), # 0x1B lattice small + ( 4, 0, -10, 0, 0, 0, 0, 0, 0, 9, 0, -53), # 0x1C lattice left + ( 5, 0, -10, 0, 0, 0, 0, 0, 0, 9, 0, -53), # 0x1D lattice right + ( 97, 1, 0, 98, 1, 2, 0, 0, 43, 100, 0, 0), # 0x1E debris with torch +] + +TILE_NAMES = { + 0x00: "empty", 0x01: "floor", 0x02: "spike", 0x03: "pillar", + 0x04: "gate", 0x05: "stuck_floor", 0x06: "close_button", 0x07: "door_top_floor", + 0x08: "big_pillar_bottom", 0x09: "big_pillar_top", 0x0A: "potion", + 0x0B: "loose_floor", 0x0C: "door_top", 0x0D: "mirror", 0x0E: "debris", + 0x0F: "open_button", 0x10: "leveldoor_left", 0x11: "leveldoor_right", + 0x12: "chomper", 0x13: "torch", 0x14: "wall", 0x15: "skeleton", + 0x16: "sword", 0x17: "balcony_left", 0x18: "balcony_right", + 0x19: "lattice_pillar", 0x1A: "lattice_down", 0x1B: "lattice_small", + 0x1C: "lattice_left", 0x1D: "lattice_right", 0x1E: "debris_torch", +} + +COL_XH = [0, 4, 8, 12, 16, 20, 24, 28, 32, 36] + +# chtab -> directory/subfolder + id offset +# id_chtab_6_environment -> VPALACE, res(200+id).png +# id_chtab_7_environmentwall -> VPALACE (wall images also here, res(200+id)) +# id_chtab_1_flameswordpotion -> flames etc (skipped in static render) +CHTAB_ENV = 6 +CHTAB_WALL = 7 +CHTAB_FSP = 1 # flames/sword/potion (animated, skipped) + + +# ---- POP1 level file structure (see POP-DAT-FormatSpecifications.txt, sec 3.4) ---- +# Block layout (for the standard 2305-byte level): +# 0 .. 720 pop1 foretable (24 rooms x 30 bytes) +# 720 .. 1440 pop1 backtable (24 rooms x 30 bytes) +# 1440 .. 1696 door I (256 bytes, one per event line) +# 1696 .. 1952 door II (256 bytes) +# 1952 .. 2048 links (24 rooms x 4 bytes: L,R,Up,Dn) +# 2048 .. 2112 unknown I +# 2112 .. 2115 start position (room, location, direction) +# 2115 .. 2116 unknown II +# 2116 .. 2119 unknown III +# 2119 .. 2143 guard location (24) +# 2143 .. 2167 guard direction (24) +# 2167 .. 2191 unknown IV (a) +# 2191 .. 2215 unknown IV (b) +# 2215 .. 2239 guard skill (24) +# 2239 .. 2263 unknown IV (c) +# 2263 .. 2287 guard colour (24) +# 2287 .. 2303 unknown IV (d) +# 2303 .. 2305 0F 09 + +# foretable byte format: rrmccccc (2 random bits, 1 modifier bit, 5 code bits) +FG_RAND_MASK = 0xC0 +FG_MODBIT_MASK = 0x20 +FG_CODE_MASK = 0x1F + +# backtable: a tile modifier that depends on the group of the foretable code. +# group ids map foretable code -> "group" used to interpret backtable value. +# (see Table 7/8). We only need the group for door/event interpretation. +GROUP_OF_CODE = { + 0x00: "free", 0x01: "free", 0x02: "spike", 0x03: "none", + 0x04: "gate", 0x05: "none", 0x06: "event", 0x07: "tapest", + 0x08: "none", 0x09: "none", 0x0A: "potion", 0x0B: "none", + 0x0C: "ttop", 0x0D: "none", 0x0E: "none", 0x0F: "event", + 0x10: "none", 0x11: "none", 0x12: "chomp", 0x13: "none", + 0x14: "wall", 0x15: "none", 0x16: "none", 0x17: "none", + 0x18: "none", 0x19: "none", 0x1A: "none", 0x1B: "none", + 0x1C: "none", 0x1D: "none", 0x1E: "none", 0x1F: "none", +} + +# backtable modifier meanings per group (subset, from Table 8): +GATE_MOD = {0: "closed", 1: "open", 2: "with_lattice", 3: "alt_design"} +TTOP_MOD = {0: "normal", 1: "black", 2: "empty", 3: "?3", 4: "?4", 5: "?5", 6: "?6", 7: "?7"} +SPIKE_MOD = {0: "normal", 1: "barely_out", 2: "half_out", 3: "fully_out"} +CHOMP_MOD = {0: "normal", 1: "barely_out", 2: "half_out", 3: "fully_out", 4: "out?"} +POTION_MOD = {0: "blue", 1: "no_blue", 2: "window", 3: "spot?"} +TAPEST_MOD = {0: "normal", 1: "alt_design", 2: "black", 3: "empty"} +EVENT_MOD_DESC = { # group "event": the backtable byte IS the event id + 0: "closer", 1: "opener", +} + + +class Level: + """Full parsed POP1 level file.""" + def __init__(self, data): + self.data = data + self.rooms_fg = {} # room -> list[30] of (fg_byte, code, modbit) + self.rooms_bg = {} # room -> list[30] of backtable byte + for room in range(1, 25): + fg = []; bg = [] + for off in range(30): + i = (room - 1) * 30 + off + fb = data[i]; bb = data[720 + i] + fg.append((fb, fb & FG_CODE_MASK, (fb & FG_MODBIT_MASK) >> 5)) + bg.append(bb) + self.rooms_fg[room] = fg + self.rooms_bg[room] = bg + # door I / II (event lines 0..255) + self.door_i = data[1440:1696] + self.door_ii = data[1696:1952] + # links: 24 rooms x (L,R,Up,Dn) + self.links = {} + for room in range(1, 25): + b = data[1952 + (room - 1) * 4: 1952 + (room - 1) * 4 + 4] + self.links[room] = tuple(b) # (left, right, up, down) + # start position + self.start = tuple(data[2112:2115]) + # guards + self.guard_loc = list(data[2119:2143]) + self.guard_dir = list(data[2143:2167]) + self.guard_skill = list(data[2215:2239]) + self.guard_colour = list(data[2263:2287]) + + def door_for_event(self, event_id): + """Decode door I/II for an event line -> (room, location, trigger_next).""" + bi = self.door_i[event_id] + bii = self.door_ii[event_id] + # Byte I: t1 s4 s5 l1 l2 l3 l4 l5 + t1 = (bi >> 7) & 1 + s4 = (bi >> 6) & 1 + s5 = (bi >> 5) & 1 + l1 = (bi >> 4) & 1 + l2 = (bi >> 3) & 1 + l3 = (bi >> 2) & 1 + l4 = (bi >> 1) & 1 + l5 = bi & 1 + # Byte II: s1 s2 s3 00000 + s1 = (bii >> 7) & 1 + s2 = (bii >> 6) & 1 + s3 = (bii >> 5) & 1 + screen = (s1 << 4) | (s2 << 3) | (s3 << 2) | (s4 << 1) | s5 + loc = (l1 << 4) | (l2 << 3) | (l3 << 2) | (l4 << 1) | l5 + return screen, loc, t1 + + def backtable_desc(self, code, bb): + grp = GROUP_OF_CODE.get(code, "none") + tbl = {"gate": GATE_MOD, "ttop": TTOP_MOD, "spike": SPIKE_MOD, + "chomp": CHOMP_MOD, "potion": POTION_MOD, "tapest": TAPEST_MOD} + if code == 0x06 or code == 0x0F: # event group + act = EVENT_MOD_DESC.get(bb & 1, "?") + return f"event(id={bb}, {act})" + if grp in tbl: + return tbl[grp].get(bb, f"?0x{bb:02X}") + return f"0x{bb:02X}" + + +class Room: + def __init__(self, level, room_num): + self.level = level + self.room_num = room_num + self.fg = level.rooms_fg[room_num] + self.bg = level.rooms_bg[room_num] + self.left_room = None # соседняя комната слева (link L); load_leftroom + + def tile(self, row, col): + """Return (code, fg_byte, modbit, backtable_byte) for an in-room tile. + Out-of-range neighbours follow seg008 defaults: + col<0 -> берётся из ЛЕВОЙ комнаты (col 9), как load_leftroom(); + если левой комнаты нет — уровневая кромка = стена (20). + row<0 -> top level edge = floor (code 1, mod 0) + """ + if col < 0: + if self.left_room is not None and 0 <= row < 3: + return self.left_room.tile(row, 9) + return 20, 0, 0, 0 + if row < 0: + return 1, 0, 0, 0 + if not (0 <= row < 3 and 0 <= col < 10): + return 20, 0, 0, 0 + i = row * 10 + col + fb, code, mbit = self.fg[i] + return code, fb, mbit, self.bg[i] + + +# Log of every sprite loaded during a render, for the per-tile report. +SPRITE_LOG = [] + + +def load_sprite(id, chtab=CHTAB_ENV, reason=""): + """Load resource PNG для картинки id из набора chtab. + + База ресурса зависит от chtab (seg000.c:1109/1122): + chtab_6 environment -> res(200+id) + chtab_7 environmentwall -> res(360+id) + chtab_1 flame/sword/pot -> res(150+id) (анимация, статикой не грузим) + + Dungeon (IS_DUNGEON) cascade: try VDUNGEON first (dungeon-specific + overrides), then fall back to VPALACE (shared base set). + """ + if id <= 0: + return None + base = {CHTAB_ENV: 200, CHTAB_WALL: 360, CHTAB_FSP: 150}.get(chtab, 200) + fname = f"res{base + id}.png" + if IS_DUNGEON: + vd = os.path.join(VDUNGEON_DIR, fname) + if os.path.exists(vd): + try: + img = Image.open(vd).convert("RGBA") + CURRENT_TILE_LOG.append((id, "VDUNGEON", reason)) + return img + except Exception: + pass + vp = os.path.join(VPALACE_DIR, fname) + try: + img = Image.open(vp).convert("RGBA") + CURRENT_TILE_LOG.append((id, "VPALACE", reason)) + return img + except FileNotFoundError: + CURRENT_TILE_LOG.append((id, "MISSING", reason)) + return None + + +def blit(canvas, img, x, y): + """Composite img onto canvas at (x,y) using its alpha as mask (draw_main style).""" + if img is None: + return + w, h = img.size + if x + w <= 0 or y + h <= 0 or x >= canvas.width or y >= canvas.height: + return + canvas.alpha_composite(img, (x, y)) + + +def blit_black(canvas, img, x, y): + """blitters_9_black (seg008.c:1054): рисует МАСКУ спрайта ЧЁРНЫМ — + непрозрачная область формы заливается чёрным (тень-силуэт), а не + пикселями картинки. Используется для floor-right в draw_tile_floorright + (тень уступа тайла снизу-слева).""" + if img is None: + return + w, h = img.size + if x + w <= 0 or y + h <= 0 or x >= canvas.width or y >= canvas.height: + return + alpha = img.split()[3] + solid = Image.new("RGBA", img.size, (0, 0, 0, 255)) + canvas.alpha_composite(Image.composite(solid, Image.new("RGBA", img.size, (0, 0, 0, 0)), alpha), (x, y)) + + +# ---- Dungeon wall_pattern() (seg008.c:1928) ---- +# PRNG: seg009.c:321 random_seed = random_seed*214013 + 2531011; return (random_seed>>16) % (max+1) +_random_seed = 0 + + +def prandom(maxval): + global _random_seed + _random_seed = (_random_seed * 214013 + 2531011) & 0xFFFFFFFF + return (_random_seed >> 16) % (maxval + 1) + + +# wall sprite ids (chtab_7 / RSET_WALL = 7) res(200+id).png +RES_WALL_FACE_MAIN, RES_WALL_FACE_TOP, RES_WALL_CENTRE_BASE, RES_WALL_CENTRE_MAIN = 1, 2, 3, 4 +RES_WALL_RIGHT_BASE, RES_WALL_RIGHT_MAIN, RES_WALL_SINGLE_BASE, RES_WALL_SINGLE_MAIN = 5, 6, 7, 8 +RES_WALL_LEFT_BASE, RES_WALL_LEFT_MAIN, RES_WALL_DIVIDER1, RES_WALL_DIVIDER2 = 9, 10, 11, 12 +RES_WALL_RNDBLOCK = 13 +RES_WALL_MARK_TL, RES_WALL_MARK_BL, RES_WALL_MARK_TR, RES_WALL_MARK_BR = 14, 15, 16, 17 + +WALL_MODIFIER_SWS, WALL_MODIFIER_SWW, WALL_MODIFIER_WWS, WALL_MODIFIER_WWW = 0, 1, 2, 3 + +# Сплошная грань стены (chtab_7), индекс = wall_modifier (seg008.c:567/687): +WALL_FRAM_BOTTOM = [7, 9, 5, 3] # нижняя часть (draw_tile_bottom) +WALL_FRAM_MAIN = [8, 10, 6, 4] # основная часть (draw_tile_fore) + +LPOS = [58, 41, 37, 20, 16] # draw_left_mark vertical offsets +RPOS = [52, 42, 31, 21] # draw_right_mark vertical offsets + +TBL_LINE = [0, 10, 20] # смещение строки в 30-тайловом массиве комнаты + + +def wall_modifier(room, drawn_row, col): + """Модификатор стены (SWS/SWW/WWS/WWW = 0..3) по соседям слева/справа. + seg008.c:1255-1305 (без USE_FAKE_TILES): сосед считается стеной, если + его тип == wall(20). На кромке комнаты без линка сосед по умолчанию + считается стеной (wall_to_left/right = 1).""" + left_code = room.tile(drawn_row, col - 1)[0] if col > 0 else 20 + right_code = room.tile(drawn_row, col + 1)[0] if col < 9 else 20 + wall_left = (left_code == 20) + wall_right = (right_code == 20) + if wall_left and wall_right: + return WALL_MODIFIER_WWW # 3 + if wall_left: + return WALL_MODIFIER_WWS # 2 + if wall_right: + return WALL_MODIFIER_SWW # 1 + return WALL_MODIFIER_SWS # 0 + + +def _wall_add(canvas, which_table, id, xh, xl, ybottom, reason): + """Emulate ptr_add_table(RSET_WALL, id, xh, xl, ybottom, blit, 0). + which_table 0 -> backtable (blit transparent), 1 -> foretable. + RSET_WALL id -> res(200+id).png, cascade VDUNGEON->VPALACE.""" + img = load_sprite(id, CHTAB_WALL, reason=reason) + if img is None: + return + y = ybottom - img.height + 1 + x = xh * 8 + xl + blit(canvas, img, x, y) + + +def draw_left_mark(canvas, decal_variant, arg2, arg1, drawn_row, col): + image_id = RES_WALL_MARK_TL + if decal_variant % 2: + image_id = RES_WALL_MARK_BL + lv2 = 0 + if decal_variant > 3: + lv2 = arg1 + 6 + elif decal_variant > 1: + lv2 = arg2 + 6 + xh = COL_XH[col] + draw_bottom_y = 63 * drawn_row + 65 + _wall_add(canvas, 1, image_id, xh + (1 if decal_variant in (2, 3) else 0), + lv2, draw_bottom_y - LPOS[decal_variant], + reason=f"wall left-mark var{decal_variant}") + + +def draw_right_mark(canvas, decal_variant, arg1, drawn_row, col): + image_id = RES_WALL_MARK_TR + if decal_variant % 2: + image_id = RES_WALL_MARK_BR + a = 24 if decal_variant < 2 else arg1 - 3 + xh = COL_XH[col] + draw_bottom_y = 63 * drawn_row + 65 + _wall_add(canvas, 1, image_id, xh + (1 if decal_variant > 1 else 0), + a, draw_bottom_y - RPOS[decal_variant], + reason=f"wall right-mark var{decal_variant}") + + +def wall_pattern(canvas, room, drawn_row, col, which_part, which_table): + """Replay seg008.c wall_pattern() for a dungeon wall tile. + which_part: 0 = bottom part (draw_tile_bottom), 1 = fore part (draw_tile_fore). + which_table: 0 = backtable, 1 = foretable.""" + global _random_seed + xh = COL_XH[col] + draw_bottom_y = 63 * drawn_row + 65 + draw_main_y = draw_bottom_y - 3 + + saved_seed = _random_seed + # seg008.c:1941 — зерно зависит от НОМЕРА КОМНАТЫ (не строки!): + # random_seed = drawn_room + tbl_line[drawn_row] + drawn_col + room_num = getattr(room, "room_num", 1) + _random_seed = room_num + TBL_LINE[drawn_row] + col + prandom(1) # discard + is_dungeon = IS_DUNGEON + + middle_divider = prandom(1) + middle_divider_offset = prandom(4) + bottom_divider = prandom(1) + bottom_divider_offset = prandom(4) + + # Модификатор стены (какие соседи — стены) — тот же, что для сплошной грани. + bg = wall_modifier(room, drawn_row, col) + + if bg == WALL_MODIFIER_WWW: + if which_part != 0: + if prandom(4) == 0: + _wall_add(canvas, which_table, RES_WALL_RNDBLOCK, xh, 0, + draw_bottom_y - 42, reason="wall rndblock") + _wall_add(canvas, which_table, RES_WALL_DIVIDER1 + middle_divider, xh + 1, + middle_divider_offset, draw_bottom_y - 21, reason="wall middle-divider") + _wall_add(canvas, which_table, RES_WALL_DIVIDER1 + bottom_divider, xh, + bottom_divider_offset, draw_bottom_y, reason="wall bottom-divider") + if which_part != 0 and is_dungeon: + if prandom(4) == 0: + draw_right_mark(canvas, prandom(3), middle_divider_offset, drawn_row, col) + if prandom(4) == 0: + draw_left_mark(canvas, prandom(4), + middle_divider_offset - middle_divider, + bottom_divider_offset - bottom_divider, drawn_row, col) + elif bg == WALL_MODIFIER_SWS: + if is_dungeon and which_part != 0: + if prandom(6) == 0: + draw_left_mark(canvas, prandom(1), + middle_divider_offset - middle_divider, + bottom_divider_offset - bottom_divider, drawn_row, col) + elif bg == WALL_MODIFIER_SWW: + if which_part != 0: + if prandom(4) == 0: + _wall_add(canvas, which_table, RES_WALL_RNDBLOCK, xh, 0, + draw_bottom_y - 42, reason="wall rndblock") + _wall_add(canvas, which_table, RES_WALL_DIVIDER1 + middle_divider, xh + 1, + middle_divider_offset, draw_bottom_y - 21, reason="wall middle-divider") + if is_dungeon: + if prandom(4) == 0: + draw_right_mark(canvas, prandom(3), middle_divider_offset, drawn_row, col) + if prandom(4) == 0: + draw_left_mark(canvas, prandom(3), + middle_divider_offset - middle_divider, + bottom_divider_offset - bottom_divider, drawn_row, col) + elif bg == WALL_MODIFIER_WWS: + if which_part != 0: + _wall_add(canvas, which_table, RES_WALL_DIVIDER1 + middle_divider, xh + 1, + middle_divider_offset, draw_bottom_y - 21, reason="wall middle-divider") + _wall_add(canvas, which_table, RES_WALL_DIVIDER1 + bottom_divider, xh, + bottom_divider_offset, draw_bottom_y, reason="wall bottom-divider") + if which_part != 0 and is_dungeon: + if prandom(4) == 0: + draw_right_mark(canvas, prandom(1) + 2, middle_divider_offset, drawn_row, col) + if prandom(4) == 0: + draw_left_mark(canvas, prandom(4), + middle_divider_offset - middle_divider, + bottom_divider_offset - bottom_divider, drawn_row, col) + + _random_seed = saved_seed + + +# Per-tile sprite log: draw_tile resets CURRENT_TILE_LOG, load_sprite appends to it. +CURRENT_TILE_LOG = [] +TILE_LOGS = {} # (row, col) -> list of (id, source, reason) + +# seg008.c:1108 — вертикальные слайсы решётки ворот (portcullis). +DOOR_FRAM_SLICE = [67, 59, 58, 57, 56, 55, 54, 53, 52] +# seg008.c:517 — правый край шипов по кадру get_spike_frame. +SPIKES_FRAM_RIGHT = [0, 134, 135, 136, 137, 138, 137, 135, 134, 0] +# seg008.c:450 — «blueline»-декали на ЗАДНЕЙ стене (силуэты кладки/окна), +# рисуются правым соседом по МОДИФИКАТОРУ соседа слева: +# tile_left == empty -> blueline_fram1[mod] (окно/решётка-фрагменты 124..126) +# на высоте blueline_fram_y[mod] + main_y; +# tile_left == floor -> blueline_fram3[mod] (кирпичные силуэты 44/45) +# на main_y - 20 (если mod != !!level_type). +BLUELINE_FRAM1 = [0, 124, 125, 126] +BLUELINE_FRAM_Y = [0, -20, -20, 0] +BLUELINE_FRAM3 = [44, 44, 45, 45] + + +def draw_gate_back(canvas, modifier_left, draw_xh, draw_bottom_y, draw_main_y): + """seg008.c:1110 draw_gate_back — портикулис (решётка) ворот. Рисуется + на тайле СПРАВА от ворот (draw_tile_anim_right, tile_left==gate). У нас + это col 0 room1, где сосед слева — ворота из room 5. modifier_left — + модификатор тайла-ворот (степень поднятия решётки).""" + x = draw_xh * 8 + gate_top_y = draw_bottom_y - 62 + gate_openness = (min(modifier_left, 188) >> 2) + 1 + gate_bottom_y = draw_main_y - gate_openness + if gate_bottom_y + 12 < draw_main_y: + img = load_sprite(50, reason="gate bottom+B") # gate поднята высоко + if img: + blit(canvas, img, x, gate_bottom_y - img.height + 1) + else: + # id 47 (правая грань ворот) уже рисует draw_tile_right — не дублируем. + img = load_sprite(51, reason="gate bottom") + if img: + blit(canvas, img, x, (gate_bottom_y - 2) - img.height + 1) + # Стек 8px-слайсов решётки снизу вверх. + ybottom = gate_bottom_y - 12 + if ybottom < 192: + while ybottom >= 0 and ybottom > 7 and (ybottom - 7) > gate_top_y: + img = load_sprite(52, reason="gate slice 8px") + if img: + blit(canvas, img, x, ybottom - img.height + 1) + ybottom -= 8 + gate_frame = ybottom - gate_top_y + 1 + if 0 < gate_frame < 9: + img = load_sprite(DOOR_FRAM_SLICE[gate_frame], reason="gate top slice") + if img: + blit(canvas, img, x, ybottom - img.height + 1) + + +def draw_tile(canvas, room, drawn_row, col, left_edge_code=20): + global CURRENT_TILE_LOG + CURRENT_TILE_LOG = [] + code, fg_byte, modbit, bg_mod = room.tile(drawn_row, col) + # tile_left: колонка слева. col-1 == -1 -> Room.tile берёт из левой + # комнаты (или уровневая кромка = стена), см. Room.tile / load_leftroom. + left_code, _, _, left_mod = room.tile(drawn_row, col - 1) + draw_xh = COL_XH[col] + x = draw_xh * 8 + draw_bottom_y = 63 * drawn_row + 65 + draw_main_y = draw_bottom_y - 3 + + # For topright/floorright we need the tile below-left. + # Room below = same room (no room links) -> row_below = drawn_row+1 if <3 else treat as empty/floor top edge. + row_below = drawn_row + 1 + if row_below > 2: + # below the bottom row of the level: floor top edge + rbl_code, rbl_mod = 1, 0 + else: + rbl_code, _, _, rbl_mod = room.tile(row_below, col - 1) + row_below_left_code = rbl_code + + t = TILE_TABLE[code] + lt = TILE_TABLE[left_code] + + # ---- draw_tile_floorright (seg008.c:392) ---- + # Рисуется ТОЛЬКО когда can_see_bottomleft(): текущий тайл — + # empty/bigpillar_top/doortop/lattice_down. Иначе topright соседа + # снизу-слева перекрыт и рисовать его нельзя (иначе «арки» повсюду — + # это была главная причина замусоренного рендера). + if code in (0, 9, 12, 26): # can_see_bottomleft + _draw_topright(canvas, row_below_left_code, rbl_mod, x, draw_bottom_y, + reason=f"topright(below-left tile {row_below_left_code:02X})") + if lt[4]: # tile_table[tile_left].floor_right + # blitters_9_black: ЧЁРНЫЙ силуэт (тень уступа), не светлый спрайт. + img = load_sprite(42, reason=f"floor-right shadow(left tile {left_code:02X})") + blit_black(canvas, img, x, draw_main_y + TILE_TABLE[1][5] - (img.height if img else 0) + 1) + + # ---- draw_tile_anim_topright ---- (gate top mask) - skipped: no anim + # ---- draw_tile_right (seg008.c:456) ---- + if code != 20: # у самой стены правый край не рисуется + level_type = 0 if IS_DUNGEON else 1 + if left_code == 20: + # сосед слева — стена: её правая грань в текущую колонку (chtab_7 id 1) + img = load_sprite(1, CHTAB_WALL, reason="wall right-face(left tile 14)") + ry = TILE_TABLE[20][5] # right_y стены = 2 + blit(canvas, img, x, ry + draw_main_y - (img.height if img else 0) + 1) + elif left_code == 0: + # сосед слева — empty: фрагмент окна/решётки по его модификатору + if left_mod <= 3: + bid = BLUELINE_FRAM1[left_mod] + if bid: + img = load_sprite(bid, reason=f"blueline-window(left empty mod{left_mod})") + blit(canvas, img, x, + BLUELINE_FRAM_Y[left_mod] + draw_main_y - (img.height if img else 0) + 1) + elif left_code == 1: + # сосед слева — floor: правый треугольник (42) + СИЛУЭТ КЛАДКИ (44/45) + # на задней стене, если модификатор пола != !!level_type. + img = load_sprite(42, reason="floor-right(part of left tile 01)") + ry = TILE_TABLE[1][5] # right_y пола = 2 + blit(canvas, img, x, ry + draw_main_y - (img.height if img else 0) + 1) + num = left_mod if left_mod <= 3 else 0 + if num != level_type: + bid = BLUELINE_FRAM3[num] + img = load_sprite(bid, reason=f"brick-silhouette(left floor mod{num})") + blit(canvas, img, x, draw_main_y - 20 - (img.height if img else 0) + 1) + else: + rid = lt[3] # tile_left.right_id + if rid: + ry = lt[5] + img = load_sprite(rid, reason=f"right(part of left tile {left_code:02X})") + blit(canvas, img, x, ry + draw_main_y - (img.height if img else 0) + 1) + # stripe: only drawn when tbl_level_type != 0 (palace). Dungeon: skip. + sid = lt[6] + if sid and not IS_DUNGEON: + img = load_sprite(sid, reason=f"stripe(left tile {left_code:02X})") + blit(canvas, img, x, draw_main_y - 27 - (img.height if img else 0) + 1) + + # ---- draw_tile_anim_right (seg008.c:530) ---- + # «Анимированные» части соседа слева, проецируемые в текущий тайл. + # Статикой (кадр покоя) рисуем те, что формируют ГЕОМЕТРИЮ фона: + # gate -> решётка (portcullis) на границе с левой комнатой; + # loose -> правый треугольный край шаткой плиты (иначе провал в полу); + # spike -> статичный кадр шипов по модификатору. + # torch-flame/chomper — чистая анимация, пропускаем (отдельные sprite_t). + if left_code == 4: # tiles_4_gate + draw_gate_back(canvas, left_mod, draw_xh, draw_bottom_y, draw_main_y) + elif left_code == 11: # tiles_11_loose — правый край плиты (loose_fram_right[0]=42) + img = load_sprite(42, reason="loose right (part of left tile 0B)") + if img: + blit(canvas, img, x, (draw_bottom_y - 1) - img.height + 1) + elif left_code == 2: # tiles_2_spike — статичный кадр (spikes_fram_right) + sf = 5 if (left_mod & 0x80) else left_mod + sid = SPIKES_FRAM_RIGHT[sf] if 0 <= sf < len(SPIKES_FRAM_RIGHT) else 0 + if sid: + img = load_sprite(sid, reason="spike right (part of left tile 02)") + if img: + blit(canvas, img, x, (draw_main_y - 7) - img.height + 1) + + # ---- draw_tile_bottom (seg008.c:570) ---- + if code == 20: + # Сплошная НИЖНЯЯ грань стены (chtab_7 wall_fram_bottom) + декали. + wmod = wall_modifier(room, drawn_row, col) + img = load_sprite(WALL_FRAM_BOTTOM[wmod], CHTAB_WALL, reason="wall bottom-face") + if img: + blit(canvas, img, x, draw_bottom_y - img.height + 1) + wall_pattern(canvas, room, drawn_row, col, 0, 0) + else: + bid = t[8] + img = load_sprite(bid, reason=f"bottom({TILE_NAMES.get(code,'?')})") + if img: + blit(canvas, img, x, draw_bottom_y - img.height + 1) + + # ---- draw_loose (seg008.c:600) — статический кадр loose floor ---- + # Нижняя часть шаткой плиты (loose_fram_bottom[0]=43) на draw_bottom_y. + if code == 11: # 0x0B loose floor + img = load_sprite(43, reason="loose bottom (draw_loose)") + if img: + blit(canvas, img, x, draw_bottom_y - img.height + 1) + + # ---- draw_tile_base (seg008.c:611) ---- + base_id = t[0] + base_y = t[2] + if code == 11: # loose floor: базовая (верхняя) часть = loose_fram_left[0] = 41 + base_id = 41 + img = load_sprite(base_id, reason=f"base({TILE_NAMES.get(code,'?')})") + if img: + blit(canvas, img, x, base_y + draw_main_y - img.height + 1) + + # ---- draw_tile_anim ---- (spike/potion/chomper animated) skipped + + # ---- draw_tile_fore (seg008.c:690) ---- + if code == 20: + # Основная (средняя) грань стены (chtab_7 wall_fram_main) + декали/марки. + wmod = wall_modifier(room, drawn_row, col) + img = load_sprite(WALL_FRAM_MAIN[wmod], CHTAB_WALL, reason="wall main-face") + if img: + blit(canvas, img, x, draw_main_y - img.height + 1) + wall_pattern(canvas, room, drawn_row, col, 1, 1) + fid = t[9] + if fid: + # fore_x — в единицах xh (×8 пикс!), не в пикселях: seg008.c:743 + # xh = fore_x + draw_xh, затем x = xh*8. fore_y — в пикселях. + fx = t[10] * 8 + x + fy = t[11] + draw_main_y + img = load_sprite(fid, reason=f"fore({TILE_NAMES.get(code,'?')})") + if img: + blit(canvas, img, fx, fy - img.height + 1) + + # Save per-tile log (keyed by logical room position: row=drawn_row, col) + TILE_LOGS[(drawn_row, col)] = list(CURRENT_TILE_LOG) + + +def _draw_topright(canvas, tiletype, modifier, x, draw_bottom_y, reason=""): + if tiletype in (7, 12): # doortop_with_floor / doortop -> palace uses frame + return + elif tiletype == 20: # wall + img = load_sprite(2, CHTAB_WALL, reason=reason) + else: + tid = TILE_TABLE[tiletype][7] + if tid == 0: + return + img = load_sprite(tid, reason=reason) + if img: + blit(canvas, img, x, draw_bottom_y - img.height + 1) + + +def render_room(room_num): + with open(os.path.join(LEVEL_DIR, f"res{2000 + room_num}.bin"), "rb") as f: + data = f.read() + level = Level(data) + room = Room(level, room_num) + # load_leftroom(): левая комната по линку L — её col 9 виден на границе + # (col 0) текущей комнаты (напр. решётка ворот из соседней комнаты). + left_link = level.links[room_num][0] + if left_link: + room.left_room = Room(level, left_link) + + canvas = Image.new("RGBA", (320, 192), (0, 0, 0, 0)) + # rows drawn 2,1,0 (bottom first) + for drawn_row in (2, 1, 0): + for col in range(10): + draw_tile(canvas, room, drawn_row, col) + + return canvas, room, level + + +def render_single_tile(code, modifier=0, left_code=20, below_left_code=1): + """Render one tile in isolation on a 40x64 canvas (matching SDLPoP tile box).""" + TILE_W, TILE_H = 40, 64 + canvas = Image.new("RGBA", (TILE_W, TILE_H), (0, 0, 0, 0)) + # Place tile as if it were at drawn_row=2 (bottom), col=0 + drawn_row = 2 + col = 0 + # Build a fake Room backed by single values + class FakeRoom: + def tile(self, r, c): + if c < 0: + return left_code, 0, 0, 0 + if r < 0: + return 1, 0, 0, 0 + if r == drawn_row and c == 0: + return code, 0, 0, modifier + return below_left_code, 0, 0, 0 + room = FakeRoom() + draw_tile(canvas, room, drawn_row, col, left_edge_code=left_code) + return canvas + + +def main(): + import sys + room_num = 1 + if len(sys.argv) > 1: + room_num = int(sys.argv[1]) + + canvas, room, level = render_room(room_num) + out = f"./room{room_num}_render.png" + # Прозрачный фон (задняя чёрная стена, которую мы не рисуем) -> ЧЁРНЫМ, + # а не белым как показывает вьюер прозрачность. Композитим по альфе. + flat = Image.new("RGB", canvas.size, (0, 0, 0)) + flat.paste(canvas, (0, 0), canvas) + flat.save(out) + print(f"Saved {out} ({canvas.width}x{canvas.height})") + + # debug grid with code labels (на чёрном фоне) + from PIL import ImageDraw + dbg = flat.copy() + d = ImageDraw.Draw(dbg) + for r in range(3): + by = 63 * r + 65 + for c in range(10): + x = COL_XH[c] * 8 + d.rectangle([x, by - 63, x + 31, by], outline=(255, 0, 0), width=1) + code, _, _, _ = room.tile(r, c) + d.text((x + 2, by - 60), f"{code:02X}", fill=(255, 255, 0)) + dbg.save(f"./room{room_num}_debug.png") + print(f"Saved room{room_num}_debug.png") + + # Per-tile individual renders (the 30 tiles of the room) + # For proper neighbour context, render each tile inside its real room, + # then crop its 32x64 (40x64) column box. + tiles_dir = f"./tiles_room{room_num}" + os.makedirs(tiles_dir, exist_ok=True) + for r in range(3): + by = 63 * r + 65 + for c in range(10): + x = COL_XH[c] * 8 + code, _, _, _ = room.tile(r, c) + # crop the column box (40 wide to include left overlap, 64 tall above bottom) + box = (x, by - 64, x + 40, by) + tile_img = canvas.crop(box) + tile_img.save(os.path.join(tiles_dir, f"r{r}c{c}_{code:02X}.png")) + print(f"Saved 30 tile crops to {tiles_dir}/") + # Per-tile report: which images each tile pulls and why. + print() + print("=" * 78) + print(f"PER-TILE SPRITE REPORT (room {room_num}, {'DUNGEON' if IS_DUNGEON else 'PALACE'})") + print("=" * 78) + for r in range(3): + for c in range(10): + code, fg_byte, mbit, bb = room.tile(r, c) + name = TILE_NAMES.get(code, f"?0x{code:02X}") + btd = level.backtable_desc(code, bb) + line = (f"\n[{r},{c}] code=0x{code:02X} ({name}) " + f"fg=0x{fg_byte:02X} mbit={mbit} backtable=0x{bb:02X} ({btd})") + # If this tile is an event activator (group 'event'), show the door it drives. + if code in (0x06, 0x0F): + scr, loc, trig = level.door_for_event(bb) + line += f"\n -> DOOR EVENT id={bb}: screen={scr} loc={loc} trigger_next={trig}" + print(line) + logs = TILE_LOGS.get((r, c), []) + if not logs: + print(" (no sprites drawn)") + for sid, source, reason in logs: + print(f" res{200+sid:03d}.png <- {source:8s} {reason}") + print() + print("=" * 78) + + # Level-level summary: start position, links, door events, guards. + print() + print("LEVEL STRUCTURE SUMMARY") + print("-" * 78) + print(f"start position: room={level.start[0]} loc={level.start[1]} dir={level.start[2]}") + links = level.links.get(room_num, (0, 0, 0, 0)) + print(f"room {room_num} links: L={links[0]} R={links[1]} Up={links[2]} Dn={links[3]}") + # guard in this room + gi = room_num - 1 + gloc = level.guard_loc[gi] + if gloc < 30: + gr, gc = gloc // 10, gloc % 10 + print(f"guard: loc=tile(r{gr},c{gc}) dir={level.guard_dir[gi]} " + f"skill={level.guard_skill[gi]} colour={level.guard_colour[gi]}") + else: + print("guard: none") + # door events driven by any tile in this room + print("door events referenced by this room's tiles:") + seen = set() + for off in range(30): + fb, code, mbit = room.fg[off] + bb = room.bg[off] + if code in (0x06, 0x0F): + if bb not in seen: + seen.add(bb) + scr, loc, trig = level.door_for_event(bb) + print(f" event id={bb} (tile off {off}): screen={scr} loc={loc} trigger_next={trig}") + if not seen: + print(" (none)") + + print() + print("Room", room_num, "foreground codes (3 rows x 10 cols):") + for r in range(3): + print(" ", " ".join(f"{room.tile(r,c)[0]:02X}" for c in range(10))) + + +if __name__ == "__main__": + main() diff --git a/applications/PoP/toolchain/room1_debug.png b/applications/PoP/toolchain/room1_debug.png new file mode 100644 index 0000000..9c30c66 Binary files /dev/null and b/applications/PoP/toolchain/room1_debug.png differ diff --git a/applications/PoP/toolchain/room1_render.png b/applications/PoP/toolchain/room1_render.png new file mode 100644 index 0000000..16a3d76 Binary files /dev/null and b/applications/PoP/toolchain/room1_render.png differ diff --git a/applications/PoP/toolchain/tiles_room1/r0c0_00.png b/applications/PoP/toolchain/tiles_room1/r0c0_00.png new file mode 100644 index 0000000..3f83ec2 Binary files /dev/null and b/applications/PoP/toolchain/tiles_room1/r0c0_00.png differ diff --git a/applications/PoP/toolchain/tiles_room1/r0c1_00.png b/applications/PoP/toolchain/tiles_room1/r0c1_00.png new file mode 100644 index 0000000..6925c1b Binary files /dev/null and b/applications/PoP/toolchain/tiles_room1/r0c1_00.png differ diff --git a/applications/PoP/toolchain/tiles_room1/r0c2_00.png b/applications/PoP/toolchain/tiles_room1/r0c2_00.png new file mode 100644 index 0000000..ab917a9 Binary files /dev/null and b/applications/PoP/toolchain/tiles_room1/r0c2_00.png differ diff --git a/applications/PoP/toolchain/tiles_room1/r0c3_01.png b/applications/PoP/toolchain/tiles_room1/r0c3_01.png new file mode 100644 index 0000000..009f8a1 Binary files /dev/null and b/applications/PoP/toolchain/tiles_room1/r0c3_01.png differ diff --git a/applications/PoP/toolchain/tiles_room1/r0c4_01.png b/applications/PoP/toolchain/tiles_room1/r0c4_01.png new file mode 100644 index 0000000..53030d3 Binary files /dev/null and b/applications/PoP/toolchain/tiles_room1/r0c4_01.png differ diff --git a/applications/PoP/toolchain/tiles_room1/r0c5_01.png b/applications/PoP/toolchain/tiles_room1/r0c5_01.png new file mode 100644 index 0000000..50f344a Binary files /dev/null and b/applications/PoP/toolchain/tiles_room1/r0c5_01.png differ diff --git a/applications/PoP/toolchain/tiles_room1/r0c6_01.png b/applications/PoP/toolchain/tiles_room1/r0c6_01.png new file mode 100644 index 0000000..53030d3 Binary files /dev/null and b/applications/PoP/toolchain/tiles_room1/r0c6_01.png differ diff --git a/applications/PoP/toolchain/tiles_room1/r0c7_01.png b/applications/PoP/toolchain/tiles_room1/r0c7_01.png new file mode 100644 index 0000000..ebed99f Binary files /dev/null and b/applications/PoP/toolchain/tiles_room1/r0c7_01.png differ diff --git a/applications/PoP/toolchain/tiles_room1/r0c8_14.png b/applications/PoP/toolchain/tiles_room1/r0c8_14.png new file mode 100644 index 0000000..dab7a5d Binary files /dev/null and b/applications/PoP/toolchain/tiles_room1/r0c8_14.png differ diff --git a/applications/PoP/toolchain/tiles_room1/r0c9_14.png b/applications/PoP/toolchain/tiles_room1/r0c9_14.png new file mode 100644 index 0000000..abd7d75 Binary files /dev/null and b/applications/PoP/toolchain/tiles_room1/r0c9_14.png differ diff --git a/applications/PoP/toolchain/tiles_room1/r1c0_13.png b/applications/PoP/toolchain/tiles_room1/r1c0_13.png new file mode 100644 index 0000000..097334b Binary files /dev/null and b/applications/PoP/toolchain/tiles_room1/r1c0_13.png differ diff --git a/applications/PoP/toolchain/tiles_room1/r1c1_13.png b/applications/PoP/toolchain/tiles_room1/r1c1_13.png new file mode 100644 index 0000000..b4b0c0c Binary files /dev/null and b/applications/PoP/toolchain/tiles_room1/r1c1_13.png differ diff --git a/applications/PoP/toolchain/tiles_room1/r1c2_01.png b/applications/PoP/toolchain/tiles_room1/r1c2_01.png new file mode 100644 index 0000000..a6cf256 Binary files /dev/null and b/applications/PoP/toolchain/tiles_room1/r1c2_01.png differ diff --git a/applications/PoP/toolchain/tiles_room1/r1c3_03.png b/applications/PoP/toolchain/tiles_room1/r1c3_03.png new file mode 100644 index 0000000..9e269c0 Binary files /dev/null and b/applications/PoP/toolchain/tiles_room1/r1c3_03.png differ diff --git a/applications/PoP/toolchain/tiles_room1/r1c4_00.png b/applications/PoP/toolchain/tiles_room1/r1c4_00.png new file mode 100644 index 0000000..d47d89f Binary files /dev/null and b/applications/PoP/toolchain/tiles_room1/r1c4_00.png differ diff --git a/applications/PoP/toolchain/tiles_room1/r1c5_14.png b/applications/PoP/toolchain/tiles_room1/r1c5_14.png new file mode 100644 index 0000000..73b2d40 Binary files /dev/null and b/applications/PoP/toolchain/tiles_room1/r1c5_14.png differ diff --git a/applications/PoP/toolchain/tiles_room1/r1c6_14.png b/applications/PoP/toolchain/tiles_room1/r1c6_14.png new file mode 100644 index 0000000..59e1bcb Binary files /dev/null and b/applications/PoP/toolchain/tiles_room1/r1c6_14.png differ diff --git a/applications/PoP/toolchain/tiles_room1/r1c7_14.png b/applications/PoP/toolchain/tiles_room1/r1c7_14.png new file mode 100644 index 0000000..da81a02 Binary files /dev/null and b/applications/PoP/toolchain/tiles_room1/r1c7_14.png differ diff --git a/applications/PoP/toolchain/tiles_room1/r1c8_14.png b/applications/PoP/toolchain/tiles_room1/r1c8_14.png new file mode 100644 index 0000000..f668ced Binary files /dev/null and b/applications/PoP/toolchain/tiles_room1/r1c8_14.png differ diff --git a/applications/PoP/toolchain/tiles_room1/r1c9_14.png b/applications/PoP/toolchain/tiles_room1/r1c9_14.png new file mode 100644 index 0000000..64ade2d Binary files /dev/null and b/applications/PoP/toolchain/tiles_room1/r1c9_14.png differ diff --git a/applications/PoP/toolchain/tiles_room1/r2c0_14.png b/applications/PoP/toolchain/tiles_room1/r2c0_14.png new file mode 100644 index 0000000..e2909b4 Binary files /dev/null and b/applications/PoP/toolchain/tiles_room1/r2c0_14.png differ diff --git a/applications/PoP/toolchain/tiles_room1/r2c1_14.png b/applications/PoP/toolchain/tiles_room1/r2c1_14.png new file mode 100644 index 0000000..a8f1180 Binary files /dev/null and b/applications/PoP/toolchain/tiles_room1/r2c1_14.png differ diff --git a/applications/PoP/toolchain/tiles_room1/r2c2_14.png b/applications/PoP/toolchain/tiles_room1/r2c2_14.png new file mode 100644 index 0000000..162c6b2 Binary files /dev/null and b/applications/PoP/toolchain/tiles_room1/r2c2_14.png differ diff --git a/applications/PoP/toolchain/tiles_room1/r2c3_14.png b/applications/PoP/toolchain/tiles_room1/r2c3_14.png new file mode 100644 index 0000000..48ec563 Binary files /dev/null and b/applications/PoP/toolchain/tiles_room1/r2c3_14.png differ diff --git a/applications/PoP/toolchain/tiles_room1/r2c4_0E.png b/applications/PoP/toolchain/tiles_room1/r2c4_0E.png new file mode 100644 index 0000000..88b0c98 Binary files /dev/null and b/applications/PoP/toolchain/tiles_room1/r2c4_0E.png differ diff --git a/applications/PoP/toolchain/tiles_room1/r2c5_03.png b/applications/PoP/toolchain/tiles_room1/r2c5_03.png new file mode 100644 index 0000000..a5077bf Binary files /dev/null and b/applications/PoP/toolchain/tiles_room1/r2c5_03.png differ diff --git a/applications/PoP/toolchain/tiles_room1/r2c6_0B.png b/applications/PoP/toolchain/tiles_room1/r2c6_0B.png new file mode 100644 index 0000000..8c01774 Binary files /dev/null and b/applications/PoP/toolchain/tiles_room1/r2c6_0B.png differ diff --git a/applications/PoP/toolchain/tiles_room1/r2c7_01.png b/applications/PoP/toolchain/tiles_room1/r2c7_01.png new file mode 100644 index 0000000..0352215 Binary files /dev/null and b/applications/PoP/toolchain/tiles_room1/r2c7_01.png differ diff --git a/applications/PoP/toolchain/tiles_room1/r2c8_01.png b/applications/PoP/toolchain/tiles_room1/r2c8_01.png new file mode 100644 index 0000000..992c339 Binary files /dev/null and b/applications/PoP/toolchain/tiles_room1/r2c8_01.png differ diff --git a/applications/PoP/toolchain/tiles_room1/r2c9_14.png b/applications/PoP/toolchain/tiles_room1/r2c9_14.png new file mode 100644 index 0000000..0f30768 Binary files /dev/null and b/applications/PoP/toolchain/tiles_room1/r2c9_14.png differ diff --git a/toolchain/make_hdd.sh b/toolchain/make_hdd.sh new file mode 100755 index 0000000..8578fc0 --- /dev/null +++ b/toolchain/make_hdd.sh @@ -0,0 +1,52 @@ +#!/bin/bash +# make_hdd.sh OUT.chd FILE1 [FILE2 ...] +# +# Собирает 32 МБ RAW-образ жёсткого диска (MBR + один раздел FAT16) с +# указанными файлами в корне и конвертит его в CHD для MAME (монтируется +# как -hard2). Быстрая альтернатива floppy (mc.img): MAME эмулирует FDD +# с реальной черепашьей скоростью, HDD — в разы быстрее. +# +# Требует: mtools (mpartition/mformat/mcopy) + chdman (из MAME). +# Sprinter DSS штатно монтирует стандартный MBR+FAT16 (подтверждено). +# +# Использование: +# toolchain/make_hdd.sh mame/v306/IMG/test_hdd.chd prog.exe data1 data2 ... +set -e + +if [ $# -lt 2 ]; then + echo "usage: $0 OUT.chd FILE1 [FILE2 ...]" >&2 + exit 1 +fi + +OUT_CHD="$1"; shift + +# Геометрия: 128*16*32*512 = 32 МБ. BPS=512. +CYLS=128; HEADS=16; SECS=32 +SECTORS=$((CYLS * HEADS * SECS)) # 65536 + +# macOS mktemp требует X'ы в КОНЦЕ шаблона. +RAW=$(mktemp /tmp/sphdd.raw.XXXXXX) +MTC=$(mktemp /tmp/sphdd.mtc.XXXXXX) +trap 'rm -f "$RAW" "$MTC"' EXIT + +dd if=/dev/zero of="$RAW" bs=512 count=$SECTORS status=none + +# mtools-диск z: = раздел 1 нашего RAW-образа. +printf 'drive z: file="%s" partition=1\n' "$RAW" > "$MTC" +export MTOOLSRC="$MTC" MTOOLS_SKIP_CHECK=1 + +# MBR + раздел 1 на весь диск (тип FAT16), затем формат FAT16. +mpartition -I -c -t $CYLS -h $HEADS -s $SECS z: +mformat z: + +for f in "$@"; do + mcopy -o "$f" z: +done + +echo "--- содержимое образа ---" +mdir z: + +# RAW -> CHD (без сжатия = быстро; та же геометрия). +chdman createhd -i "$RAW" -o "$OUT_CHD" -c none \ + -chs $CYLS,$HEADS,$SECS -ss 512 -f +echo "готово: $OUT_CHD" diff --git a/toolchain/png_strip.py b/toolchain/png_strip.py new file mode 100644 index 0000000..d7c860a --- /dev/null +++ b/toolchain/png_strip.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +""" +png_strip.py — генерик-конвертер произвольного набора PNG-кадров (РАЗНОГО +размера каждый) в одну raw-ленту + палитру для sprite-движка Sprinter +(формат getimage/atlas — см. libbgi/include/sprite.h, toolchain/mkatlas.py). + +Не завязан ни на какой конкретный проект/игру — просто берёт список +картинок, которые вы сами укажете, и паковает их в ленту с общим +канвасом (кадр движка обязан быть фиксированного размера). + +Использование: + png_strip.py OUT.raw OUT.pal img1.png img2.png ... [опции] + +Опции: + --canvas WxH размер канваса кадра (по умолчанию — максимум + ширины/высоты среди входных картинок) + --anchor MODE bottom-center (по умолчанию) | top-left | center + --base-index N с какого индекса палитры начинать свои цвета + (по умолчанию 16 — под 16 EGA-слотов initgraph) + --ega добавить в палитру 16 EGA-цветов BGI перед своими + (по умолчанию включено; --no-ega выключает) + --seed-pal FILE переиспользовать цвета уже существующей палитры + (EGA16 + N своих) — свои индексы этой ленты + продолжаются ПОСЛЕ них (не с --base-index), а + OUT.pal получится ПОЛНОЙ merged-палитрой + (ega+seed+свои), готовой как --seed-pal для + следующего вызова. Так несколько независимо + собранных лент (напр. разные типы тайлов) можно + свести в ОДНУ палитру для одновременного показа + на экране (см. applications/PoP/poc/tools/ + build_room_palette.py). + +Каждая картинка — источник ОДНОГО кадра ленты (не атлас-сетка; +для сетки — см. mkatlas.py raw:FWxFH:NXxNY после этого шага, если +нужно). Прозрачность — альфа < 128 → индекс 0xFF (сентинел движка). + +Дальше: toolchain/mkatlas.py OUT.atl OUT.raw:CW x CH:1xN +""" +import argparse +import sys +from PIL import Image + +EGA = [ + (0, 0, 0), (168, 0, 0), (0, 168, 0), (168, 168, 0), + (0, 0, 168), (168, 0, 168), (0, 84, 168), (168, 168, 168), + (84, 84, 84), (255, 84, 84), (84, 255, 84), (255, 255, 84), + (84, 84, 255), (255, 84, 255), (84, 255, 255), (255, 255, 255), +] + + +def die(msg): + sys.exit(f"png_strip: {msg}") + + +def main(): + ap = argparse.ArgumentParser(add_help=False) + ap.add_argument("out_raw") + ap.add_argument("out_pal") + ap.add_argument("images", nargs="+") + ap.add_argument("--canvas", default=None) + ap.add_argument("--anchor", default="bottom-center", + choices=["bottom-center", "top-left", "center"]) + ap.add_argument("--base-index", type=int, default=16) + ap.add_argument("--ega", dest="ega", action="store_true", default=True) + ap.add_argument("--no-ega", dest="ega", action="store_false") + ap.add_argument("--seed-pal", default=None) + args = ap.parse_args() + + imgs = [Image.open(p).convert("RGBA") for p in args.images] + + seed_records = [] + if args.seed_pal: + raw = open(args.seed_pal, "rb").read() + if len(raw) % 4: + die(f"--seed-pal {args.seed_pal}: длина не кратна 4") + n_ega = 16 if args.ega else 0 + for i in range(n_ega, len(raw) // 4): + b, g, r, _ = raw[i * 4:i * 4 + 4] + seed_records.append((r, g, b)) + args.base_index = 16 + len(seed_records) + + if args.canvas: + cw, ch = (int(v) for v in args.canvas.lower().split("x")) + else: + cw = max(im.width for im in imgs) + ch = max(im.height for im in imgs) + if cw > 64 or ch > 64: + die(f"канвас {cw}x{ch} превышает лимит движка 64x64") + for p, im in zip(args.images, imgs): + if im.width > cw or im.height > ch: + die(f"{p}: {im.width}x{im.height} не влезает в канвас {cw}x{ch}") + + colors = {} + for i, rgb in enumerate(seed_records): + colors[rgb] = 16 + i + + def color_index(r, g, b): + key = (r, g, b) + if key not in colors: + idx = args.base_index + (len(colors) - len(seed_records)) + if idx >= 0xFF: + die("палитра переполнена (>= 0xFF своих цветов)") + colors[key] = idx + return colors[key] + + data = bytearray() + for im in imgs: + if args.anchor == "top-left": + ox, oy = 0, 0 + elif args.anchor == "center": + ox, oy = (cw - im.width) // 2, (ch - im.height) // 2 + else: # bottom-center + ox, oy = (cw - im.width) // 2, ch - im.height + canvas = [0xFF] * (cw * ch) + for y in range(im.height): + for x in range(im.width): + r, g, b, a = im.getpixel((x, y)) + if a < 128: + continue + canvas[(oy + y) * cw + (ox + x)] = color_index(r, g, b) + data.extend(canvas) + open(args.out_raw, "wb").write(data) + + pal = bytearray() + if args.ega: + for b, g, r in EGA: + pal += bytes((b, g, r, 0)) + new_colors = [rgb for rgb in colors if rgb not in seed_records] + for (r, g, b) in (*seed_records, *new_colors): + pal += bytes((b, g, r, 0)) + open(args.out_pal, "wb").write(pal) + + print(f"png_strip: {len(imgs)} кадров, канвас {cw}x{ch}, " + f"{len(data)} Б -> {args.out_raw}") + print(f" палитра {len(seed_records)} seed + {len(new_colors)} новых " + f"своих цветов ({len(pal)} Б) -> {args.out_pal}") + print(f"дальше: mkatlas.py OUT.atl {args.out_raw}:{cw}x{ch}:1x{len(imgs)}") + + +if __name__ == "__main__": + main() diff --git a/toolchain/room_compose.py b/toolchain/room_compose.py new file mode 100644 index 0000000..e438178 --- /dev/null +++ b/toolchain/room_compose.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +""" +room_compose.py — генерик-компоновщик полноэкранного фона комнаты из +тайлов ПРОИЗВОЛЬНОГО размера (см. applications/PoP/docs/PORT_PLAN.md §4: +один полноэкранный фон на комнату готовится офлайн, поверх — спрайты +через sprite.h). Не завязан ни на какой конкретный проект/игру — просто +берёт раскладку тайлов, которую вы сами опишете, и картинки, которые +сами укажете. + +Раскладка — JSON: + { + "rows": [ + {"height": 20, "tiles": ["a.png", "b.png", ...]}, + {"height": 60, "tiles": [...]}, + ... + ] + } + +Тайлы в ряду кладутся слева направо; высота ряда — из "height" (тайл +меньшей высоты — сверху, остаток — прозрачность 0xFF; выше — ошибка). +Ширина итоговой картинки = сумма ширин тайлов первого ряда — остальные +ряды обязаны давать ТУ ЖЕ сумму (иначе ошибка, во избежание кривого +экрана по краю). + +Использование: + room_compose.py OUT.raw OUT.pal --layout layout.json [--base-index N] + +Выход: + OUT.raw — ГОТОВЫЙ формат getimage (uint16 w, uint16 h LE + пиксели + построчно) — грузится fread'ом целиком и передаётся + напрямую в gfx_blit(x, y, buf). + OUT.pal — палитра (EGA 16 + свои цвета), формат gfx_pal_fload. +""" +import argparse +import json +import struct +import sys +from pathlib import Path +from PIL import Image + +EGA = [ + (0, 0, 0), (168, 0, 0), (0, 168, 0), (168, 168, 0), + (0, 0, 168), (168, 0, 168), (0, 84, 168), (168, 168, 168), + (84, 84, 84), (255, 84, 84), (84, 255, 84), (255, 255, 84), + (84, 84, 255), (255, 84, 255), (84, 255, 255), (255, 255, 255), +] + + +def die(msg): + sys.exit(f"room_compose: {msg}") + + +def main(): + ap = argparse.ArgumentParser(add_help=False) + ap.add_argument("out_raw") + ap.add_argument("out_pal") + ap.add_argument("--layout", required=True) + ap.add_argument("--base-index", type=int, default=16) + ap.add_argument("--ega", dest="ega", action="store_true", default=True) + ap.add_argument("--no-ega", dest="ega", action="store_false") + args = ap.parse_args() + + spec = json.loads(Path(args.layout).read_text()) + rows_spec = spec["rows"] + if not rows_spec: + die("layout: пустой список rows") + + rows_imgs = [] + width = None + for ri, row in enumerate(rows_spec): + rh = int(row["height"]) + tiles = row["tiles"] + if not tiles: + die(f"row {ri}: пустой список tiles") + imgs = [Image.open(t).convert("RGBA") for t in tiles] + for t, im in zip(tiles, imgs): + if im.height > rh: + die(f"{t}: высота {im.height} > высоты ряда {rh}") + row_w = sum(im.width for im in imgs) + if width is None: + width = row_w + elif row_w != width: + die(f"row {ri}: ширина {row_w} != ширины первого ряда {width} " + f"(все ряды должны давать одинаковую суммарную ширину)") + rows_imgs.append((rh, imgs)) + + height = sum(rh for rh, _ in rows_imgs) + if width > 65535 or height > 65535: + die(f"{width}x{height}: превышает предел формата (u16)") + + colors = {} + + def color_index(r, g, b): + key = (r, g, b) + if key not in colors: + idx = args.base_index + len(colors) + if idx >= 0xFF: + die("палитра переполнена (>= 0xFF своих цветов)") + colors[key] = idx + return colors[key] + + canvas = [0xFF] * (width * height) + y0 = 0 + for rh, imgs in rows_imgs: + x0 = 0 + for im in imgs: + for y in range(im.height): + for x in range(im.width): + r, g, b, a = im.getpixel((x, y)) + if a < 128: + continue + canvas[(y0 + y) * width + (x0 + x)] = color_index(r, g, b) + x0 += im.width + y0 += rh + + with open(args.out_raw, "wb") as f: + f.write(struct.pack(" {args.out_raw} ({4 + width * height} Б, вкл. getimage-заголовок)") + print(f" палитра {len(colors)} своих цветов " + f"({len(pal)} Б) -> {args.out_pal}") + + +if __name__ == "__main__": + main()