Compare commits
128 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4cffbc9aa4 | |||
| 24bb724c22 | |||
| eef6eebd8c | |||
| 3de8c7500c | |||
| 8e33cd07bc | |||
| a5773ab654 | |||
| 68b8fe5851 | |||
| 64ce6339eb | |||
| 1214785a56 | |||
| 63a25530f5 | |||
| fb495ace42 | |||
| e1846369ad | |||
| 1c91c5f92a | |||
| 6769b6a01b | |||
| 5f5eefc9fc | |||
| 517d225091 | |||
| fc69316c2c | |||
| 08ebf09d4d | |||
| 50aa652dbe | |||
| 08c6a8504f | |||
| 723da3c5c1 | |||
| e143ee010a | |||
| b106aba59b | |||
| 5e09bd9d5a | |||
| 6582154381 | |||
| c127a4b0d2 | |||
| 63a3b60524 | |||
| 86d7615841 | |||
| 3d586af031 | |||
| 95c22be9bd | |||
| d1bc97c589 | |||
| bf7ddfd4da | |||
| 7f3e32ddd6 | |||
| 21f978d441 | |||
| 8b30dc20c8 | |||
| 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 | |||
| 52636a5d6e | |||
| 3bf50f7ff7 | |||
| 657c6d2955 | |||
| b26560c409 | |||
| 25cb7ba554 | |||
| dc37a14010 | |||
| ecb419efda | |||
| 5a48f7fafb | |||
| e761d21505 | |||
| 72466f7cad | |||
| d652f89240 | |||
| 46bd9ad0f1 | |||
| 5086c47f0f | |||
| 8a952b99eb | |||
| 5184415fc4 | |||
| a6fe50e247 | |||
| 110f69fb2e | |||
| 8531b25e75 | |||
| 7187752b29 | |||
| a4c8c79428 | |||
| 60373930fb | |||
| 057dd615ba | |||
| 48d552bf3a | |||
| 4a081501d8 | |||
| 46553f4e07 | |||
| ae23d2dea2 | |||
| e5866d6ba4 | |||
| adf667c087 | |||
| ffd179e064 | |||
| 961cfb786d | |||
| e1450ba7b4 | |||
| 3a33b30c07 | |||
| fabbc8129c | |||
| 977e2d3d4a | |||
| a43b4d6703 | |||
| e3342b63f3 | |||
| c88f127057 | |||
| 386837fc25 | |||
| 40ab5b9b48 | |||
| bc3483c2dd | |||
| 733746572c | |||
| 1b78dda125 | |||
| 05916a3cc6 | |||
| ee87ae1bd9 | |||
| 858f748e7a | |||
| c0dd1621e6 | |||
| 68d5be6e47 | |||
| 17639ed62d | |||
| 6d515e4b9a |
@@ -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 # опционально, чтобы не ругался на "лишние" инклюды под другой таргет
|
||||
@@ -3,7 +3,7 @@
|
||||
# ===========================================================================
|
||||
|
||||
# `build/` directories anywhere in the tree
|
||||
# (top-level build/, lib/build/, toolchain/*/build/, ...)
|
||||
# (top-level build/, libc/build/, libbgi/build/, toolchain/*/build/, ...)
|
||||
build/
|
||||
|
||||
# sprinter-cc per-example intermediate directory
|
||||
@@ -40,7 +40,31 @@ tests/*/*.cdb
|
||||
tests/*/*.mem
|
||||
tests/*/*.rst
|
||||
|
||||
# libc archive (built from libc/, see lib/Makefile)
|
||||
# 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)
|
||||
lib/*.lib
|
||||
|
||||
# Host-built mkexe binary + test outputs (input fixtures *.bin/*.ihx kept)
|
||||
@@ -89,3 +113,10 @@ mame/
|
||||
|
||||
# Claude Code local settings (per-machine, not for the repo)
|
||||
.claude/
|
||||
|
||||
# Тяжёлые справочные материалы, НЕ версионируются: docs/extra — архивы
|
||||
# исходников (~570 МБ, четыре почти одинаковых bad_apple), docs/sources —
|
||||
# клоны чужих репозиториев (Sprinter-BIOS, Estex-DSS, SaymanNsk) со своими
|
||||
# .git внутри (в коммите стали бы битыми gitlink-ссылками).
|
||||
docs/extra/
|
||||
docs/sources/
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
# Sprinter C-Compiler — правила проекта
|
||||
|
||||
Target-слой SDCC 4.5 (z80) для компьютера Sprinter Sp2000: crt0,
|
||||
линковка, libc, mkexe. Общение и комментарии — на русском.
|
||||
|
||||
## Сборка и проверка
|
||||
|
||||
```
|
||||
make # tools + lib + libbgi + все тесты (45) + examples
|
||||
make -C libc # только libc → lib/sprinter.lib (fast) + sprinter_safe.lib
|
||||
make -C libbgi # только BGI → lib/bgi256.lib (fast) + bgi256_safe.lib
|
||||
make floppy # упаковать все .exe в mame/v306/IMG/mc.img
|
||||
make size-check # размерный регресс: _CODE vs docs/size_baseline.tsv
|
||||
make size-baseline # принять текущие размеры эталоном
|
||||
```
|
||||
|
||||
Обе библиотеки собираются в двух вариантах: fast (дефолт; `-D*_NOCHECK` —
|
||||
параметр-валидации вырезаны) и safe (линкуется по `sprinter-cc --safe`).
|
||||
Критичные гарды (напр. _fd_guard — 9-й OPEN вешает DSS) — в ОБОИХ.
|
||||
|
||||
Графика (BGI) — отдельная библиотека libbgi/ (см. ниже). Программа,
|
||||
использующая graphics.h, собирается с `--gfx 256` (или `--gfx 16` в
|
||||
Фазе 2): sprinter-cc подлинкует lib/bgi256.lib и добавит -I libbgi/include.
|
||||
|
||||
Одиночный тест: `cd tests/<имя> && make run` (пакует exe + EXTRA_DATA на
|
||||
дискету и запускает 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`
|
||||
/ `make -C libbgi clean`) не обязательна — stale .rel чистятся
|
||||
автоматически; `make size-check` обязателен (рост _CODE без причины —
|
||||
регрессия).
|
||||
|
||||
## Правила libc
|
||||
|
||||
- **1 публичная функция = 1 .c-модуль** (линкер тянет .rel целиком —
|
||||
гранулярность файлов = гранулярность DCE). Никакой группировки
|
||||
«используются вместе». Internal-хелперы — тоже по одному на модуль
|
||||
(`_`-префикс); общие статики — в отдельные data-модули
|
||||
(`_xxx_state.c`); internal-заголовки (`_file.h`, `_gfx.h`, …) —
|
||||
рядом с исходниками, НЕ в libc/include.
|
||||
- Имя файла = имя функции. libc/Makefile собирает wildcard'ом —
|
||||
ничего регистрировать не надо.
|
||||
- Комментарии — на русском; шапка модуля объясняет что/зачем + ABI.
|
||||
- File-scope переменные НЕ инициализировать `= 0` (crt0 зануляет
|
||||
_DATA; см. memory/sdcc_static_storage_gotcha).
|
||||
- asm-связки между модулями: `call/jp _global` — ок; `jr/djnz` через
|
||||
границу и fall-through — НЕЛЬЗЯ (docs/libc-split-asm-cases.md).
|
||||
- Заголовки: сначала пробовать include_next-паттерн; полная замена
|
||||
SDCC-заголовка обязана дублировать его контракт
|
||||
(docs/libc-headers.md).
|
||||
- Справочник API — docs/libc-reference.md (обновлять при добавлении
|
||||
функций).
|
||||
|
||||
## ABI и платформа (кратко; детали в memory/)
|
||||
|
||||
- SDCC `__sdcccall(1)`: arg1 → HL (8-бит → A), arg2 → DE, остальные
|
||||
на стеке (callee-pops в __naked); **возврат int/ptr в DE**, uint8 в A.
|
||||
IX callee-saved (в __naked с IX — push/pop обязательны).
|
||||
- ESTEX (rst #0x10): CF=1 — ошибка, код в A → `call __errno_set`;
|
||||
все регистры клобберятся (IX сохранять); стек обязан быть в W2.
|
||||
- BIOS (rst #0x08): строки/буферы в #4000-#BFFF.
|
||||
- Квирки: ESTEX WRITE возвращает DE=0 на успехе (судить по CF/A);
|
||||
лимит 8 файловых манипуляторов, 9-й OPEN ВЕШАЕТ DSS (_fd_guard);
|
||||
ENV $46: A=0 = NOT FOUND.
|
||||
- Перед обвинением компилятора/железа — подтвердить артефактом
|
||||
(сгенерированный .asm в libc/build/ или libbgi/build/, дамп, репро) — см.
|
||||
memory/defer_unexplained_quirks.
|
||||
|
||||
## Структура
|
||||
|
||||
- `libc/<area>/*.c` — модули libc (ядро, БЕЗ графики); `libc/include/` — публичные заголовки libc
|
||||
- `libbgi/` — графика BGI (отдельная библиотека): `common/` — mode-agnostic (один исходник, .rel в обеих driver-библиотеках), `bgi256/` + `bgi16/` — mode-specific leaf'ы (реальные реализации, без обёрток); `include/` — graphics.h + gfx.h; `_bgi.h` — внутренний заголовок. Собирает `lib/bgi256.lib` (и `bgi16.lib` в Фазе 2). Выбор режима линковкой: `--gfx 256` / `--gfx 16`.
|
||||
- `runtime/` — crt0-семейство, heap, bank (bank.s собирается per-build)
|
||||
- `bin/sprinter-cc` — обёртка компилятора; `toolchain/mkexe` — упаковщик
|
||||
- `tests/` — по одному API/фиче; `examples/` — реальные приложения
|
||||
- `docs/` — дизайн-доки; `docs/TODO.md` — roadmap
|
||||
- `third_party/solid-c/` — нативный Sprinter C (референс, CP866;
|
||||
их ABI несовместим — только как образец)
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# make build host tools, libc archive, all tests, all apps
|
||||
# make tools build only host tools (mkexe)
|
||||
# make lib build lib/sprinter.lib (libc archive used by sprinter-cc)
|
||||
# make lib build lib/sprinter.lib (libc) + lib/bgi256.lib (libbgi)
|
||||
# make tests build all libc feature tests under tests/
|
||||
# make examples build all real applications under examples/
|
||||
# make floppy package every .exe + test fixtures into mame/v306/IMG/mc.img
|
||||
@@ -13,12 +13,16 @@
|
||||
# Most heavy lifting is delegated to sub-Makefiles.
|
||||
|
||||
# Small libc-feature tests (one program per .c-language feature or libc API).
|
||||
TESTS := hello banked bankedbg strtest cat seek malloc mem_test argv errno \
|
||||
rt_test openenv ls conio attrprob timedir mouse banklocl stdlib \
|
||||
assrtest ptime stattest filetest gfx_demo gfx_d16 gfx_text gfx_mous
|
||||
|
||||
TESTS := hello hello2 simple banked bankedbg banktest strtest cat seek \
|
||||
malloc mem_test argv errno rt_test openenv ls conio conio2 \
|
||||
attrprob timedir mouse banklocl stdlib assrtest ptime stattest \
|
||||
filetest fdmax fbench solidt irqtest cbltest cblwav cblstream dec_test gets stest2 winrest \
|
||||
bios_text text_palette \
|
||||
gfx_demo gfx_dbuf bgitest bgi_img accfill
|
||||
# gfx_d16 / gfx_text / gfx_mous — 16-цветные; убраны до Фазы 2 (bgi16.lib
|
||||
# ещё не собирается). Вернуть мигрированными на BGI --gfx 16.
|
||||
# Larger end-user applications under examples/.
|
||||
APPS := mdview
|
||||
APPS := mdview mdview2
|
||||
|
||||
MAME_DIR := mame/v306
|
||||
FLOPPY_IMG := $(MAME_DIR)/IMG/mc.img
|
||||
@@ -31,9 +35,11 @@ ALL_EXES := $(TEST_EXES) $(APP_EXES)
|
||||
DATA_FILES := \
|
||||
tests/cat/test.txt \
|
||||
tests/seek/big.txt \
|
||||
tests/cblwav/speech.pcm \
|
||||
examples/mdview/SAMPLE.MD
|
||||
|
||||
.PHONY: all tools lib tests examples check clean sdcc floppy $(TESTS) $(APPS)
|
||||
.PHONY: all tools lib tests examples check clean sdcc floppy \
|
||||
size-check size-baseline $(TESTS) $(APPS)
|
||||
|
||||
all: tools lib tests examples
|
||||
|
||||
@@ -41,7 +47,8 @@ tools:
|
||||
$(MAKE) -C toolchain/mkexe
|
||||
|
||||
lib:
|
||||
$(MAKE) -C lib
|
||||
$(MAKE) -C libc
|
||||
$(MAKE) -C libbgi
|
||||
|
||||
check: tools
|
||||
$(MAKE) -C toolchain/mkexe check
|
||||
@@ -66,9 +73,18 @@ floppy: tests examples tests/seek/big.txt
|
||||
@echo "Floppy ready: $(FLOPPY_IMG)"
|
||||
@echo "Run: cd $(MAME_DIR) && ./run_mame.sh"
|
||||
|
||||
# Размерный регресс: сверить _CODE всех программ с docs/size_baseline.tsv.
|
||||
size-check:
|
||||
python3 toolchain/size_check.py
|
||||
|
||||
# Принять текущие размеры как эталон (после осознанных изменений).
|
||||
size-baseline:
|
||||
python3 toolchain/size_check.py --update
|
||||
|
||||
clean:
|
||||
$(MAKE) -C toolchain/mkexe clean
|
||||
$(MAKE) -C lib clean
|
||||
$(MAKE) -C libc clean
|
||||
$(MAKE) -C libbgi clean
|
||||
@for t in $(TESTS); do $(MAKE) -C tests/$$t clean; done
|
||||
@for a in $(APPS); do $(MAKE) -C examples/$$a clean; done
|
||||
|
||||
|
||||
@@ -36,7 +36,9 @@ LIB := $(PROJ_ROOT)/lib/sprinter.lib
|
||||
|
||||
MAME_DIR := $(PROJ_ROOT)/mame/v306
|
||||
FLOPPY_IMG := $(MAME_DIR)/IMG/mc.img
|
||||
HDD_IMG := $(MAME_DIR)/IMG/test_hdd.chd
|
||||
MAKE_DISK := $(MAME_DIR)/make_disk.py
|
||||
MAKE_HDD := $(PROJ_ROOT)/toolchain/make_hdd.sh
|
||||
RUN_MAME := $(MAME_DIR)/run_mame.sh
|
||||
|
||||
# Optional knobs — see top of file.
|
||||
@@ -51,14 +53,24 @@ CC_FLAGS += $(EXTRA_FLAGS)
|
||||
|
||||
all: $(EXAMPLE).exe
|
||||
|
||||
$(EXAMPLE).exe: $(SOURCES) $(MKEXE) $(LIB)
|
||||
# runtime/*.s (crt0-семейство, bank.s, heap.s) собираются per-build
|
||||
# внутри sprinter-cc — без этой зависимости их правка не перелинкует
|
||||
# уже собранный exe (кусало: фикс bank.s не подхватился).
|
||||
RUNTIME_DEPS := $(wildcard $(PROJ_ROOT)/runtime/*.s)
|
||||
|
||||
$(EXAMPLE).exe: $(SOURCES) $(MKEXE) $(LIB) $(RUNTIME_DEPS)
|
||||
$(SPRINTER_CC) $(CC_FLAGS) -o $@ $(SOURCES)
|
||||
|
||||
$(MKEXE):
|
||||
$(MAKE) -C $(PROJ_ROOT)/toolchain/mkexe
|
||||
|
||||
# $(LIB) = lib/sprinter.lib (libc). Графика (bgi256.lib) собирает
|
||||
# libbgi/Makefile; гоним и его — иначе standalone `make` в тесте с
|
||||
# --gfx 256 не найдёт bgi256.lib при линковке. Оба инкрементальные,
|
||||
# на повторном запуске ничего не пересобирают.
|
||||
$(LIB):
|
||||
$(MAKE) -C $(PROJ_ROOT)/lib
|
||||
$(MAKE) -C $(PROJ_ROOT)/libc
|
||||
$(MAKE) -C $(PROJ_ROOT)/libbgi
|
||||
|
||||
clean:
|
||||
rm -rf .sprinter-cc-* $(EXAMPLE).exe
|
||||
@@ -75,4 +87,14 @@ floppy: $(EXAMPLE).exe
|
||||
run: floppy
|
||||
cd $(MAME_DIR) && ./run_mame.sh
|
||||
|
||||
.PHONY: all clean floppy run
|
||||
# `make hdd` packs this program (+ optional EXTRA_DATA files) into the MAME
|
||||
# HDD image mounted as disk D: (-hard2 test_hdd.chd). Гораздо быстрее FDD —
|
||||
# используется MCP-мостом к MAME (run_bridge.sh). После пересборки образа
|
||||
# MAME ОБЯЗАН полный рестарт (chdman -f = новый inode; см. memory).
|
||||
hdd: $(EXAMPLE).exe
|
||||
$(MAKE_HDD) $(HDD_IMG) $(EXAMPLE).exe $(EXTRA_DATA)
|
||||
@echo
|
||||
@echo "HDD (D:) ready: $(HDD_IMG) (with $(EXAMPLE).exe$(if $(EXTRA_DATA), + $(EXTRA_DATA)))"
|
||||
@echo "ВНИМАНИЕ: перезапусти MAME (run_bridge.sh) — образ пересобран."
|
||||
|
||||
.PHONY: all clean floppy run hdd
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
# Prince of Persia → ZX Sprinter — правила подпроекта
|
||||
|
||||
Порт Prince of Persia (DOS/Apple II) на Sprinter Sp2000 поверх нашего
|
||||
sprinter-cc / libc / libbgi. Действуют правила корневого
|
||||
`CLAUDE.md` (сборка, libc, ABI, MAME-автотест); ниже — только специфика PoP.
|
||||
Общение и комментарии — на русском.
|
||||
|
||||
## Главное правило: SDLPoP — источник истины. Сначала читай, потом кодь
|
||||
|
||||
**`SDLPoP/src/` (github.com/NagyD/SDLPoP, GPLv3) — ЕДИНСТВЕННЫЙ авторитетный
|
||||
источник того, как оригинальный движок это делает.** Правило без исключений:
|
||||
|
||||
1. **Перед реализацией ЛЮБОЙ функции** (движение, коллизия, окклюзия,
|
||||
падение, loose-полы, стражники, отрисовка, тайминги, любые числовые
|
||||
константы) — СНАЧАЛА найди и прочитай соответствующий код в `SDLPoP/src/`,
|
||||
и портируй по нему. Не пиши по памяти, не выводи логику «из общих
|
||||
соображений», не угадывай значения — это источник багов, которые потом
|
||||
ловятся в MAME часами.
|
||||
2. **По любому вопросу «как в оригинале должно быть»** (что окклюдит что,
|
||||
в каком порядке слои, когда меняется тайл, какая скорость/задержка,
|
||||
что делает такой-то кадр анимации) — ответ ищи в `SDLPoP/src/`, а не
|
||||
строй гипотезу. Если в SDLPoP не нашёл — это повод копать дальше в
|
||||
исходнике, а не додумывать.
|
||||
3. Расхождение нашей реализации с SDLPoP — по умолчанию **баг у нас**, пока
|
||||
не доказано обратное (наша платформа/ABI требует отличия — тогда явно
|
||||
зафиксировать почему в комментарии).
|
||||
|
||||
Карта сегментов: `seg005` control-диспетчер, `seg006` play_kid/коллизия/
|
||||
seqtbl, `seg007` mob/loose/падающие объекты, `seg008` отрисовка тайлов/
|
||||
слои/окклюзия, `seg009` чтение ресурсов. Слои окклюзии у нас = слои SDLPoP.
|
||||
См. memory `pop_check_sdlpop_first`.
|
||||
|
||||
Вторичные референсы (когда в SDLPoP непонятно/нужен другой ракурс):
|
||||
- `Prince-of-Persia-Apple-II/` — оригинальный 6502-исходник 1989 (Мехнер).
|
||||
- `PR/` (github.com/NagyD/PR, GPLv2) — Princed Resources.
|
||||
- `mininim/` — независимая реализация.
|
||||
|
||||
Все эти папки — **справочник логики/структур/констант и источник ассетов**,
|
||||
но НЕ код для копирования (лицензии несовместимы, наш ABI другой): читаем
|
||||
и переписываем под наш движок, а не вставляем куски.
|
||||
|
||||
## Ассеты
|
||||
|
||||
Готовые распакованные VGA-256 ассеты (то, что нужно под 320×256×256) —
|
||||
`SDLPoP/data/` (`res<id>.png`/`.pal`/`.bin`). Брать оттуда, а НЕ писать свой
|
||||
декодер DOS `.DAT`. Локальные `.DAT` — в `MSDOS/`.
|
||||
|
||||
**Каноническая спецификация форматов `.DAT` — `docs/POP-DAT-FormatSpecifications.pdf`**
|
||||
(грепаемая копия — `docs/POP-DAT-FormatSpecifications.txt`): первоисточник
|
||||
Princed для DAT v1.0 (контейнер/индекс/чек-сумма, кодеки RLE/LZG, палитры,
|
||||
формат уровней, звук), на нём построены и SDLPoP, и Princed Resources. Наши
|
||||
разборы (`docs/MSDOS_RESOURCE_FORMAT.md` / `docs/APPLEII_RESOURCE_FORMAT.md` /
|
||||
`docs/README.md`) — практические заметки/сверки; при расхождении источник
|
||||
истины — спецификация. Формат уровня почти идентичен в Apple II и DOS.
|
||||
|
||||
Упаковка ассетов под Sprinter (атласы `.atl`, палитра) — python-скрипты в
|
||||
`toolchain/` (`render_room.py`, `pop_pack_bg.py`, `pop_pack_kid.py`,
|
||||
`pop_extract_kid_data.py`). Их дёргают Makefile'ы тестов.
|
||||
|
||||
## Структура папки
|
||||
|
||||
- `docs/` — планы и форматы: `PORT_PLAN.md` (общий план фаз),
|
||||
`KID_PLAN.md`, `double_buffer_plan.md`, `loose_floors_plan.md`, форматы
|
||||
ресурсов. Начинать чтение отсюда.
|
||||
- `roomtest/` — **активная разработка**: комната 1 + Kid (анимация, ввод,
|
||||
коллизия, падение, зацеп, fore-окклюзия, loose-полы). Свой `CLAUDE.md`.
|
||||
- `poc/` — ранний proof-of-concept (снег/атлас/kbd_raw); ассеты в `poc/res/`.
|
||||
- `bgtest/`, `coltest/` — отдельные проверки фона/коллизии.
|
||||
- `toolchain/` — python-упаковщики ассетов + эталонные PNG (`1.1-2.png`).
|
||||
- `SDLPoP/`, `PR/`, `Prince-of-Persia-Apple-II/`, `mininim/`, `MSDOS/` —
|
||||
референсы/оригинальные данные (см. выше).
|
||||
|
||||
## Ключевые архитектурные решения (memory/)
|
||||
|
||||
- `pop_port_project` — общий статус порта.
|
||||
- `pop_banking_architecture` — будущее: big+BANK_W1, графику нельзя в W3,
|
||||
один файл = один банк = прямые вызовы, main резидентен.
|
||||
- `pop_background_strategy` — фон = композиция тайлов в рантайме (вариант 3).
|
||||
- `pop_kid_plan` / `pop_hang_state` / `pop_fore_layer` /
|
||||
`pop_fall_debug_baseline` — этапы Kid.
|
||||
- `kbd_raw_fifo_drain` — held-state клавиатуры (единственный принципиальный
|
||||
пробел движка, закрыт `<kbd_raw.h>`): вычерпывать FIFO SIO циклом.
|
||||
- `png_strip_padding_tradeoff`, `pop_tile_atlas_palette_merge` — квирки
|
||||
упаковки ассетов.
|
||||
@@ -0,0 +1,35 @@
|
||||
# Prince of Persia на ZX Sprinter
|
||||
|
||||
Порт Prince of Persia на компьютер Sprinter Sp2000 поверх нашего
|
||||
target-слоя SDCC (sprinter-cc / libc / libbgi). Цель — 320×256×256 (режим
|
||||
0x81), VGA-256 ассеты оригинала переносятся почти впрямую.
|
||||
|
||||
Общий план и статус фаз — [`docs/PORT_PLAN.md`](docs/PORT_PLAN.md).
|
||||
Правила работы для ИИ-сессий — [`CLAUDE.md`](CLAUDE.md).
|
||||
|
||||
## Что где
|
||||
|
||||
| Папка | Назначение |
|
||||
|-------|-----------|
|
||||
| `roomtest/` | **Активная разработка.** Комната 1 уровня 1 живой композицией тайлов + Kid: анимация (seqtbl), управление с клавиатуры, коллизия, падение, зацеп/подтягивание, fore-окклюзия, проваливающиеся полы. Свой README/CLAUDE. |
|
||||
| `docs/` | Планы (`PORT_PLAN`, `KID_PLAN`, `double_buffer_plan`, `loose_floors_plan`) и разбор форматов ресурсов Apple II / DOS. |
|
||||
| `toolchain/` | Python-упаковщики ассетов под Sprinter (рендер комнат, атласы тайлов/Kid, извлечение данных анимации) + эталонные скриншоты. |
|
||||
| `poc/` | Ранний proof-of-concept (снег, атлас, raw-клавиатура). Ассеты в `poc/res/`. |
|
||||
| `bgtest/`, `coltest/` | Точечные проверки фона и коллизии. |
|
||||
| `SDLPoP/`, `PR/`, `mininim/` | Референсные реализации движка (GPL) — читаем логику/константы, НЕ копируем код. `SDLPoP/data/` — источник распакованных VGA-ассетов. |
|
||||
| `Prince-of-Persia-Apple-II/` | Оригинальный 6502-исходник 1989 г. |
|
||||
| `MSDOS/` | Локальные `.DAT`-ресурсы DOS-версии. |
|
||||
|
||||
## Референсы = только справочник
|
||||
|
||||
`SDLPoP/`, `PR/`, `mininim/`, `Prince-of-Persia-Apple-II/` используются как
|
||||
справочник структур/логики и как источник готовых ассетов — их код НЕ
|
||||
копируется в наш порт (лицензии несовместимы, ABI другой). Любая механика
|
||||
сверяется с `SDLPoP/src/` **до** реализации.
|
||||
|
||||
## Форматы ресурсов
|
||||
|
||||
Формат уровня почти идентичен в Apple II и DOS (2304 / 2305 байт,
|
||||
`blueprnt`). Графика различается принципиально, но брать распакованные PNG
|
||||
из `SDLPoP/data/` практичнее, чем декодировать сырой `.DAT`. Подробности —
|
||||
[`docs/README.md`](docs/README.md).
|
||||
@@ -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,301 @@
|
||||
# Формат ресурсов Prince of Persia (MS-DOS, каталог `MSDOS/`)
|
||||
|
||||
Документ описывает бинарный формат `*.DAT`-файлов ресурсов DOS-версии PoP.
|
||||
Исходников для этой версии нет, поэтому всё, что ниже — результат
|
||||
структурного (эмпирического) анализа реальных файлов из `MSDOS/`, а не чтения
|
||||
кода. Уровень уверенности указан для каждого раздела. Все находки проверены
|
||||
скриптами (Python), которые разбирают файл и валидируют согласованность
|
||||
(например: смещение+размер последней записи таблицы точно совпадает с
|
||||
началом самой таблицы — то есть данные и каталог стыкуются без дыр).
|
||||
|
||||
**Основной источник спецификации формата — `POP-DAT-FormatSpecifications.pdf`**
|
||||
(и его текстовая конверсия `POP-DAT-FormatSpecifications.txt` в этой же папке,
|
||||
для grep/цитирования): *«Prince of Persia — Specifications of File Formats»*,
|
||||
Princed Development Team, 2008 — каноническая спецификация формата `DAT v1.0`,
|
||||
на которой построен и SDLPoP, и Princed Resources. Разбирает контейнер, индекс,
|
||||
чек-сумму, кодеки изображений (RLE / LZG), палитры, формат уровней (room
|
||||
mapping, wall-drawing, room-linking, guards, start position, door events),
|
||||
звук (digital waves / MIDI / PC speaker), бинарные файлы и Mac-варианты. При
|
||||
любом расхождении между эмпирическими находками ниже и этим документом —
|
||||
источником истины считать спецификацию (сверять §-номера: её §3.x).
|
||||
|
||||
Дополнительно как справка при реализации (порт на 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,89 @@
|
||||
# Форматы ресурсов Prince of Persia — сводка
|
||||
|
||||
**Каноническая спецификация форматов** — `POP-DAT-FormatSpecifications.pdf`
|
||||
(+ текстовая конверсия `POP-DAT-FormatSpecifications.txt` для grep/цитирования):
|
||||
*«Prince of Persia — Specifications of File Formats»*, Princed Development Team,
|
||||
2008. Это первоисточник формата `DAT v1.0` (контейнер, индекс, чек-сумма,
|
||||
кодеки RLE/LZG, палитры, уровни, звук), на котором построены и SDLPoP, и
|
||||
Princed Resources. Документы ниже — наши практические заметки/сверки; при
|
||||
расхождении источником истины считать спецификацию.
|
||||
|
||||
Цель этих документов — подготовить почву для будущего порта 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,112 @@
|
||||
# План: порт `clip_char()` — обрезка спрайта персонажа
|
||||
|
||||
Статус: в работе с 2026-07-28. Контекст: баг «спуск Кида с кнопки в комнате 8»
|
||||
(Kid просвечивает в щель между кнопкой и ближним столбом, мусор на кромке).
|
||||
|
||||
## Симптом
|
||||
|
||||
room 8, Kid спускается (climbdown) с тайла-кнопки у ближней колонны. Спрайт
|
||||
Кида нарисован ЦЕЛИКОМ, включая часть, которая в оригинале обрезана по линии
|
||||
пола: видно «просвет» Кида в щели между кнопкой и колонной и мусор на кромке.
|
||||
|
||||
## Корень
|
||||
|
||||
Не портирован `clip_char()` (SDLPoP `seg006.c:1749`, вызывается из
|
||||
`add_kid_to_objtable`/`add_guard_to_objtable`, `seg008.c:1671/1690`, ПОСЛЕ
|
||||
`set_char_collision`/`set_objtile_at_char`/`redraw_at_char*` и ПЕРЕД
|
||||
`add_objtable`). Оригинал кладёт в objtable не только позицию спрайта, но и
|
||||
прямоугольник клипа `obj_clip_{top,bottom,left,right}`; блиттер рисует только
|
||||
внутри него. Мы рисуем без клипа вообще.
|
||||
|
||||
## Что делает оригинал (дословно)
|
||||
|
||||
`reset_obj_clip()` → `left=0, top=0, right=320, bottom=192`.
|
||||
|
||||
Дальше (упрощая C-трюк с глобалью `curr_tile2`, которую ставит каждый
|
||||
`get_tile`: `X == wall || tile_is_floor(curr_tile2)` — это «тайл X = стена
|
||||
ИЛИ пол»):
|
||||
|
||||
```
|
||||
T_L = get_tile(room, char_col_left, char_top_row)
|
||||
T_R = get_tile(room, char_col_right, char_top_row)
|
||||
if (T_L — стена или пол) &&
|
||||
( (action == stand && (frame == 79 || frame == 81)) /* прыжок вверх / зацеп */
|
||||
|| (T_R — стена или пол) )
|
||||
{
|
||||
clip_row = Char.curr_row + 1;
|
||||
clip_y = y_clip[clip_row]; /* y_clip[] = {-60, 3, 66, 129, 192} */
|
||||
if (clip_row == 1 || (clip_y < obj_y && clip_y - 15 < char_top_y))
|
||||
obj_clip_top = char_top_y = clip_y;
|
||||
}
|
||||
```
|
||||
|
||||
Смысл: `y_clip[row+1]` — верхняя граница СВОЕЙ полосы ряда. Если над головой
|
||||
пол/стена — всё, что выше этой линии, не рисуется (персонаж «уходит под пол»).
|
||||
Для ряда 0 (`clip_row == 1`) клип применяется БЕЗУСЛОВНО.
|
||||
|
||||
Метрики из `set_char_collision` (seg006:0723):
|
||||
- `char_x_left = obj_x/2 + 58` (и `-= char_width_half`, если смотрит вправо)
|
||||
- `char_x_right = char_x_left + char_width_half`, `char_width_half = (w+1)/2`
|
||||
- `char_top_y = obj_y - h + 1`; если `>= 192` → `0`
|
||||
- `char_top_row = y_to_row_mod4(char_top_y)`
|
||||
- `char_col_left = MAX(get_tile_div_mod(char_x_left), 0)`,
|
||||
`char_col_right = MIN(get_tile_div_mod(char_x_right), 9)`
|
||||
|
||||
Второй блок `clip_char` — `obj_clip_right` (doortop / стена / зеркало для
|
||||
виса-полёта-подъёма при взгляде влево, кадры 137..139) и ветка кадров 224..228
|
||||
(выход в дверь уровня). **Фаза 2**, не в этом заходе.
|
||||
|
||||
## Наши точки касания
|
||||
|
||||
- `roomtest/pop_kid.c` `kid_draw()` — сам считает `obj_x/obj_y/w/h` и блитит
|
||||
через `gfx_blit_cols(bx, top + POP_YOFF, img, flip)`; запоминает
|
||||
прямоугольник в `kid_l{x,y,w,h}[page]` для `kid_heal()`.
|
||||
- `roomtest/pop_map.c` — там `get_tile()`, `tile_is_floor()`, `y_to_row()`,
|
||||
`get_tile_div_mod_m7()`, Kid-структура. Аналогичный расчёт габарита уже
|
||||
есть в `check_spike_below()` (строка ~1198) — брать за образец.
|
||||
- `libbgi/common/gfx_blit_cols.c` — клип по экрану есть (при `y < 0`
|
||||
пропускает `sy` верхних строк колонки), но произвольной верхней границы нет.
|
||||
|
||||
## Шаги
|
||||
|
||||
1. **libbgi**: `gfx_blit_cols_part(int x, int y, const void *img, uint8_t flip,
|
||||
int sy, int h)` — «пропустить `sy` верхних строк спрайта, нарисовать `h`».
|
||||
Реализация = тело `gfx_blit_cols` с предустановленными `sy`/`h` (источник
|
||||
`+= sy`, экранный `y += sy`). Прототип в `libbgi/include/gfx.h`.
|
||||
Стоимость: 0 в общем пути (обычный `gfx_blit_cols` вызывает то же ядро).
|
||||
2. **pop_map.c**: `int pop_clip_char_top(int obj_x, int obj_y, uint16_t w,
|
||||
uint16_t h)` — порт первого блока `clip_char`; возвращает КОМНАТНЫЙ y
|
||||
(0 = клипа нет). Экспорт в `pop_map.h`.
|
||||
3. **pop_kid.c**: в `kid_draw()` после расчёта `top` —
|
||||
`ct = pop_clip_char_top(...)`; если `ct > top` → `sy = ct - top`,
|
||||
рисовать `gfx_blit_cols_part(bx, ct + POP_YOFF, img, flip, sy, h - sy)`;
|
||||
в `kid_l*[page]` класть ОБРЕЗАННЫЙ прямоугольник (иначе heal чистит лишнее
|
||||
и стирает кромку пола). То же для `kid_draw_splash` — там оригинал делает
|
||||
`reset_obj_clip()`, т.е. splash НЕ клипится (ничего не менять).
|
||||
4. **Сборка**: `make -C libbgi`, `make -C applications/PoP/roomtest`,
|
||||
`make size-check`; вывести свободное место в W2/W3 (порог 512 Б).
|
||||
5. **Проверка в MAME** (`mame_hdd_test_disk`, полный рестарт после
|
||||
пересборки образа): комната 6 → переход в 8 → влезть на кнопку → спуск.
|
||||
Сверять с эталоном: живой SDLPoP той же позой (см. `pop_check_sdlpop_first`).
|
||||
|
||||
## Риски / что проверить отдельно
|
||||
|
||||
- `char_top_row` считается `y_to_row_mod4` — у нас `y_to_row()` даёт −1 для
|
||||
полосы у потолка; `get_tile(row −1)` уже умеет ряд 2 верхнего соседа.
|
||||
- Kid ниже комнаты (`char_top_y >= 192` → `0`) — обязателен ресет, иначе клип
|
||||
прыгнет.
|
||||
- `clip_row == 1` (ряд 0) — клип БЕЗУСЛОВНЫЙ: проверить, что не режет Кида в
|
||||
обычной стойке на ряду 0 (условие внешнего `if` про пол/стену над головой
|
||||
должно отсекать).
|
||||
- Клип меняет прямоугольник heal → возможен «хвост» на второй странице
|
||||
дабл-буфера: проверять оба кадра (SPACE — выключить дабл-буфер).
|
||||
|
||||
## Дальше (Фаза 2, отдельно)
|
||||
|
||||
`obj_clip_right` (doortop/стена/зеркало) — нужен для виса и подъёма при
|
||||
взгляде влево; и `obj_clip_left` для зеркала (уровень 4). Требует клипа по
|
||||
колонкам в `gfx_blit_cols` (обрезка справа = уменьшить `w`) — дёшево, но
|
||||
без тестовой сцены проверять нечем.
|
||||
|
||||
См. memory: `pop_clip_char_todo`, `pop_backtable_vs_midtable`,
|
||||
`pop_check_sdlpop_first`, `gfx_blit_noclip_fast`.
|
||||
@@ -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,264 @@
|
||||
# Интерактивные объекты (кнопки/гейты/пики) + HP/смерть — ПОДРОБНЫЙ план
|
||||
|
||||
Статус: **план** (2026-07-20). Реализация — отдельной сессией. Документ
|
||||
самодостаточный: рассчитан на старт «с чистого листа» (пустой контекст).
|
||||
Всё сверено с `applications/PoP/SDLPoP/src/` и данными `res2001.bin`.
|
||||
|
||||
Правило проекта (см. `applications/PoP/CLAUDE.md`): **SDLPoP — источник истины**,
|
||||
перед кодингом читать соответствующий код seg*.c, не гадать.
|
||||
|
||||
---
|
||||
|
||||
## 0. КОНТЕКСТ: текущее состояние `applications/PoP/roomtest` (что уже готово)
|
||||
|
||||
roomtest — живой прототип порта PoP: комната 1 уровня 1 живой композицией
|
||||
тайлов + Kid (анимация/управление/коллизия/падение/зацеп/переходы). Собрать:
|
||||
`cd applications/PoP/roomtest && make`. Тест в MAME: см. memory
|
||||
`mame_mcp_bridge`/`mame_hdd_test_disk` (канонический цикл: `make` → пересобрать
|
||||
`mame/v306/IMG/test_hdd.chd` через `toolchain/make_hdd.sh` со всеми ассетами →
|
||||
рестарт `run_bridge.sh` → `resume` → ~13с бут → `type_string("d:{ENTER}roomtest.exe{ENTER}")`).
|
||||
Отладка: клавиши `1`=freeze / `2`=resume в roomtest; MCP-мост `mame-z80`
|
||||
(read_logical_memory, set_breakpoint, disassemble); адреса символов —
|
||||
`.sprinter-cc-roomtest/roomtest.map` (сдвигаются при пересборке!).
|
||||
|
||||
### Модули (все в `applications/PoP/roomtest/`)
|
||||
- `roomtest.c` — главный цикл (дабл-буфер 2 стр.), `enter_room(room)`,
|
||||
обработчики переходов. File-static рабочие массивы (W2):
|
||||
`room_fg[30]`, `room_bg[30]`, `lcol_fg/lcol_bg[3]`, `rcol_fg/rcol_bg[3]`,
|
||||
`below_fg[10]`, `cur_room`.
|
||||
- `pop_level.c/.h` — **уровень из файла** (Фаза L1):
|
||||
- `pop_level_load("res2001.bin")` — читает сырой blueprnt в EMM-страницу
|
||||
(данные с offset `0x100`, ISR-стаб как атлас).
|
||||
- `pop_room_load(room, fg,bg, lcol_fg,lcol_bg, rcol_fg,rcol_bg, below_fg)` —
|
||||
извлекает комнату (fg маскирован `&0x1F`, bg raw) + срезы соседей:
|
||||
leftcol=col9 левого соседа, rightcol=col0 правого, belowrow=row0 нижнего.
|
||||
- `pop_room_link(room, side)` — связь (side 0=L,1=R,2=U,3=D; 0=нет).
|
||||
- `pop_level_start_room/pos/dir()`.
|
||||
- `pop_bg.c/.h` — отрисовка тайлов (порт seg008 draw_tile), fore-окклюзия над
|
||||
Kid (`pop_fore_over_kid`, порт set_char_collision+redraw_at_char/char2),
|
||||
**loose-полы** (shake/bake/mob). `draw_tile` — статическая, знает
|
||||
`draw_gate_back` (грань гейта из левой комнаты).
|
||||
- `pop_kid.c/.h` — анимация Kid (интерпретатор seqtbl `play_seq`, порт seg006),
|
||||
`Kid` struct (frame,x,y,dir,curr_col,curr_row,action,fall_x,fall_y,repeat,
|
||||
curr_seq); `knock`-флаг; `kid_cur_dx/flags`, `kid_fp_*` (футпринт).
|
||||
- `pop_ctrl.c/.h` — ввод (порт seg005 control) через `<kbd_raw.h>`.
|
||||
- `pop_map.c/.h` — коллизия/физика (порт seg005/006). Ключевое:
|
||||
- `pop_map_set(fg)` — карта текущей комнаты.
|
||||
- `pop_map_set_edges(l,r,u,d, lcol_fg, rcol_fg)` — связи + кромки швов для
|
||||
коллизии (`get_tile(col=-1)`=lcol, `get_tile(col=10)`=rcol; порт
|
||||
find_room_of_tile).
|
||||
- `pop_phys_tick()` — кадр физики (fall/land/wall/knock/leave).
|
||||
- Переходы: `pop_fell_out` (вниз, y>=211), `pop_leave_dir` (1=left,2=right,
|
||||
x∓140).
|
||||
- **loose-состояние**: `pop_loose_modif[30]` (публично, читает pop_bg),
|
||||
`loose_bake[30]`, `loose_rest[30]` (static); `pop_loose_tick()`,
|
||||
`pop_loose_reset()` (сброс при смене комнаты).
|
||||
|
||||
### Что сделано по фазам
|
||||
- **L1** — данные уровня из файла (room1_data.h удалён). Коммит `21f978d`.
|
||||
- **L2** — переход в комнату снизу (провал/спуск), фиксы окклюзии. Коммит `7f3e32d`.
|
||||
- **L3** — переходы вбок (право+лево) через швы. **Не закоммичено** на момент
|
||||
написания (вместе с этим планом). **L3-вверх (climb-up в комнату сверху) —
|
||||
НЕ сделано.**
|
||||
- **Loose-полы** (тряска knock / падение mob+окклюзия) — коммит `8b30dc2`.
|
||||
|
||||
### Известные ОГРАНИЧЕНИЯ (важно для этого плана)
|
||||
1. **Нет персистентности тайлов**: `enter_room`→`pop_room_load` каждый раз
|
||||
перезагружает ИСХОДНЫЕ тайлы из level-страницы. Изменения (упавший loose,
|
||||
открытый гейт) при повторном входе ТЕРЯЮТСЯ. Для кнопок/гейтов это
|
||||
блокер (см. P0).
|
||||
2. **Нет HP/смерти** Кида (нужно для пик).
|
||||
3. Loose-механика — частный случай trob (нужно обобщить).
|
||||
|
||||
---
|
||||
|
||||
## 1. ДАННЫЕ УРОВНЯ (формат, offsets, объекты)
|
||||
|
||||
Сырой `res2001.bin` (2305 Б) = blueprnt DAT 1.0 (Table 6 в
|
||||
`POP-DAT-FormatSpecifications.txt`). Читается в EMM-страницу с offset `0x100`.
|
||||
Тайл-код = байт `& 0x1F`; верхние биты (модификатор BLUETYPE) сейчас отброшены.
|
||||
|
||||
| Блок | Offset | Размер |
|
||||
|------|--------|--------|
|
||||
| foretable (fg) | 0 | 720 (24 комн × 30) |
|
||||
| backtable (bg=modifier) | 720 | 720 |
|
||||
| **LINKLOC** (doorlink1) | **1440** | 256 |
|
||||
| **LINKMAP** (doorlink2) | **1696** | 256 |
|
||||
| links (roomlinks) | 1952 | 96 (24×{L,R,U,D}) |
|
||||
| start_position | 2112 | 3 (room,pos,dir) |
|
||||
|
||||
Тайл-коды: `0x00`empty `0x01`floor `0x02`**SPIKE** `0x03`pillar `0x04`**GATE**
|
||||
`0x06`**DROP-кнопка(closer)** `0x0B`loose `0x0F`**RAISE-кнопка(opener)**
|
||||
`0x10`lvldoor-L `0x11`lvldoor-R `0x13`torch `0x14`wall.
|
||||
|
||||
### Объекты уровня 1 (по комнатам)
|
||||
```
|
||||
room 5: DROP(0,2)m11 RAISE(0,4)m9 GATE(0,5)m2 RAISE(0,6)m8 GATE(0,9)m1
|
||||
room 6: RAISE(0,2)m7 SPIKE(2,3) SPIKE(2,4) <-- тестовая
|
||||
room 7: RAISE(0,2)m6 GATE(0,9)m2
|
||||
room 8: RAISE(0,6)m5 GATE(0,9)m2 RAISE(1,7)m4
|
||||
room 9: RAISE(0,0)m3 (+ lvldoor(1,3)/(1,4) — выход на level2, отложено)
|
||||
room12: RAISE(0,3)m2 GATE(0,9)m2 SPIKE(2,4)
|
||||
room10/13/14/16/19/24: только SPIKE
|
||||
room20: DROP(1,4)m1 RAISE(1,7)m0
|
||||
```
|
||||
(m = modifier тайла = ИНДЕКС в LINKLOC/LINKMAP.)
|
||||
|
||||
### Room6 (тестовая) — раскладка
|
||||
```
|
||||
fg row0: 13 01 0F 00 03 01 01 13 01 03 (0,2)=RAISE-кнопка, (0,0)/(0,7)=torch
|
||||
fg row1: 14 14 14 00 14 14 14 14 14 14 (1,3)=empty
|
||||
fg row2: 14 14 14 02 02 14 14 14 14 14 (2,3)(2,4)=SPIKE
|
||||
links: L=8 R=2 U=5 D=0
|
||||
```
|
||||
- **Шахта пик**: col3 (row0=empty, row1=empty, row2=spike) + col4 (spike).
|
||||
- **Гейт, видимый у ЛЕВОЙ кромки room6, — это гейт room8 (0,9)**, отрисованный
|
||||
в col0 room6 через левый шов (L=8, leftcol=col9 room8). В room6 гейта НЕТ.
|
||||
|
||||
### Связь кнопка→цель (декод LINKLOC/LINKMAP), проверено:
|
||||
- `get_doorlink_tile(i) = d1[i] & 0x1F`
|
||||
- `get_doorlink_next(i) = !(d1[i] & 0x80)` (0 = конец цепочки)
|
||||
- `get_doorlink_room(i) = ((d1[i]&0x60)>>5) + ((d2[i]&0xE0)>>3)`
|
||||
- `get_doorlink_timer(i) = d2[i] & 0x1F`
|
||||
- где `d1`=LINKLOC@1440, `d2`=LINKMAP@1696. Цепочка: idx++ пока next.
|
||||
|
||||
**Кнопка room6 (0,2) mod=7 → цель: room8 tile(0,9) = ГЕЙТ.** Подтверждено
|
||||
в SDLPoP-скринах: нажатие кнопки поднимает решётку у левой кромки room6.
|
||||
Связь КРОСС-КОМНАТНАЯ (кнопка в room6, гейт в room8) и задаётся таблицей,
|
||||
а НЕ позицией. Кнопка может открывать НЕСКОЛЬКО гейтов в разных комнатах.
|
||||
|
||||
---
|
||||
|
||||
## 2. МЕХАНИКА SDLPoP (точные ссылки)
|
||||
|
||||
### 2.1 Trob-система (анимируемые тайлы)
|
||||
- `add_trob(room,tilepos,type)` seg007:0A5A — в список анимируемых.
|
||||
- Каждый кадр `redraw_needed_tiles`/`process_trobs` продвигает; диспетч по
|
||||
типу тайла → `animate_button/animate_door/animate_spike/animate_loose`
|
||||
(seg007:0033+ таблица `animate_*`).
|
||||
- Состояние тайла хранится в `curr_room_modif[tilepos]` (per-room modifier).
|
||||
- У нас есть частный случай для loose (`pop_loose_tick`+`pop_loose_modif[30]`).
|
||||
|
||||
### 2.2 Пики (spike)
|
||||
- **Триггер выдвижения** — `check_spike_below()` seg006:1199 (зовётся в
|
||||
физике Кида каждый кадр): для каждой колонки футпринта Кида
|
||||
(`get_tile_div_mod_m7(char_x_left)`..`char_x_right`) идёт ВНИЗ от
|
||||
`Char.curr_row` через НЕ-floor тайлы; если встретил `tiles_2_spike` →
|
||||
`start_anim_spike(room,tilepos)`. → Кид у края (0,2) правым краём задевает
|
||||
col3 → скан вниз col3 (empty/empty/spike) → пики вылезают.
|
||||
- `start_anim_spike` seg007:596: если `modif<=0`: `modif==0` → add_trob(type1)
|
||||
+ звук; `modif<0` (кроме 0xFF disabled) → `modif=0x8F`.
|
||||
- `animate_spike` seg007:317: автомат по modif — выдвиг `++modif` (1..4; на 5 →
|
||||
`0x8F`; на 9 → 0, trob кончился); убирание `& 0x80` → `--modif` (на 0 → `=6`).
|
||||
`0xFF` = disabled (не двигать).
|
||||
- `is_spike_harmful` seg007:1178: modif `0/-1`→0 (безопасно); `<0`→1;
|
||||
`1..4`→2; `>=5`→0.
|
||||
- **Смерть**: `check_spiked` seg006:0968 — если тайл под Кидом = spike И harmful
|
||||
И кадр бега (7..14) / старта прыжка (34..39) с harmful>=2, ИЛИ кадр приземления
|
||||
(43/26) с harmful!=0 → `spiked()`. Падение на пики — отдельный путь (см.
|
||||
`is_dead` seg006:1907, frame_177_spiked). Осторожный ШАГ по невыдвинутым — ок.
|
||||
|
||||
### 2.3 Кнопки
|
||||
- `trigger_button(playsound, button_type, modifier)` seg007:0C53: modifier =
|
||||
индекс LINKLOC. `link_timer = get_doorlink_timer(mod)`; если `!=0x1F`
|
||||
(не заклинено): `set_doorlink_timer(mod,5)`; если был `<2` → `add_trob`
|
||||
(кнопка нажимается) + звук; затем `do_trigger_list(mod, button_type)`.
|
||||
- `do_trigger_list` seg007:09E5: идёт по цепочке LINKLOC от idx, для каждой
|
||||
цели `trigger_1(target_type,room,tilepos,button_type)` → если >=0
|
||||
`add_trob(room,tilepos,result)`.
|
||||
- `animate_button` seg007:0D3A: `timer=get_doorlink_timer(mod)-1`;
|
||||
`set_doorlink_timer(mod,timer)`; `timer<2` → кнопка отжимается.
|
||||
- Когда Кид ВСТАЁТ на кнопку: `check_press`-путь seg006 (opener → trigger_button,
|
||||
closer → тоже; если Кид мёртв — `died_on_button`). RAISE=`tiles_15_opener`
|
||||
(0x0F), DROP=`tiles_6_closer` (0x06).
|
||||
|
||||
### 2.4 Гейты
|
||||
- `trigger_1` seg007:0999 → для `tiles_4_gate` → `trigger_gate`.
|
||||
- `trigger_gate(room,tilepos,button_type)` seg007:092C: modif = высота открытия.
|
||||
opener: `0xFF`→игнор; `>=188`(открыт)→держать `238`; иначе `modif=(modif+3)&0xFC`,
|
||||
return 1 (открывать). closer/иначе: `modif!=0` → return 3 (закрыть быстро).
|
||||
- `animate_door` seg007:0522: анимация открытия/закрытия; `door_delta[]={-1,4,4}`,
|
||||
`gate_close_speeds[]={0,0,0,20,40,60,80,100,120}`. Гейт медленно закрывается
|
||||
после истечения таймера кнопки.
|
||||
- Отрисовка: кадры гейта; у нас `draw_gate_back` в pop_bg (грань из левой комн.).
|
||||
- **Проходимость**: Кид блокируется недостаточно открытым гейтом (коллизия
|
||||
как стена, порог по высоте открытия); проходит при `modif` открытом.
|
||||
|
||||
---
|
||||
|
||||
## 3. Поправки к описанию пользователя (что важно)
|
||||
|
||||
1. Гейт НЕ в room6 (0,0) — он в **room8 (0,9)**, виден через левый шов; связь
|
||||
кросс-комнатная (по таблице LINKLOC, не по позиции).
|
||||
2. Кнопка может открывать несколько гейтов в разных комнатах.
|
||||
3. Пики выдвигаются по `check_spike_below` (Кид над колонкой с пиками),
|
||||
имеют состояния (не всегда смертельны), убираются со временем.
|
||||
4. **Кросс-комнатное состояние тайлов ДОЛЖНО ПЕРСИСТИТЬ** — блокер (см. P0).
|
||||
5. HP/смерть — новая подсистема.
|
||||
6. Кнопка сама анимируется (нажата/отжата).
|
||||
|
||||
---
|
||||
|
||||
## 4. ПЛАН РЕАЛИЗАЦИИ (фазы)
|
||||
|
||||
### P0 — Персистентное per-room modifier-состояние + trob-каркас (ПРЕРЕКВИЗИТ)
|
||||
**Проблема:** нажатие кнопки в room6 меняет modif гейта room8 (не текущей
|
||||
комнаты); при входе в room8 нужно отрисовать гейт в текущем состоянии. Плюс
|
||||
это чинит «re-entry восстанавливает тайлы» (loose/гейты).
|
||||
|
||||
Дизайн (предложение — уточнить в реализации):
|
||||
- Массив `room_modif[24][30]` (или lazy per-visited-room) — modifier каждого
|
||||
тайла каждой комнаты. Инициализируется из bg уровня при первой загрузке
|
||||
комнаты; далее ЖИВЁТ (не перезагружается). ~720 Б — влезает в W2 (или в
|
||||
EMM-страницу уровня рядом с данными: остаётся >10КБ).
|
||||
- `enter_room` берёт modif из `room_modif[room]`, а fg — из level-страницы
|
||||
(fg почти не меняется; исключения — loose→empty, надо тоже персистить: либо
|
||||
отдельный `room_fg_override`, либо флаг «loose упал»).
|
||||
- Обобщить loose-trob: единый список trob (room,tilepos,type) + диспетчер
|
||||
`animate_*` по коду тайла. Loose (`pop_loose_*`) — перевести на него.
|
||||
- Кросс-комнатный trigger: `add_trob` в НЕ текущую комнату меняет
|
||||
`room_modif[room][tilepos]`; анимация продвигается даже для невидимой комнаты
|
||||
(в оригинале — да; можно упростить: для невидимой комнаты гейт сразу в
|
||||
финальном состоянии, анимировать только при видимости — решить при реализации).
|
||||
|
||||
### Фаза S — Пики (самодостаточно; отладит HP/смерть)
|
||||
Порядок:
|
||||
1. `room_modif` для пик (из P0 или временно локально).
|
||||
2. `check_spike_below` (порт seg006:1199) — в `pop_phys_tick`.
|
||||
3. `start_anim_spike` + `animate_spike` (порт seg007) — состояние в modif.
|
||||
4. `is_spike_harmful` + `check_spiked` (порт seg006:0968).
|
||||
5. **HP/смерть**: ввести `hitp_curr` (старт напр. 3); `take_hp`; при пиках —
|
||||
мгновенная смерть; seq смерти (`seq_22_crushed`/`frame_177_spiked`..185);
|
||||
анимация смерти; респавн (kid_init на старте или чек-поинт).
|
||||
6. Отрисовка: кадры выдвижения пик. В pop_bg есть `SPIKES_FRAM_RIGHT` — нужны
|
||||
pop-out кадры (spikes_fram по modif) + fore над Кидом.
|
||||
|
||||
### Фаза B — Кнопки + гейты (нужен P0)
|
||||
Порядок:
|
||||
1. Доступ к LINKLOC/LINKMAP из pop_level (добавить геттеры doorlink1/2[i] с
|
||||
маппингом W0 или скопировать таблицы в W2 при load — 512 Б).
|
||||
2. `pop_map` детект «Кид встал на кнопку» (check_press-путь) → `trigger_button`.
|
||||
3. `trigger_button` → таймер + `do_trigger_list` (обход цепочки) →
|
||||
`trigger_gate` для целей → изменить `room_modif[целевой]`.
|
||||
4. `animate_button` (кнопка отжимается) + `animate_door` (гейт откр/закр +
|
||||
авто-закрытие).
|
||||
5. Отрисовка гейта (кадры по modif) через ЛЕВЫЙ шов (гейт room8 в room6) +
|
||||
при входе в room8. Расширить `draw_gate_back`/добавить `draw_gate`.
|
||||
6. Коллизия: закрытый гейт = стена (порог по высоте открытия); открытый —
|
||||
проход. Учесть кросс-комнатный гейт на шве (проход влево room6→room8).
|
||||
|
||||
**Порядок фаз:** P0 → S → B. S в основном независим (кроме HP/trob-каркаса),
|
||||
но проще и отладит смерть/анимацию; B требует P0 (кросс-комнатное состояние).
|
||||
|
||||
---
|
||||
|
||||
## 5. Открытые вопросы / грабли
|
||||
- Персистентность `fg` для loose (тайл→empty): решить в P0 (override-массив или
|
||||
флаг), иначе упавший loose «вернётся».
|
||||
- Анимация trob в НЕВИДИМОЙ комнате: упростить (финальное состояние сразу) или
|
||||
портировать честно.
|
||||
- Двоебуфер: любой транзиент (пики/гейт/кнопка) финализировать перерисовкой
|
||||
«покоя» на ОБЕИХ страницах (урок из loose — см. memory `pop_loose_floors`).
|
||||
- Дверь уровня (lvldoor room9) + переход на level 2 — ОТДЕЛЬНО, отложено.
|
||||
- L3-**вверх** (climb-up в комнату сверху) — ещё не сделан; можно закрыть до
|
||||
объектов или параллельно.
|
||||
@@ -0,0 +1,370 @@
|
||||
# roomtest — план v2: размер кода и раскладка по окнам/банкам/страницам
|
||||
|
||||
Статус: **план для отдельной сессии**, составлен 2026-07-29 по свежему замеру.
|
||||
Заменяет `size_optimization_plan.md` (v1, 2026-07-21): часть его пунктов уже
|
||||
сделана, часть опиралась на неверную модель банкинга. Документ самодостаточный
|
||||
— рассчитан на старт с пустого контекста.
|
||||
|
||||
Повод: перед стражами и боёвкой (новый код ~5–8 КБ) надо понять, куда он
|
||||
поместится, и заранее развести код так, чтобы банкованные модули не упёрлись в
|
||||
ограничения окна W3.
|
||||
|
||||
---
|
||||
|
||||
## 0. Что уже сделано из v1 (не повторять)
|
||||
|
||||
- `--opt-code-size` и `--max-allocs 100000` **уже включены по умолчанию** в
|
||||
`bin/sprinter-cc` (v1 §2.1 закрыт, выигрыш получен).
|
||||
- Лишние блиты переднего слоя убраны (v1 §7 п.0): `fore_tile` больше не рисует
|
||||
`bottom_id`, `_CODE` −388 Б.
|
||||
- `gfx_blit_noclip` в libbgi (v1 §8 шаг 1): фоновые блиты в 2.9× дешевле.
|
||||
- `--w3` как резидент окна 3 реализован и обкатан (memory `w3_resident_code`).
|
||||
|
||||
---
|
||||
|
||||
## 1. ЗАМЕР (сборка 2026-07-29, коммит 1214785)
|
||||
|
||||
Команда: `--memory small --gfx 256 --w3 pop_trob.c pop_map.c --w3 pop_bg.c`.
|
||||
|
||||
### 1.1 Окна
|
||||
|
||||
| Область | Занято | Свободно | Примечание |
|
||||
|---|---|---|---|
|
||||
| W1+W2 `_CODE` | 24 881 Б | — | 0x4100…0xA231 |
|
||||
| W1+W2 `_HOME`+`_GSINIT`+`_DATA`+`_BSS` | ~4 240 Б | — | до 0xB2E4 |
|
||||
| **W1+W2 куча** | 0 (никто не malloc'ит) | **2 076 Б** | 0xB2E4…0xBB00 |
|
||||
| W1+W2 стек | — | 1 279 Б | 0xBB00…0xBFFE |
|
||||
| **W3 резидент** | 14 376 Б | **2 008 Б** | 0xC000…0xF828 |
|
||||
| EMM-страницы | 37 атласов + 1 уровень | ~215 страниц свободно | `sprinter_emm_budget` |
|
||||
|
||||
**Итого запаса до стены: ≈ 4 КБ** (2 КБ в W1/W2 + 2 КБ в W3). Стражи туда
|
||||
не влезут.
|
||||
|
||||
### 1.2 Код по модулям (точно, из `.rel`)
|
||||
|
||||
```
|
||||
W1/W2 (_CODE 24 881): W3 резидент (_W3CODE 14 376):
|
||||
pop_kid 6 963 pop_bg 11 643
|
||||
pop_map 6 157 pop_trob 2 733
|
||||
roomtest 2 583
|
||||
pop_level 1 425
|
||||
pop_ctrl 1 145
|
||||
crt0 333
|
||||
libc+libbgi ~6 275
|
||||
```
|
||||
|
||||
### 1.3 Крупнейшие функции/данные (из `.lst`)
|
||||
|
||||
```
|
||||
pop_bg : draw_tile 3080, other_overlay_tile 1146, wall_pattern 944,
|
||||
mob_render 720, mob_tick_one 661, overlay_mid_tile 498,
|
||||
fore_only_tile 409, climb_overlay_tile 391, tile_table 371
|
||||
pop_map : check_loose_fall_on_kid 674, check_bumped 567, jump_up_or_grab 413,
|
||||
get_tile 266, do_knock 243, check_press 238, check_leave 212
|
||||
pop_kid : kid_seqtbl 2310 + kid_frames 1205 + kid_seq_off 230 = 3745 Б ДАННЫХ
|
||||
(в _CODE!), собственно кода ~3.2 КБ
|
||||
roomtest : enter_room+main ~2.1 КБ
|
||||
pop_level: room_bg_ptr 1084 (+ 515 Б таблиц LINKLOC/LINKMAP в _DATA)
|
||||
```
|
||||
|
||||
### 1.4 `_DATA` (3 710 Б)
|
||||
|
||||
```
|
||||
pop_trob 963 (room_modif[24][30] = 720 + trobs + rest-массивы)
|
||||
pop_level 515 (копии LINKLOC/LINKMAP уровня)
|
||||
pop_map 163, roomtest 141, pop_kid 130, pop_bg 115, pop_ctrl 13
|
||||
libc: _irq_state 818, _kbdraw_state 515, _gfx_pal_buf 256, прочее ~200
|
||||
```
|
||||
|
||||
### 1.5 Находки замера (мелкие, но чинить)
|
||||
|
||||
1. **`--w3` берёт ОДИН файл на флаг.** В `Makefile` написано
|
||||
`--w3 pop_trob.c pop_map.c --w3 pop_bg.c`, и это значит «W3 = pop_trob и
|
||||
pop_bg», а `pop_map.c` компилируется как обычный исходник в W1/W2. Судя по
|
||||
`.sprinter-cc-roomtest/w3_pop_map.rel` (устаревший артефакт), когда-то
|
||||
pop_map был в W3. **Решить осознанно** (см. §4) и записать явно:
|
||||
`--w3 pop_trob.c --w3 pop_bg.c`.
|
||||
2. `libc` тянет `_irq_state` 818 Б + `_kbdraw_state` 515 Б в `_DATA`.
|
||||
`__irq_vec_buf` (513 Б) — таблица векторов IM2; `__kbdraw_down` (512 Б) —
|
||||
битмап клавиш на 512 скан-кодов. Оба можно ужать (см. §5.4), это ~0.7 КБ
|
||||
в самом дефицитном окне.
|
||||
|
||||
---
|
||||
|
||||
## 2. ПРАВИЛА ПЛАТФОРМЫ, ОТ КОТОРЫХ ПЛЯШЕТ РАСКЛАДКА
|
||||
|
||||
Это главное, что изменилось по сравнению с v1: модель банкинга уточнена по
|
||||
`bin/sprinter-cc` (справка `--w3`/`--bank`) и по коду libbgi.
|
||||
|
||||
**(R1) Резидент W3 (`--w3`) и банки W3 (`--bank`) делят одно окно.**
|
||||
Резидент лежит на своей странице 0xC000…0xFFFF; трамплин на время вызова
|
||||
`__banked` подменяет страницу W3 на банковую и возвращает резидентную назад.
|
||||
|
||||
**(R2) Из банка резидент W3 НЕДОСТИЖИМ — и транзитивно тоже.**
|
||||
Пока исполняется банк, резидентной страницы в адресном пространстве нет.
|
||||
Значит нельзя не только `bank → pop_bg()`, но и `bank → pop_map() → pop_bg()`.
|
||||
**Это ключевое ограничение при выборе, что делать банком.**
|
||||
|
||||
**(R3) Резидент → банк работает** (через трамплин в W1), резидент → W1/W2 —
|
||||
тоже.
|
||||
|
||||
**(R4) Графические примитивы libbgi звать можно откуда угодно.**
|
||||
`_bgi_begin` читает текущую страницу W3 из порта 0xE2, а `_bgi_end` её
|
||||
возвращает — то есть скобка корректна и из банка, и из резидента. Нельзя
|
||||
только одно: **звать `_bgi_begin`/`_bgi_end` ИЗ кода, который сам лежит в W3**
|
||||
(после подмены страницы исчезнет исполняемый код — проверено, белый экран).
|
||||
Поэтому `pop_bg` (резидент W3) обязан пользоваться готовыми примитивами
|
||||
(`gfx_blit*`, `bar`, …), а батчинг скобки на весь `draw_tile` (v1 §8 шаг 1)
|
||||
для него **невозможен** без переноса самого `draw_tile` в W1/W2.
|
||||
|
||||
**(R5) `--w3` кладёт в W3 код И rodata модуля** (`--codeseg/--constseg
|
||||
W3CODE`), а писучие статики оставляет в `_DATA` (W2). То есть `const`-таблицы
|
||||
переносятся в W3 бесплатно вместе с модулем (так уже лежит `tile_table` 371 Б).
|
||||
|
||||
**(R6) Данные в EMM-странице читаются, только пока страница в окне.**
|
||||
`gfx_w0_map(page)` / `gfx_w0_unmap()` — окно W0 (0x0000…0x3FFF), первые 0x100
|
||||
занимает ISR-стаб. Так уже работает `pop_level`. Цена — пара `OUT` на
|
||||
маппинг, поэтому годится для «пачками», а не для чтения по байту в горячем
|
||||
цикле.
|
||||
|
||||
---
|
||||
|
||||
## 3. ЧТО ДЕЛАТЬ НЕЛЬЗЯ (анти-паттерны, чтобы не потерять время)
|
||||
|
||||
- **Нельзя банковать `pop_bg`.** Он вызывается из pop_map, pop_trob, roomtest,
|
||||
pop_kid — то есть из главного цикла на каждом кадре; плюс он сам держит
|
||||
`tile_table` и всю отрисовку. Банк дал бы трамплин на каждый блит.
|
||||
- **Нельзя банковать `pop_map`, пока `pop_map` зовёт `pop_bg`** (R2). Сейчас
|
||||
зовёт: `pop_loose_tick` и компания (~30 вызовов графики).
|
||||
- **Нельзя тащить `kid_frames` в EMM «в лоб»**: он читается несколько раз за
|
||||
кадр из коллизии (`kid_cur_dx`/`kid_cur_flags` → `dx_weight`,
|
||||
`char_x_forward_edge`, …). Нужен кэш кадра (см. §5.1) — иначе маппинг
|
||||
страницы окажется в горячем пути.
|
||||
- **Нельзя «причёсывать» семейство `draw_tile` ради экономии, не имея
|
||||
пиксельного теста.** Мы неделю выравнивали слои по SDLPoP; любой рефактор
|
||||
этой зоны проверять диффом страниц (заморозка кадра клавишей `1` + сравнение
|
||||
VRAM обеих страниц, приём из memory `mame_mcp_bridge`).
|
||||
|
||||
---
|
||||
|
||||
## 4. ЦЕЛЕВАЯ РАСКЛАДКА
|
||||
|
||||
Принцип: **W3-резидент = «толстая графика, которую зовёт только главный цикл»;
|
||||
W1/W2 = ядро, которое должно быть достижимо ОТОВСЮДУ (включая банки); банки =
|
||||
новая холодная логика (стражи, боёвка, будущие уровни)**.
|
||||
|
||||
```
|
||||
W1/W2 (всегда отображено) W3 резидент (стр. 0xC000) Банки W3
|
||||
────────────────────────── ───────────────────────── ─────────
|
||||
libc + libbgi pop_bg (отрисовка тайлов) guards.c
|
||||
pop_kid (интерпретатор+рисование) pop_trob (анимации тайлов) fight.c
|
||||
pop_map (коллизия/физика/предметы) pop_loose.c (loose+потолок) debug/roomnav
|
||||
pop_geom (общая геометрия/тайлы) enter_room-часть roomtest?
|
||||
pop_ctrl (ввод/диспетчер)
|
||||
roomtest (главный цикл)
|
||||
```
|
||||
|
||||
Почему так:
|
||||
|
||||
- **`pop_map` остаётся в W1/W2** — его зовут и главный цикл, и (в будущем)
|
||||
банк стражей; в W3 его класть нельзя именно из-за R2. Для этого из него надо
|
||||
вынести графическую часть (loose/потолок) — она уезжает в W3 к `pop_bg`
|
||||
(§5.3). После выноса `pop_map` становится **чистой логикой без единого
|
||||
вызова графики** — тот самый bank-safe API.
|
||||
- **`pop_kid` остаётся в W1/W2**: `play_seq`/`kid_set_seq`/`Kid` нужны и
|
||||
стражам (у стражей ТА ЖЕ seqtbl), а `kid_draw` зовёт только libbgi (R4).
|
||||
- **`pop_trob` остаётся резидентом**: его зовёт только главный цикл, и он сам
|
||||
зовёт `pop_bg` — идеальный житель W3.
|
||||
- **Банк стражей не зовёт ничего из W3.** Рисование стражей — либо через
|
||||
libbgi напрямую (R4), либо (лучше) резидентный `guard_draw()` в W1/W2 рядом
|
||||
с `kid_draw`, а банк только считает состояние. Тот же приём мы уже
|
||||
используем для `pop_item_taken`/`pop_loose_fell`/`pop_ceil_fell`: банк
|
||||
выставляет флаг — резидент рисует.
|
||||
|
||||
---
|
||||
|
||||
## 5. ПЛАН РАБОТ
|
||||
|
||||
Порядок выбран так, чтобы каждый шаг был проверяем отдельно и давал место
|
||||
следующему.
|
||||
|
||||
### Шаг 1. `kid_data.h` (3 745 Б) → EMM-страница + порт `load_frame` — **самый большой выигрыш**
|
||||
|
||||
Сейчас `kid_seqtbl` (2310) + `kid_frames` (1205) + `kid_seq_off` (230) лежат в
|
||||
`_CODE` окна W1/W2 — это 15 % всего дефицитного пространства.
|
||||
|
||||
Как переносить:
|
||||
|
||||
1. `pop_extract_kid_data.py` дополнительно пишет `kid_data.bin` (те же три
|
||||
таблицы подряд, фиксированные смещения).
|
||||
2. Грузим её в отдельную EMM-страницу тем же способом, что уровень
|
||||
(`pop_level_load` — готовый образец), хэндл держим в `pop_kid`.
|
||||
3. **Порт `load_frame` (seg006) и глобала `cur_frame`** — в оригинале ровно
|
||||
так и сделано: раз за тик кадр копируется в структуру, а весь остальной код
|
||||
читает `cur_frame`, а не таблицу. У нас `kid_cur_dx()/kid_cur_flags()`
|
||||
станут чтением из `cur_frame` (5 байт в `_DATA`).
|
||||
4. `play_seq` оборачивается в один `gfx_w0_map(kid_data_page)` … `unmap` на
|
||||
вызов (в тике, не в отрисовке — конфликта с атласом в W0 нет).
|
||||
|
||||
Выигрыш: **−3 745 Б из W1/W2**, цена — один маппинг страницы за тик и 5 байт
|
||||
`_DATA`. Дополнительный бонус: `load_frame`/`cur_frame` — шаг К СХОДСТВУ с
|
||||
оригиналом, а не отход от него.
|
||||
|
||||
Риск: сломать `play_seq` (сердце анимации). Проверка: прогон по комнатам с
|
||||
эталонными позами (вис, подтягивание, прыжки, подъём меча).
|
||||
|
||||
### Шаг 2. Модуль `pop_geom.c` — дедуп + bank-safe фундамент
|
||||
|
||||
Сейчас продублировано между модулями:
|
||||
|
||||
| что | где | сколько |
|
||||
|---|---|---|
|
||||
| `y_to_row`/`y_to_row_mod4` | pop_bg + pop_map | 2 копии |
|
||||
| `char_dx_forward` | pop_kid + pop_map | 2 копии |
|
||||
| `x_bump[20]` | pop_kid (uint8) + pop_map (int16) | 20 + 40 Б, РАЗНЫЕ типы |
|
||||
| `y_land[5]` | pop_kid + pop_map | 10 + 10 Б |
|
||||
| `tile_is_floor` | pop_map (+ проверка кодов в roomtest) | 2 места |
|
||||
| 32-битный LCG `prandom` | pop_bg (`prandom`) + pop_trob (`trob_prandom`) | 2 копии по ~60 Б + 2 сида |
|
||||
|
||||
Собрать в один W1/W2-модуль `pop_geom.c`: таблицы `x_bump/y_land/dir_front/
|
||||
dir_behind`, `y_to_row`, `char_dx_forward`, `get_tile_div_mod(_m7)`,
|
||||
`tile_is_floor`, `prandom`. Выигрыш прямой — сотни байт (оценка 250–400 Б),
|
||||
но главное — **это и есть тот «чистый» API, который потом сможет звать банк**
|
||||
(R2): вся геометрия оказывается в W1/W2 по определению.
|
||||
|
||||
Осторожно: `prandom` у pop_bg и pop_trob — РАЗНЫЕ последовательности с разными
|
||||
сидами (стены vs фазы факелов). Объединять функцию можно, **сиды — нет**:
|
||||
передавать сид указателем/по индексу, иначе поедет раскладка кладки.
|
||||
|
||||
### Шаг 3. Вынести loose/потолок из `pop_map` в W3
|
||||
|
||||
`pop_map` — единственный модуль W1/W2, который зовёт графику, и делает это
|
||||
ровно в одном логическом блоке: `pop_loose_tick` + `check_press` + `do_knock` +
|
||||
`fell_on_your_head` + `check_loose_fall_on_kid` + плита-потолок (~1.4–2 КБ).
|
||||
|
||||
Вынести их в `pop_loose.c`, собираемый `--w3` рядом с `pop_bg`/`pop_trob`.
|
||||
Тогда:
|
||||
|
||||
- `pop_map` = чистая логика (bank-safe, R2 соблюдён);
|
||||
- W1/W2 худеет ещё на ~1.5–2 КБ;
|
||||
- W3 растёт на столько же — а место там появится после шага 4.
|
||||
|
||||
### Шаг 4. Перебалансировка резидента W3
|
||||
|
||||
После шага 3 в W3 будет тесно (14.4 + 2 ≈ 16.4 КБ > 16 КБ). Разгружаем:
|
||||
|
||||
1. **`wall_pattern` (944 Б) + `mob_render`/`mob_tick_one` (1381 Б)** — кандидаты
|
||||
на переезд в W1/W2: их зовёт только `pop_bg`/`pop_loose`, но сами они уже
|
||||
пользуются только libbgi (R4), значит из W1/W2 работают и остаются
|
||||
достижимыми из банка.
|
||||
2. `tile_table` и мелкие const-таблицы pop_bg (371 + ~300 Б) можно унести в
|
||||
EMM-страницу **уровня** (там ~13.8 КБ свободно) — но только если чтение
|
||||
происходит под уже замапленной страницей. Сейчас `draw_tile` читает
|
||||
`tile_table` ВНЕ W0-контекста → потребуется явный маппинг на тайл. **Не
|
||||
делать раньше замера**: 30 тайлов на входе в комнату × map/unmap — терпимо,
|
||||
а вот в покадровых редроях (пики/loose/кнопка) — уже горячий путь.
|
||||
3. Если и этого мало — `enter_room` (~2.1 КБ, зовётся только при смене комнаты)
|
||||
переносится в резидент W3 или в БАНК (он вызывается из главного цикла =
|
||||
резидента, значит банк допустим по R3).
|
||||
|
||||
### Шаг 5. Данные
|
||||
|
||||
1. **`room_modif[24][30]` = 720 Б** (pop_trob, `_DATA`). Нужен произвольный
|
||||
доступ каждый кадр (анимации, ворота) — в EMM не годится. Но 24 комнаты ×
|
||||
30 байт хранятся ЦЕЛИКОМ, хотя одновременно живут modif'ы только текущей
|
||||
комнаты и соседей по швам. Вариант: хранить полный массив в EMM-странице
|
||||
уровня, а в `_DATA` держать кэш на 2–3 комнаты (свою + левого/правого
|
||||
соседа) с записью обратно при смене комнаты. Выигрыш ~600 Б, цена —
|
||||
аккуратность на швах (кнопка в одной комнате открывает ворота в другой).
|
||||
**Делать последним** — это самая «тонкая» правка по семантике.
|
||||
2. **`dl1[256]`+`dl2[256]` = 512 Б** (pop_level, `_DATA` — копии LINKLOC/
|
||||
LINKMAP уровня) — читаются при нажатии кнопки
|
||||
и при отрисовке нажатой кнопки. Кандидат на чтение прямо из страницы
|
||||
уровня (она и так маппится) — но проверить, что `pop_doorlink2` не зовётся
|
||||
из отрисовки в тот момент, когда в W0 атлас. Выигрыш ~500 Б.
|
||||
3. **`_kbdraw_down[512]` 512 Б** (libc): проверено — это **байт на скан-код**
|
||||
(`libc/kbd/_kbdraw_state.c`), хотя комментарий называет его битовой картой.
|
||||
Упаковка в биты даёт −448 Б, но добавляет сдвиг/маску в ISR-трамплин и в
|
||||
`kbd_raw_down`. Трогать осторожно: raw-клавиатура уже дважды была
|
||||
источником залипаний (memory `kbd_raw_fifo_drain`,
|
||||
`kbd_overrun_wipe_modifiers`) — правку сопровождать прогоном docs/kbd-games.
|
||||
4. **`__irq_vec_buf` 513 Б** (libc IM2): таблица векторов обязана быть
|
||||
выровнена и полна — не трогать.
|
||||
|
||||
### Шаг 6. Контракт банка стражей (проектируется ДО написания кода)
|
||||
|
||||
Когда дойдём до стражей:
|
||||
|
||||
- `guards.c` собирается `--bank 1=guards.c`, режим `huge` (или `big` с
|
||||
`BANKED=W1`, если W3 окажется тесен для трамплинов).
|
||||
- **Банк зовёт только:** `pop_map` (чистая логика после шага 3), `pop_kid`
|
||||
(`play_seq`, `kid_set_seq`, `cur_frame`), `pop_geom`, libc/libbgi.
|
||||
- **Банк НЕ зовёт:** `pop_bg`, `pop_trob`, `pop_loose` (резидент W3) — ни
|
||||
прямо, ни через промежуточные функции. Нужна отрисовка — выставляет флаг,
|
||||
рисует резидент (идиома `pop_item_taken`).
|
||||
- Первым делом — **пробник** (`tests/` или `--bank` на пустышке): банк зовёт
|
||||
`pop_map`-функцию, та зовёт libbgi-примитив; убедиться в MAME, что скобка
|
||||
W3 корректно возвращает банковую страницу (R4) — это проверка модели, а не
|
||||
веры в неё.
|
||||
|
||||
### Шаг 7. Мелкий дедуп в `pop_bg` (после того, как появится тест страниц)
|
||||
|
||||
- Пять функций-редроев (`pop_spike_redraw`, `pop_loose_shake_draw`,
|
||||
`pop_floor_bake`, `pop_button_redraw`, `pop_leveldoor_redraw`) отличаются
|
||||
только прямоугольником heal, банком и набором тайлов — свести к одному
|
||||
параметризованному хелперу (оценка −150…250 Б).
|
||||
- `env_b/wall_b/fore_b/pot_b` — четыре одинаковых обёртки над `blit_b`
|
||||
(оставить: экономия единицы байт, читаемость дороже).
|
||||
- `overlay_mid_tile` / `fore_only_tile` / `climb_overlay_tile` / `draw_tile`
|
||||
— общая структура «взять code/lcode, посчитать x/dmy/dby, разобрать слои».
|
||||
Тут экономия потенциально сотни байт, но это **та самая зона риска из §3** —
|
||||
только с пиксельным диффом до/после и по одному слою за раз.
|
||||
|
||||
---
|
||||
|
||||
## 6. Ожидаемый итог
|
||||
|
||||
| Шаг | W1/W2 | W3 | Риск |
|
||||
|---|---|---|---|
|
||||
| 1. kid_data → EMM + load_frame | **−3 745** | — | средний (сердце анимации) |
|
||||
| 2. pop_geom (дедуп) | −250…400 | — | низкий |
|
||||
| 3. loose → W3 | −1 500…2 000 | +1 500…2 000 | низкий (перенос как есть) |
|
||||
| 4. разгрузка W3 (wall_pattern, mob) | +2 300 | −2 300 | низкий |
|
||||
| 5. данные (room_modif, LINKLOC, kbd) | −1 000…1 600 | — | средний/высокий |
|
||||
| 7. дедуп редроев pop_bg | — | −150…250 | средний |
|
||||
|
||||
Суммарно: **W1/W2 освобождается ~4.5–6 КБ**, W3 остаётся примерно в нынешнем
|
||||
объёме, но становится «правильно заполненным» — в нём только то, что банк
|
||||
никогда не позовёт. Плюс открывается путь к банкам: стражи и боёвка получают
|
||||
до 16 КБ на банк, не трогая резидент.
|
||||
|
||||
---
|
||||
|
||||
## 7. Как мерить и проверять (обязательно к каждому шагу)
|
||||
|
||||
1. **До/после по `.rel`** — точные размеры на модуль:
|
||||
`for f in .sprinter-cc-roomtest/*.rel; do grep '^A ' $f; done`
|
||||
(области `_CODE`/`_W3CODE`/`_DATA`). Итоги окон печатает сам `sprinter-cc`.
|
||||
2. **Функции** — из `.lst` (метки `_name:` и адреса), скрипт в истории этой
|
||||
сессии; полезно ловить «функция распухла после рефактора».
|
||||
3. **MAME**: любой перенос кода между окнами/страницами — это класс «молча
|
||||
ломается» (`sprinter_memory_modes`). Минимум: комната 1 (loose), 12
|
||||
(вис/подтягивание), 15 (меч), 9 (дверь уровня), 6 (кнопка/ворота).
|
||||
4. **Пиксельный дифф** для правок отрисовки: заморозить кадр (`1`), сравнить
|
||||
обе страницы дабл-буфера через `vram` (см. memory `mame_mcp_bridge`) и/или
|
||||
сверить с эталонным рендером `render_room.py`.
|
||||
5. **Скорость** — после шагов 1 и 4 замерить кадр маркерами в порт 0xFE
|
||||
(приём из v1 §8), чтобы маппинг страницы за тик не съел бюджет.
|
||||
|
||||
## 8. Ссылки
|
||||
|
||||
- `bin/sprinter-cc` — справка по `--w3`, `--bank`, `--memory`, `--memory-manual`.
|
||||
- `runtime/crt0_banked.s`, `runtime/bank.s` — трамплины и захват резидентной
|
||||
страницы W3.
|
||||
- `libbgi/common/_bgi_begin.c` / `_bgi_end.c` — механика скобки W3 (R4).
|
||||
- memory: `w3_resident_code`, `pop_banking_architecture`, `sdcc_banking`,
|
||||
`bank_local_data_pattern`, `sprinter_memory_modes`, `memory_modes_implemented`,
|
||||
`sprinter_emm_budget`, `mame_mcp_bridge`, `avoid_32bit_arith_z80`,
|
||||
`libc_one_function_per_module`.
|
||||
- `applications/PoP/docs/size_optimization_plan.md` — v1 (замер 2026-07-21,
|
||||
раздел §8 про скорость отрисовки актуален и не дублируется здесь).
|
||||
@@ -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,68 @@
|
||||
# PoP roomtest — модель `kid_room ≠ drawn_room` (баг #4)
|
||||
|
||||
Порт straddle-модели SDLPoP: персонаж может находиться в СОСЕДНЕЙ комнате,
|
||||
пока на экране ещё ТЕКУЩАЯ (drawn_room). Источник истины — SDLPoP.
|
||||
|
||||
## Факты из SDLPoP (подтверждено чтением исходника)
|
||||
|
||||
- `Char.room` (реальная комната персонажа) ≠ `drawn_room` (отрисованная) —
|
||||
штатное состояние.
|
||||
- **Коллизия через ±140:** `xpos_in_drawn_room()` (seg004:0405) сдвигает
|
||||
xpos на `±TILE_SIZEX*SCREEN_TILECOUNTX = ±140`, когда `curr_room` колонки
|
||||
(`curr_row_coll_room[col]`) ≠ `drawn_room` (room_L/room_BL → −140,
|
||||
room_R/room_BR → +140). Т.е. коллизия строится по РЕАЛЬНЫМ тайлам соседей.
|
||||
- **Смена экрана:** `check_the_end()` (seg000:0FBD): `if (next_room!=0 &&
|
||||
next_room!=drawn_room) { drawn_room=next_room; load_room_links; redraw }`.
|
||||
`next_room` ставится в `exit_room()` (= `Char.room` ПОСЛЕ успешного
|
||||
`leave_room`). Значит drawn_room следует за Char.room, но Char.room меняется
|
||||
ТОЛЬКО при реальном пересечении шва (leave_room, seg002:0504) на «легальном»
|
||||
кадре/действии (не turn/climb/standup).
|
||||
- **Отрисовка левого соседа:** только `load_leftroom()` (col9 левого соседа в
|
||||
левую кромку); правый сосед НЕ рисуется (изометрия). Окклюзия ворот на шве —
|
||||
только левая (seg008:696).
|
||||
- **Ceiling-полоса:** `draw_room` рисует доп. ряд из `room_A` (row2, draw_main_y
|
||||
=-1). (Уже реализовано, баг #3.)
|
||||
|
||||
## Текущее состояние нашего движка (до #4)
|
||||
|
||||
`cur_room` (=drawn_room) ВСЕГДА == комната Kid. Шов подделан: Kid остаётся в
|
||||
drawn_room с `curr_col=-1/10` + снапшоты соседей `g_lcol/g_rcol` (коллизия ±1
|
||||
кол) / `lcol_bg` (openness ворот). Уход из комнаты — `pop_leave_dir`/`enter_room`
|
||||
МГНОВЕННО при пересечении порога `char_x`. Отсюда #4: экран переключается
|
||||
раньше, чем в оригинале (Kid должен «отступить» за кромку, оставив старую
|
||||
комнату).
|
||||
|
||||
## План (инкременты, каждый проверяется в MAME)
|
||||
|
||||
### S1. Данные + рендер-смещение Kid
|
||||
- Ввести `kid_room` (реальная комната Kid) отдельно от `cur_room`(=drawn_room).
|
||||
- `kid_x_offset()` = разница комнат: kid_room == left(drawn) → лог. x Kid −140
|
||||
(рисуется за левой кромкой); right → +140; равны → 0. (порт
|
||||
xpos_in_drawn_room).
|
||||
- `kid_draw`/heal/fore используют смещение (Kid рисуется частично за кромкой).
|
||||
- Проверка: Kid у шва рисуется со сдвигом, экран не дёргается.
|
||||
|
||||
### S2. Коллизия по kid_room
|
||||
- Коллизионный контекст (`g_fg`/edges/`g_room`/modif в pop_map) следует за
|
||||
`kid_room`, а не за drawn_room. Когда kid_room≠drawn_room — грузим
|
||||
соседа как коллизионную комнату (curr_col 0..9 в кадре kid_room).
|
||||
- Отрисовка (room_fg и т.п.) остаётся по drawn_room.
|
||||
- Порт `curr_row_coll_room[]`/`xpos_in_drawn_room` можно упростить: держим
|
||||
ОДИН коллизионный room (kid_room) + существующие снапшоты кромок для ±1 кол.
|
||||
|
||||
### S3. Отложенная смена drawn_room
|
||||
- Уход (`check_leave`/`check_leave_below`): ставит `kid_room=сосед`,
|
||||
репроецирует Kid (x∓140, col∓10) — но drawn_room НЕ меняет сразу.
|
||||
- `check_the_end`-эквивалент в главном цикле: `if (kid_room != drawn_room &&
|
||||
<условие коммита>) enter_room(kid_room)`. Условие коммита — по SDLPoP:
|
||||
как только Char.room сменилась легальным leave (не bumped/turn). Для
|
||||
«bumped назад за кромку» drawn_room остаётся (симптом #4).
|
||||
- Проверка сценариев #4/#5 в MAME.
|
||||
|
||||
### S4. Полировка
|
||||
- Окклюзия/ceiling у шва при straddle, BUG-OCCL-1 (глубина), правый край.
|
||||
|
||||
## Связанные баги (bug_list.md)
|
||||
BUG-CEIL-1 (руки при прыжке вверх), BUG-CEIL-2 (loose в потолке),
|
||||
BUG-CEIL-3 (потолок над анимируемыми воротами), BUG-OCCL-1 (тень дальней
|
||||
колонны). Memory: `pop_seam_room_model`.
|
||||
@@ -0,0 +1,335 @@
|
||||
# roomtest — план оптимизации по размеру + переход на huge/banking
|
||||
|
||||
Статус: **план для отдельной сессии** (2026-07-21). Документ самодостаточный
|
||||
(рассчитан на старт с пустого контекста). Цель — освободить место: сейчас
|
||||
`applications/PoP/roomtest` в режиме `small` почти упёрся в потолок 32 КБ.
|
||||
|
||||
Правило проекта (`applications/PoP/CLAUDE.md`): механику/раскладку памяти
|
||||
сверять с исходником и с memory (`sprinter_memory_modes`, `sdcc_banking`,
|
||||
`bank_local_data_pattern`, `pop_banking_architecture`). Перед оптимизацией —
|
||||
`make size-check`-подобный замер до/после (здесь — руками по `.map`).
|
||||
|
||||
---
|
||||
|
||||
## 0. Как мерить
|
||||
|
||||
- Сборка: `cd applications/PoP/roomtest && make roomtest.exe` (режим `small`,
|
||||
`--gfx 256`). Карта символов — `.sprinter-cc-roomtest/roomtest.map`
|
||||
(адреса сдвигаются при каждой пересборке!).
|
||||
- Размеры областей — из `.map` (`_CODE`, `_DATA`, `_BSS`).
|
||||
- Вклад модулей в `_CODE` — атрибуция диапазонов между символами по модулю
|
||||
(скрипт-однострочник на python в истории; группировать символы `.map` по
|
||||
3-й колонке-модулю и суммировать `addr[i+1]-addr[i]`).
|
||||
- MAME-проверка после изменений раскладки ОБЯЗАТЕЛЬНА (режимы памяти —
|
||||
типовой источник «молча ломается», см. `sprinter_memory_modes`).
|
||||
|
||||
## 1. ТЕКУЩЕЕ СОСТОЯНИЕ (замер 2026-07-21)
|
||||
|
||||
Режим `small` = единое пространство **W1+W2 = 0x4000..0xBFFF (32 КБ)**; CODE с
|
||||
0x4100, DATA/BSS/heap цепляются ЗА CODE автоматически (`--data-loc 0` =
|
||||
linker chains), стек — вверху W2.
|
||||
|
||||
| Область | Размер | Диапазон |
|
||||
|---------|--------|----------|
|
||||
| `_CODE` | ~27 250 Б (0x6A6F) | 0x4100–0xAB6F |
|
||||
| `_HOME` | 227 Б | 0xAB6F |
|
||||
| `_DATA` | 3 449 Б (0x0D79) | 0xAC78–0xB9F1 |
|
||||
| `_BSS` | 290 Б | |
|
||||
|
||||
**Образ ≈ 31.2 КБ; до верха W2 (0xBFFF) остаётся ≈ 1.3 КБ на кучу+стек.**
|
||||
Куча в roomtest почти не используется (атласы/уровень — в EMM-страницах),
|
||||
но запас критично мал.
|
||||
|
||||
### Вклад модулей в _CODE (по .map, приблизительно)
|
||||
```
|
||||
7003 pop_bg (вся отрисовка тайлов/слоёв/wall_pattern)
|
||||
6326 pop_kid (из них ~3745 Б — СТАТ. ТАБЛИЦЫ kid_data.h, см. ниже)
|
||||
3762 pop_map (коллизия/физика/пики)
|
||||
1455 pop_trob (кнопки/ворота/пики-каркас)
|
||||
1224 pop_level (загрузка уровня, doorlink)
|
||||
914 roomtest (главный цикл)
|
||||
~7000 libc/libbgi (gfx_blit*, atlas_load, kbd_raw, open/read, irq, div/mul…)
|
||||
```
|
||||
|
||||
### Крупные СТАТИЧЕСКИЕ данные (сейчас в _CODE как `const`)
|
||||
- **`kid_data.h` — самый большой кусок, ~3.7 КБ**, живёт в _CODE (атрибутируется
|
||||
pop_kid):
|
||||
- `kid_seqtbl[2310]` — байткод последовательностей (play_seq).
|
||||
- `kid_frames[241]` × 5 Б = 1205 Б — таблица кадров (image,dx,dy,flags,sword).
|
||||
- `kid_seq_off[115]` × 2 Б = 230 Б — смещения seq.
|
||||
- `pop_bg`: `tile_table[31]`×12 = 372 Б + ~20 мелких const-таблиц (COL_XH,
|
||||
WALL_FRAM_*, SPIKES_FRAM_{RIGHT,LEFT,FORE}, LOOSE_FRAM_*, DOOR_FRAM_SLICE,
|
||||
BLUELINE_*, LPOS/RPOS, FLOOR_LEFT_OVERLAY) — суммарно ~0.5–0.7 КБ.
|
||||
- `pop_map`: `x_bump[20]`, `y_land[5]`, `wall_dl/dr`, `dir_front/behind` — ~100 Б.
|
||||
- В `_DATA` (W2, не CODE): `room_modif[24][30]`=720 Б + копии LINKLOC/LINKMAP=512 Б
|
||||
(pop_trob/pop_level) + рабочие массивы roomtest.
|
||||
|
||||
---
|
||||
|
||||
## 2. ПУТЬ A — оптимизация КОДА (без смены модели)
|
||||
|
||||
1. **Компиляторные флаги** (`bin/sprinter-cc`): попробовать `--opt-code-size`
|
||||
у SDCC и подобрать `--max-allocs` (сейчас дефолт 100000; меньше = мельче код,
|
||||
но медленнее компиляция; см. `mdview2_size_budget` — там `--max-allocs`
|
||||
давал −1.4 КБ). Замерить каждый модуль отдельно.
|
||||
2. **Дедуп подстановки нажатой кнопки**: логика `opener→floor / closer→stuck`
|
||||
по таймеру связи ПРОДУБЛИРОВАНА в `draw_tile` и `fore_tile` (pop_bg.c).
|
||||
Вынести в `static inline`/helper `subst_pressed_button(code,mod)`.
|
||||
3. **wall_pattern / prandom** (pop_bg): 32-битный LCG (`unsigned long`) —
|
||||
пользователь не любит 32-бит (см. `avoid_32bit_arith_z80`); но это PRNG
|
||||
оригинала (нужен для совпадения раскладки стен) — трогать осторожно, только
|
||||
если найдётся 16-битный эквивалент, дающий ТУ ЖЕ последовательность.
|
||||
4. **Ревизия дублей**: `y_to_row` определён в pop_bg И pop_map; мелкие
|
||||
геометрические хелперы дублируются — свести в один internal-модуль.
|
||||
5. `/simplify`-проход по последним правкам Фазы B (pop_trob/pop_bg).
|
||||
|
||||
Ожидаемый выигрыш пути A: единицы–первые сотни байт на пункт; в сумме,
|
||||
оптимистично, ~1–2 КБ. Недостаточно как единственная мера.
|
||||
|
||||
---
|
||||
|
||||
## 3. ПУТЬ B — вынос СТАТ. ДАННЫХ в EMM-страницы (с атласами / с level)
|
||||
|
||||
**Идея (по замечанию пользователя):** EMM-страницы атласов и уровня
|
||||
использованы лишь частично (страница 16 КБ, данных меньше), в «хвосте» —
|
||||
свободное место. Часть `const`-таблиц можно хранить ТАМ, а не в _CODE/_DATA,
|
||||
если таблица читается ИМЕННО ТОГДА, когда нужная страница уже в W0.
|
||||
|
||||
**Механика W0:** атласы блитятся из W0 (`_gfx_w0_state`: `_gfx_w0_cur` —
|
||||
спрайт-страница в W0; ISR-стаб `_gfx_w0_isr` возвращает её после прерывания).
|
||||
Уровень (pop_level) маппит свою страницу в W0 на время извлечения
|
||||
(`gfx_w0_map`/`gfx_w0_unmap`). → пока страница в W0, CPU может читать и
|
||||
данные из неё по адресам 0x0000..0x3FFF.
|
||||
|
||||
**Категоризация таблиц по W0-контексту (задача сессии — уточнить по каждой):**
|
||||
|
||||
- **(a) Читается, когда в W0 АТЛАС** → хранить в свободном хвосте атлас-страницы.
|
||||
Кандидаты — таблицы, которые нужны В МОМЕНТ блита конкретного атласа.
|
||||
ГРАБЛИ: `draw_tile` читает `tile_table`/`COL_XH` ДО блита (чтобы решить, какой
|
||||
спрайт/куда) — в этот момент в W0 может быть ДРУГАЯ страница (DSS/предыдущий
|
||||
атлас). Т.е. большинство draw-таблиц читаются ВНЕ W0-атлас-контекста →
|
||||
«в лоб» не переносятся. Нужен аудит КАЖДОГО чтения: гарантирована ли нужная
|
||||
страница в W0 в этот тик.
|
||||
- **(b) Читается, когда в W0 LEVEL** → хранить с уровнем (в его странице; там
|
||||
~13.7 КБ свободно из 16). Кандидаты: константы декода doorlink, разбор
|
||||
комнат — всё, что pop_level делает под `gfx_w0_map(lvl_page)`.
|
||||
- **(c) Нужна и там, и там** → дублировать в обеих страницах ЛИБО оставить
|
||||
резидентной (если дубли дороже экономии).
|
||||
- **(d) Читается в чистой ЛОГИКЕ (W0 не важен)** → перенос требует ЯВНОГО
|
||||
`gfx_w0_map` на каждое чтение (дорого, особенно в горячих циклах) → как
|
||||
правило оставить резидентной.
|
||||
|
||||
**Отдельно `kid_data.h` (3.7 КБ — самый жирный кандидат):**
|
||||
- `kid_frames`/`kid_seqtbl` читаются в `play_seq` (ЧИСТАЯ логика, каждый тик) И
|
||||
в `kid_draw` (блит из kid-атласа, kid-страница в W0). Т.е. частично (a),
|
||||
частично (d). Перенос всей таблицы в kid-атлас-страницу заставит `play_seq`
|
||||
делать `gfx_w0_map` на каждый шаг байткода → замерить стоимость (может убить
|
||||
бюджет спрайтов, см. `sprite_engine_perf`). Вариант: держать в EMM отдельной
|
||||
страницей данных Kid и маппить один раз на кадр вокруг kid_tick+kid_draw.
|
||||
- Это самый большой одиночный выигрыш (−3.7 КБ из _CODE), но и самый рискованный
|
||||
по скорости — приоритетный к ПРОТОТИПИРОВАНИЮ и замеру.
|
||||
|
||||
**Паттерн переноса writable/const данных в банк/страницу:** см. memory
|
||||
`bank_local_data_pattern` (--codeseg/--constseg/--dataseg BANKn + trampoline-fix
|
||||
+ mkexe -p 0) и `sdcc_static_storage_gotcha`.
|
||||
|
||||
### 3.1 Свободное место в страницах (замер 2026-07-21, страница = 16384 Б)
|
||||
```
|
||||
BG-атласы: размер свободно
|
||||
pop_env0.atl 10578 5806
|
||||
pop_env1.atl 12449 3935 <- САМАЯ ТЕСНАЯ из bg
|
||||
pop_env2.atl 10798 5586
|
||||
pop_env3.atl 5032 11352 <- много места
|
||||
pop_env4.atl 8498 7886
|
||||
pop_wall.atl 11543 4841
|
||||
pop_fore.atl 7763 8621
|
||||
Kid-атласы (28 стр): free min=6161 max=15452 avg=9722
|
||||
Level (res2001.bin): данные 2305, свободно ~13823 (16384 − 0x100 стаб − 2305)
|
||||
```
|
||||
|
||||
**Выводы по вместимости:**
|
||||
- **Макс. данных в ОДНОМ атлас-банке = свободный хвост ЭТОЙ страницы** (см.
|
||||
таблицу). Связывающее ограничение — самая тесная нужная страница (env1 =
|
||||
3935 Б; не перегружать её).
|
||||
- Если страница будет маппиться в **W0** — минус ~0x100 Б на ISR-стаб (как
|
||||
level). Атлас-страницы стаб УЖЕ содержат (atlas_load патчит) → данные класть
|
||||
в хвост ПОСЛЕ атласа.
|
||||
- **`kid_data.h` (3.7 КБ) влезает в kid-страницу** (min free 6161) или в
|
||||
отдельную выделенную страницу данных Kid — предпочтительно отдельную (маппить
|
||||
раз на кадр, не конфликтуя с kid-атласами блита).
|
||||
- **Level-таблицы** — вагон места в level-странице (~13.8 КБ).
|
||||
- **BG draw-таблицы** (~0.7 КБ) влезут в env3/fore/env4 (много free), НО см.
|
||||
граблю W0-контекста в §3(a) — читаются ли они, когда нужная страница в W0.
|
||||
- **Выделенная страница ТОЛЬКО под данные** (не делить с атласом) = до ~16 КБ
|
||||
(−0x100 стаб при W0-маппинге). EMM-бюджет это позволяет (см.
|
||||
`sprinter_emm_budget`: 215/3440 КБ free на старте).
|
||||
- **Принудительно уменьшать макс. атлас (репак мельче) — КРАЙНИЙ случай:** это
|
||||
резко поднимет число атлас-банков (сейчас 5 env-страниц адресуются как id>>5;
|
||||
дробление ломает эту адресацию и множит страницы). Сначала использовать
|
||||
СУЩЕСТВУЮЩИЙ свободный хвост и отдельные data-страницы.
|
||||
|
||||
---
|
||||
|
||||
## 4. ПУТЬ C — переход на huge (banked code)
|
||||
|
||||
### 4.1 Что такое huge сейчас (`bin/sprinter-cc`, `runtime/crt0_banked`)
|
||||
- `--memory huge`: `MODE_CODE_LOC=0x4100`, **`MODE_DATA_LOC=0x8000` (ФИКС.)**,
|
||||
banked code в W3. crt0_banked, как crt0_small, авто-детектит W2.
|
||||
Помечено `[TODO]` — не обкатано.
|
||||
- Отличие от small: small цепляет DATA сразу за CODE (`--data-loc 0`); huge
|
||||
ФИКСИРУЕТ DATA на 0x8000.
|
||||
|
||||
### 4.2 ТРЕБОВАНИЕ (по пользователю): huge должен переносить DATA динамически
|
||||
Сейчас huge жёстко кладёт DATA на 0x8000. Если РЕЗИДЕНТНЫЙ CODE вылезет за
|
||||
0x8000 (W1 = только 0x4000..0x7FFF ≈ 16 КБ; резидент > 16 КБ лезет в W2) →
|
||||
коллизия с DATA. **Надо научить huge класть DATA динамически ЗА резидентным
|
||||
CODE (как small: `--data-loc 0` + crt0 считает старт), а не на фикс 0x8000.**
|
||||
Тогда huge = «small-раскладка резидента (W1+W2, DATA за CODE) + ДОП. код в
|
||||
банках W3». Это первый пункт работ по huge.
|
||||
|
||||
### 4.3 КОНФЛИКТ: графика тоже хочет W3 (ключевой риск)
|
||||
`pop_banking_architecture` прямо говорит: **графику нельзя в W3** (блиты/атласы
|
||||
используют окна; см. §4.5). Поэтому в банки W3 можно выносить ТОЛЬКО
|
||||
НЕ-графические блоки, и такой банк НЕ должен во время своего исполнения держать
|
||||
графику в W3. Если W3-банкованная функция ЗОВЁТ графику (которой нужен W3),
|
||||
трамплин обязан сохранить/восстановить банк вокруг вызова (проверить, что
|
||||
banking-ABI это делает — `sdcc_banking`). Альтернатива без этого риска —
|
||||
**big + BANK_W1** (банк кода в W1, не W3), рекомендованная в
|
||||
`pop_banking_architecture` именно из-за W3-графики. Сессия должна выбрать:
|
||||
huge(W3) с аккуратным save/restore ИЛИ big(BANK_W1).
|
||||
|
||||
### 4.4 Какие блоки МОЖНО вынести (не работают с графикой напрямую)
|
||||
Замер graphics-ref по модулям (grep `gfx_|blit|env_b|wall_b|fore_b|setfillstyle|
|
||||
bar(|GFX_BANK|initgraph`):
|
||||
```
|
||||
pop_bg.c : 83 — РЕЗИДЕНТ (вся отрисовка)
|
||||
roomtest.c : 23 — РЕЗИДЕНТ (главный цикл + флип страниц)
|
||||
pop_level.c : 17 — использует gfx_w0_map (W0, не W3-блиты) — ПОГРАНИЧНЫЙ
|
||||
pop_kid.c : 12 — kid_draw = графика; НО play_seq — чистая логика (можно split)
|
||||
pop_ctrl.c : 0 — КАНДИДАТ В БАНК (ввод/диспетчер control)
|
||||
pop_map.c : 0 — КАНДИДАТ В БАНК (коллизия/физика, ~3.8 КБ) — лучший по объёму
|
||||
pop_trob.c : 0 — КАНДИДАТ В БАНК (кнопки/ворота/пики-логика)
|
||||
```
|
||||
- **Лучшие кандидаты в W3-банк(и): pop_map + pop_trob + pop_ctrl** (нет прямой
|
||||
графики; вместе ~5.3 КБ CODE). Освобождают резидент → он влезает в W1.
|
||||
- **Осторожно с межбанковыми вызовами:** pop_map/pop_trob ЗОВУТ pop_bg
|
||||
(перерисовка loose/пик/кнопок/шва) и pop_kid (play_seq/kid_set_seq). Это
|
||||
кросс-банк вызовы через трамплин (`sdcc_banking`: стек +3 байта, виртуальный
|
||||
24-битный адрес). Правило `pop_banking_architecture`: «один файл = один банк
|
||||
= прямые вызовы», main резидентен. Проверить, что трамплин сохраняет W3
|
||||
вокруг вызова в графический pop_bg (см. §4.3).
|
||||
- **pop_kid split** (по желанию): вынести play_seq/seqtbl-интерпретатор
|
||||
(логика + таблицы kid_data.h) в банк, оставить kid_draw/kid_heal резидентными.
|
||||
Даёт и −код, и −данные из резидента, но требует аккуратного разделения TU
|
||||
(1 функция = 1 модуль, см. `libc_one_function_per_module`).
|
||||
- **pop_level: пограничный** — не блитит, но маппит уровень в W0; банковать
|
||||
можно, если W0-логика совместима с трамплином (проверить ISR-стаб взаимодействие).
|
||||
|
||||
### 4.5 Почему графику нельзя в W3 (контекст)
|
||||
Блиттер держит спрайт-страницу атласа в **W0** (`_gfx_w0_state`,
|
||||
`_gfx_w0_isr`). Ускоритель/адресация видео — отдельная тема (см.
|
||||
`sprinter_accelerator`, `sprinter_graphics`). W3 в banked-раскладке — окно
|
||||
кода-банка; смешивать с окном, которое графика перемапливает, нельзя без
|
||||
save/restore. Детально — `pop_banking_architecture`, `graphics_constraints`.
|
||||
|
||||
---
|
||||
|
||||
## 5. РЕКОМЕНДУЕМЫЙ ПОРЯДОК РАБОТ (для след. сессии)
|
||||
|
||||
1. **Замер-базлайн** (CODE/DATA/BSS + per-module) — зафиксировать до.
|
||||
2. **Путь A** дешёвые пункты (флаги, дедуп кнопки, дедуп y_to_row) — быстрый −1..2 КБ.
|
||||
3. **huge §4.2**: научить huge класть DATA динамически (как small) — инфраструктурный
|
||||
пререквизит, без него банкинг не даст гибкости. Обкатать в MAME на текущем
|
||||
резиденте (пока без выноса — просто huge-раскладка = small + пустой W3).
|
||||
4. **huge §4.4**: вынести pop_map (+pop_trob, +pop_ctrl) в W3-банк(и); проверить
|
||||
кросс-банк вызовы в pop_bg (§4.3) в MAME. ЛИБО выбрать big+BANK_W1.
|
||||
5. **Путь B** (по остатку нужды): прототип выноса `kid_data.h` в EMM-страницу
|
||||
Kid с маппингом раз на кадр; замерить скорость (`sprite_engine_perf`).
|
||||
Затем аудит draw-таблиц по W0-контексту (§3 a/b/c/d).
|
||||
|
||||
## 6. Ссылки
|
||||
- `bin/sprinter-cc` (§162+ — резолв memory-mode → CODE_LOC/DATA_LOC).
|
||||
- `runtime/crt0_small.*`, `runtime/crt0_banked.*`, `runtime/bank.s`.
|
||||
- memory: `sprinter_memory_modes`, `memory_modes_implemented`,
|
||||
`setwin2_for_w2_alloc`, `sdcc_banking`, `bank_local_data_pattern`,
|
||||
`pop_banking_architecture`, `avoid_32bit_arith_z80`,
|
||||
`libc_one_function_per_module`, `sprite_engine_perf`, `mdview2_size_budget`.
|
||||
- `applications/PoP/roomtest/bug_list.md` — открытые баги Фазы B (не блокируют
|
||||
оптимизацию, но держать в уме при рефакторе pop_map/pop_bg).
|
||||
|
||||
---
|
||||
|
||||
## 7. Лишние блиты в горячем пути (добавлено 2026-07-27)
|
||||
|
||||
Найдено при разборе окклюзии по эталону SDLPoP: **наш «передний слой» рисовал
|
||||
спрайты, которых в оригинале там нет** — это и артефакты, и лишняя работа
|
||||
каждый кадр. Исправлено: `fore_tile` (вызывается для КАЖДОГО тайла футпринта
|
||||
Kid, обычно 2–4 за кадр) рисовал ещё и `bottom_id` — переднюю кромку пола; в
|
||||
оригинале `draw_tile_fore` (seg008:690) добавляет только `add_foretable`-часть,
|
||||
а `bottom` идёт через `draw_tile_bottom` в backtable (ПОД персонажем).
|
||||
Итог: −2..4 блита за кадр, `_CODE` −388 Б, ушла «тень» у основания колонны.
|
||||
|
||||
**Что проверить тем же методом (по одному вопросу к каждому месту: а есть ли
|
||||
этот спрайт в оригинале в ЭТОЙ таблице?):**
|
||||
|
||||
1. `pop_room_draw`/`draw_tile` — вызовы на входе в комнату не критичны по
|
||||
скорости, но по ним стоит сверить состав слоёв (backtable vs foretable).
|
||||
2. `overlay_mid_tile` — сейчас точный порт midtable-части `draw_tile2`;
|
||||
проверить, не рисуем ли `base_id` там, где оригинал его не рисует
|
||||
(loose: base=0, потому что кадр плиты идёт через `draw_loose` в backtable).
|
||||
3. `pop_loose_mob_tick` — перерисовка соседнего тайла (`draw_tile(mob_row,
|
||||
mob_col+1)`) КАЖДЫЙ кадр падения: в оригинале это `set_redraw_full` на
|
||||
один кадр; можно ограничить только тайлом, который реально пересекается
|
||||
с куском.
|
||||
4. `pop_ceil_shake_draw` — heal 64×8 + два `draw_tile(-1,·)` на кадр тряски;
|
||||
проверить, нужен ли второй тайл (правую грань loose в полосе потолка
|
||||
оригинал не рисует вовсе — `draw_tile_aboveroom` без `draw_tile_anim_right`).
|
||||
5. `fore_only_tile` для полосы потолка: вызывается для всех колонок габарита,
|
||||
а оригинал (`redraw_needed_above`) — только для колонок с флагом
|
||||
`redraw_frames_above`; сузить до колонок, реально задетых спрайтом.
|
||||
6. `wall_pattern` внутри fore/overlay — тяжёлая (PRNG + до 4 блитов); проверить,
|
||||
не зовём ли её там, где оригинал ограничивается `wall_fram_main`.
|
||||
|
||||
---
|
||||
|
||||
## 8. Скорость отрисовки: замеры и запас (2026-07-27)
|
||||
|
||||
Профилирование в MAME (маркеры в порт 0xFE + `wpiset … totalcycles`, приём из
|
||||
memory `mame_mcp_bridge`). Кадр Sprinter = **430 080 тактов**.
|
||||
|
||||
**Стоимость блита почти НЕ зависит от размера** — платим за проход по цепочке
|
||||
`gfx_blit → gfx_blit_part → _gfx_blit_full` (16-битная арифметика, клип,
|
||||
пересчёт src, нарезка полос >256), а не за пиксели:
|
||||
|
||||
| путь (спрайт 32×3) | тактов |
|
||||
|---|---|
|
||||
| `gfx_blit` (общее ядро, с клипом) | 13 288 |
|
||||
| линейное спрайтовое ядро без клипа (`putsprite` при `gfx_sprite_clip(0)`) | 4 617 |
|
||||
|
||||
Отсюда `draw_tile(0,0)` тайла шва (9 блитов) стоил **183 690 тактов = 43 %
|
||||
кадра**; сам `bar` — только 13 308.
|
||||
|
||||
**СДЕЛАНО (шаг 1):** в libbgi добавлен `gfx_blit_noclip()`
|
||||
(`common/gfx_blit_noclip.c`, прототип в `include/gfx.h`) — блит без клипа в
|
||||
ТЕКУЩЕМ банке через линейное ядро; `pop_bg.blit_b` уходит на него, когда
|
||||
спрайт целиком на экране и не нужен `g_clip_top`. Выигрыш ~2.9× на каждом
|
||||
фоновом блите (подтверждено в MAME).
|
||||
**ВАЖНО:** W3-скобку (`_bgi_begin/_bgi_end`) ставит САМА libbgi — вызывать её
|
||||
из модуля, собранного с `--w3`, нельзя: после `_bgi_begin` окно W3 занято
|
||||
видеобанком и код вызывающего исчезает из адресного пространства (проверено:
|
||||
белый экран).
|
||||
|
||||
**ЗАПАС (шаг 2), когда перестанет хватать бюджета кадра:**
|
||||
|
||||
1. **Батчинг W3-скобки** — одна `_bgi_begin/_bgi_end` на весь `draw_tile`
|
||||
вместо скобки на блит; нужен публичный batch-API в libbgi (как у
|
||||
спрайтового движка). Осторожно: между begin/end стоит `DI` — длинная
|
||||
серия задержит кадровое прерывание.
|
||||
2. **Решётка ворот одним спрайтом** — `draw_gate_back` рисует бары по одному
|
||||
(`env 52`, до 7 блитов). Сгенерировать в атласе «столб решётки» (повтор
|
||||
бара на высоту тайла) и выводить одним `gfx_blit_part` с обрезкой по фазе
|
||||
`gate_bot_y & 7`: 7 блитов → 1.
|
||||
3. **Не перерисовывать статичные части шва** — грань ворот (env 47, 26×62),
|
||||
пол (41) и кромка (43) при анимации решётки не меняются; если стирать
|
||||
только полосу баров, уйдут ещё 3 блита из 9.
|
||||
4. См. также §7 (лишние блиты, которых нет в оригинале).
|
||||
@@ -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,34 @@
|
||||
/* 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_POT_ATL "pop_pot.atl" /* chtab_1: зелья */
|
||||
#define POP_PAL_POT 0x40
|
||||
#define POP_BG_PAL "pop_bg.pal"
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,41 @@
|
||||
/* 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"
|
||||
#define KID_SWORD_ATL "sword.atl" /* chtab_0: меч в руке */
|
||||
#define KID_SWORD_ID0 20 /* индекс в атласе = sword_tbl.id - ID0 */
|
||||
#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 |