Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5ef084eca4 | |||
| 36a5a5e194 | |||
| 237f780e0e | |||
| 023b45eb85 | |||
| fce3e58830 | |||
| 3c0baacbf6 | |||
| cbae48dbc4 | |||
| 419d5e4eff | |||
| 5935724897 | |||
| 78f2aaec1a | |||
| ea57a03543 | |||
| cd8d566d82 | |||
| 484b18d10c | |||
| e14b6745f7 | |||
| 1b4fbeaa6b | |||
| 2c6f4e33c3 | |||
| 6b6986d9b6 | |||
| b9ddce8d34 | |||
| 0eec977630 | |||
| 02f7afe765 | |||
| b010d24792 | |||
| 6dea6955c2 | |||
| 8792594c5a | |||
| eb9ca0179d | |||
| 0e935343d9 | |||
| c4a512200f | |||
| 88a00bb4ec | |||
| ee1ca00c6f | |||
| 72ce66275e | |||
| 78161561e7 | |||
| 786836e2e7 | |||
| 9f8aa6fc28 | |||
| c9ac0999fd | |||
| 5de2f06cfb | |||
| ba09c0bd05 | |||
| 38bfabb2ca | |||
| 40e896a73d | |||
| 5f0c46f0ea | |||
| 580837d2ee | |||
| 2c414da465 | |||
| e553e5e6c9 | |||
| f0f0ab9276 | |||
| 9b1ce71130 | |||
| 029607971f |
@@ -0,0 +1,13 @@
|
|||||||
|
CompileFlags:
|
||||||
|
Add:
|
||||||
|
- -Ilibbgi/include
|
||||||
|
- -Ilibc/include
|
||||||
|
- -I.
|
||||||
|
- --target=z80-unknown-unknown
|
||||||
|
- -mz80
|
||||||
|
- -std=c99
|
||||||
|
- -D__SDCC # если нужно гасить SDCC-специфику через #ifdef
|
||||||
|
Remove:
|
||||||
|
- --target=z80-unknown-unknown
|
||||||
|
Diagnostics:
|
||||||
|
UnusedIncludes: None # опционально, чтобы не ругался на "лишние" инклюды под другой таргет
|
||||||
@@ -40,6 +40,30 @@ tests/*/*.cdb
|
|||||||
tests/*/*.mem
|
tests/*/*.mem
|
||||||
tests/*/*.rst
|
tests/*/*.rst
|
||||||
|
|
||||||
|
# applications/<app>/<prog>/ — реальные приложения (та же схема, что
|
||||||
|
# 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)
|
# libc + libbgi archives (built by libc/Makefile + libbgi/Makefile)
|
||||||
lib/*.lib
|
lib/*.lib
|
||||||
|
|
||||||
|
|||||||
@@ -7,20 +7,28 @@ Target-слой SDCC 4.5 (z80) для компьютера Sprinter Sp2000: crt0
|
|||||||
|
|
||||||
```
|
```
|
||||||
make # tools + lib + libbgi + все тесты (45) + examples
|
make # tools + lib + libbgi + все тесты (45) + examples
|
||||||
make -C libc # только libc → lib/sprinter.lib
|
make -C libc # только libc → lib/sprinter.lib (fast) + sprinter_safe.lib
|
||||||
make -C libbgi # только BGI → lib/bgi256.lib (и bgi16.lib в Фазе 2)
|
make -C libbgi # только BGI → lib/bgi256.lib (fast) + bgi256_safe.lib
|
||||||
make floppy # упаковать все .exe в mame/v306/IMG/mc.img
|
make floppy # упаковать все .exe в mame/v306/IMG/mc.img
|
||||||
make size-check # размерный регресс: _CODE vs docs/size_baseline.tsv
|
make size-check # размерный регресс: _CODE vs docs/size_baseline.tsv
|
||||||
make size-baseline # принять текущие размеры эталоном
|
make size-baseline # принять текущие размеры эталоном
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Обе библиотеки собираются в двух вариантах: fast (дефолт; `-D*_NOCHECK` —
|
||||||
|
параметр-валидации вырезаны) и safe (линкуется по `sprinter-cc --safe`).
|
||||||
|
Критичные гарды (напр. _fd_guard — 9-й OPEN вешает DSS) — в ОБОИХ.
|
||||||
|
|
||||||
Графика (BGI) — отдельная библиотека libbgi/ (см. ниже). Программа,
|
Графика (BGI) — отдельная библиотека libbgi/ (см. ниже). Программа,
|
||||||
использующая graphics.h, собирается с `--gfx 256` (или `--gfx 16` в
|
использующая graphics.h, собирается с `--gfx 256` (или `--gfx 16` в
|
||||||
Фазе 2): sprinter-cc подлинкует lib/bgi256.lib и добавит -I libbgi/include.
|
Фазе 2): sprinter-cc подлинкует lib/bgi256.lib и добавит -I libbgi/include.
|
||||||
|
|
||||||
Одиночный тест: `cd tests/<имя> && make run` (пакует ТОЛЬКО этот exe
|
Одиночный тест: `cd tests/<имя> && make run` (пакует exe + EXTRA_DATA на
|
||||||
+ EXTRA_DATA на дискету и запускает MAME). Тесты в MAME гоняет
|
дискету и запускает MAME автоматически через `toolchain/mame_interactive.py`,
|
||||||
пользователь — готовь дискету и проси прогнать.
|
снимает скриншоты, выводит пути). Для сложных сценариев (диалог, несколько
|
||||||
|
шагов ввода) — прямой вызов:
|
||||||
|
`python3 toolchain/mame_interactive.py tests/<имя>/<имя>.exe --snap T1,T2 --timeout T`.
|
||||||
|
Скриншоты лежат в `mame/v306/snap_auto/sprinter/` (читаются инструментом Read).
|
||||||
|
Подробности: `docs/mame-autotest.md`.
|
||||||
|
|
||||||
После правок libc/libbgi: пересборка от чистого листа (`make -C libc clean`
|
После правок libc/libbgi: пересборка от чистого листа (`make -C libc clean`
|
||||||
/ `make -C libbgi clean`) не обязательна — stale .rel чистятся
|
/ `make -C libbgi clean`) не обязательна — stale .rel чистятся
|
||||||
|
|||||||
@@ -16,9 +16,9 @@
|
|||||||
TESTS := hello hello2 simple banked bankedbg banktest strtest cat seek \
|
TESTS := hello hello2 simple banked bankedbg banktest strtest cat seek \
|
||||||
malloc mem_test argv errno rt_test openenv ls conio conio2 \
|
malloc mem_test argv errno rt_test openenv ls conio conio2 \
|
||||||
attrprob timedir mouse banklocl stdlib assrtest ptime stattest \
|
attrprob timedir mouse banklocl stdlib assrtest ptime stattest \
|
||||||
filetest fdmax fbench solidt irqtest cbltest cblwav dec_test gets stest2 winrest \
|
filetest fdmax fbench solidt irqtest cbltest cblwav cblstream dec_test gets stest2 winrest \
|
||||||
bios_text text_palette \
|
bios_text text_palette \
|
||||||
gfx_demo gfx_dbuf
|
gfx_demo gfx_dbuf bgitest bgi_img accfill
|
||||||
# gfx_d16 / gfx_text / gfx_mous — 16-цветные; убраны до Фазы 2 (bgi16.lib
|
# gfx_d16 / gfx_text / gfx_mous — 16-цветные; убраны до Фазы 2 (bgi16.lib
|
||||||
# ещё не собирается). Вернуть мигрированными на BGI --gfx 16.
|
# ещё не собирается). Вернуть мигрированными на BGI --gfx 16.
|
||||||
# Larger end-user applications under examples/.
|
# Larger end-user applications under examples/.
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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 <graphics.h>
|
||||||
|
#include <gfx.h>
|
||||||
|
#include <sprite.h>
|
||||||
|
#include <conio.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
/* Имена файлов — плоская ФС диска (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;
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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 <graphics.h>
|
||||||
|
#include <gfx.h>
|
||||||
|
#include <conio.h>
|
||||||
|
|
||||||
|
#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;
|
||||||
|
}
|
||||||
@@ -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.*`).
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
# Prince of Persia — Kid (персонаж): анализ и план
|
||||||
|
|
||||||
|
Статус: план (2026-07-16). Опирается на разбор `SDLPoP/src/seg006.c`
|
||||||
|
(ядро физики/управления Kid), `seqtbl.c` (таблицы последовательностей),
|
||||||
|
`types.h` (char_type, seq_*, SEQ_*, actions_*), `SDLPoP/data/KID` (спрайты).
|
||||||
|
Фон уже готов и проверен на MAME (`applications/PoP/roomtest`, см.
|
||||||
|
`memory/pop_background_strategy`) — Kid развиваем в том же `roomtest` как
|
||||||
|
новый PoC (решение пользователя: старый `poc/` не трогаем).
|
||||||
|
|
||||||
|
**Копирайт:** спрайты Kid (`SDLPoP/data/KID`) — Broderbund/Ubisoft.
|
||||||
|
Использование настоящей графики Kid — сознательное решение пользователя
|
||||||
|
(в отличие от плейсхолдера в старом `poc/`, см. PORT_PLAN §5.1).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Как устроен персонаж в оригинале (что портируем)
|
||||||
|
|
||||||
|
### 1.1 Состояние — `char_type` (14 полей, types.h)
|
||||||
|
|
||||||
|
```
|
||||||
|
frame текущий номер кадра (индекс во frame_table_kid)
|
||||||
|
x, y позиция (byte; x — с учётом direction)
|
||||||
|
direction -1 влево / 0 вправо
|
||||||
|
curr_col, логическая клетка (тайл), где персонаж
|
||||||
|
curr_row
|
||||||
|
action КАТЕГОРИЯ действия (actions_*, см. 1.2)
|
||||||
|
fall_x, скорость падения (fall_y<22 = 1 ряд, <33 = 2 ряда)
|
||||||
|
fall_y
|
||||||
|
room комната
|
||||||
|
repeat счётчик для удержания-ввода (напр. повторный прыжок)
|
||||||
|
sword есть ли меч (бой — вне Фазы 1)
|
||||||
|
alive жив/мёртв
|
||||||
|
curr_seq УКАЗАТЕЛЬ в seqtbl (байткод текущей последовательности)
|
||||||
|
```
|
||||||
|
|
||||||
|
Состояние крошечное — легко живёт в W2.
|
||||||
|
|
||||||
|
### 1.2 Категории действия — `actions_*` (9 шт)
|
||||||
|
|
||||||
|
`0 stand`, `1 run_jump`, `2 hang_climb`, `3 in_midair`, `4 in_freefall`,
|
||||||
|
`5 bumped`, `6 hang_straight`, `7 turn`, `99 hurt`. `action` определяет,
|
||||||
|
как `check_action()`/`play_kid()` реагируют на ввод и физику каждый тик.
|
||||||
|
|
||||||
|
### 1.3 Движок анимации/движения — ГЛАВНОЕ
|
||||||
|
|
||||||
|
**Движение НЕ физика, а байткод + per-frame смещения** (подтверждает
|
||||||
|
PORT_PLAN §6). Три уровня:
|
||||||
|
|
||||||
|
1. **`seqtbl`** — байткод-программа на действие. Опкоды (types.h):
|
||||||
|
`SEQ_DX`(0xFB) сдвиг x на amount×direction, `SEQ_DY`(0xFA) сдвиг y,
|
||||||
|
`SEQ_FLIP`(0xFE) разворот, `SEQ_JMP`(0xFF)/`SEQ_JMP_IF_FEATHER`(0xF7),
|
||||||
|
`SEQ_UP`/`SEQ_DOWN`(0xFD/0xFC) смена ряда, `SEQ_ACTION`(0xF9) задать
|
||||||
|
`Char.action`, `SEQ_SET_FALL`(0xF8), `SEQ_KNOCK_UP/DOWN`, `SEQ_SOUND`,
|
||||||
|
`SEQ_DIE`/`SEQ_END_LEVEL`/`SEQ_GET_ITEM`. **Байт < 0xF0 = НОМЕР КАДРА**
|
||||||
|
→ ставит `Char.frame` и play_seq возвращается (один кадр за тик).
|
||||||
|
2. **`play_seq()`** (seg006.c:570) — интерпретатор: крутит опкоды из
|
||||||
|
`seqtbl + Char.curr_seq`, пока не встретит кадр. ~15 case — портируется
|
||||||
|
1-в-1. **Квирк:** seqtbl использует АБСОЛЮТНЫЕ DOS-адреса в JMP;
|
||||||
|
`SEQTBL_0 = seqtbl - SEQTBL_BASE(0x196E)` — при порте пересчитать
|
||||||
|
базу (JMP-адреса в наших данных).
|
||||||
|
3. **`frame_table_kid[]`** (seg006.c:127, ~180 кадров) — на КАЖДЫЙ кадр:
|
||||||
|
`{image, sword_flags, dx, dy, flags}`. `image` — индекс спрайта Kid;
|
||||||
|
`dx/dy` — смещение позиции ЭТОГО кадра; `flags`: 0x1F weight_x, 0x20
|
||||||
|
thin, 0x40 needs_floor, 0x80 even/odd-pixel (влияет на x-рендер).
|
||||||
|
|
||||||
|
**Тик персонажа:** `play_kid()` (диспетчер по action+вводу) → `play_seq()`
|
||||||
|
(двигает curr_seq, ставит кадр, применяет seq-dx/dy) → `frame_table[frame]`
|
||||||
|
даёт image+собственные dx/dy → позиция и спрайт. У нас это ложится на
|
||||||
|
`sprite_frame`+`sprite_move` (НЕ `sprite_anim`/`sprite_moveto` — см.
|
||||||
|
PORT_PLAN §6: авторские таблицы, не автопрогрессия).
|
||||||
|
|
||||||
|
### 1.4 Управление — `control_kid()`/`read_user_control()` (seg006.c)
|
||||||
|
|
||||||
|
Читает ввод (у нас — held-state `kbd_raw`, уже готово, §2 PORT_PLAN) и по
|
||||||
|
`Char.action` выбирает последовательность (`seqtbl_offset_char(seq_id)`).
|
||||||
|
Логика «что можно из какого состояния» — ядро ощущения PoP.
|
||||||
|
|
||||||
|
### 1.5 Взаимодействие с картой — collision (seg006.c)
|
||||||
|
|
||||||
|
`check_on_floor()`/`start_fall()` — пол под ногами / падение в яму;
|
||||||
|
`in_wall()` — упор в стену (сдвиг наружу); `check_grab()`/
|
||||||
|
`can_grab_front_above()` — зацеп за уступ; `fell_out()` — вывалиться из
|
||||||
|
комнаты; `check_spiked()`/loose — ловушки; `fall_accel()`/`fall_speed()` —
|
||||||
|
ускорение падения. Всё читает ТИП тайла (`get_tile`) — у нас это уже
|
||||||
|
разобранные `fg[]/bg[]` (level.h/room1_data.h).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Спрайты Kid (219 шт, 16 цветов, 177 КБ)
|
||||||
|
|
||||||
|
- 219 PNG (`data/KID`), 16-цветные (палитра `res400.pal`, 16×RGB как env/
|
||||||
|
wall), макс кадр **53×35** — влезает в лимит движка 64×64. 177 КБ в
|
||||||
|
8bpp.
|
||||||
|
- `frame_table_kid` отображает кадр→`image` (индекс спрайта). Число
|
||||||
|
РАЗЛИЧНЫХ image — уточнить (≤219); паковать те, что реально используются
|
||||||
|
платформинг-последовательностями Фазы 1 (не все 219 — бой/катсцены
|
||||||
|
отдельно).
|
||||||
|
- **Палитра:** Kid 16 цветов → слоты Sprinter `0x70-0x7F` (env 0x50, wall
|
||||||
|
0x60 уже заняты; Kid не пересекается). Пиксель i: 0→0xFF, i→0x70+i.
|
||||||
|
Тот же пайплайн, что `pop_pack_bg.py`.
|
||||||
|
- **Атлас:** прямая адресация по номеру image (как фон): `kid[img>>5]`,
|
||||||
|
idx `img&31`; ~7 EMM-страниц (или SHIFT=4). Свой пакер `pop_pack_kid.py`
|
||||||
|
(переиспользовать код `pop_pack_bg.py`).
|
||||||
|
|
||||||
|
### 2.1 РЕШЕНИЕ ДО СТАРТА: per-frame offset vs padding
|
||||||
|
|
||||||
|
Кадры Kid — РАЗНОГО размера, а `sprite_t` рисует от угла фикс. w/h. Два
|
||||||
|
пути (см. PORT_PLAN §6.1, `memory/png_strip_padding_tradeoff`):
|
||||||
|
- **Padding** (bottom-center) — просто, но 219×53×35 ≈ 406 КБ (раздув ×2.3).
|
||||||
|
- **Per-frame offset** — хранить XCO/YCO кадра (у оригинала он и есть,
|
||||||
|
`APPLEII_RESOURCE_FORMAT §2.2`), рисовать `blit(x+xco, y+yco)`; паддинг не
|
||||||
|
нужен, память по факту (177 КБ). Требует лёгкого расширения хранения
|
||||||
|
(offset рядом с кадром) ИЛИ ручного смещения в коде рендера Kid.
|
||||||
|
**Рекомендация:** per-frame offset — оригинал так и делает (frame_table dx/dy
|
||||||
|
+ image XCO/YCO), даёт точное позиционирование И экономию. Хранить xco/yco
|
||||||
|
в нашей копии frame_table (добавить 2 байта/кадр — ~360 Б). Не тянуть
|
||||||
|
расширение `sprite.h` — рисовать Kid прямым `gfx_blit(x+xco, y+yco, img)`
|
||||||
|
(как фон), НЕ через retained `sprite_t`, раз позиция и кадр всё равно
|
||||||
|
задаются вручную каждый тик.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Данные для порта (объём)
|
||||||
|
|
||||||
|
- `frame_table_kid` → C-массив ~180×(5+2 offset) ≈ 1.3 КБ (const, ROM).
|
||||||
|
- `seqtbl` (нужные последовательности) → C-массив байт. Весь seqtbl ~1-2 КБ;
|
||||||
|
для Фазы 1 можно взять только платформинг-последовательности (вырезать
|
||||||
|
бой/гардов 55-92) — оценить после разметки. JMP-адреса пересчитать под
|
||||||
|
свою базу.
|
||||||
|
- Спрайты — атласы (EMM, не W2).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Фазы работы (по твоему списку, порядок по зависимостям)
|
||||||
|
|
||||||
|
**Фаза K0 — конвейер спрайтов + отрисовка одного кадра**
|
||||||
|
- `pop_pack_kid.py`: 219 (или подмножество) → `kid*.atl` + `kid.pal`
|
||||||
|
(слоты 0x70), таблица кадр→image + xco/yco.
|
||||||
|
- Отрисовать Kid ОДНИМ кадром (stand) в roomtest поверх фона на верном
|
||||||
|
тайле — проверить палитру/позицию/прозрачность на MAME.
|
||||||
|
- Артефакт-цель: Kid стоит на уступе комнаты 1 как в `1.1-2.png`.
|
||||||
|
|
||||||
|
**Фаза K1 — движок анимации (play_seq + frame_table)**
|
||||||
|
- Портировать `play_seq()` (интерпретатор) + `frame_table_kid` + минимальный
|
||||||
|
`seqtbl` (stand/run/turn).
|
||||||
|
- Прогнать несколько последовательностей вручную (stand→run→stop) —
|
||||||
|
проверить, что кадры и смещения совпадают с оригиналом (сверять с
|
||||||
|
SDLPoP/скриншотами, тайминг 50 Гц).
|
||||||
|
|
||||||
|
**Фаза K2 — управление на месте + ходьба (твои а, б)**
|
||||||
|
- `control_kid` подмножество: stand (2), run (1/84/13), turn (5/6),
|
||||||
|
standing_jump (3), crouch (50/49), safe_step (29-44 — аккуратный шаг).
|
||||||
|
- Held-state через `kbd_raw` (готово).
|
||||||
|
|
||||||
|
**Фаза K3 — коллизия с картой (твой п.3)**
|
||||||
|
- `check_on_floor`/`start_fall` — падение в ямы (тип тайла под ногами из
|
||||||
|
`fg[]`); `in_wall`/стоп у стены; `fell_out` (край экрана — пока без
|
||||||
|
перехода комнат).
|
||||||
|
- Падения/приземления (seq 7/17/19/20) + `fall_accel/fall_speed`.
|
||||||
|
|
||||||
|
**Фаза K4 — прыжки и повисание (твои а-прыжок, в)**
|
||||||
|
- run_jump (4), jump_up (28/14), grab (8/16/24), climb_up (10)/down (68),
|
||||||
|
hang (25/6), release (11/23). Это самый «PoP-овый» кусок — сверять
|
||||||
|
дистанции/тайминг с оригиналом (не на глаз).
|
||||||
|
|
||||||
|
**Фаза K5 — прочее (твой г)**
|
||||||
|
- drink (78), level_door (70), crouch_hop (79), spiked/loose/chomped
|
||||||
|
(ловушки, если тайлы есть в комнате), death (71).
|
||||||
|
|
||||||
|
Бой (меч, seq 55-92, стражники — seg005) — ВНЕ этого плана (отдельная фаза
|
||||||
|
полного приложения, PORT_PLAN §7 Фаза 3).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Риски/решения ДО кода (правило defer_unexplained_quirks)
|
||||||
|
|
||||||
|
1. **Per-frame offset** (§2.1) — решить до K0 (влияет на формат данных).
|
||||||
|
Рекомендация: xco/yco в frame_table, прямой blit.
|
||||||
|
2. **seqtbl rebasing** — JMP-адреса абсолютные (SEQTBL_BASE 0x196E); при
|
||||||
|
порте пересчитать в оффсеты своего массива. Проверить на 1-2 seq.
|
||||||
|
3. **Тайминг** — оригинал (DOS) фиксированный тик; наш 50 Гц. Если
|
||||||
|
логическая частота кадров иная — пересчёт dx/dy (PORT_PLAN §8.4).
|
||||||
|
Сверять дистанцию бега/прыжка с эталоном.
|
||||||
|
4. **Число реально нужных кадров/последовательностей** для Фазы 1 —
|
||||||
|
разметить (вырезать бой/катсцены/гардов), чтобы не тянуть все 219
|
||||||
|
спрайта и весь seqtbl.
|
||||||
|
5. **Копирайт графики Kid** — подтверждено решение пользователя (§вводная).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Что переиспользуем (готово)
|
||||||
|
|
||||||
|
- Фон комнаты (`pop_bg.c`) — Kid рисуется ПОВЕРХ (сейчас — прямым blit;
|
||||||
|
heal против фона — когда/если понадобится через RAM-копию, фон её уже
|
||||||
|
заполняет, `GFX_BANK_TRANSPARENT`).
|
||||||
|
- `kbd_raw` held-state (§2 PORT_PLAN) — готов и проверен.
|
||||||
|
- Пакер спрайтов/палитра (`pop_pack_bg.py`) — шаблон для `pop_pack_kid.py`.
|
||||||
|
- Разобранная карта комнаты (`fg[]/bg[]`, level.h) — для коллизий.
|
||||||
|
- `gfx_blit`/`gfx_w0_map` из W0-атласа — проверенный путь (bgtest/roomtest).
|
||||||
@@ -0,0 +1,291 @@
|
|||||||
|
# Формат ресурсов Prince of Persia (MS-DOS, каталог `MSDOS/`)
|
||||||
|
|
||||||
|
Документ описывает бинарный формат `*.DAT`-файлов ресурсов DOS-версии PoP.
|
||||||
|
Исходников для этой версии нет, поэтому всё, что ниже — результат
|
||||||
|
структурного (эмпирического) анализа реальных файлов из `MSDOS/`, а не чтения
|
||||||
|
кода. Уровень уверенности указан для каждого раздела. Все находки проверены
|
||||||
|
скриптами (Python), которые разбирают файл и валидируют согласованность
|
||||||
|
(например: смещение+размер последней записи таблицы точно совпадает с
|
||||||
|
началом самой таблицы — то есть данные и каталог стыкуются без дыр).
|
||||||
|
|
||||||
|
Для справки при последующей реализации (порт на ZX Sprinter) стоит держать в
|
||||||
|
уме два внешних проекта:
|
||||||
|
|
||||||
|
- **SDLPoP** (github.com/NagyD/SDLPoP, GPLv3) — open-source реализация
|
||||||
|
DOS-версии на основе дизассемблирования оригинального `PRINCE.EXE`. Содержит
|
||||||
|
рабочий код чтения `.DAT`-файлов и полный кодек изображений/уровней. Точные
|
||||||
|
структуры (`dat_table_type` и т.п.), процитированные ниже, получены через
|
||||||
|
автоматический пересказ содержимого файла третьей стороной, а не через
|
||||||
|
прямое чтение исходника — поэтому такие детали помечены как "требует сверки
|
||||||
|
при реализации", в отличие от эмпирически подтверждённых байтовых оффсетов.
|
||||||
|
- **Princed Resources / PR** (github.com/NagyD/PR, princed.org, GPLv2) — это
|
||||||
|
профильный инструмент именно для распаковки/запаковки `.DAT`-ресурсов PoP
|
||||||
|
(версии DAT 1 и 2), сделанный тем же автором. В его документации
|
||||||
|
(`doc/Dataformats.md`) официально описаны экспортные форматы ресурсов —
|
||||||
|
это подтверждает и уточняет часть находок ниже (см. §3–4), и является более
|
||||||
|
надёжным источником, чем самостоятельная догадка по байтам.
|
||||||
|
|
||||||
|
**Важная находка:** репозиторий SDLPoP в папке `data/` содержит не только
|
||||||
|
код движка, но и **реальные ресурсы игры** — как сырые `.DAT`-контейнеры, так
|
||||||
|
и уже распакованные поштучно файлы (PNG-кадры спрайтов, `.pal`-палитры,
|
||||||
|
`.bin`-дампы уровней), см. §7. Это готовый источник ассетов и одновременно
|
||||||
|
независимая проверка формата, описанного в этом документе.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Общий контейнер `.DAT` (уверенность: высокая, подтверждено на 28 файлах)
|
||||||
|
|
||||||
|
Каждый `*.DAT`-файл (кроме служебных `config.dat`/`setup.dat`, см. §5) — это
|
||||||
|
простой архив-контейнер: блок данных + оглавление (каталог ресурсов) в конце
|
||||||
|
файла.
|
||||||
|
|
||||||
|
### 1.1 Заголовок файла (6 байт, смещение 0x00)
|
||||||
|
|
||||||
|
| Смещение | Размер | Поле | Значение |
|
||||||
|
|----------|--------|--------------|----------|
|
||||||
|
| 0x00 | 4 | `tableOffset`| LE u32. Абсолютное смещение в файле, с которого начинается таблица оглавления. Совпадает с "концом данных". |
|
||||||
|
| 0x04 | 2 | `tableSize` | LE u16. Размер таблицы оглавления в байтах. |
|
||||||
|
|
||||||
|
Инвариант, подтверждённый на всех 28 `.dat`-файлах в каталоге:
|
||||||
|
|
||||||
|
```
|
||||||
|
tableOffset + tableSize == размер файла (без исключений)
|
||||||
|
```
|
||||||
|
|
||||||
|
Данные ресурсов идут сразу после заголовка, начиная с байта 0x06, и
|
||||||
|
заканчиваются на `tableOffset`.
|
||||||
|
|
||||||
|
### 1.2 Таблица оглавления (по смещению `tableOffset`, длиной `tableSize`)
|
||||||
|
|
||||||
|
Таблица — плоский массив записей по 8 байт. Количество записей:
|
||||||
|
`tableSize / 8` (округление вниз; в файле почти всегда остаётся 2 "лишних"
|
||||||
|
байта в хвосте таблицы — назначение не установлено, вероятно, служебное поле
|
||||||
|
инструмента-упаковщика или паддинг; на итоговый разбор не влияет).
|
||||||
|
|
||||||
|
Запись (8 байт):
|
||||||
|
|
||||||
|
| Смещение в записи | Размер | Поле | Описание |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 0 | 2 | `size` | LE u16 — размер данных ресурса в байтах |
|
||||||
|
| 2 | 2 | `id` | LE u16 — идентификатор ресурса |
|
||||||
|
| 4 | 2 | `offset` | LE u16 — **абсолютное** смещение данных ресурса в файле (не относительное!) |
|
||||||
|
| 6 | 2 | `reserved` | во всех проверенных записях (сотни штук) всегда `0x0000` |
|
||||||
|
|
||||||
|
Проверено на `levels.dat`: 16 записей, `id`=2000..2015, и `offset[i] + size[i]
|
||||||
|
== offset[i+1]` для всех соседних записей, а последняя запись заканчивается
|
||||||
|
ровно на `tableOffset` — то есть данные абсолютно плотно упакованы, без
|
||||||
|
пробелов, для этого файла. В других файлах (например `guard.dat`) между
|
||||||
|
записями изредка есть небольшие зазоры в несколько байт (вероятно, выравнивание
|
||||||
|
или "мёртвые" байты от инструмента-компоновщика) — не является нарушением
|
||||||
|
формата.
|
||||||
|
|
||||||
|
### 1.3 Диапазоны `id` по типам файлов (собрано эмпирически)
|
||||||
|
|
||||||
|
Похоже, что числовые ID образуют условные "пространства имён" по типу
|
||||||
|
контента — вероятно, глобальные константы в оригинальном коде:
|
||||||
|
|
||||||
|
| Файл(ы) | Диапазон `id` | Кол-во записей | Предполагаемое содержимое |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `levels.dat` | 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<id>.png` | id-диапазон (751-784) точно совпадает с нашим разбором `guard.dat` |
|
||||||
|
| `VPALACE/res200.pal`, `res201.png`, `res202.png`, … | палитра (JASC `.pal`) + PNG-кадры фонов дворца, **VGA-вариант (256 цветов)** | id-диапазон (200+) совпадает с `vpalace.dat` |
|
||||||
|
| `LEVELS/res2000.bin` … `res2015.bin` | сырые дампы уровней по 2304-2305 байт | id совпадает с `levels.dat`; содержимое `res2001.bin` **сверено побайтово** с нашим id=2001 — тайловые данные совпадают |
|
||||||
|
| `KID/`, `PRINCE/`, `SHADOW/`, `SKEL/`, `VIZIER/`, `FAT/`, `TITLE/`, `VDUNGEON/`, `PV/`, `IBM_SND1/`, `IBM_SND2/`, `font/`, `music/` | аналогичные наборы для остальных ресурсов | не проверялись по отдельности, но структура (папка на каждый `.dat`, файлы `res<id>.ext`) наблюдается одинаково |
|
||||||
|
|
||||||
|
**Вывод:** это данные из немного **другого релиза DOS-версии**, чем те, что
|
||||||
|
лежат у нас в `MSDOS/` (см. расхождение в размере `digisnd`/`midisnd`), но
|
||||||
|
формат контейнера и нумерация `id` — те же самые. Практически это значит:
|
||||||
|
|
||||||
|
1. Для получения играбельных PNG-спрайтов и VGA-фонов **не нужно
|
||||||
|
реализовывать декодер сжатия пикселей** — можно взять готовые файлы
|
||||||
|
`data/<ИМЯ>/res<id>.png` напрямую как исходный материал для конвертации
|
||||||
|
под видеорежим ZX Sprinter (в т.ч. `VPALACE`/`VDUNGEON` — уже
|
||||||
|
256-цветный VGA-арт, что прямо отвечает на вопрос про полноцветность).
|
||||||
|
2. Если в проекте важно использовать именно ту версию контента, что в наших
|
||||||
|
`MSDOS/*.dat` (а не версию из SDLPoP) — распаковку своих файлов всё же
|
||||||
|
придётся делать (кодек пикселей по-прежнему не восстановлен для сырых
|
||||||
|
`.DAT`, см. §3), либо принять решение работать с версией SDLPoP как
|
||||||
|
мастер-источником ассетов вместо своей.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Служебные не-ресурсные файлы
|
||||||
|
|
||||||
|
- `config.dat`, `setup.dat` — 28 байт, не являются ресурсными контейнерами
|
||||||
|
(не проходят проверку §1.1 — "размер" получается больше самого файла).
|
||||||
|
Скорее всего простые бинарные структуры настроек (звук/видеорежим,
|
||||||
|
выбранный на этапе `SETUP.EXE`/`INSTALL.EXE`), не связаны с игровым
|
||||||
|
контентом.
|
||||||
|
- `desktopd.cfg`, `setup.cfg` — текстовые/бинарные конфиги DOS-инсталлятора,
|
||||||
|
вне скоупа игровых ресурсов.
|
||||||
|
- `PRINCE.EXE` / `PRINCE.REM` — почти идентичны (отличие в единичных байтах
|
||||||
|
в районе смещения ~0x4ED0), похоже на кряк/патч одного байта проверки —
|
||||||
|
не относится к формату ресурсов.
|
||||||
|
- `old-games.nfo` — ASCII-арт NFO релиз-группы (old-games.ru), не игровые
|
||||||
|
данные.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Итоговая таблица уверенности
|
||||||
|
|
||||||
|
| Раздел | Уверенность | Как подтверждено |
|
||||||
|
|---|---|---|
|
||||||
|
| Контейнер `.DAT` (заголовок + таблица) | Высокая | Проверено скриптом на всех 28 файлах, инвариант offset+size выполняется без исключений; независимо подтверждено именованием `res<id>.*` в SDLPoP `data/` |
|
||||||
|
| ID-пространства ресурсов | Средняя-высокая | Наблюдение по диапазонам + сверка с `res<id>` именами файлов SDLPoP и побайтовым содержимым `res2001.bin` |
|
||||||
|
| Формат уровня = формату Apple II | Высокая (по размеру и содержимому), служебный блок id=2000 — открытый вопрос | Совпадение размера (2304 vs 2305), тайловые байты сходятся с `res2001.bin` из SDLPoP |
|
||||||
|
| Формат изображений/спрайтов (сырой `.DAT`) | Низкая-средняя | Контейнер подтверждён, кодек пикселей — нет; но практически закрыто наличием готовых PNG в SDLPoP `data/` (§5) |
|
||||||
|
| Формат звука (тип контейнера) | Высокая для MIDI-семейств, средняя для digisnd/ibm_snd | Явные MIDI-сигнатуры `MThd`/`MTrk` видны в байтах |
|
||||||
|
|
||||||
|
**Рекомендация для дальнейшей работы:** для получения арт-ассетов (спрайты,
|
||||||
|
фоны, палитры) — использовать готовые распакованные файлы из
|
||||||
|
`github.com/NagyD/SDLPoP/tree/master/data` (§5), это быстрее и надёжнее
|
||||||
|
самостоятельной реализации декодера. Декодер сырого `.DAT`-формата
|
||||||
|
изображений и точную семантику служебных полей `digisnd`/`ibm_snd`
|
||||||
|
(§3, §4) стоит восстанавливать только если понадобится читать именно нашу
|
||||||
|
локальную копию `MSDOS/*.dat` "как есть" — тогда ориентир прежний: исходник
|
||||||
|
SDLPoP (`src/seg009.c`, `src/data.c`/`data.h`).
|
||||||
@@ -0,0 +1,509 @@
|
|||||||
|
# Prince of Persia на ZX Sprinter — план порта
|
||||||
|
|
||||||
|
Статус: план (2026-07-15). §2 (A: kbd_mod_state / B: kbd_raw) —
|
||||||
|
РЕАЛИЗОВАНО и частично проверено в MAME (tests/kbdraw, 2026-07-15,
|
||||||
|
подробности в §2.2); PoC (§5) и остальные фазы — не начаты. Опирается на
|
||||||
|
`APPLEII_RESOURCE_FORMAT.md` / `MSDOS_RESOURCE_FORMAT.md` / `README.md` в
|
||||||
|
этой папке, на текущий sprinter-cc/libc/libbgi (см. §1) и на локальные копии
|
||||||
|
`applications/PoP/SDLPoP` (github.com/NagyD/SDLPoP, GPLv3) и
|
||||||
|
`applications/PoP/PR` (github.com/NagyD/PR, GPLv2) — используются только как
|
||||||
|
справочник по структурам/константам оригинального движка и как источник
|
||||||
|
готовых распакованных ассетов (`SDLPoP/data/`), не как код для копирования.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Что уже есть в sprinter-cc и библиотеках (используем как есть)
|
||||||
|
|
||||||
|
Собрано из `docs/TODO.md`, `docs/libc-reference.md`, `docs/sprite-api-design.md`,
|
||||||
|
`libbgi/include/{gfx.h,sprite.h,graphics.h}`, `examples/rpgwalk`.
|
||||||
|
|
||||||
|
- **Графика 320×256×256** (`GFX_MODE_320x256x256`, режим 0x81) — разрешение и
|
||||||
|
глубина цвета совпадают почти впрямую с VGA-ассетами оригинала
|
||||||
|
(`SDLPoP/data/VPALACE`, `VDUNGEON` — уже 256-цветные PNG). Не нужно ужимать
|
||||||
|
в EGA/CGA палитру.
|
||||||
|
- **BGI-слой** (`graphics.h`) — примитивы, палитра, текст, `getimage/putimage`
|
||||||
|
— Фазы 1-2d готовы и проверены в MAME.
|
||||||
|
- **Спрайтовый движок v2** (`sprite.h`, ветка `sprite-engine-v2`) — ровно то,
|
||||||
|
что нужно персонажам PoP:
|
||||||
|
- retained-модель (`sprite_update`/`sprite_flip`, double-buffer, dirty-биты,
|
||||||
|
heal+blit за один проход);
|
||||||
|
- кадровая анимация по ленте (`sprite_anim`, LOOP/PINGPONG/ONCE,
|
||||||
|
горизонтальная/вертикальная лента) и tween-перемещение
|
||||||
|
(`sprite_moveto`, DDA без knowledge-heavy арифметики);
|
||||||
|
- Y-сортировка слоями (`gfx_sprite_ysort`, `layer`) — то, что нужно для
|
||||||
|
«Кид перед/за стражником» без ручной пересортировки;
|
||||||
|
- атласы в EMM-страницах (`atlas_t`/`atlas_load`) — на восьмерых
|
||||||
|
персонажей в `rpgwalk` уже работает: прямой прецедент для Кида/стражника;
|
||||||
|
- ограничение кадра ≤ 64×64 — с запасом (см. §3: кадры Кида в оригинале
|
||||||
|
~12-30 × 39-42 px).
|
||||||
|
- **Frame pacing** (`gfx_set_fps_div`) + цепочка кадровых IRQ — стабильный
|
||||||
|
логический тик независимо от рендер-нагрузки экрана (проверено MAME).
|
||||||
|
- **EMM-бюджет**: ~3.3 МБ свободно на старте (`memory/sprinter_emm_budget`) —
|
||||||
|
с большим запасом на все спрайт-атласы и предрендеренные фоны комнат (см.
|
||||||
|
§4) даже без выгрузки неиспользуемых уровней.
|
||||||
|
- **Файловый ввод-вывод** (FILE* v2, `fopen/fread/...`) — для загрузки
|
||||||
|
уровней/атласов/палитр с дискеты, по образцу `rpgwalk` (`atlas_load`,
|
||||||
|
`gfx_pal_fload`).
|
||||||
|
- **Клавиатура (событийная)** — `kbhit/getch/getkey` (ASCII + `KEY_*` скан-код
|
||||||
|
для стрелок), см. §2 — это НЕ то, что нужно для управления Кидом один в
|
||||||
|
один (см. ниже).
|
||||||
|
- **Звук** — `cbl.h` (потоковый CBL/COVOX, callback-модель, verified MAME) —
|
||||||
|
подходит для оцифрованных эффектов (`digisnd*.dat` — PC-звук
|
||||||
|
~11 кГц 8-бит, см. `MSDOS_RESOURCE_FORMAT.md` §4).
|
||||||
|
|
||||||
|
Вывод: **движок отрисовки и анимации почти не требует нового кода** —
|
||||||
|
самый близкий по духу пример (`rpgwalk`: атласы, анимация, tween, дабл-буфер,
|
||||||
|
FPS-делитель) переносится на PoP почти без изменений архитектуры.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Единственный принципиальный пробел: удержание клавиш
|
||||||
|
|
||||||
|
**Спайк проведён (2026-07-15), вопрос закрыт артефактами — не догадкой.**
|
||||||
|
|
||||||
|
`getch`/`getkey` — это события ESTEX WAITKEY/SCANKEY (по нажатию), без чёткой
|
||||||
|
информации о СОСТОЯНИИ (что зажато прямо сейчас, несколько клавиш
|
||||||
|
одновременно). Prince of Persia на управлении требует именно состояния:
|
||||||
|
держать направление (бег) + одновременно нажать вверх (прыжок вперёд), держать
|
||||||
|
Shift (модификатор) + направление и т.д.
|
||||||
|
|
||||||
|
### 2.1 Находки
|
||||||
|
|
||||||
|
1. **`docs/converted/ProgrammerManual.txt` документирует функцию, которую мы
|
||||||
|
раньше пропустили: `CTRLKEY` (ESTEX $33h)** — «Получить состояние
|
||||||
|
клавиатуры». Дословно: «данные берутся не из буфера клавиатуры (как в
|
||||||
|
остальных функциях), а непосредственно из результатов ПОСЛЕДНЕГО
|
||||||
|
сканирования» — то есть это НАСТОЯЩЕЕ live-state, не событие. Но
|
||||||
|
покрывает только модификаторы: Left/Right Shift, Ctrl, Alt,
|
||||||
|
Rus/Lat, Num/Scroll/Caps Lock, Insert (не обычные клавиши вроде стрелок).
|
||||||
|
Готовое решение для «держать Shift = бежать» — тривиальная обёртка,
|
||||||
|
без архитектурных рисков.
|
||||||
|
2. Для ОБЫЧНЫХ клавиш (стрелки, буквы) такого live-state нет нигде в ESTEX —
|
||||||
|
`WAITKEY`/`SCANKEY`/`TESTKEY` ($30/$31/$37h) — все три отдают ОДИНАКОВЫЙ
|
||||||
|
формат «очередное нажатие», без release. `TESTKEY` не удаляет событие из
|
||||||
|
буфера (полезно для «подсмотреть, не потребляя»), но это тоже разовое
|
||||||
|
нажатие, не состояние.
|
||||||
|
3. Автоповтор клавиатуры (typematic) не годится как замена held-state:
|
||||||
|
`MAME_MCP_GUIDE.md` фиксирует задержку до первого повтора ~1 секунда
|
||||||
|
(типично для PS/2) — на порядок медленнее кадра (20 мс), не подходит для
|
||||||
|
платформера.
|
||||||
|
4. **Решающий артефакт — `libc/irq/_irq_tramp.c` (сам трамплин прерывания,
|
||||||
|
не гипотеза):** вектор 0xFF общий для кадра/клавиатуры/CBL. Ветка
|
||||||
|
клавиатуры (бит 0 порта 0x19 = SIO-A RR0 «байт принят») делает буквально
|
||||||
|
`jp 0x0038` (прямиком в DSS) **до какого-либо чтения порта данных 0x18 И
|
||||||
|
до нашей кадровой цепочки (`_irq_chain`)** — наш `irq_chain_add`
|
||||||
|
вообще не видит клавиатурные прерывания, они физически не доходят до
|
||||||
|
цепочки (см. `tr_notkbd`/`tr_frame` разбор в файле). Значит текущая
|
||||||
|
инфраструктура (тот же механизм, что несёт FPS-делитель) НЕ дает
|
||||||
|
зацепки для клавиатуры без правки самого трамплина.
|
||||||
|
5. Регистр данных SIO (порт 0x18) — аппаратный приёмный буфer, чтение
|
||||||
|
деструктивно (дёргает байт из очереди); кто прочитал первым, тот и
|
||||||
|
владеет байтом. Значит «подглядеть, не мешая DSS» технически
|
||||||
|
невозможно — необходимо либо совсем не трогать этот путь (статус-кво),
|
||||||
|
либо взять его СЕБЕ полностью на время геймплея.
|
||||||
|
|
||||||
|
### 2.2 Рекомендация (конкретная, не три равнозначных варианта)
|
||||||
|
|
||||||
|
**A. Тривиально, почти без риска — обернуть `CTRLKEY` ($33h)** отдельной
|
||||||
|
функцией (например `kbd_mod_state()` в `<conio.h>`) — даёт настоящий
|
||||||
|
held-state для Shift/Ctrl/Alt. Можно делать хоть сейчас, не архитектурное
|
||||||
|
решение.
|
||||||
|
|
||||||
|
**B. Для обычных клавиш (стрелки и т.д.) — по прецеденту CBL.** В
|
||||||
|
`_irq_tramp.c` уже есть пример «приватного» пути на том же векторе 0xFF,
|
||||||
|
который сознательно НЕ чейнится к DSS (CBL: бит 7 порта 0xFE, свой
|
||||||
|
хук `_irq_cbl_hook`, полный сейв, свой `reti`). Предлагаемый новый
|
||||||
|
компонент `<kbd_raw.h>` — симметричный: ветка по биту 0 порта 0x19 читает
|
||||||
|
порт 0x18 САМА (декодирует PS/2 make/break, `0xF0`-префикс — протокол
|
||||||
|
уже задокументирован в `docs/samples/sprinterKeybLib.asm`), ведёт битовую
|
||||||
|
карту «клавиша N зажата», и НЕ прыгает в DSS, пока путь активен —
|
||||||
|
жизненный цикл `kbd_raw_open()`/`kbd_raw_close()` один в один как у
|
||||||
|
`cbl_open`/`cbl_close`.
|
||||||
|
|
||||||
|
**Важное следствие (сообщить пользователю явно, не прятать):** пока
|
||||||
|
`kbd_raw_open()` активен, DSS вообще не получает клавиатурных байт —
|
||||||
|
`kbhit/getch/getkey/CTRLKEY` заведомо не будут работать, ESC для выхода
|
||||||
|
в DSS-смысле тоже (нужно проверять raw-битовую карту самим). Это
|
||||||
|
нормально для активной фазы геймплея (у самой игры и так свой цикл
|
||||||
|
ввода), но означает: экраны/паузы, которым нужен ESTEX-ввод (например,
|
||||||
|
диалог сохранения через `fopen`, если тот когда-либо потребует ввода
|
||||||
|
с консоли), должны на это время `kbd_raw_close()`.
|
||||||
|
|
||||||
|
**Не рекомендую вариант «таймаут-эвристика поверх SCANKEY»** — after
|
||||||
|
находки о typematic-задержке ~1с он не даёт нужной задержки для игры;
|
||||||
|
рекомендация A+B закрывает потребность без компромиссов.
|
||||||
|
|
||||||
|
**Статус: A+B РЕАЛИЗОВАНЫ (2026-07-15, по согласованию с пользователем).**
|
||||||
|
|
||||||
|
- A: `kbd_mod_state()` — `libc/conio/kbd_mod_state.c` + `<conio.h>`
|
||||||
|
(`KBD_MOD_*`).
|
||||||
|
- B: `<kbd_raw.h>` (`libc/kbd/`) + правка `libc/irq/_irq_tramp.c`
|
||||||
|
(новая ветка на бите 0 порта 0x19: raw активен → сама читает порт
|
||||||
|
0x18, декодирует make/break, НЕ чейнится к DSS; raw выключен —
|
||||||
|
поведение как раньше, без изменений). Трамплин вырос со 150 до
|
||||||
|
220 байт — `_IRQ_TRAMP_BUF_SIZE` поднят с 224 до 288 (было 4 байта
|
||||||
|
запаса, стало ≥60). `make -C libc` (fast+safe) — чисто.
|
||||||
|
- **Верификация в MAME** (`tests/kbdraw`, полный цикл open→держать→
|
||||||
|
отпустить→ESC-выход→close): `KBD_LEFT` (0x16B, расширенный код
|
||||||
|
E0 6B) — down на нажатие, up на отпускание, ТОЧНО совпало с
|
||||||
|
константой из `<kbd_raw.h>`; `KBD_ESC` (0x76, обычный код) —
|
||||||
|
корректно закрыл raw-канал и вернул DSS (`IM` вернулся в 1).
|
||||||
|
Побочно найдено и задокументировано в `docs/libc-reference.md`
|
||||||
|
(`<kbd_raw.h>`): MAME-мостовой `press_key` дёргает ОБЕ клавиатуры
|
||||||
|
(PC+ZX) одновременно и через ZX-путь давал паразitный незатухающий
|
||||||
|
бит — не относится к реальному сценарию (пользователь подтвердил:
|
||||||
|
матрица на Sprinter давно не используется), но означает, что
|
||||||
|
будущие MAME-тесты этой функции надо гонять через `:kbd:ms_naturl:*`
|
||||||
|
напрямую, не через удобный `press_key`. UP/DOWN/RIGHT/SPACE/SHIFT
|
||||||
|
константы — НЕ перепроверены поштучно (тот же общеизвестный
|
||||||
|
стандарт PS/2 Set 2, что и подтверждённые LEFT/ESC — проверить перед
|
||||||
|
использованием в PoC, если управление будет ощущаться неверно).
|
||||||
|
- На реальном железе — не проверено (только MAME).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Формат данных — что напрямую переносим из docs/*RESOURCE_FORMAT.md
|
||||||
|
|
||||||
|
- **Уровень** (`BLUETYPE`/`BLUESPEC`/`LINKLOC`/`LINKMAP`/`MAP`/`INFO`,
|
||||||
|
2304 байта, 24 экрана × 30 тайлов) — читаем один раз при загрузке уровня
|
||||||
|
в свою C-структуру (прямой memcpy дампа файла, поля читаем по офсетам
|
||||||
|
из `APPLEII_RESOURCE_FORMAT.md` §1). DOS `levels.dat` даёт то же самое
|
||||||
|
+1 байт в конце записи — отбросить.
|
||||||
|
- **Графика фона/спрайтов** — кодек сжатия DOS `.DAT` не восстановлен и
|
||||||
|
восстанавливать не будем: используем уже распакованные PNG из
|
||||||
|
`SDLPoP/data/{KID,GUARD,VPALACE,VDUNGEON,...}` (см.
|
||||||
|
`MSDOS_RESOURCE_FORMAT.md` §5, §7 — тот же контейнерный формат/нумерация,
|
||||||
|
просто другой релиз сборки данных). Измерено локально: кадры Кида —
|
||||||
|
~12×39 .. 30×42 px (P-режим, 4-бит палитра), фоновые тайлы подземелья —
|
||||||
|
32 px по ширине (10 колонок × 32 = 320 — сходится с шириной экрана), высота
|
||||||
|
тайла 20/60/62 px (неоднородные ряды пола/потолка/арок) — укладывается в
|
||||||
|
лимит спрайтового движка (кадр ≤ 64×64) без всяких изменений движка.
|
||||||
|
- **Звук** — `digisnd*.dat` (PC-звук 8-бит ~11 кГц) — конвертация в сырой
|
||||||
|
PCM и проигрывание через `cbl_open`/`cbl_push_*`; `ibm_snd*.dat` (PC-спикер
|
||||||
|
тройки «частота×2Б + длительность») — тривиальный бипер, не требует CBL.
|
||||||
|
MIDI-семейство (`midisnd`, `mt32snd`, `prince.dat`) — вне скоупа (нет
|
||||||
|
синтеза MIDI на платформе; не блокирует геймплей).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Стратегия фона — ПЕРЕСМОТРЕНО 2026-07-15: тайловый рендерер В РАНТАЙМЕ
|
||||||
|
|
||||||
|
**Было** (первая версия плана): офлайн-склейка каждой комнаты в готовую
|
||||||
|
растровую картинку 320×~193, `gfx_blit` целиком при входе — обоснование
|
||||||
|
было «ноль нового кода в libbgi». Пересчёт по факту наличия структурных
|
||||||
|
данных комнаты (§3.4 формата, `level.h`) показал: 16 уровней × 24 комнаты ×
|
||||||
|
~60-80 КБ/картинка — это **30+ МБ**, при том что одна и та же картинка
|
||||||
|
тайла (пол/стена/колонна) переиспользуется в десятках комнат — офлайн-
|
||||||
|
склейка печёт её заново в каждую копию.
|
||||||
|
|
||||||
|
**Стало**: тайлы — переиспользуемый набор картинок ОДИН на визуальный
|
||||||
|
стиль (не на комнату), структурные данные комнаты — компактные (60 байт:
|
||||||
|
30×foretable+30×backtable, все 16 уровней ≈ 37 КБ, см. `level.h`).
|
||||||
|
`room_draw()` (applications/PoP/poc/room.c) проходит 30 тайлов комнаты и
|
||||||
|
зовёт `gfx_blit` для каждого, читая картинку из таблицы по типу тайла
|
||||||
|
(`tile_images[TILE_TYPE]`). Итог: десятки-сотни КБ переиспользуемых
|
||||||
|
тайл-картинок на весь визуальный стиль + ~37 КБ структуры уровней —
|
||||||
|
вместо 30+ МБ.
|
||||||
|
|
||||||
|
**Почему это НЕ бьёт по бюджету кадра**: `room_draw()` зовётся ОДИН РАЗ
|
||||||
|
при входе в комнату (смена комнаты — не every-frame событие), не в
|
||||||
|
игровом цикле — это не `sprite_update`, тактовый бюджет кадра не
|
||||||
|
затронут.
|
||||||
|
|
||||||
|
Анимированные тайлы (факел, шипы, дверь-плита) по-прежнему рисуются как
|
||||||
|
отдельные `sprite_t` поверх фона — движок это уже умеет (Y-order/layers,
|
||||||
|
dirty-биты, heal против фона через ОЗУ-копию); `room_draw()` кладёт в
|
||||||
|
ОЗУ-копию именно статичную геометрию (пол/стены/колонны Фазы 1 — §5.2),
|
||||||
|
поверх неё heal спрайтов работает как обычно.
|
||||||
|
|
||||||
|
`toolchain/room_compose.py` (генерик-компоновщик тайлов в одну картинку,
|
||||||
|
§6.1) остаётся полезным ИНСТРУМЕНТОМ конвертации отдельных тайл-картинок
|
||||||
|
(PNG → getimage raw), просто теперь его выход — 32 маленьких файла
|
||||||
|
`tileNN.raw` (по одному на тип тайла), а не один большой файл на комнату;
|
||||||
|
сама раскладка/повторное использование по комнатам — в C-коде
|
||||||
|
(`room_draw`), не в офлайн-склейке.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Proof-of-Concept — цель: доказать, что порт вообще ощущается как PoP
|
||||||
|
|
||||||
|
**Объём**: одна комната (например Level 1, экран старта Кида), без
|
||||||
|
переходов между экранами, без стражников (стретч-цель, не обязательна).
|
||||||
|
|
||||||
|
**Что показываем**:
|
||||||
|
1. Кид на экране, с закреплённым офлайн-конвертированным набором кадров
|
||||||
|
(подмножество: idle, walk L/R, jump-начало/дуга/приземление, стоп-на-краю,
|
||||||
|
возможно повисание на краю) — атлас в W0-странице, по образцу `rpgwalk`.
|
||||||
|
2. Управление: держать влево/вправо — идёт; отпустил — тормозит/стоит;
|
||||||
|
нажатие вверх во время бега — прыжок вперёд (дуга по авторским таблицам
|
||||||
|
смещений, не по gravity-физике «с нуля» — см. §6). Здесь же проверяется
|
||||||
|
решение по §2 (реальный held-state).
|
||||||
|
3. Столкновения: пол/край экрана/провал — по факту чтения тайла из
|
||||||
|
`BLUETYPE` под ногами (без LINKLOC-триггеров пока).
|
||||||
|
4. Стабильный кадр 50 Гц через уже готовый `gfx_wait_vsync`/дабл-буфер
|
||||||
|
(без FPS-делителя — Кид анимируется каждый видеокадр, как в оригинале).
|
||||||
|
|
||||||
|
**Критерий успеха**: субъективно «прыжок ощущается как в PoP» (дистанция и
|
||||||
|
тайминг прыжка сверены с оригинальными таблицами, не подобраны на глаз —
|
||||||
|
см. §6), управление отзывчивое (не событийное с задержкой), сцена не мерцает
|
||||||
|
на стыке спрайт/фон.
|
||||||
|
|
||||||
|
**Не входит в PoC**: стражники/бой, звук, HUD/таймер, переходы между
|
||||||
|
комнатами, ловушки/триггеры, титры/меню, сохранения.
|
||||||
|
|
||||||
|
**Расположение**: `applications/PoP/poc/` (свой sprinter-cc проект + Python
|
||||||
|
конвертер ассетов, по структуре `examples/rpgwalk`).
|
||||||
|
|
||||||
|
### 5.1 Статус (2026-07-15) — первая итерация: управление + коллизия края
|
||||||
|
|
||||||
|
Сделано и проверено в MAME (`applications/PoP/poc/`, `make run`):
|
||||||
|
держать LEFT/RIGHT (`kbd_raw_down`, raw-канал из §2) — идёт непрерывно,
|
||||||
|
отпустил — стоит на месте (не событийно, реальный held-state);
|
||||||
|
столкновение с краями экрана (клип по `MINX`/`MAXX`); анимация
|
||||||
|
ходьбы/разворота лицом по направлению (`sprite_anim` пинг-понг);
|
||||||
|
дабл-буфер + `gfx_wait_vsync` — без видимого мерцания. Сборка —
|
||||||
|
`--memory huge` без `--bank` (§10, подтверждено рабочим).
|
||||||
|
|
||||||
|
**Важное отступление от плана (осознанно, не молча):** персонаж —
|
||||||
|
ВРЕМЕННАЯ заглушка (лицензированный спрайт-пак
|
||||||
|
`third_party/16x16-RPG-characters` через `tools/gen_kid_placeholder.py`,
|
||||||
|
тот же источник, что уже использует `examples/rpgwalk`), а НЕ
|
||||||
|
конвертированная графика оригинальной Prince of Persia. Причина:
|
||||||
|
исходный набор кадров Кида (`SDLPoP/data/KID`) — копирайт
|
||||||
|
Broderbund/Ubisoft; автоматический конвейер, который систематически
|
||||||
|
извлекает и переупаковывает его в новый формат, — это на практике
|
||||||
|
внутрипроектное решение, которое стоит принимать пользователю явно
|
||||||
|
для каждого шага, а не проводить асинхронно агентом без лишнего
|
||||||
|
подтверждения. Сама графика — не то, что проверяет PoC (§5 явно:
|
||||||
|
цель — ощущение управления/коллизий, не визуальная точность). Замена
|
||||||
|
на настоящую графику Кида — отдельный шаг, на усмотрение пользователя.
|
||||||
|
|
||||||
|
**Ещё не сделано** (следующие итерации §5): авторские таблицы
|
||||||
|
смещений кадров (§6 — движение при ходьбе линейное, px/кадр),
|
||||||
|
реальный уровень/фон по `BLUETYPE`/`LEVEL1` (сейчас — плейсхолдер:
|
||||||
|
плоский пол на весь экран, без ямы/выступа), `kbd_mod_state`/
|
||||||
|
Shift-бег не подключены к игровому циклу (обёртка готова с Фазы A).
|
||||||
|
|
||||||
|
**Прыжок/присед добавлены и ПРОВЕРЕНЫ (2026-07-15)**: состояние
|
||||||
|
`jumping`/`jump_t`/`crouching`, своя приблизительная дуга прыжка
|
||||||
|
(`jump_height[]`, 40 кадров) — не авторская таблица, см. §6.1.
|
||||||
|
HUD-текст статуса (нет отдельной позы).
|
||||||
|
|
||||||
|
Живое тестирование пользователем нашло реальный баг: держа UP чуть
|
||||||
|
дольше 0.8 с (длительность дуги), получали ДВА прыжка подряд — код
|
||||||
|
проверял `kbd_raw_down(KBD_UP)` как уровень (держится, пока клавиша
|
||||||
|
физически зажата), а не как фронт нажатия, поэтому в момент
|
||||||
|
приземления «UP всё ещё зажат» тут же триггерил новый прыжок.
|
||||||
|
Исправлено edge-detect'ом (`up_prev` — предыдущее состояние UP,
|
||||||
|
триггер только на переход 0→1). Проверено брейкпоинтом в отладчике
|
||||||
|
MAME на адресе входа в код прыжка: за одно длинное удержание UP
|
||||||
|
брейкпоинт срабатывает РОВНО ОДИН РАЗ — фикс подтверждён на уровне
|
||||||
|
кода, не только «на глаз».
|
||||||
|
|
||||||
|
Побочный урок (см. `docs/libc-reference.md` `<kbd_raw.h>`): моя
|
||||||
|
более ранняя попытка проверить UP/DOWN/RIGHT по скриншотам после
|
||||||
|
`press_key` ошибочно решила, что скрипт их не нажимает вообще —
|
||||||
|
на самом деле нажимает исправно, просто скриншот ловил случайный
|
||||||
|
момент дуги. Брейкпоинт/watchpoint на конкретный адрес кода —
|
||||||
|
надёжнее скриншота для таких проверок.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Модель движения: авторские таблицы кадров, не физика с нуля
|
||||||
|
|
||||||
|
Оригинальный движок PoP не считает прыжок как непрерывную физику
|
||||||
|
(gravity/velocity каждый тик) — движение персонажа задано таблицами кадров
|
||||||
|
анимации, где у части кадров зашито фиксированное смещение (dx, dy) для
|
||||||
|
ЭТОГО конкретного кадра последовательности (структура видна и в
|
||||||
|
исходниках Apple II — `SEQTABLE.S`/`MOVER.S`, и в SDLPoP `seg003.c`/`seq*`
|
||||||
|
таблицах). Практическое следствие для нашего движка:
|
||||||
|
- **Не использовать** `sprite_anim`/`sprite_moveto` для основного
|
||||||
|
персонажа как есть (они лианейно тянут по таймеру/тянут к линейной
|
||||||
|
цели) — вместо этого приложение само на каждый логический тик:
|
||||||
|
переключает кадр (`sprite_frame`, атлас как лента поз, не «прогрессия
|
||||||
|
первый..последний» автоматом) и одновременно применяет dx,dy ЭТОГО
|
||||||
|
кадра к позиции (`sprite_move`).
|
||||||
|
- Готовая автоматика движка (`sprite_anim`/`sprite_moveto`/tween,
|
||||||
|
Y-сортировка) остаётся полезной для декоративных/фоновых элементов
|
||||||
|
(факелы, патрулирующий стражник вне боя — почти один в один паттерн
|
||||||
|
`rpgwalk`).
|
||||||
|
- Источник таблиц смещений: переснять из `Prince-of-Persia-Apple-II/01 POP
|
||||||
|
Source/Source/{MOVER.S,SEQTABLE.S,FRAMEADV.S}` и/или
|
||||||
|
`SDLPoP/src/seq*.c` — задача Фазы 1 полной реализации (§7), не PoC
|
||||||
|
(для PoC можно взять урезанный набор смещений вручную по количеству
|
||||||
|
пикселей на кадр, посчитанному по видео/скриншотам оригинала, и уточнить
|
||||||
|
позже).
|
||||||
|
|
||||||
|
### 6.1 Инструмент конвертации кадров разного размера (`toolchain/png_strip.py`)
|
||||||
|
|
||||||
|
Кадры персонажа в оригинале — РАЗНОГО размера каждый (bbox зависит от
|
||||||
|
позы; `sprite_t` нашего движка (`libbgi/include/sprite.h`) хранит ОДИН
|
||||||
|
фиксированный w/h на весь спрайт и рисует от угла, без per-frame
|
||||||
|
смещения — в отличие от оригинала, где на каждый кадр было своё XCO/YCO
|
||||||
|
(`APPLEII_RESOURCE_FORMAT.md` §2.2). `toolchain/png_strip.py` (генерик,
|
||||||
|
не завязан на PoP — принимает произвольный список PNG) закрывает это
|
||||||
|
ПАДДИНГОМ: канвас = макс. w/h среди кадров ленты, якорь по умолчанию
|
||||||
|
bottom-center («ноги на месте»), остальное — прозрачность.
|
||||||
|
|
||||||
|
**Компромисс, не полноценное решение**: один сильно выбивающийся по
|
||||||
|
размеру кадр в ленте раздувает канвас (и память) ВСЕХ кадров этой же
|
||||||
|
ленты. Смягчается группировкой по похожим размерам в отдельные атласы
|
||||||
|
(не одна лента на все позы актора — так уже сделано для ходьбы отдельно
|
||||||
|
от прыжка).
|
||||||
|
|
||||||
|
**Полноценное решение (кандидат в будущее расширение библиотеки, НЕ
|
||||||
|
делать без предложения и подтверждения пользователя)**: per-frame
|
||||||
|
смещение в `sprite_t` (аналог XCO/YCO оригинала) — тогда паддинг
|
||||||
|
не нужен вообще, экономия памяти по полной. Делать только если память
|
||||||
|
станет РЕАЛЬНОЙ проблемой (не гипотетической) — тогда предложить как
|
||||||
|
отдельную правку `sprite.h`/движка. Подробности компромисса —
|
||||||
|
memory/png_strip_padding_tradeoff.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Полноценное приложение — фазы (после PoC)
|
||||||
|
|
||||||
|
Порядок — по риску и зависимостям, не по геймплейной важности.
|
||||||
|
|
||||||
|
**Фаза 0 — инфраструктура порта** (расширяет PoC, не переписывает):
|
||||||
|
- Хелд-стейт клавиатуры — финальное решение и реализация по §2 (после
|
||||||
|
подтверждения пользователем).
|
||||||
|
- Полный конвертер уровней (все 15 файлов `levels.dat`/`LEVELn`) → бинарный
|
||||||
|
формат приложения (можно 1-в-1 raw dump, читать по офсетам в рантайме —
|
||||||
|
не обязательно разворачивать в C-struct с указателями).
|
||||||
|
- Полный конвертер фона (24 экрана × N уровней) в растры + конвертер
|
||||||
|
спрайт-лент Кид/стражник/скелет/тень/Джаффар в атласы `.atl` (расширение
|
||||||
|
`conv_sprites.py`/формата `.atl`, если частот кадров/атласов на актора не
|
||||||
|
хватит текущего лимита — см. риск в §8).
|
||||||
|
|
||||||
|
**Фаза 1 — Кид, полный набор действий**: стоять/идти/бежать/тормозить/
|
||||||
|
разворот/прыжок (на месте, вперёд, «прыжок с разбега»)/повисание на
|
||||||
|
краю/подтягивание/спуск по свисанию/приседание/питьё зелья/смерть от
|
||||||
|
провала. Переходы между экранами (`MAP`-граф, `INFO.KidStartScrn`).
|
||||||
|
|
||||||
|
**Фаза 2 — мир и ловушки**: нажимные плиты/двери через граф
|
||||||
|
`LINKLOC`/`LINKMAP` (см. `APPLEII_RESOURCE_FORMAT.md` §1.2), шипы
|
||||||
|
(выдвижение/втягивание/заклинивание), шаткие плиты (loose, обрушение),
|
||||||
|
зелья (эффект по `BLUESPEC×32`), стартовые позиции по `INFO`.
|
||||||
|
|
||||||
|
**Фаза 3 — бой**: подбор/выхватывание меча, состояние стойки, парирование/
|
||||||
|
удар, коллизия клинков — по логике `AUTO.S`/`seg003-006.c` (референс, не
|
||||||
|
копия). Стражник: базовое AI-поведение по `GdStartProg` (несколько
|
||||||
|
шаблонов программ), Y-сортировка слоями уже есть в движке для «кто
|
||||||
|
спереди/сзади».
|
||||||
|
|
||||||
|
**Фаза 4 — разнообразие противников**: скелет, тень (копия анимации Кида —
|
||||||
|
подтверждено побайтовым совпадением данных, см. `MSDOS_RESOURCE_FORMAT.md`
|
||||||
|
§3), толстый стражник/визирь (общая база анимации с визирем).
|
||||||
|
|
||||||
|
**Фаза 5 — звук**: CBL-эффекты (шаги, удары, двери, падение) из
|
||||||
|
`digisnd*.dat`→PCM; PC-спикер тройки (`ibm_snd*.dat`) как опциональный
|
||||||
|
дешёвый бипер без CBL, если формат подтвердится простым парсингом.
|
||||||
|
|
||||||
|
**Фаза 6 — оболочка**: титры, меню/выбор уровня, HUD (таймер/жизни),
|
||||||
|
сохранение прогресса (FILE*), финальные катсцены — по минимуму,
|
||||||
|
геймплейно не критично.
|
||||||
|
|
||||||
|
**Фаза 7 — стабилизация**: полный прогон всех 14 уровней в MAME
|
||||||
|
(`mame_interactive.py`), затем на реальном железе; профилирование бюджета
|
||||||
|
кадра по методике `sprite_engine_perf`/`sprite-api-design.md` §9д на самых
|
||||||
|
насыщенных экранах (несколько стражников + ловушки одновременно —
|
||||||
|
проверить лимит ~21 спрайт/кадр и Y-sort лимит 32); при необходимости —
|
||||||
|
банкинг (`--memory big/huge`) для кода/уровня, если размер вылезет за
|
||||||
|
tiny/small.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Риски, требующие спайка/артефакта до架构 решений
|
||||||
|
|
||||||
|
(по правилу `defer_unexplained_quirks` — не гадать, проверять)
|
||||||
|
|
||||||
|
1. **Held-state клавиатуры** (§2) — блокирует даже PoC, если решать
|
||||||
|
«правильно»; иначе PoC на компромиссном варианте 2 (таймаут-эвристика).
|
||||||
|
2. **Бюджет спрайтов на насыщенный экран** — сцена с 2+ стражниками +
|
||||||
|
несколько анимированных ловушек может приблизиться к лимиту
|
||||||
|
~21 спрайт/кадр (`sprite_engine_perf`) — нужна прикидка по реальным
|
||||||
|
уровням (сколько объектов одновременно активно в худшем экране).
|
||||||
|
3. **Ёмкость одного атласа/страницы EMM на актора** — у Кида ~220 кадров
|
||||||
|
(все действия) против 4×12 у `rpgwalk` — потребуется либо несколько
|
||||||
|
атласов на актора с переключением по фазе действия (стоять/идти отдельно
|
||||||
|
от боя), либо расширение формата `.atl`/загрузчика на мульти-страничные
|
||||||
|
атласы — оценить фактический байтовый вес конвертированных кадров Кида
|
||||||
|
прежде чем проектировать.
|
||||||
|
4. **Тайминг оригинала** — сверить логическую частоту кадров анимации
|
||||||
|
оригинала (Apple II ~60 Гц NTSC / DOS — фиксированный таймер) с 50 Гц
|
||||||
|
Sprinter; если оригинал считался на другой частоте — потребуется
|
||||||
|
коэффициент пересчёта смещений кадров (§6), иначе прыжки/бег будут
|
||||||
|
визуально быстрее/медленнее эталона.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Режим памяти сборки
|
||||||
|
|
||||||
|
Пользователь предложил `huge` (горячий код в W1, данные в W2, редко
|
||||||
|
вызываемая логика — банками в W3) как целевой режим. Согласен, с уточнением
|
||||||
|
по срокам принятия решения.
|
||||||
|
|
||||||
|
**`huge` — правильная цель для ПОЛНОГО приложения**, но не то, с чего надо
|
||||||
|
стартовать:
|
||||||
|
|
||||||
|
- Layout `huge` (см. `memory_modes_implemented`): CODE_LOC=0x4100 (W1),
|
||||||
|
DATA_LOC=0x8000 (W2), банки — W3 (порт 0xE2), `crt0_banked` +
|
||||||
|
автодетект W2 (как `small`). Состояние приложения (структуры Кида,
|
||||||
|
уровня, массив `sprite_t`) остаётся в обычном W2-heap ДАЖЕ если код,
|
||||||
|
который его трогает, забанкован — `malloc` из банка возвращает
|
||||||
|
W2-указатель (`bank_local_data_pattern`), так что данные не привязаны к
|
||||||
|
конкретному банку.
|
||||||
|
- Оверхед `__banked`-вызова (trampoline: +3 байта на стеке между ret и
|
||||||
|
аргументами, виртуальный 24-битный адрес, см. `sdcc_banking`) — фиксированная
|
||||||
|
небольшая цена ЗА ВЫЗОВ, не за такт. Это не страшно для функций, которые
|
||||||
|
вызываются РЕДКО за кадр (AI одного стражника, диалог, переход между
|
||||||
|
комнатами) — страшно было бы забанковать что-то, что дёргается ВНУТРИ
|
||||||
|
горячего цикла отрисовки (там уже и так основной бюджет уходит на
|
||||||
|
`sprite_update`/блиты — см. `sprite_engine_perf`, ~19.5К тактов/спрайт).
|
||||||
|
Правило простое: **не банковать код на пути "раз в кадр на объект",
|
||||||
|
банковать код на пути "раз в кадр на комнату/раз в переход/раз в
|
||||||
|
редкое событие"**: логика ИИ стражника целиком, диалоги/катсцены, меню/
|
||||||
|
титры/выбор уровня, парсинг уровня при входе в комнату, сериализация
|
||||||
|
сохранений — хорошие кандидаты в банки; тик Кида, чтение столкновений,
|
||||||
|
вызов `sprite_update`/`gfx_wait_vsync`, обработка ввода — должны остаться
|
||||||
|
небанкованными (W1/W2).
|
||||||
|
- Гранулярность банкования — целый файл (`--bank N=FILE.c`), это уже
|
||||||
|
системный паттерн проекта (тот же принцип, что и «1 файл = 1 юнит DCE» в
|
||||||
|
libc) — значит выгодно с САМОГО начала Фазы 1 (не задним числом) резать
|
||||||
|
исходники приложения по границе «горячее/холодное» файл-в-файл: например
|
||||||
|
`kid_tick.c`/`collision.c`/`room.c`/`input.c` — неизменно вне банков;
|
||||||
|
`guard_ai_*.c`/`dialogue.c`/`menu.c`/`levelload.c`/`combat.c` — кандидаты
|
||||||
|
под `--bank`. Тогда переход на `huge` позже — это правка Makefile/
|
||||||
|
sprinter-cc-вызова (`--memory huge --bank N=file.c ...`), а не рефакторинг
|
||||||
|
логики.
|
||||||
|
|
||||||
|
**Уточнение (проверено в `bin/sprinter-cc`, строки ~342-350): можно сразу
|
||||||
|
собирать PoC на `--memory huge` без единого `--bank`.** Скрипт сам
|
||||||
|
подставляет стаб `const unsigned char n_banks = 0;`, когда `--bank` не
|
||||||
|
передан ни один раз — `crt0_banked` линкуется и корректно пропускает цикл
|
||||||
|
загрузки банков при старте. Layout при этом byte-в-byte совпадает с тем,
|
||||||
|
что делает `crt0_small` для режима `small` (CODE 0x4100/W1, DATA 0x8000/W2,
|
||||||
|
автодетект W2) — разница только в том, что попутно линкуется сам
|
||||||
|
`bank.s` (таблица `_bank_pages` + trampoline-инфраструктура), это
|
||||||
|
незначительный довесок к размеру, не к рантайм-цене. Значит **PoC можно
|
||||||
|
сразу собирать вызовом `sprinter-cc --memory huge` без `--bank`-флагов** —
|
||||||
|
и когда в полном приложении появятся первые «холодные» файлы, переход на
|
||||||
|
банкование — это просто добавление `--bank N=file.c`, без смены
|
||||||
|
`--memory`/адресов/crt0. Сборочная конфигурация не потребует миграции
|
||||||
|
между PoC и полным приложением.
|
||||||
|
|
||||||
|
Единственное, что стоит сделать уже в Фазе 1 полного приложения (не в
|
||||||
|
PoC) — планировать структуру исходников с расчётом на будущий файл-в-файл
|
||||||
|
сплит под банки (см. выше), раз гранулярность банкования — целый файл.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Что нужно от пользователя, прежде чем двигаться дальше
|
||||||
|
|
||||||
|
- Подтверждение направления по §2 (какой из трёх вариантов held-state
|
||||||
|
клавиатуры пробовать первым, или сначала спайк-эксперимент в MAME).
|
||||||
|
- Подтверждение объёма PoC (§5) — устраивает ли «одна комната без
|
||||||
|
стражников», или сразу закладывать хотя бы одного патрулирующего
|
||||||
|
стражника (это не архитектурно сложнее — Y-order и tween уже есть,
|
||||||
|
просто больше конвертации ассетов).
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
# Форматы ресурсов Prince of Persia — сводка
|
||||||
|
|
||||||
|
Цель этих документов — подготовить почву для будущего порта Prince of Persia
|
||||||
|
на ZX Sprinter, разобрав, как устроены ресурсы игры в двух доступных нам
|
||||||
|
версиях:
|
||||||
|
|
||||||
|
- [`APPLEII_RESOURCE_FORMAT.md`](./APPLEII_RESOURCE_FORMAT.md) — формат
|
||||||
|
уровней и графики по официально опубликованным исходникам 1989 года
|
||||||
|
(6502-ассемблер). Уверенность высокая везде — восстановлено прямым чтением
|
||||||
|
кода движка, а не догадками.
|
||||||
|
- [`MSDOS_RESOURCE_FORMAT.md`](./MSDOS_RESOURCE_FORMAT.md) — формат `.DAT`
|
||||||
|
ресурсов DOS-версии (исходников нет). Восстановлено эмпирически (разбор
|
||||||
|
байтов + перепроверка скриптами) и сверено с документацией открытых
|
||||||
|
сторонних инструментов (SDLPoP, Princed Resources).
|
||||||
|
|
||||||
|
## Главный вывод
|
||||||
|
|
||||||
|
**Формат уровня практически идентичен в обеих версиях**: Apple II `LEVELn`
|
||||||
|
занимает ровно 2304 байта (структура `blueprnt` — тайлы, связи
|
||||||
|
плит/дверей, граф экранов, метаданные старта Кида/стражников), а запись
|
||||||
|
уровня в DOS `levels.dat` занимает 2305 байт с байтовыми значениями тайлов
|
||||||
|
того же диапазона. То есть Джордан Мехнер перенёс формат карты уровня в
|
||||||
|
DOS-порт практически без изменений (+1 байт, вероятно контрольная сумма от
|
||||||
|
DOS-упаковщика). Это значит: раскладку `BLUETYPE`/`BLUESPEC`/`LINKLOC`/
|
||||||
|
`LINKMAP`/`MAP`/`INFO`, задокументированную по Apple II исходникам, можно
|
||||||
|
применять напрямую и к DOS `levels.dat`.
|
||||||
|
|
||||||
|
Формат же **графики отличается принципиально**: на Apple II это простой
|
||||||
|
несжатый rowbyte-формат hi-res экрана с плоской таблицей указателей; в DOS —
|
||||||
|
контейнер с оглавлением ресурсов (id/size/offset), с отдельными вариантами
|
||||||
|
под CGA/EGA/VGA — точный кодек пикселей внутри сырого `.DAT`-чанка не
|
||||||
|
восстановлен ни для той, ни для другой версии до конца. **Но для DOS-графики
|
||||||
|
это не блокирует работу**: в репозитории github.com/NagyD/SDLPoP (папка
|
||||||
|
`data/`) уже лежат готовые распакованные PNG для каждого спрайта/фона
|
||||||
|
(включая VGA-256-цветный вариант `VPALACE`/`VDUNGEON` — то, что нужно под
|
||||||
|
320×256×256 Sprinter), см. §5 `MSDOS_RESOURCE_FORMAT.md`. Это другой
|
||||||
|
релиз/сборка данных, чем наш локальный `MSDOS/` (некоторые звуковые `.dat`
|
||||||
|
отличаются по размеру), но нумерация ресурсов и формат контейнера — те же,
|
||||||
|
что подтверждено побайтовой сверкой уровня `res2001.bin`.
|
||||||
|
|
||||||
|
## Общий контейнерный формат DOS `.DAT` (кратко)
|
||||||
|
|
||||||
|
```
|
||||||
|
[0x00] u32 LE tableOffset — смещение начала таблицы оглавления
|
||||||
|
[0x04] u16 LE tableSize — размер таблицы оглавления
|
||||||
|
[0x06..tableOffset) — данные ресурсов (конкатенация чанков)
|
||||||
|
[tableOffset..tableOffset+tableSize)
|
||||||
|
— массив записей по 8 байт:
|
||||||
|
u16 size, u16 id, u16 offset(абсолютный), u16 reserved(=0)
|
||||||
|
```
|
||||||
|
|
||||||
|
Инвариант `tableOffset + tableSize == размер файла` подтверждён на всех 28
|
||||||
|
`.dat`-файлах в `MSDOS/`, и независимо — именованием файлов `res<id>.*` в
|
||||||
|
`data/` репозитория SDLPoP.
|
||||||
|
|
||||||
|
## Готовые ассеты для порта (важно для практической работы)
|
||||||
|
|
||||||
|
`github.com/NagyD/SDLPoP/tree/master/data` содержит не только код движка, но
|
||||||
|
и сами ресурсы игры — как сырые `.DAT`, так и распакованные поштучно файлы
|
||||||
|
(`res<id>.png` для спрайтов/фонов, `res<id>.pal` для палитр, `res<id>.bin`
|
||||||
|
для уровней). Для арт-ассетов (в т.ч. нужного полноцветного VGA-варианта
|
||||||
|
дворца/подземелий) практичнее взять их оттуда напрямую, чем писать свой
|
||||||
|
декодер сжатия пикселей DOS `.DAT`.
|
||||||
|
|
||||||
|
## Что дальше (не сделано в этом заходе)
|
||||||
|
|
||||||
|
1. Точный кодек сжатия пикселей спрайтов в сыром DOS `.DAT` (нужен только
|
||||||
|
если понадобится читать именно нашу локальную копию `MSDOS/*.dat`
|
||||||
|
"как есть", а не ассеты из SDLPoP `data/`) — сверка с исходником SDLPoP,
|
||||||
|
`src/seg009.c`.
|
||||||
|
2. Семантика служебных полей `digisnd*.dat`/`ibm_snd*.dat` перед сырыми
|
||||||
|
сэмплами/нотами (частично прояснено документацией Princed Resources —
|
||||||
|
PC speaker: 1 байт заголовка + повторяющиеся тройки байт "2 байта частоты
|
||||||
|
+ 1 байт длительности"; WAV: 8 бит, моно, unsigned, 11025 Гц).
|
||||||
|
3. Назначение бит `secmask` в `BLUETYPE` (Apple II) и служебного блока
|
||||||
|
`id=2000` в начале DOS `levels.dat` (16 байт в нашей копии, но 2305 байт
|
||||||
|
в версии SDLPoP — расхождение между релизами, не разобрано).
|
||||||
|
4. Оценка, какие видеорежимы/цветовые палитры ZX Sprinter реалистично
|
||||||
|
покрывают исходную графику (CGA/EGA/VGA варианты в DOS-ресурсах против
|
||||||
|
hi-res Apple II) — отдельная архитектурная задача порта, не формат
|
||||||
|
ресурсов как таковой.
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
# Double buffer (два экрана + флип) — план
|
||||||
|
|
||||||
|
Цель: убрать мерцание/тиринг при перерисовке слоёв (Kid ↔ fore/пол-оверлей)
|
||||||
|
и гарантировать, что на экране ВСЕГДА готовый кадр с правильным порядком
|
||||||
|
слоёв. Нужно для отладки fore-слоя (видно момент композиции, а не
|
||||||
|
промежуточные состояния heal-рендера). **Тумблер обязателен** —
|
||||||
|
однобуферный режим удобнее для отладки багов рисования.
|
||||||
|
|
||||||
|
## Что уже готово (libbgi — трогать НЕ нужно)
|
||||||
|
|
||||||
|
- Две графических страницы 0/1: `gfx_set_draw_page(p)` (двигает
|
||||||
|
`_gfx_addr_base` 0xC000/0xC140 — рисуют все примитивы),
|
||||||
|
`gfx_set_visible_page(p)` (ESTEX $54 SELPAGE — display-учёт DSS).
|
||||||
|
- `gfx_wait_vsync()` — луч (bit5 порта 0xFE), момент без разрывов.
|
||||||
|
- Паттерн из gfx.h: `set_draw_page(hidden); draw(); wait_vsync();
|
||||||
|
set_visible_page(hidden);`
|
||||||
|
- Замечание gfx.h: у каждой страницы СВОЯ плоскость палитры
|
||||||
|
(page0→pal0, page1→pal1) — для seamless грузить одну палитру в ОБЕ.
|
||||||
|
|
||||||
|
## Текущая модель рендера roomtest (однобуфер)
|
||||||
|
|
||||||
|
`roomtest.c`: `draw_page=0`, `visible_page=0` фиксированы. Фон комнаты
|
||||||
|
рисуется ОДИН раз в видео-ОЗУ + теневую копию (GFX_BANK_TRANSPARENT).
|
||||||
|
Цикл: 3× `gfx_wait_vsync` (пейсинг) → `pop_ctrl_tick` → `kid_heal()`
|
||||||
|
(восстановить прямоугольник Кида из тени) → `kid_tick`/`pop_phys_tick`
|
||||||
|
→ `kid_draw` → `pop_fore_over_kid`. Мерцание = heal+draw+fore длиннее
|
||||||
|
бланка, луч ловит промежуток.
|
||||||
|
|
||||||
|
## Работа на стороне PoP
|
||||||
|
|
||||||
|
1. **Инициализация обеих страниц**: `pop_room_draw` в page 0 И page 1
|
||||||
|
(теневая копия одна — общая, из неё heal'ит любая страница).
|
||||||
|
2. **Палитра в обе плоскости**: сейчас `gfx_pal_fload(0,...)` + sync.
|
||||||
|
Продублировать в plane 1 (проверить сигнатуру gfx_pal_fload/sync —
|
||||||
|
plane-параметр).
|
||||||
|
3. **Per-page heal-история** (ядро): вынести `kid_lx/ly/lw/lh` в
|
||||||
|
массивы `[2]`, индекс = страница, в которую рисуем. `kid_heal(page)`
|
||||||
|
восстанавливает СВОЙ прошлый прямоугольник (кадр -2, т.к. рисуем
|
||||||
|
через страницу). То же для fore/пол-оверлея, если они рисуют вне
|
||||||
|
футпринта Кида.
|
||||||
|
4. **Ping-pong в цикле**:
|
||||||
|
```
|
||||||
|
uint8_t back = dbuf ? (front ^ 1) : 0;
|
||||||
|
gfx_set_draw_page(back);
|
||||||
|
kid_heal(back); kid_tick; phys; kid_draw; fore;
|
||||||
|
gfx_wait_vsync();
|
||||||
|
if (dbuf) { gfx_set_visible_page(back); front = back; }
|
||||||
|
```
|
||||||
|
5. **Тумблер** `dbuf`: off → draw==visible==0, без флипа, heal[0] —
|
||||||
|
бит-в-бит текущее поведение. Управление — клавишей (напр. F2) или
|
||||||
|
compile-флагом.
|
||||||
|
6. **Пейсинг**: сейчас 3× vsync/лог.кадр. При флипе — один vsync перед
|
||||||
|
свопом; недостающий пейсинг добрать `gfx_set_fps_div(3)` или ручным
|
||||||
|
счётом кадров, чтобы скорость игры не изменилась.
|
||||||
|
|
||||||
|
## Порядок
|
||||||
|
|
||||||
|
- D1: обе страницы + палитра в обе плоскости; ping-pong без per-page
|
||||||
|
heal (проверить, что флип работает, фон корректен на обеих).
|
||||||
|
- D2: per-page heal-история (kid_l*[2]) — убрать «хвост» Кида.
|
||||||
|
- D3: тумблер dbuf + сверка однобуферного пути с текущим (регресс-нет).
|
||||||
|
- D4: пейсинг (fps_div) — вернуть исходную скорость.
|
||||||
|
|
||||||
|
## Связанные
|
||||||
|
|
||||||
|
Рендер-модель — [[pop_fore_layer]], [[pop_background_strategy]];
|
||||||
|
heal — kid_heal/gfx_heal. Будущие динамические слои (loose-floors
|
||||||
|
[[двойной буфер требует их per-page перерисовки]]) должны рисоваться в
|
||||||
|
обе страницы по той же дисциплине.
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
# Loose floors (проваливающиеся полы) — план порта
|
||||||
|
|
||||||
|
Разбор SDLPoP (seg007 loose/trob/mob, seg008 draw_loose). ПЛАН, ещё не
|
||||||
|
реализовано. Тайл в комнате 1: `[2,6] = 0x0B = tiles_11_loose`.
|
||||||
|
|
||||||
|
## 1. Хранение состояния
|
||||||
|
|
||||||
|
- **Тип тайла**: `curr_room_tiles[tilepos] & 0x1F == 11` (tiles_11_loose).
|
||||||
|
Бит `0x20` = «solid» loose (авто-падающий вариант, ур.13 — от шага НЕ
|
||||||
|
падает). После падения тайл → `0` (tiles_0_empty).
|
||||||
|
- **Модификатор** `curr_room_modif[tilepos]` = состояние анимации:
|
||||||
|
- `0` — покой (обычный loose-пол);
|
||||||
|
- `0x80..0x83` — **трясётся** (бит7); за ~4 кадра затухает обратно в 0;
|
||||||
|
- `1..11` — **обратный отсчёт до падения** (на нём что-то стоит);
|
||||||
|
достигает `loose_floor_delay = 11` → падает.
|
||||||
|
|
||||||
|
## 2. Анимация тряски (shake) — когда включается
|
||||||
|
|
||||||
|
- **Триггер = do_knock** (seg007:0FE0): на ЖЁСТКОМ приземлении в кадрах
|
||||||
|
посадки играет `SEQ_KNOCK_DOWN` → взводит `knock` → `check_knock()` →
|
||||||
|
`do_knock(room, curr_row − (knock>0))`.
|
||||||
|
- `do_knock(room, row)`: по всем колонкам ряда — если тайл loose →
|
||||||
|
`loose_make_shake()`.
|
||||||
|
- `loose_make_shake()` (seg007:0FB4): если `modif==0` (и не ур.13) →
|
||||||
|
`modif = 0x80`, `add_trob(type 1)`.
|
||||||
|
- **Отсюда кейс пользователя**: Kid падает/приземляется на `[2,4]` →
|
||||||
|
do_knock трясёт ВСЕ loose-тайлы ряда 2 → `[2,6]` трясётся. (Через
|
||||||
|
knock-смещение ряда может задеть и соседний ряд.)
|
||||||
|
- `animate_loose` (кадрово): `++modif`; при бите7 трясёт до `>=0x84` →
|
||||||
|
сброс в 0, `trob.type=-1`. `loose_shake()` играет звук
|
||||||
|
(sound 20/21/22) по таблице `loose_sound[]`.
|
||||||
|
|
||||||
|
## 3. Анимация падения (fall) — когда включается
|
||||||
|
|
||||||
|
- **Триггер = make_loose_fall(1)** (seg007:0EF6), вызывается когда:
|
||||||
|
- Kid СТОИТ на loose-тайле — `check_press()` (seg006): кадр с
|
||||||
|
FRAME_NEEDS_FLOOR над loose → make_loose_fall(1);
|
||||||
|
- зацеп/подтягивание на loose (`check_grab`, `check_jump_up`);
|
||||||
|
- пробой сверху: кадр 79 (jumphang) над loose → make_loose_fall(1);
|
||||||
|
- авто-падающие (ур.13) — `make_loose_fall(-(prandom&0x0F))`.
|
||||||
|
- `make_loose_fall(modifier)`: если НЕ solid (`tiles & 0x20 == 0`) и
|
||||||
|
`(sbyte)modif <= 0` → `modif = modifier`, `add_trob(type 0)`.
|
||||||
|
- `animate_loose`: `++modif`; когда `modif >= 11` (loose_floor_delay) →
|
||||||
|
`remove_loose()` (тайл → empty) + `add_mob()` (спавн падающего куска).
|
||||||
|
|
||||||
|
## 4. Падающий кусок (mob)
|
||||||
|
|
||||||
|
- `add_mob()` кладёт `curmob` в `mobs[]` (до 14). `do_mobs()` каждый
|
||||||
|
кадр: `move_mob()` (гравитация, y растёт) + `check_loose_fall_on_kid()`
|
||||||
|
(урон Киду/страже, если попал).
|
||||||
|
- Приземление куска → тайл под ним `curr_room_tiles[...] = tiles_14_debris`
|
||||||
|
(seg007 move_mob:1053). Т.е. **loose(11) упал → сверху empty(0), снизу
|
||||||
|
debris(14)**.
|
||||||
|
|
||||||
|
## 5. Отрисовка по статусу
|
||||||
|
|
||||||
|
- Куски тайла: `loose_fram_left[]={41,69,41,70,70,41,41,41,70,70,70,0}`,
|
||||||
|
`loose_fram_right[]={42,71,...}`, `loose_fram_bottom[]={43,73,...}`
|
||||||
|
(env-спрайты, seg008:518/596/608).
|
||||||
|
- Индекс кадра = `get_loose_frame(modifier)` (seg008): `0` = ровный
|
||||||
|
(41/42/43); `1..10` = дрожащие варианты (69–74); при бите7/большой
|
||||||
|
задержке — низкие индексы.
|
||||||
|
- **До падения**: рисуем loose с `get_loose_frame(modif)` (0 = ровно,
|
||||||
|
иначе колеблется). **После**: сверху empty, снизу debris(14) — обычная
|
||||||
|
статическая отрисовка (у нас уже есть tile 0x0E/14 debris в tile_table).
|
||||||
|
|
||||||
|
## 6. Что нужно в нашем движке (сейчас НЕТ)
|
||||||
|
|
||||||
|
Наш `pop_bg` рисует комнату СТАТИЧЕСКИ один раз. Loose-полы требуют
|
||||||
|
**динамического тайлового слоя**:
|
||||||
|
1. **Массив модификаторов** `room_modif[30]` (у нас есть `bg[30]` — можно
|
||||||
|
переиспользовать/рядом) — состояние каждого тайла.
|
||||||
|
2. **Очередь trob** (список анимируемых тайлов) + `animate_loose` пер-кадр
|
||||||
|
→ перерисовка ТОЛЬКО изменившихся тайлов (как heal-прямоугольник Kid).
|
||||||
|
3. **make_loose_fall / do_knock / loose_make_shake** — триггеры (из
|
||||||
|
физики Kid: приземление→knock, стойка на loose→fall).
|
||||||
|
4. **mob-система** (падающий кусок): минимум 1–2 mob'а, гравитация,
|
||||||
|
приземление → debris. Урон Киду (`check_loose_fall_on_kid`) — можно
|
||||||
|
Фазой 2.
|
||||||
|
5. **Перерисовка тайла**: `draw_tile(row,col)` у нас уже умеет loose
|
||||||
|
(`code==11`, `loose_fram_*` в env) — нужно вызывать его выборочно с
|
||||||
|
текущим модификатором (сейчас draw_tile берёт статический bg).
|
||||||
|
|
||||||
|
**Порядок реализации (предложение):**
|
||||||
|
- L1: room_modif[] + выборочная перерисовка тайла по модификатору
|
||||||
|
(draw_loose с get_loose_frame) — статика→динамика одного тайла.
|
||||||
|
- L2: trob-очередь + animate_loose (тряска по do_knock на приземлении).
|
||||||
|
- L3: make_loose_fall (стойка на loose) + отсчёт + remove → empty.
|
||||||
|
- L4: mob (падающий кусок → debris снизу).
|
||||||
|
- L5: урон Киду от падающего куска.
|
||||||
|
|
||||||
|
## Конкретика из SDLPoP (сверено 2026-07-18, готово к реализации)
|
||||||
|
|
||||||
|
Таблицы (seg008.c), индекс = `get_loose_frame(modif)`:
|
||||||
|
- `loose_fram_left[] = {41,69,41,70,70,41,41,41,70,70,70,0}`
|
||||||
|
- `loose_fram_right[] = {42,71,42,72,72,42,42,42,72,72,72,0}`
|
||||||
|
- `loose_fram_bottom[]= {43,73,43,74,74,43,43,43,74,74,74,0}`
|
||||||
|
- `get_loose_frame(m)`: если `(m&0x80)` (или delay>11) → `m&=0x7F; if(m>10) return 1;` → `return m;`
|
||||||
|
- `y_loose_land[] = {2,65,128,191,254}` (mob), `loose_floor_delay = 11`.
|
||||||
|
|
||||||
|
Триггеры (call-sites):
|
||||||
|
- **make_loose_fall(modifier=1)** — из `check_press()` (seg006): когда Kid
|
||||||
|
СТОИТ на тайле (FRAME_NEEDS_FLOOR, action < hang_climb / turn / bumped) и
|
||||||
|
`get_tile_at_char()==11`; ИЛИ `frame==79` (прыжок вверх) и
|
||||||
|
`get_tile_above_char()==11` (пробой сверху). `tile_is_floor(11)==1` —
|
||||||
|
Kid стоит на loose (start_fall НЕ зовётся). Тело:
|
||||||
|
`if(!(tile&0x20) && (sbyte)modif<=0){ modif=modifier; add_trob(type0); }`
|
||||||
|
- **do_knock(row)** — на ЖЁСТКОМ приземлении (SEQ_KNOCK_DOWN→check_knock,
|
||||||
|
seg003): по всем колонкам ряда `if(tile==11) loose_make_shake()`
|
||||||
|
(`if(modif==0){ modif=0x80; add_trob(type1); }`).
|
||||||
|
- **animate_loose** (каждый кадр, seg007:816): `++modif`; если `&0x80`
|
||||||
|
(тряска): `if(modif>=0x84){modif=0; trob=-1;}`; иначе (отсчёт):
|
||||||
|
`if(modif>=11){ remove_loose(tile→0); trob=-1; add_mob(); } else shake`.
|
||||||
|
|
||||||
|
## Интеграция в наш движок (roomtest) — подход
|
||||||
|
|
||||||
|
Наш движок ПЕКЁТ комнату один раз (двойной буфер: своя ОЗУ-копия на
|
||||||
|
страницу). Loose требует динамики + общего состояния pop_map↔pop_bg:
|
||||||
|
1. **Общая МУТАБЕЛЬНАЯ копия комнаты**: roomtest.c держит `uint8_t
|
||||||
|
room_fg[30]` (копия room1_fg) и передаёт ОДИН указатель и в
|
||||||
|
`pop_room_draw`, и в `pop_map_set` → мутации loose видны обоим.
|
||||||
|
(Сейчас g_fg — `const`; сделать неконстантным.)
|
||||||
|
2. **Состояние**: `uint8_t pop_loose_modif[30]` (0 / 0x80.. / 1..11).
|
||||||
|
3. **Модель** (pop_map): `check_press()` в `pop_phys_tick` (make_loose_fall
|
||||||
|
при стоянии на 11), `animate` каждый кадр.
|
||||||
|
4. **Перерисовка** (pop_bg, на back-странице каждый кадр):
|
||||||
|
- тряска/отсчёт (код всё ещё 11): `gfx_heal(tile rect)` (вернуть
|
||||||
|
печёный фон) + нарисовать loose-кадр (left/right/bottom по
|
||||||
|
get_loose_frame) банком SPRITE;
|
||||||
|
- падение (11→0): mutate `g_fg[pos]=0` + «запечь пустоту» на ОБЕИХ
|
||||||
|
страницах (bake-счётчик 2: чёрный bar + draw_tile empty банком NORMAL
|
||||||
|
на текущей странице 2 кадра подряд) → дальше heal показывает пусто.
|
||||||
|
5. **L4 mob**: падающий кусок → debris(14) снизу (y_loose_land); пока
|
||||||
|
отложено — упавший loose = пусто. **L5** урон — позже.
|
||||||
|
|
||||||
|
Риск: перерисовка динамического тайла в двойном буфере (per-page heal +
|
||||||
|
bake) — единственное тонкое место; остальное — прямой порт логики выше.
|
||||||
|
|
||||||
|
## Связанные
|
||||||
|
|
||||||
|
Триггеры завязаны на физику Kid ([[pop_hang_state]] check_press/check_grab,
|
||||||
|
приземление land/SEQ_KNOCK_DOWN). Отрисовка — [[pop_fore_layer]] /
|
||||||
|
[[pop_background_strategy]] (draw_tile уже знает loose_fram_*).
|
||||||
@@ -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)
|
||||||
@@ -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 <stdint.h>
|
||||||
|
|
||||||
|
/* 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
|
||||||
@@ -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 <graphics.h>
|
||||||
|
#include <gfx.h>
|
||||||
|
#include <sprite.h>
|
||||||
|
#include <kbd_raw.h>
|
||||||
|
#include <conio.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#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;
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
After Width: | Height: | Size: 87 B |
|
After Width: | Height: | Size: 155 B |
|
After Width: | Height: | Size: 631 B |
|
After Width: | Height: | Size: 624 B |
|
After Width: | Height: | Size: 825 B |
|
After Width: | Height: | Size: 130 B |
|
After Width: | Height: | Size: 236 B |
|
After Width: | Height: | Size: 832 B |
|
After Width: | Height: | Size: 911 B |
|
After Width: | Height: | Size: 236 B |
|
After Width: | Height: | Size: 236 B |
|
After Width: | Height: | Size: 208 B |
|
After Width: | Height: | Size: 206 B |
|
After Width: | Height: | Size: 208 B |
|
After Width: | Height: | Size: 199 B |
|
After Width: | Height: | Size: 190 B |
|
After Width: | Height: | Size: 698 B |
|
After Width: | Height: | Size: 478 B |
|
After Width: | Height: | Size: 889 B |
|
After Width: | Height: | Size: 236 B |
|
After Width: | Height: | Size: 764 B |
|
After Width: | Height: | Size: 821 B |
|
After Width: | Height: | Size: 831 B |
|
After Width: | Height: | Size: 825 B |
|
After Width: | Height: | Size: 236 B |
|
After Width: | Height: | Size: 236 B |
|
After Width: | Height: | Size: 715 B |
|
After Width: | Height: | Size: 130 B |
|
After Width: | Height: | Size: 867 B |
|
After Width: | Height: | Size: 650 B |
|
After Width: | Height: | Size: 131 B |
|
After Width: | Height: | Size: 759 B |
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||