Compare commits
107 Commits
52636a5d6e
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 2f3e854854 | |||
| 75a51fb1db | |||
| a32b66700f | |||
| 59e51f7e83 | |||
| b22cee3456 | |||
| 2e90eaf7d7 | |||
| 3983fa4513 | |||
| d014a3f577 | |||
| 4f7d9c0596 | |||
| 37fc572cc3 | |||
| 99b430f2ed | |||
| d438a1d3da | |||
| 79ea473910 | |||
| cf06896dbd | |||
| 8bbc6b4d07 | |||
| 565ba98852 | |||
| ac9871c58c | |||
| 1e6c377edc | |||
| 0280b05933 | |||
| f2093e0d89 | |||
| af5f0a4638 | |||
| 3dbad6120c | |||
| 1dc89b0f26 | |||
| e1ee447b7f | |||
| 18f5115e0d | |||
| b3bf2cca3d | |||
| 3b2dd8bfcc | |||
| ecf5ecfc14 | |||
| 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 |
@@ -0,0 +1,13 @@
|
|||||||
|
CompileFlags:
|
||||||
|
Add:
|
||||||
|
- -Ilibbgi/include
|
||||||
|
- -Ilibc/include
|
||||||
|
- -I.
|
||||||
|
- --target=z80-unknown-unknown
|
||||||
|
- -mz80
|
||||||
|
- -std=c99
|
||||||
|
- -D__SDCC # если нужно гасить SDCC-специфику через #ifdef
|
||||||
|
Remove:
|
||||||
|
- --target=z80-unknown-unknown
|
||||||
|
Diagnostics:
|
||||||
|
UnusedIncludes: None # опционально, чтобы не ругался на "лишние" инклюды под другой таргет
|
||||||
@@ -40,6 +40,30 @@ tests/*/*.cdb
|
|||||||
tests/*/*.mem
|
tests/*/*.mem
|
||||||
tests/*/*.rst
|
tests/*/*.rst
|
||||||
|
|
||||||
|
# applications/<app>/<prog>/ — реальные приложения (та же схема, что
|
||||||
|
# examples/ и tests/, но на уровень глубже: applications/PoP/roomtest/...)
|
||||||
|
applications/*/*/*.exe
|
||||||
|
applications/*/*/*.asm
|
||||||
|
applications/*/*/*.lst
|
||||||
|
applications/*/*/*.lk
|
||||||
|
applications/*/*/*.ihx
|
||||||
|
applications/*/*/*.noi
|
||||||
|
applications/*/*/*.sym
|
||||||
|
applications/*/*/*.map
|
||||||
|
applications/*/*/*.rel
|
||||||
|
applications/*/*/*.cdb
|
||||||
|
applications/*/*/*.mem
|
||||||
|
applications/*/*/*.rst
|
||||||
|
|
||||||
|
# PoP-порт: внешние референс-репозитории (собственные git-клоны — НЕ
|
||||||
|
# часть этого репозитория) + оригинальные game-данные (копирайт, только
|
||||||
|
# для реверса форматов на этой машине).
|
||||||
|
applications/PoP/SDLPoP/
|
||||||
|
applications/PoP/mininim/
|
||||||
|
applications/PoP/PR/
|
||||||
|
applications/PoP/Prince-of-Persia-Apple-II/
|
||||||
|
applications/PoP/MSDOS/
|
||||||
|
|
||||||
# libc + libbgi archives (built by libc/Makefile + libbgi/Makefile)
|
# libc + libbgi archives (built by libc/Makefile + libbgi/Makefile)
|
||||||
lib/*.lib
|
lib/*.lib
|
||||||
|
|
||||||
@@ -89,3 +113,10 @@ mame/
|
|||||||
|
|
||||||
# Claude Code local settings (per-machine, not for the repo)
|
# Claude Code local settings (per-machine, not for the repo)
|
||||||
.claude/
|
.claude/
|
||||||
|
|
||||||
|
# Тяжёлые справочные материалы, НЕ версионируются: docs/extra — архивы
|
||||||
|
# исходников (~570 МБ, четыре почти одинаковых bad_apple), docs/sources —
|
||||||
|
# клоны чужих репозиториев (Sprinter-BIOS, Estex-DSS, SaymanNsk) со своими
|
||||||
|
# .git внутри (в коммите стали бы битыми gitlink-ссылками).
|
||||||
|
docs/extra/
|
||||||
|
docs/sources/
|
||||||
|
|||||||
@@ -7,20 +7,28 @@ Target-слой SDCC 4.5 (z80) для компьютера Sprinter Sp2000: crt0
|
|||||||
|
|
||||||
```
|
```
|
||||||
make # tools + lib + libbgi + все тесты (45) + examples
|
make # tools + lib + libbgi + все тесты (45) + examples
|
||||||
make -C libc # только libc → lib/sprinter.lib
|
make -C libc # только libc → lib/sprinter.lib (fast) + sprinter_safe.lib
|
||||||
make -C libbgi # только BGI → lib/bgi256.lib (и bgi16.lib в Фазе 2)
|
make -C libbgi # только BGI → lib/bgi256.lib (fast) + bgi256_safe.lib
|
||||||
make floppy # упаковать все .exe в mame/v306/IMG/mc.img
|
make floppy # упаковать все .exe в mame/v306/IMG/mc.img
|
||||||
make size-check # размерный регресс: _CODE vs docs/size_baseline.tsv
|
make size-check # размерный регресс: _CODE vs docs/size_baseline.tsv
|
||||||
make size-baseline # принять текущие размеры эталоном
|
make size-baseline # принять текущие размеры эталоном
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Обе библиотеки собираются в двух вариантах: fast (дефолт; `-D*_NOCHECK` —
|
||||||
|
параметр-валидации вырезаны) и safe (линкуется по `sprinter-cc --safe`).
|
||||||
|
Критичные гарды (напр. _fd_guard — 9-й OPEN вешает DSS) — в ОБОИХ.
|
||||||
|
|
||||||
Графика (BGI) — отдельная библиотека libbgi/ (см. ниже). Программа,
|
Графика (BGI) — отдельная библиотека libbgi/ (см. ниже). Программа,
|
||||||
использующая graphics.h, собирается с `--gfx 256` (или `--gfx 16` в
|
использующая graphics.h, собирается с `--gfx 256` (или `--gfx 16` в
|
||||||
Фазе 2): sprinter-cc подлинкует lib/bgi256.lib и добавит -I libbgi/include.
|
Фазе 2): sprinter-cc подлинкует lib/bgi256.lib и добавит -I libbgi/include.
|
||||||
|
|
||||||
Одиночный тест: `cd tests/<имя> && make run` (пакует ТОЛЬКО этот exe
|
Одиночный тест: `cd tests/<имя> && make run` (пакует exe + EXTRA_DATA на
|
||||||
+ EXTRA_DATA на дискету и запускает MAME). Тесты в MAME гоняет
|
дискету и запускает MAME автоматически через `toolchain/mame_interactive.py`,
|
||||||
пользователь — готовь дискету и проси прогнать.
|
снимает скриншоты, выводит пути). Для сложных сценариев (диалог, несколько
|
||||||
|
шагов ввода) — прямой вызов:
|
||||||
|
`python3 toolchain/mame_interactive.py tests/<имя>/<имя>.exe --snap T1,T2 --timeout T`.
|
||||||
|
Скриншоты лежат в `mame/v306/snap_auto/sprinter/` (читаются инструментом Read).
|
||||||
|
Подробности: `docs/mame-autotest.md`.
|
||||||
|
|
||||||
После правок libc/libbgi: пересборка от чистого листа (`make -C libc clean`
|
После правок libc/libbgi: пересборка от чистого листа (`make -C libc clean`
|
||||||
/ `make -C libbgi clean`) не обязательна — stale .rel чистятся
|
/ `make -C libbgi clean`) не обязательна — stale .rel чистятся
|
||||||
|
|||||||
@@ -16,9 +16,9 @@
|
|||||||
TESTS := hello hello2 simple banked bankedbg banktest strtest cat seek \
|
TESTS := hello hello2 simple banked bankedbg banktest strtest cat seek \
|
||||||
malloc mem_test argv errno rt_test openenv ls conio conio2 \
|
malloc mem_test argv errno rt_test openenv ls conio conio2 \
|
||||||
attrprob timedir mouse banklocl stdlib assrtest ptime stattest \
|
attrprob timedir mouse banklocl stdlib assrtest ptime stattest \
|
||||||
filetest fdmax fbench solidt irqtest cbltest cblwav dec_test gets stest2 winrest \
|
filetest fdmax fbench solidt irqtest cbltest cblwav cblstream dec_test gets stest2 winrest \
|
||||||
bios_text text_palette \
|
bios_text text_palette \
|
||||||
gfx_demo gfx_dbuf
|
gfx_demo gfx_dbuf bgitest bgi_img accfill
|
||||||
# gfx_d16 / gfx_text / gfx_mous — 16-цветные; убраны до Фазы 2 (bgi16.lib
|
# gfx_d16 / gfx_text / gfx_mous — 16-цветные; убраны до Фазы 2 (bgi16.lib
|
||||||
# ещё не собирается). Вернуть мигрированными на BGI --gfx 16.
|
# ещё не собирается). Вернуть мигрированными на BGI --gfx 16.
|
||||||
# Larger end-user applications under examples/.
|
# Larger end-user applications under examples/.
|
||||||
|
|||||||
@@ -36,7 +36,9 @@ LIB := $(PROJ_ROOT)/lib/sprinter.lib
|
|||||||
|
|
||||||
MAME_DIR := $(PROJ_ROOT)/mame/v306
|
MAME_DIR := $(PROJ_ROOT)/mame/v306
|
||||||
FLOPPY_IMG := $(MAME_DIR)/IMG/mc.img
|
FLOPPY_IMG := $(MAME_DIR)/IMG/mc.img
|
||||||
|
HDD_IMG := $(MAME_DIR)/IMG/test_hdd.chd
|
||||||
MAKE_DISK := $(MAME_DIR)/make_disk.py
|
MAKE_DISK := $(MAME_DIR)/make_disk.py
|
||||||
|
MAKE_HDD := $(PROJ_ROOT)/toolchain/make_hdd.sh
|
||||||
RUN_MAME := $(MAME_DIR)/run_mame.sh
|
RUN_MAME := $(MAME_DIR)/run_mame.sh
|
||||||
|
|
||||||
# Optional knobs — see top of file.
|
# Optional knobs — see top of file.
|
||||||
@@ -85,4 +87,14 @@ floppy: $(EXAMPLE).exe
|
|||||||
run: floppy
|
run: floppy
|
||||||
cd $(MAME_DIR) && ./run_mame.sh
|
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,43 @@
|
|||||||
|
# Идеи и вопросы «на подумать» (PoP)
|
||||||
|
|
||||||
|
Не план работ, а список того, что осознанно отложено: каждая запись —
|
||||||
|
гипотеза с причиной, по которой её стоит проверить, и с тем, что мешает
|
||||||
|
сделать это прямо сейчас.
|
||||||
|
|
||||||
|
## Заменить генератор псевдослучайных чисел
|
||||||
|
|
||||||
|
Сейчас стоит LCG оригинала, шаг на ассемблере (~1 020 тактов), бит-в-бит
|
||||||
|
совместимый с SDLPoP. Есть более дешёвые Z80-генераторы (86–148 тактов),
|
||||||
|
но потолок выигрыша — 2 814 тактов за кадр, 0.65 %, и он растворяется в
|
||||||
|
обёртках вызова. Тексты процедур, разбор качества и порядок действий —
|
||||||
|
`prng_alternatives.md`. Первый шаг там не про генератор: слить приведение
|
||||||
|
к диапазону в ту же asm-процедуру, чтобы на вызов был один `call`, а не три.
|
||||||
|
|
||||||
|
## Отключать мышь на время игры
|
||||||
|
|
||||||
|
**Гипотеза.** Мышь на Sprinter — источник прерываний (обёртки RST 30h,
|
||||||
|
см. memory `mouse_api`). Игре она не нужна вообще: управление —
|
||||||
|
raw-клавиатура (`<kbd_raw.h>`), которую мы и так забираем у DSS целиком.
|
||||||
|
Значит каждое мышиное прерывание за кадр — украденные такты в бюджете,
|
||||||
|
который у нас и без того занят на 86 %.
|
||||||
|
|
||||||
|
**Откуда взялось (2026-07-30).** При замере бюджета по 100 кадрам три
|
||||||
|
кадра выбились до 552–647 К тактов (1.28–1.51 кадра) при типичных 371 К.
|
||||||
|
Причиной оказалось движение мыши на ХОСТЕ: при неподвижной мыши 225
|
||||||
|
кадров подряд прошли без единого превышения. То есть эффект реальный и
|
||||||
|
измеримый, просто в тесте он был наведён извне.
|
||||||
|
|
||||||
|
**Что проверить.**
|
||||||
|
1. Есть ли у драйвера мыши (RST 30h) функция «выключить/включить» —
|
||||||
|
разобрать список из 14 обёрток; если нет явной, посмотреть, что делает
|
||||||
|
«hide cursor» и снимает ли она обработчик.
|
||||||
|
2. Сколько тактов реально стоит одно мышиное прерывание на нашем железе
|
||||||
|
(замер: breakpoint на входе ISR + totalcycles, при движении мыши).
|
||||||
|
3. Не ломает ли отключение выход в DSS: состояние обязано
|
||||||
|
восстанавливаться при `exit`, включая аварийный (atexit).
|
||||||
|
|
||||||
|
**Почему не сейчас.** Выигрыш проявляется только когда игрок реально
|
||||||
|
двигает мышью, то есть в норме его нет; а риск оставить систему без мыши
|
||||||
|
после выхода — заметный. Делать после того, как закроем стражей и
|
||||||
|
займёмся бюджетом всерьёз (там же, где батчинг кроссбанковых вызовов и
|
||||||
|
возможный возврат `pop_bg` в резидент `--w3`).
|
||||||
@@ -0,0 +1,446 @@
|
|||||||
|
# roomtest — план v2: размер кода и раскладка по окнам/банкам/страницам
|
||||||
|
|
||||||
|
Статус: **план для отдельной сессии**, составлен 2026-07-29 по свежему замеру.
|
||||||
|
Заменяет `size_optimization_plan.md` (v1, 2026-07-21): часть его пунктов уже
|
||||||
|
сделана, часть опиралась на неверную модель банкинга. Документ самодостаточный
|
||||||
|
— рассчитан на старт с пустого контекста.
|
||||||
|
|
||||||
|
Повод: перед стражами и боёвкой (новый код ~5–8 КБ) надо понять, куда он
|
||||||
|
поместится, и заранее развести код так, чтобы банкованные модули не упёрлись в
|
||||||
|
ограничения окна W3.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## СТАТУС ВЫПОЛНЕНИЯ (обновлено 2026-07-29, коммит ecf5ecf)
|
||||||
|
|
||||||
|
| Шаг | Статус | Факт |
|
||||||
|
|-----|--------|------|
|
||||||
|
| 1. `kid_data.h` → EMM-страница + `load_frame`/`cur_frame` | **сделан** | `_CODE` −3 247 Б; попутно найден и обойдён баг кодогенерации SDCC (см. ниже) |
|
||||||
|
| 2. `pop_geom.c` (дедуп геометрии + PRNG) | **сделан** | −41 Б `_CODE`, −11 Б W3; ценность — не байты, а bank-safe слой |
|
||||||
|
| 3. `pop_map` без вызовов графики | **сделан** (фазы 1a/1b) | через пометки перерисовки, см. ниже |
|
||||||
|
| 4. Разгрузка/перебалансировка W3 | **сделан** | резидент = `pop_bg` + `pop_gdraw` (отрисовка стража, 2026-07-29); W3 14 376 → 12 819 (свободно 3 565 Б) |
|
||||||
|
| 5. Данные (`room_modif`, `dl1/dl2`, `_kbdraw_down`) | не начат | потенциал ~1.5 КБ |
|
||||||
|
| 6. Контракт банка стражей + пробник | **пробник сделан** | `tests/w3bankgfx` — модель подтверждена в MAME, см. ниже |
|
||||||
|
| 7. Дедуп семейства `draw_tile` | отложен по решению пользователя | «мороки много, выгода не так велика» |
|
||||||
|
|
||||||
|
**Замер сейчас против замера §1:** `_CODE` 24 881 → 24 421, куча W2 2 076 → 2 592 Б,
|
||||||
|
W3-резидент 14 376 → 11 632 (свободно 2 008 → 4 752 Б). Сумма кода упала
|
||||||
|
на ~3.2 КБ (данные Kid уехали в EMM), остальное — перераспределение.
|
||||||
|
|
||||||
|
**Замер 2026-07-29 (после стража).** Появление стража съело кучу до 805 Б;
|
||||||
|
разгрузка — вынос ОТРИСОВКИ стража в резидент (`pop_gdraw.c`, `--w3`), логика
|
||||||
|
и состояние остались в W1/W2, чтобы банк `guards.c` их видел (R2). Итог:
|
||||||
|
`_CODE` 26 149 → 25 703, куча **805 → 1 245 Б**, W3-резидент 11 656 → 12 819
|
||||||
|
(свободно 4 728 → 3 565 Б), банк 1 — 236 / 16 384 Б. Граница «что резидент»
|
||||||
|
теперь формулируется одним правилом: **резидент = только то, что рисует и
|
||||||
|
зовётся исключительно из главного цикла**; всё, что может понадобиться банку,
|
||||||
|
остаётся в W1/W2.
|
||||||
|
|
||||||
|
### Что сделано вместо §5.3 (вынос loose в W3)
|
||||||
|
|
||||||
|
Вместо переноса кода между окнами выбран (по обсуждению с пользователем)
|
||||||
|
**порт архитектуры оригинала**: логика ставит пометку, отрисовка идёт
|
||||||
|
отдельным проходом — `set_redraw_*` (seg007) + `redraw_needed` (seg008:0178).
|
||||||
|
Появился `pop_redraw.c/.h`; `pop_trob` и `pop_map` больше не рисуют тайлы.
|
||||||
|
Наши самодельные счётчики (`spike_rest`, `button_rest`, `ldoor_rest`,
|
||||||
|
`loose_bake`, `loose_rest`, `ceil_rest`, `ceil_bake`, `land_bake`) удалены —
|
||||||
|
их роль (вторая страница дабл-буфера) взял счётчик страниц в пометке.
|
||||||
|
|
||||||
|
**Два исключения остались** (обе — функции ТОЛЬКО главного цикла, звать из
|
||||||
|
банка нельзя):
|
||||||
|
- `pop_process_trobs` — пламя факела и пузырёк зелья (покадровый оверлей);
|
||||||
|
- `pop_loose_tick` — падающий кусок (mob): spawn/tick/pos. В оригинале это
|
||||||
|
отдельная подсистема (`mobs` + `draw_moving`), разделение на логику и
|
||||||
|
отрисовку — задел следующей фазы.
|
||||||
|
|
||||||
|
### Пробник банка (2026-07-29): модель ПОДТВЕРЖДЕНА
|
||||||
|
|
||||||
|
`tests/w3bankgfx` (huge + `--w3 res.c` + `--bank 1=bank1.c`, графика 256):
|
||||||
|
|
||||||
|
- банк рисует примитивом libbgi НАПРЯМУЮ — работает; страница W3 внутри
|
||||||
|
банка до блита, после блита и после возврата из вызванной им W1/W2-функции
|
||||||
|
одна и та же (0xF0), резидент — 0xF3. То есть `_bgi_begin`/`_bgi_end`
|
||||||
|
корректно возвращают ИМЕННО банковую страницу (правило R4);
|
||||||
|
- вызов W1/W2-функции из банка работает, и она тоже может рисовать;
|
||||||
|
- резидент W3 жив и вызывается после возврата из банка (R3).
|
||||||
|
|
||||||
|
**Дополнительно выяснено (важно для стражей):** писучие статики
|
||||||
|
`__banked`-модуля линкуются В СТРАНИЦУ БАНКА (0x1C000+) — снаружи их не
|
||||||
|
прочитать, из W1/W2 по 0xC000 видна резидентная страница. Значит всё
|
||||||
|
состояние банкованного кода (позиции стражей, таймеры боя) обязано жить в
|
||||||
|
W1/W2 как обычные глобалы, а банк — только код.
|
||||||
|
|
||||||
|
Ещё одна мина, найденная там же: инлайновый `in a,(#0xE2)` посреди тела
|
||||||
|
функции затирает A, куда SDCC уже положил параметр (у нас из-за этого цвет
|
||||||
|
заливки стал номером страницы, и «резидент не рисовал»). Читать порт
|
||||||
|
отдельной `__naked`-функцией.
|
||||||
|
|
||||||
|
### Найденная по дороге ловушка компилятора
|
||||||
|
|
||||||
|
`(const T *)КОНСТАНТА + var*K` SDCC 4.5 может собрать неверно: умножение
|
||||||
|
делает в 16 битах, а потом берёт только младший байт (`ld c,l` / `inc b`).
|
||||||
|
Кадры Kid с индексом ≥ 52 читались из чужой строки таблицы, у бега/шага
|
||||||
|
пропадал `FRAME_NEEDS_FLOOR` и персонаж проваливался сквозь пол. Лечение —
|
||||||
|
считать адрес в `uint16_t` и кастовать один раз. Тот же паттерн в
|
||||||
|
`pop_level.c` компилируется ПРАВИЛЬНО, т.е. полагаться на «у соседа
|
||||||
|
работает» нельзя. Подробности: memory `sdcc_z80_const_ptr_index_bug`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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,148 @@
|
|||||||
|
# Генераторы псевдослучайных чисел: запасные варианты
|
||||||
|
|
||||||
|
Что сейчас стоит в порте, какие есть альтернативы и сколько на них реально
|
||||||
|
можно выиграть. Заготовка на случай, если упрёмся в бюджет кадра —
|
||||||
|
**сейчас менять ничего не нужно**.
|
||||||
|
|
||||||
|
## Что стоит сейчас
|
||||||
|
|
||||||
|
`pop_geom.c`, ветка `POP_PRANDOM_EXACT=1` (по умолчанию) — LCG оригинала
|
||||||
|
`s = s*214013 + 2531011`, шаг написан на Z80-ассемблере (единственное такое
|
||||||
|
место в порте). Схема Горнера по разреженной записи константы:
|
||||||
|
|
||||||
|
```
|
||||||
|
214013 = ((((1<<1)+1)<<2 + 1)<<4 + 1)<<10 - 3
|
||||||
|
```
|
||||||
|
|
||||||
|
17 удвоений, три сложения, одно вычитание; величина `3*s`, нужная в конце,
|
||||||
|
попадается по дороге на втором шаге. Тело — **≈1 020 тактов** по статическому
|
||||||
|
подсчёту. Бит-в-бит совместим с SDLPoP, поэтому по картинке можно сверяться
|
||||||
|
с эталоном.
|
||||||
|
|
||||||
|
Вторая ветка, `POP_PRANDOM_EXACT=0` — xorshift16 + шаг Вейля на C.
|
||||||
|
Совместимость теряется.
|
||||||
|
|
||||||
|
Замер в MAME, комната 3, 175 кадров (медиана кадра):
|
||||||
|
|
||||||
|
| вариант | кадр | prandom → torch_draw |
|
||||||
|
|---|---|---|
|
||||||
|
| C, бит-в-бит (16-битные половины) | 403 632 | 10 933 |
|
||||||
|
| C, xorshift16 + Вейль | 397 986 | 7 927 |
|
||||||
|
| **asm, бит-в-бит (сейчас)** | **400 800** | **9 331** |
|
||||||
|
|
||||||
|
## Вариант A — комбинированный LFSR + LCG, ~148 тактов
|
||||||
|
|
||||||
|
Период > 4 млрд (lcm(65536, 65535) ≈ 4.29e9), младшие биты не вырождены.
|
||||||
|
|
||||||
|
```z80
|
||||||
|
prng16:
|
||||||
|
seed1=$+1
|
||||||
|
ld hl, 9999
|
||||||
|
ld b, h
|
||||||
|
ld c, l
|
||||||
|
add hl, hl
|
||||||
|
add hl, hl
|
||||||
|
inc l
|
||||||
|
add hl, bc
|
||||||
|
ld (seed1), hl
|
||||||
|
seed2=$+1
|
||||||
|
ld hl, 987
|
||||||
|
add hl, hl
|
||||||
|
sbc a, a
|
||||||
|
and 101101b
|
||||||
|
xor l
|
||||||
|
ld l, a
|
||||||
|
ld (seed2), hl
|
||||||
|
add hl, bc
|
||||||
|
ret
|
||||||
|
```
|
||||||
|
|
||||||
|
Устройство: `seed1` — LCG `x = 5x + 1` (по модулю 2^16; `inc l` вместо
|
||||||
|
`inc hl` — экономия байта, на период не влияет). `seed2` — 16-битный
|
||||||
|
LFSR Галуа: сдвиг влево, и если выехала единица, XOR младшего байта с маской
|
||||||
|
`0x2D` (примитивный многочлен `x^16 + x^5 + x^3 + x^2 + 1`). На выходе
|
||||||
|
сумма обоих состояний — она и разрушает регулярность младших бит LCG.
|
||||||
|
|
||||||
|
**Что мешает взять как есть:** сиды зашиты в код (SMC), а нам нужны ДВЕ
|
||||||
|
независимые последовательности — раскладка кладки и анимация тайлов.
|
||||||
|
Пришлось бы передавать состояние через указатель, как сейчас у `pop_prandom`
|
||||||
|
(это +20…40 тактов, не принципиально).
|
||||||
|
|
||||||
|
## Вариант B — xorshift(7,9,8), ~86 тактов
|
||||||
|
|
||||||
|
Самый быстрый, период 65535.
|
||||||
|
|
||||||
|
```z80
|
||||||
|
xrnd:
|
||||||
|
ld hl, 1 ; seed must not be 0
|
||||||
|
ld a, h
|
||||||
|
rra
|
||||||
|
ld a, l
|
||||||
|
rra
|
||||||
|
xor h
|
||||||
|
ld h, a
|
||||||
|
ld a, l
|
||||||
|
rra
|
||||||
|
ld a, h
|
||||||
|
rra
|
||||||
|
xor l
|
||||||
|
ld l, a
|
||||||
|
xor h
|
||||||
|
ld h, a
|
||||||
|
ld (xrnd+1), hl
|
||||||
|
ret
|
||||||
|
```
|
||||||
|
|
||||||
|
**Две оговорки.** Ноль — неподвижная точка, а сид раскладки кладки у нас
|
||||||
|
считается как `номер комнаты + смещение ряда + колонка` и вполне может
|
||||||
|
оказаться нулём: нужен либо guard, либо шаг Вейля поверх. И тот же SMC-сид,
|
||||||
|
что в варианте A.
|
||||||
|
|
||||||
|
## Чего НЕ брать: RND из Apple II
|
||||||
|
|
||||||
|
Оригинальный `Prince-of-Persia-Apple-II`:
|
||||||
|
|
||||||
|
```
|
||||||
|
RNDseed := (5 * RNDseed + 23) mod 256
|
||||||
|
```
|
||||||
|
|
||||||
|
```asm
|
||||||
|
RND
|
||||||
|
lda RNDseed
|
||||||
|
asl
|
||||||
|
asl
|
||||||
|
clc
|
||||||
|
adc RNDseed
|
||||||
|
clc
|
||||||
|
adc #23
|
||||||
|
sta RNDseed
|
||||||
|
rts
|
||||||
|
```
|
||||||
|
|
||||||
|
Полный период 256 (`a ≡ 1 mod 4`, `c` нечётное), и для своего движка он
|
||||||
|
работал. Нам не годится: у LCG по модулю 256 младшие биты вырождены — бит 0
|
||||||
|
просто чередуется. Наши вызовы это увидят: раскладка кладки берёт
|
||||||
|
`prandom(1)` (ОДИН бит) и `prandom(4)`, то есть вместо шума получилась бы
|
||||||
|
аккуратная шахматка.
|
||||||
|
|
||||||
|
## Сколько реально можно выиграть
|
||||||
|
|
||||||
|
Меньше, чем кажется по числам 86/148 против 1 020. Тело генератора — уже не
|
||||||
|
весь расход: остаются обёртка `pop_prandom`, приведение к диапазону
|
||||||
|
`pop_rnd_fit` и ABI вызова. Верхняя граница выигрыша видна из замера выше:
|
||||||
|
между нынешним asm-LCG и самым дешёвым из проверенных вариантов разница
|
||||||
|
**2 814 тактов за кадр (0.65 %)** при двух вызовах за кадр, и это ПОТОЛОК —
|
||||||
|
любой из вариантов A/B ниже него не опустится.
|
||||||
|
|
||||||
|
Порядок действий, если понадобится:
|
||||||
|
|
||||||
|
1. Сначала убрать обёртки: слить `pop_rnd_fit` в ту же asm-процедуру, чтобы
|
||||||
|
на вызов приходился один `call`, а не три. Это ничего не ломает и не
|
||||||
|
трогает совместимость с эталоном.
|
||||||
|
2. И только если этого мало — менять генератор, начиная с варианта A
|
||||||
|
(качество последовательности у него не хуже LCG, в отличие от B).
|
||||||
|
|
||||||
|
Важно помнить: число вызовов вырастет с боёвкой. Сейчас их два за кадр
|
||||||
|
(факелы), а `guard_advance` / `guard_block` / `guard_strike` дёргают
|
||||||
|
`prandom(255)` каждый по разу за кадр боя — то есть при драке станет 5–6, и
|
||||||
|
цена вопроса вырастет во столько же раз.
|
||||||
@@ -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 0 /* индекс в атласе = 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 |