Compare commits
87 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5323b168a7 | |||
| e4695b8281 | |||
| 50c6e56b7b | |||
| 8e389c03f8 | |||
| 05bcd8197e | |||
| 25abf8ea36 | |||
| f206a4cca6 | |||
| aaa480d0f2 | |||
| 8548aa9132 | |||
| 400f5cba63 | |||
| cb995bf0fc | |||
| 38fb3c03bb | |||
| 879f2bae31 | |||
| b3f9a7430c | |||
| fee3bdb354 | |||
| 6a97124e0d | |||
| 8610a8c178 | |||
| 81e8ed4676 | |||
| 8490288d79 | |||
| 31d0075090 | |||
| 6176f6da31 | |||
| 7377bf6a2c | |||
| b34997073e | |||
| 63bfd997a9 | |||
| decbec79de | |||
| b0e7130d0b | |||
| 4e12aa50d1 | |||
| c218e8b983 | |||
| a3aaa30e53 | |||
| 90e304f071 | |||
| b55d4d11e3 | |||
| ca67895667 | |||
| a5252b1c60 | |||
| 3a0e847353 | |||
| df5071a967 | |||
| 2349481b86 | |||
| 589894c50d | |||
| ea8efdb0fd | |||
| 623199337e | |||
| 602c3a20fa | |||
| 4ac3584bf9 | |||
| 3bf28da8ee | |||
| 770b946a36 | |||
| 89e9663753 | |||
| ea07a8d7b0 | |||
| d23126983e | |||
| 50570881f5 | |||
| f25ed37d85 | |||
| 3449f6f8c9 | |||
| 6dabe9b4b1 | |||
| a05970cd36 | |||
| 2176c12cc5 | |||
| 4e43890fce | |||
| 25b2db8b0b | |||
| f76914849f | |||
| 6cc8611d03 | |||
| 659071838d | |||
| f8e96c0495 | |||
| 76f02e76db | |||
| c71981fdf9 | |||
| 808c2a5349 | |||
| 31b82661eb | |||
| 4b74478d19 | |||
| 747783c422 | |||
| a3c5c600da | |||
| faec9a7d3a | |||
| 5d912f0f3f | |||
| 4086dde1f1 | |||
| 0b15ed7b8f | |||
| f66fd0e1b6 | |||
| e5179af9d8 | |||
| 06a1772011 | |||
| 5ede8b9045 | |||
| 0db4f94707 | |||
| 4a939e42f8 | |||
| 0086ac80c2 | |||
| 7e6b38cca6 | |||
| 65797af44c | |||
| d86adc54c4 | |||
| 16d3262340 | |||
| 4327ac88b9 | |||
| ba3aca07bf | |||
| 34ba5f71d5 | |||
| ffb59b3470 | |||
| 5756101d29 | |||
| 47a4b084c4 | |||
| 7fd7f28ffc |
@@ -0,0 +1,27 @@
|
||||
# Настройки Codex
|
||||
|
||||
`config.toml` и запускной скрипт `toolchain/run-mame-mcp.sh` хранятся в Git.
|
||||
Запускайте Codex из корня репозитория или любой вложенной папки.
|
||||
Корень определяется через `git rev-parse --show-toplevel`.
|
||||
|
||||
На каждом компьютере установите `uv` и добавьте его в PATH процесса Codex.
|
||||
По умолчанию сервер расположен в `mame/sources/MAME/src/mame_mcp.py`.
|
||||
Каталог `mame/` не хранится в Git и должен быть установлен отдельно.
|
||||
Первый запуск uv может потребовать сеть для установки Python 3.12 и mcp<2.
|
||||
|
||||
Если расположение отличается, создайте `.codex/mame.local.env` (игнорируется Git):
|
||||
|
||||
```sh
|
||||
MAME_UV="/opt/homebrew/bin/uv"
|
||||
MAME_MCP_SCRIPT="mame/sources/MAME/src/mame_mcp.py"
|
||||
```
|
||||
|
||||
Путь к серверу может быть абсолютным или относительно корня проекта.
|
||||
Путь к uv может быть абсолютным, относительным к корню или именем из PATH.
|
||||
Переменные можно также передать через окружение Codex; локальный файл имеет
|
||||
приоритет. Файл читается как shell-код запускным скриптом, а не самим Codex.
|
||||
Не храните в нём секреты и не добавляйте его в Git.
|
||||
|
||||
После изменения настроек перезапустите подключение MCP или Codex.
|
||||
Инициализация MCP не требует запущенного эмулятора; для команд отладки нужен
|
||||
MAME с `mame_bridge.lua` (см. `docs/mame-autotest.md`).
|
||||
+25
-8
@@ -1,11 +1,28 @@
|
||||
[mcp_servers.mame-z80]
|
||||
command = "/Users/alex/.local/bin/uv"
|
||||
command = "sh"
|
||||
args = [
|
||||
"run",
|
||||
"--python",
|
||||
"3.12",
|
||||
"--no-project",
|
||||
"--with",
|
||||
"mcp<2",
|
||||
"/Volumes/SAM8/Projects/DIY/Z80/Sprinter/C-Compiler/mame/sources/MAME/src/mame_mcp.py",
|
||||
"-c",
|
||||
'root=$(git rev-parse --show-toplevel) || exit; exec sh "$root/toolchain/run-mame-mcp.sh"',
|
||||
]
|
||||
startup_timeout_sec = 60
|
||||
|
||||
[mcp_servers.mame-z80.tools.clear_breakpoint]
|
||||
approval_mode = "approve"
|
||||
|
||||
[mcp_servers.mame-z80.tools.list_breakpoints]
|
||||
approval_mode = "approve"
|
||||
|
||||
[mcp_servers.mame-z80.tools.press_key]
|
||||
approval_mode = "approve"
|
||||
|
||||
[mcp_servers.mame-z80.tools.step_out]
|
||||
approval_mode = "approve"
|
||||
|
||||
[mcp_servers.mame-z80.tools.debugger_command]
|
||||
approval_mode = "approve"
|
||||
|
||||
[mcp_servers.mame-z80.tools.pause]
|
||||
approval_mode = "approve"
|
||||
|
||||
[mcp_servers.mame-z80.tools.status]
|
||||
approval_mode = "approve"
|
||||
|
||||
+24
@@ -78,6 +78,10 @@ toolchain/mkexe/tests/*.actual
|
||||
*.obj
|
||||
*.dSYM/
|
||||
|
||||
# Python host-tools: bytecode всегда воспроизводим и не входит в исходники.
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
# ===========================================================================
|
||||
# Vendored / downloaded
|
||||
# ===========================================================================
|
||||
@@ -121,3 +125,23 @@ mame/
|
||||
# .git внутри (в коммите стали бы битыми gitlink-ссылками).
|
||||
docs/extra/
|
||||
docs/sources/
|
||||
|
||||
# Записи музыки DOS-версии PoP (43 МБ в четырёх форматах) — ИСТОЧНИК для
|
||||
# toolchain/pop_pack_music.py, а не ресурс сборки: на диск игры уходят уже
|
||||
# упакованные poc/res/music/*.bin, и они в репозитории есть. Если понадобится
|
||||
# перегенерировать музыку — положить сюда PoP1_DOS_music (flac).
|
||||
applications/PoP/PoP1_DOS_music/
|
||||
|
||||
# R1 — рабочая копия roomtest для экспериментов пользователя, в репозиторий
|
||||
# не идёт (сама roomtest и есть версируемая ветка разработки).
|
||||
applications/PoP/R1/
|
||||
|
||||
# SprPoP — автономное приложение, и правила игнора у него СВОИ:
|
||||
# applications/SprPoP/.gitignore. Он написан так, чтобы стать корневым
|
||||
# .gitignore, когда SprPoP выделят в отдельный репозиторий, — поэтому
|
||||
# здесь его содержимое НЕ дублируется (иначе разъедется). Коротко: в
|
||||
# репозиторий не идут assets/orig/ (чужие данные — их выкачивает
|
||||
# `make fetch`) и assets/packed/LEVELS/ (уровни из оригинала как есть).
|
||||
|
||||
# Локальные пути MCP; общая конфигурация .codex остаётся в Git.
|
||||
/.codex/mame.local.env
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
3.12
|
||||
@@ -56,6 +56,16 @@ make size-baseline # принять текущие размеры эталон
|
||||
- Справочник API — docs/libc-reference.md (обновлять при добавлении
|
||||
функций).
|
||||
|
||||
## Документация source debugger
|
||||
|
||||
При изменении `<sdbg.h>`, извлечения/форматирования logMessage,
|
||||
поддержанных типов/регистров, чтения указателей или маршрутов MAME/DAP
|
||||
одновременно обновлять `docs/sdbg-log-macros.md` и проверяемые примеры;
|
||||
ссылки и краткий статус синхронизировать с `docs/mame-source-debug.md`,
|
||||
`docs/vscode-sprinter-debug.md`, `docs/mame-source-debug-status.md` и
|
||||
`docs/libc-reference.md`. Финальные задачи по hex и разыменованию
|
||||
указателей пока только в плане, не считать их рабочим API.
|
||||
|
||||
## ABI и платформа (кратко; детали в memory/)
|
||||
|
||||
- SDCC `__sdcccall(1)`: arg1 → HL (8-бит → A), arg2 → DE, остальные
|
||||
|
||||
@@ -43,7 +43,7 @@ DATA_FILES := \
|
||||
examples/mdview/SAMPLE.MD
|
||||
|
||||
.PHONY: all tools lib tests examples check clean sdcc floppy \
|
||||
size-check size-baseline host-tests $(TESTS) $(APPS)
|
||||
size-check size-baseline host-tests sdbg-tests mame-sdbg-patch mame-sdbg $(TESTS) $(APPS)
|
||||
|
||||
all: tools lib tests
|
||||
|
||||
@@ -80,11 +80,28 @@ floppy: tests examples tests/seek/big.txt
|
||||
# Модульные тесты под ucsim_z80. Обвязка — testkit/, сами наборы лежат
|
||||
# рядом с кодом, который проверяют. MAME не нужна, идут за секунды;
|
||||
# ucsim идёт в комплекте нашего SDCC.
|
||||
HOST_TEST_DIRS := testkit applications/PoP/roomtest/tests-host
|
||||
# applications/PoP/roomtest заморожена (её ветка развития — SprPoP), поэтому
|
||||
# её набор здесь больше не гоняется.
|
||||
HOST_TEST_DIRS := testkit applications/SprPoP/tests/host
|
||||
|
||||
host-tests:
|
||||
@for d in $(HOST_TEST_DIRS); do $(MAKE) -C $$d || exit 1; done
|
||||
|
||||
# Отладочная карта: реальные SDCC/linker и протокол транспорта без MAME.
|
||||
# Для local pyenv: pyenv exec make sdbg-tests.
|
||||
sdbg-tests: tools lib
|
||||
python3 -m unittest discover -s tests/sdbg -v
|
||||
|
||||
# Backend удерживает MAME в stopped-loop без Cocoa debugger; Lua bridge при
|
||||
# этом обслуживается из periodic_check ядра. Повторный вызов безопасен.
|
||||
mame-sdbg-patch:
|
||||
sh toolchain/apply-mame-sdbg-patch.sh
|
||||
|
||||
# Инкрементально собирает patched checkout и устанавливает бинарник в v306.
|
||||
mame-sdbg: mame-sdbg-patch
|
||||
$(MAKE) -C mame/sources/MAME
|
||||
cp mame/sources/MAME/mame $(MAME_DIR)/mame.arm
|
||||
|
||||
# Размерный регресс: сверить _CODE всех программ с docs/size_baseline.tsv.
|
||||
size-check:
|
||||
python3 toolchain/size_check.py
|
||||
|
||||
@@ -12,7 +12,10 @@
|
||||
# # STACK_SIZE := 2048 # bytes reserved for the stack
|
||||
# # EXTRA_SRCS := helper.c util.c # additional .c files in this dir
|
||||
# # EXTRA_FLAGS := --crt0=minimal # passed through to sprinter-cc
|
||||
# # SRC_DEBUG := 1 # карта C/asm и проверенный debug-пакет
|
||||
# # SRC_DEBUG_FILES := helper.c # либо карта только выбранных TU
|
||||
# # EXTRA_DATA := test.txt # extra files to add to `make floppy`
|
||||
# # HDD_DEST_DIR := games/myapp # общий каталог файлов в `make hdd`
|
||||
#
|
||||
# include $(PROJ_ROOT)/app.mk
|
||||
#
|
||||
@@ -30,39 +33,99 @@
|
||||
# (in the repo root) still packs all examples.
|
||||
# run floppy + launch MAME
|
||||
|
||||
PYTHON ?= python3
|
||||
SPRINTER_CC := $(PROJ_ROOT)/bin/sprinter-cc
|
||||
MKEXE := $(PROJ_ROOT)/toolchain/mkexe/mkexe
|
||||
LIB := $(PROJ_ROOT)/lib/sprinter.lib
|
||||
|
||||
MAME_DIR := $(PROJ_ROOT)/mame/v306
|
||||
FLOPPY_IMG := $(MAME_DIR)/IMG/mc.img
|
||||
HDD_IMG := $(MAME_DIR)/IMG/test_hdd.chd
|
||||
# ?= — приложение со своим каталогом выхода (applications/SprPoP) держит
|
||||
# образ у себя и связывает его с MAME символьной ссылкой.
|
||||
HDD_IMG ?= $(MAME_DIR)/IMG/test_hdd.chd
|
||||
MAKE_DISK := $(MAME_DIR)/make_disk.py
|
||||
MAKE_HDD := $(PROJ_ROOT)/toolchain/make_hdd.sh
|
||||
MAKE_HDD ?= $(PROJ_ROOT)/toolchain/make_hdd.sh
|
||||
RUN_MAME := $(MAME_DIR)/run_mame.sh
|
||||
|
||||
# Optional knobs — see top of file.
|
||||
MEMORY ?= tiny
|
||||
SOURCES := $(EXAMPLE).c $(EXTRA_SRCS)
|
||||
|
||||
# SRC_DIR / BUILD_DIR — раскладка приложения, которое НЕ держит исходники и
|
||||
# выхлоп в одной папке с Makefile (applications/SprPoP: src/ и build/). По
|
||||
# умолчанию обе пусты, то есть всё как было: ./$(EXAMPLE).c → ./$(EXAMPLE).exe.
|
||||
# Пустое значение обрабатывается отдельной веткой намеренно: "./prog.exe" и
|
||||
# "prog.exe" — разные имена целей, и склеивать префикс безусловно нельзя.
|
||||
SRC_DIR ?=
|
||||
BUILD_DIR ?=
|
||||
ifeq ($(strip $(SRC_DIR)),)
|
||||
MAIN_SRC := $(EXAMPLE).c
|
||||
else
|
||||
MAIN_SRC := $(SRC_DIR)/$(EXAMPLE).c
|
||||
endif
|
||||
ifeq ($(strip $(BUILD_DIR)),)
|
||||
EXE := $(EXAMPLE).exe
|
||||
else
|
||||
EXE := $(BUILD_DIR)/$(EXAMPLE).exe
|
||||
endif
|
||||
SOURCES := $(MAIN_SRC) $(EXTRA_SRCS)
|
||||
# Аргументы упаковщика HDD. Обычно это exe и EXTRA_DATA; приложение со
|
||||
# своей раскладкой каталогов может переопределить переменную до include.
|
||||
HDD_PACK_ARGS ?= $(EXAMPLE).exe $(EXTRA_DATA)
|
||||
HDD_PACK_ARGS ?= $(EXE) $(EXTRA_DATA)
|
||||
# Общий каталог назначения внутри HDD. Пустое значение сохраняет прежнюю
|
||||
# укладку в корень; вложенные КАТАЛОГ:файл считаются относительно него.
|
||||
HDD_DEST_DIR ?=
|
||||
|
||||
CC_FLAGS := --memory $(MEMORY)
|
||||
ifneq ($(STACK_SIZE),)
|
||||
CC_FLAGS += --stack-size $(STACK_SIZE)
|
||||
endif
|
||||
CC_FLAGS += $(EXTRA_FLAGS)
|
||||
ifeq ($(SRC_DEBUG),1)
|
||||
CC_FLAGS += --src-debug
|
||||
endif
|
||||
ifneq ($(strip $(SRC_DEBUG_FILES)),)
|
||||
CC_FLAGS += $(foreach src,$(SRC_DEBUG_FILES),--src-debug-file $(src))
|
||||
endif
|
||||
|
||||
all: $(EXAMPLE).exe
|
||||
all: $(EXE)
|
||||
|
||||
# runtime/*.s (crt0-семейство, bank.s, heap.s) собираются per-build
|
||||
# внутри sprinter-cc — без этой зависимости их правка не перелинкует
|
||||
# уже собранный exe (кусало: фикс bank.s не подхватился).
|
||||
RUNTIME_DEPS := $(wildcard $(PROJ_ROOT)/runtime/*.s)
|
||||
|
||||
$(EXAMPLE).exe: $(SOURCES) $(MKEXE) $(LIB) $(RUNTIME_DEPS)
|
||||
$(SPRINTER_CC) $(CC_FLAGS) -o $@ $(SOURCES)
|
||||
# ПРОВЕРКА БАНКОВЫХ ВЫЗОВОВ — сразу после линковки, пока артефакты свежие.
|
||||
# Ловит прямой `call` в чужой банк: он собирается МОЛЧА и стреляет диким
|
||||
# переходом в пустой хвост банка (разбор — в шапке скрипта). Запускается
|
||||
# только если банки вообще есть, то есть по наличию каталога сборки с
|
||||
# bankN_*.asm; обычным небанковым программам ничего не стоит.
|
||||
BANK_CHECK := $(PROJ_ROOT)/toolchain/check_bank_calls.py
|
||||
|
||||
# ПРОВЕРКА ISR-СТАБА W0-СТРАНИЦ — там же, по свежей карте. Ловит стаб
|
||||
# _gfx_w0_isr, оставшийся в W1: программа, кладущая свои страницы в W0
|
||||
# (атласы спрайтов, gfx_w0_map), получает недетерминированные зависания,
|
||||
# когда прерывание приходит во время вызова DSS и W1 перемаплен. Только
|
||||
# предупреждение: страницы в W0 кладут не все. Разбор — в шапке скрипта.
|
||||
W0ISR_CHECK := $(PROJ_ROOT)/toolchain/check_w0_isr.py
|
||||
|
||||
# Команда/зависимости карты участвуют в пересборке, включая смену режима.
|
||||
SDBG_CONFIG := $(dir $(EXE)).resource-stamps/$(EXAMPLE)-config.json
|
||||
SDBG_MANIFEST := $(dir $(EXE)).sprinter-cc-$(EXAMPLE)/manifest.json
|
||||
.PHONY: sdbg-config-force
|
||||
SDBG_CONFIG_ARGS = $(SDBG_CONFIG) $(SPRINTER_CC) $(CC_FLAGS) $(SOURCES) $(SDBG_MANIFEST)
|
||||
|
||||
# Проверка содержимого обязательна: mtime make может иметь точность в секунду.
|
||||
# Сохранение fingerprint только после успешной сборки позволяет повторить сбой.
|
||||
$(EXE): $(SOURCES) $(MKEXE) $(LIB) $(RUNTIME_DEPS) sdbg-config-force $(SPRINTER_CC) $(wildcard $(PROJ_ROOT)/toolchain/sdbg/*.py) $(wildcard $(PROJ_ROOT)/toolchain/sdbg_*.py)
|
||||
@state=$$($(PYTHON) $(PROJ_ROOT)/toolchain/sdbg_config.py check $(SDBG_CONFIG_ARGS)) || exit $$?; \
|
||||
if [ "$$state" = changed ] || [ -n "$(filter-out sdbg-config-force,$?)" ]; then \
|
||||
mkdir -p $(dir $@); \
|
||||
SPRINTER_PYTHON="$(PYTHON)" $(SPRINTER_CC) $(CC_FLAGS) -o $@ $(SOURCES) || exit $$?; \
|
||||
d=$(dir $@).sprinter-cc-$(EXAMPLE); \
|
||||
if ls $$d/bank*_*.asm >/dev/null 2>&1; then $(PYTHON) $(BANK_CHECK) $$d || exit $$?; fi; \
|
||||
if ls $$d/*.map >/dev/null 2>&1; then $(PYTHON) $(W0ISR_CHECK) $$d || exit $$?; fi; \
|
||||
$(PYTHON) $(PROJ_ROOT)/toolchain/sdbg_config.py save $(SDBG_CONFIG_ARGS); \
|
||||
fi
|
||||
|
||||
$(MKEXE):
|
||||
$(MAKE) -C $(PROJ_ROOT)/toolchain/mkexe
|
||||
@@ -76,13 +139,13 @@ $(LIB):
|
||||
$(MAKE) -C $(PROJ_ROOT)/libbgi
|
||||
|
||||
clean:
|
||||
rm -rf .sprinter-cc-* $(EXAMPLE).exe
|
||||
rm -rf $(if $(strip $(BUILD_DIR)),$(BUILD_DIR),.sprinter-cc-* $(EXE))
|
||||
|
||||
# `make floppy` packs ONLY this program (+ optional EXTRA_DATA files) into
|
||||
# the MAME floppy image, replacing whatever was there. Handy for trying a
|
||||
# single program without rebuilding everything.
|
||||
floppy: $(EXAMPLE).exe
|
||||
python3 $(MAKE_DISK) $(FLOPPY_IMG) $(EXAMPLE).exe $(EXTRA_DATA)
|
||||
floppy: $(EXE)
|
||||
python3 $(MAKE_DISK) $(FLOPPY_IMG) $(EXE) $(EXTRA_DATA)
|
||||
@echo
|
||||
@echo "Floppy ready: $(FLOPPY_IMG) (with $(EXAMPLE).exe$(if $(EXTRA_DATA), + $(EXTRA_DATA))) "
|
||||
@echo "Run: cd $(MAME_DIR) && ./run_mame.sh"
|
||||
@@ -94,8 +157,8 @@ run: floppy
|
||||
# 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) $(HDD_PACK_ARGS)
|
||||
hdd: $(EXE)
|
||||
$(MAKE_HDD) $(if $(strip $(HDD_DEST_DIR)),--dest "$(HDD_DEST_DIR)") $(HDD_IMG) $(HDD_PACK_ARGS)
|
||||
@echo
|
||||
@echo "HDD (D:) ready: $(HDD_IMG) (with $(EXAMPLE).exe$(if $(EXTRA_DATA), + $(EXTRA_DATA)))"
|
||||
@echo "ВНИМАНИЕ: перезапусти MAME (run_bridge.sh) — образ пересобран."
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# `applications/PoP/docs` — индекс + сводка по форматам ресурсов
|
||||
|
||||
## Индекс документов (актуальность на 2026-08-01)
|
||||
## Индекс документов (актуальность на 2026-08-22)
|
||||
|
||||
**Живые планы — читать перед работой:**
|
||||
|
||||
@@ -13,9 +13,11 @@
|
||||
| [`perf_green_phase.md`](perf_green_phase.md) | **ЗЕЛЁНАЯ фаза (слой фона)**: раскладка тактов, способы ускорения (G1..G6), журнал правок — рабочий документ между сессиями. 2026-08-17 |
|
||||
| [`perf_cyan_phase.md`](perf_cyan_phase.md) | **ЦИАН фаза (персонажи + передний слой)**: раскладка тактов, способы ускорения (C1..C7), журнал правок — рабочий документ между сессиями. 2026-08-17 |
|
||||
| [`perf_backlog.md`](perf_backlog.md) | Отложенная оптимизация отрисовки с замерами 2026-08-10 + **как мерить** (wait-state'ы, границы кадра). Позиции 1–7 переехали в фазовые документы выше |
|
||||
| [`quicksave_plan.md`](quicksave_plan.md) | **QuickSave/QuickLoad**: разбор (это enhancement SDLPoP, в оригинале 1989 его НЕТ), инвентаризация нашего состояния, формат снимка, шаги QS1..QS6. План, код не начат. 2026-08-17 |
|
||||
| [`quicksave_plan.md`](quicksave_plan.md) | **QuickSave/QuickLoad** (✅ реализовано, F6/F9, POP.SAV+BAK): разбор (это enhancement SDLPoP, в оригинале 1989 его НЕТ), инвентаризация нашего состояния, формат снимка 'POPQ' v3, шаги QS1..QS6. Справочник. 2026-08-22 |
|
||||
| [`full_game_plan.md`](full_game_plan.md) | **Полноценная игра**: app state machine, title/intro, demo level 0, таймер, cutscenes, уровни 1..14, ending и Hall of Fame. Уровень 15 исключён. 2026-08-21 |
|
||||
| [`menu_settings_plan.md`](menu_settings_plan.md) | **Pause menu и Settings**: QuickSave/QuickLoad в основном menu, `POP.CFG`, один `POP.SAV` + `POP.BAK`, текущий VANILLA и задел под ENHANCED. 2026-08-21 |
|
||||
| [`menu_settings_plan.md`](menu_settings_plan.md) | **Pause menu и Settings**: QuickSave/QuickLoad в основном menu, `POP.CFG`, один `POP.SAV` + `POP.BAK`, текущий VANILLA и задел под ENHANCED; §10 — выбор UI-рендера (текстовые строки + свой растровый рендерер, референс SDLPoP: два шрифта), restart без подтверждения. 2026-08-22 |
|
||||
| [`palette_plan.md`](palette_plan.md) | **Палитры и fade**: карта всех 256 слотов (kid.pal/title/story, тайлсеты dungeon/palace, стражи), механика fade (4 ступени vs ~64 у SDLPoP), план модуля `pop_pal.c` (API load/apply/black) + переход уровня через fade. 2026-08-23 |
|
||||
| [`status_line_text.md`](status_line_text.md) | **Строка HP как статус-строка**: полная инвентаризация ВСЕХ текстов SDLPoP в `rect_bottom_text` (геометрия, семантика `text_time_total`, мигание, рестарт по истечении) + что из этого уже есть у нас. 2026-08-25 |
|
||||
| [`levels_plan.md`](levels_plan.md) | Следующий этап: уровни 2+, второй тайлсет, читы SDLPoP |
|
||||
| [`levels_12_15_plan.md`](levels_12_15_plan.md) | **Уровни 12/13** (тень, Джафар, падающие плиты) + что такое 14/15 и 0. 2026-08-13 |
|
||||
| [`midtable_analysis.md`](midtable_analysis.md) | **Слои отрисовки**: как устроены back/mid/fore и objtable в оригинале, чего стоит порт, развилки. 2026-08-13 |
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
# От roomtest к полноценной игре — сценарий и оболочка
|
||||
|
||||
Статус: **согласованный план, код не начат** (2026-08-21).
|
||||
Статус: **частично реализовано; аудит обновлён 2026-08-24**. Ранее пометка
|
||||
«завершены FG0–FG12» была неверной: для многих этапов уже есть код и
|
||||
host-тесты, но их критерии приёмки на Sprinter ещё не выполнены. Фактический
|
||||
статус каждого FG приведён в [§14](#14-этапы-реализации).
|
||||
|
||||
Этот документ описывает превращение текущего игрового цикла
|
||||
`roomtest` в законченную игру: заставка, интро, демонстрационный уровень,
|
||||
@@ -72,17 +75,24 @@ attract loop после демо или финала.
|
||||
Джаффар, специальные события, checkpoint, переходы уровней, бесшовный
|
||||
выход 12-го уровня, перенос максимального HP и звуковые эффекты.
|
||||
|
||||
Отсутствуют:
|
||||
Поверх игрового цикла уже добавлены автомат оболочки, title/story, demo
|
||||
уровень 0, global timer, сценарный интерпретатор, level-flow, ending и Hall
|
||||
of Fame. Полный маршрут также собирается в HDD-образ.
|
||||
|
||||
- верхнеуровневая оболочка приложения;
|
||||
- title/story sequence и текстовые экраны;
|
||||
- уровень 0 и записанное управление демо;
|
||||
- общий cutscene engine и PV-ресурсы;
|
||||
- сцены между уровнями;
|
||||
- глобальный таймер и time-expired sequence;
|
||||
- корректный переход из уровня 14 в ending;
|
||||
- финальная сцена и Hall of Fame;
|
||||
- возврат к title без перезапуска программы.
|
||||
Однако это **не означает готовность оболочки**. На момент аудита остаются
|
||||
существенные незакрытые места:
|
||||
|
||||
- lifecycle палитр: gameplay-переходы используют чёрный барьер без fade;
|
||||
cold start и полный набор dungeon/palace переходов ещё не прошли приёмку;
|
||||
- PV intro Princess/Jaffar уже покадровый (актёры, факелы, звёзды, часы,
|
||||
молния и foreground-колонна); сцены перед 2/4/6 и длинной веткой 12
|
||||
анимируют факелы, звёзды и песок, а сцены 8/9 и короткая ветка 12 пока
|
||||
используют статические позы с исходной длительностью;
|
||||
- demo отображается с игровой палитрой, проходит второй разворот/зацеп и
|
||||
доходит до боя; после смерти Кида корректно завершает цикл;
|
||||
- time-expired, ending и Hall of Fame имеют маршрут и реализацию UI, но не
|
||||
прошли сквозную MAME-проверку вместе с ресурсами и возвратом к title;
|
||||
- нет полного регресса EMM/FD для каждого перехода состояния.
|
||||
|
||||
## 4. Архитектура: автомат состояний приложения
|
||||
|
||||
@@ -218,6 +228,13 @@ title -> demo -> title повторяется неограниченно.
|
||||
Критерий: одинаковый игровой отрезок в NORMAL даёт то же уменьшение времени,
|
||||
что SDLPoP; сохранение/загрузка не добавляет и не отнимает тики.
|
||||
|
||||
Реализация FG4 живёт одним модулем `roomtest/pop_timer.c` в bank 9:
|
||||
`60:719`, 720 тиков на минуту, счёт только в живом игровом кадре. Settings
|
||||
хранит `TIME LIMIT: 60 MIN / UNLIMITED` в `POP.CFG`; старый семибайтный v1
|
||||
payload по-прежнему читается как `60 MIN`. Читы таймера повторяют SDLPoP,
|
||||
но из-за занятого `+/-` используют F7 (−1 минута, не ниже одной) и F8
|
||||
(+1 минута). Состояние входит в QuickSave v4.
|
||||
|
||||
## 10. Cutscene engine
|
||||
|
||||
Сцены SDLPoP состоят из небольшого набора повторяемых команд. Вместо набора
|
||||
@@ -305,24 +322,26 @@ Hall of Fame хранится на HDD в отдельном версионир
|
||||
|
||||
## 14. Этапы реализации
|
||||
|
||||
| этап | результат | критерий приёмки |
|
||||
|---|---|---|
|
||||
| **FG0** | удалить уровень 15, формализовать 1..14 | HDD не содержит res2015; переход выше 14 невозможен |
|
||||
| **FG1** | автомат состояний, текущая игра = PLAYING | старт/рестарт/выход проходят без рекурсии и утечки EMM |
|
||||
| **FG2** | QuickSave/QuickLoad | критерии `quicksave_plan.md`, включая POP.BAK |
|
||||
| **FG3** | минимальный pause menu | Resume/Save/Load/Restart/Settings/Quit работают |
|
||||
| **FG4** | глобальный таймер | совпадение с SDLPoP и корректный save/load |
|
||||
| **FG5** | text/full-screen/fade primitives | тестовые экраны и переходы на Sprinter |
|
||||
| **FG6** | build info + оригинальный title | точный порядок, корректный пропуск |
|
||||
| **FG7** | demo level 0 | бесконечный attract loop, ввод начинает игру |
|
||||
| **FG8** | cutscene interpreter + intro | интро проходит и пропускается без утечек |
|
||||
| **FG9** | сцены 2/4/6/8/9/12 | правильный вызов один раз перед уровнем |
|
||||
| **FG10** | time expired | отдельная сцена и возврат на title |
|
||||
| **FG11** | level 14 -> ending | полный маршрут после Джаффара |
|
||||
| **FG12** | Hall of Fame | запись HDD, ввод имени, возврат к attract loop |
|
||||
Легенда аудита: **✓** — критерий этапа закрыт; **~** — код существует, но
|
||||
критерий приёмки ещё не закрыт; **○** — не начат. Статус отражает состояние
|
||||
исходников и последней MAME-проверки на 2026-08-24, а не только наличие
|
||||
модуля в bank 9.
|
||||
|
||||
QuickSave допускается реализовать до остальных частей оболочки: FG2 — одна
|
||||
из ближайших самостоятельных задач.
|
||||
| этап | статус | результат и фактическое состояние | критерий приёмки |
|
||||
|---|---|---|---|
|
||||
| **FG0** | ✓ | `POP_LEVEL_LAST=14`, HDD содержит `res2000..res2014`; `t_flow` отвергает 15 | HDD не содержит res2015; переход выше 14 невозможен |
|
||||
| **FG1** | ~ | автомат `pop_app` и `t_app` реализованы; сквозной ресурсный lifecycle и контроль EMM/FD ещё не измерены | старт/рестарт/выход проходят без рекурсии и утечки EMM |
|
||||
| **FG2** | ✓ | QuickSave/QuickLoad с `POP.SAV` и `POP.BAK`; отдельно проверен в MAME 2026-08-22 | критерии `quicksave_plan.md`, включая POP.BAK |
|
||||
| **FG3** | ✓ | pause menu, Settings, подтверждения и двойной буфер реализованы; меню проверялось в MAME; добавлены SDLPoP-звуки навигации и защита CBL вокруг полного redraw/файловых операций | Resume/Save/Load/Restart/Settings/Quit работают |
|
||||
| **FG4** | ~ | `pop_timer`, настройка unlimited, F7/F8 и состояние QuickSave реализованы; есть host-тест, но нет буквального сравнения темпа со SDLPoP на всех переходах | совпадение с SDLPoP и корректный save/load |
|
||||
| **FG5** | ~ | text/full-screen/fade примитивы есть; для входа в первый уровень и границ уровней выбран мгновенный чёрный барьер без fade: CBL и яркая новая палитра включаются только после подготовки обеих страниц; Level 1 проверен в MAME | тестовые экраны и переходы на Sprinter |
|
||||
| **FG6** | ~ | title-ресурсы и порядок кадров реализованы; Enter на title и Esc на первом story в MAME переводят прямо в `FIRST_LEVEL`, минуя demo; полная cold-boot приёмка fade остаётся в FG5 | основной титул/Presents/название/Mechner идут в точном порядке `show_title()`; Enter/Space/Esc/стрелки прерывают ожидание; story/intro продолжит FG8 |
|
||||
| **FG7** | ✓ | level 0, исходная таблица `demo_moves`, demo HP=4 и блокировка игрового UI реализованы; исправлены зеркалирование auto-control, боевой AI Кида и завершение после смерти; в MAME demo проходит разворот/зацеп, доходит до боя и возвращается в attract-цикл без повторного убийства | `res2000.bin`, исходная `demo_moves`, demo HP=4; бесконечный attract loop, любой ввод начинает чистую новую игру; Pause/QuickSave/читы/таймер отключены |
|
||||
| **FG8** | ~ | data-driven interpreter и покадровый PV intro работают; в MAME проверены актёры, факелы, звёзды 1x1, часы/песок, palette-0 lightning и foreground-колонна; Enter/Esc переводят прямо в `FIRST_LEVEL`; временный темп 12,5 FPS и TODO точного pacing записаны в `impl_diff.md` | story/PV intro проходит, любой raw-ввод пропускает его без удержания EMM-страниц |
|
||||
| **FG9** | ~ | `pop_flow` корректно маршрутизирует 2/4/6/8/9/12 и ветку <=5 минут (`t_flow`); 2/4/6 и длинная 12 уже обновляют часы, песок, факелы и звёзды каждые 5 кадров Sprinter; длительности всех веток сверены с SDLPoP: 2/4/6/12 — 2,6 с, 8 — 6,0 с, 9 — 7,2 с; входная клавиша gameplay/Shift+L поглощается до сцены, а новое нажатие делает skip; анимации мыши/Princess в 8/9 и разворот Princess в короткой 12 ещё статичны | таблица flow переводит в CUTSCENE ровно перед 2/4/6/8/9/12; scene 12 выбирает короткий вариант при <=5 минутах |
|
||||
| **FG10** | ~ | переход TIME_EXPIRED и экран существуют, но это ещё статическая PV-стадия; сквозной MAME-маршрут не принят | PV-сцена истечения с пропуском, затем возврат на title/attract; новая игра сбрасывает таймер |
|
||||
| **FG11** | ~ | room 5 уровня 14 переводит в ENDING (`t_flow`); объятие/мышь заменены статической стадией, полный маршрут не принят | room 5 уровня 14 переводит в ENDING; PV-финал и Hail-экран возвращают управление оболочке |
|
||||
| **FG12** | ~ | версионированный `POP.HOF`, ввод имени и восстановление после повреждённого файла реализованы; нужна сквозная MAME-проверка ending → HOF → title | версионированный `POP.HOF`, ввод имени raw-клавиатурой, повреждённый файл = пустая таблица, затем title/attract |
|
||||
|
||||
## 15. Проверки
|
||||
|
||||
@@ -346,4 +365,3 @@ QuickSave допускается реализовать до остальных
|
||||
- replay/recording;
|
||||
- точная эмуляция SDL video/controller options;
|
||||
- профиль ENHANCED и индивидуальные switches fixes.
|
||||
|
||||
|
||||
@@ -505,6 +505,53 @@ BUG-CHEAT-FIGHT-1 (выход из боя), и лечится там же.
|
||||
«страж давит сильнее». Режим NORMAL (дефолт) даёт 102,4 мс — см.
|
||||
`frame_pacing_plan.md`.
|
||||
|
||||
## PV intro: единые 12,5 FPS вместо переменных 10/7,5/8,57 FPS
|
||||
|
||||
**Оригинал.** `proc_cutscene_frame()` двигает последовательности через
|
||||
`cutscene_frame_time`: 6 тиков 60 Гц в начале, 8 после первой речи и 7 во
|
||||
время заклинания. Это соответственно 10, 7,5 и примерно 8,57 FPS.
|
||||
|
||||
**У нас (осознанное временное отличие).** Один логический кадр PV держится
|
||||
четыре физических кадра Sprinter: номинально 50/4 = 12,5 FPS. Молния живёт
|
||||
на отдельной физической шкале и не растягивается этим делителем. Если полная
|
||||
отрисовка пересечёт дополнительный фронт, реальная частота может упасть до
|
||||
10 FPS — это допустимо на текущем этапе, но должно быть измерено.
|
||||
|
||||
**TODO.** Перевести PV-сцену на тот же anchor-based механизм точного темпа,
|
||||
который gameplay использует через `pop_beam_sample/pop_pace_end`: измерять
|
||||
число реально прошедших фронтов во время сборки кадра, держать период ровно
|
||||
четыре фронта при укладывании в бюджет и явно учитывать overrun. После замера
|
||||
можно вернуть точные переменные интервалы SDLPoP без накопления фазы.
|
||||
|
||||
## Межуровневые PV-сцены: сохранён реальный период 100 мс
|
||||
|
||||
Это правило не относится к временному темпу основного Princess/Jaffar intro
|
||||
выше. `reset_cutscene()` SDLPoP задаёт для сцен перед уровнями период
|
||||
6 кадров при 60 Гц, то есть 100 мс. На Sprinter тот же период получается
|
||||
ровно как 5 кадров при 50 Гц.
|
||||
|
||||
Суммы вызовов `proc_cutscene_frame()` перенесены без изменения реального
|
||||
времени: сцены 2/4/6 и обе ветки 12 содержат 26 логических кадров (130
|
||||
физических, 2,6 с), сцена 8 — 60 (300, 6,0 с), сцена 9 — 72 (360, 7,2 с).
|
||||
Fade in/out в эти числа не входят, как и в оригинале.
|
||||
|
||||
## Gameplay: загрузка уровней через чёрный cut, без fade
|
||||
|
||||
**Оригинал.** На границах игровых уровней использует fade out/in.
|
||||
|
||||
**У нас (решение пользователя 2026-08-24).** Вход в первый уровень и
|
||||
переход между уровнями выполняются как `старый кадр -> чёрная палитра ->
|
||||
подготовка -> новый кадр с новой палитрой`. Fade на этих двух маршрутах
|
||||
отсутствует. Сюжетные title/story/PV переходы сохраняют собственные fade и
|
||||
left-to-right эффекты.
|
||||
|
||||
Чёрная палитра устанавливается до любого HDD I/O. Загрузчики guard и
|
||||
tileset сами физически правят отдельные цветовые слоты, поэтому после них
|
||||
чёрный экран подтверждается повторно. Зеркальные атласы уровня 9 готовятся
|
||||
до финального источника палитры. CBL открывается последним: старый порядок
|
||||
`level_switch -> CBL open -> BIOS fade` давал скрежет повторяющейся половины
|
||||
аппаратного буфера на входе в Level 1; после перестановки баг исчез в MAME.
|
||||
|
||||
## Тень: кайма силуэта не подкрашивается фоном
|
||||
|
||||
**Оригинал.** Спрайт Тени не хранится — он кладётся ДВАЖДЫ: обычным
|
||||
@@ -530,3 +577,73 @@ seg008.c:1600). XOR идёт по 24-битному RGB того, что УЖЕ
|
||||
кайма станет резать глаз — вариантов два: запечь второй набор под светлый
|
||||
фон (ещё 32 страницы EMM) или считать эту кайму прозрачной (силуэт станет
|
||||
на пиксель уже). Оба хуже нынешнего; трогать только по факту жалобы.
|
||||
|
||||
|
||||
## QuickSave/QuickLoad: лейбл печатается ДО дисковой операции, а не после
|
||||
|
||||
**Как в оригинале.** SDLPoP печатает `QUICKSAVE` / `NO QUICKSAVE` (и пару
|
||||
для загрузки) уже ПО РЕЗУЛЬТАТУ операции — `process_quicksave` (seg000:497)
|
||||
сначала делает save/load, потом зовёт `display_text_bottom` и ставит
|
||||
`text_time_total = 24`. На PC это незаметно: файл пишется мгновенно.
|
||||
|
||||
**У нас.** `pop_qsave_process` заявляет строку ПЕРВЫМ действием, ещё до
|
||||
`mem_alloc_pages`/ESTEX, через `pop_status_show_now()` — та печатает её
|
||||
немедленно в ВИДИМУЮ страницу, не дожидаясь конца кадра. Отказ уже потом
|
||||
переписывает строку на `NO QUICKSAVE`/`NO QUICKLOAD` обычной заявкой.
|
||||
|
||||
**Зачем.** Запись снимка на диск занимает доли секунды, и всё это время
|
||||
игра стоит. При порядке оригинала игрок видел сначала необъяснённый фриз,
|
||||
и только по его окончании — надпись, объясняющую то, что уже прошло.
|
||||
Решение пользователя, 2026-08-25.
|
||||
|
||||
**Чем платим.** Строка успевает мигнуть даже там, где операция потом не
|
||||
удалась: сначала `QUICKSAVE`, следом `NO QUICKSAVE`. На практике отказ —
|
||||
редкость (нет места/диска), и «заявка → отказ» читается не хуже.
|
||||
|
||||
**Что проверять при регрессе.** Что после неудачной операции на экране
|
||||
остаётся именно `NO QUICKSAVE`/`NO QUICKLOAD`, а не первая строка: отказ
|
||||
идёт обычной заявкой и печатается кадровым проходом, то есть на кадр позже.
|
||||
|
||||
|
||||
## Смерть Кида: ждём кнопку и перезапускаем УРОВЕНЬ, а не игру
|
||||
|
||||
**Как в оригинале.** `play_kid` (seg006:1383) печатает «Press Button to
|
||||
Continue» с `text_time_total = 288`. Тик — это логический игровой кадр,
|
||||
720 тиков = минута, то есть 12 тиков в секунду: 288 тиков = **24 секунды**.
|
||||
Последние 72 тика (6 секунд) строка мигает с периодом 12 тиков, и на каждом
|
||||
появлении играет звук 38. Дальше развилок ровно две:
|
||||
|
||||
* игрок молчит все 24 секунды — `draw_game_frame` (seg000:958) зовёт
|
||||
`start_game()`, и игра начинается ЗАНОВО, с title, а не с уровня;
|
||||
* игрок нажимает **Enter или Shift** (не любую клавишу!) — seg000:584
|
||||
подменяет их на Ctrl+A: `if (rem_min != 0 && Kid.alive > 6 && (control_shift
|
||||
|| key == SDL_SCANCODE_RETURN)) key = SDL_SCANCODE_A | WITH_CTRL;` — и
|
||||
уровень перезапускается. Условия важны: время не должно быть исчерпано
|
||||
(иначе отработал `expired()`), а `Kid.alive > 6` даёт трупу улечься.
|
||||
|
||||
**У нас.** Обе развилки сведены к одной: 24-секундного выхода в начало
|
||||
игры нет вовсе, строка висит бессрочно (`MSG_HOLD`), а перезапускает уровень
|
||||
ЛЮБАЯ кнопка, а не только Enter/Shift (решение пользователя).
|
||||
`pop_start_level()` возвращает игрока на уровень. Место возрождения выбирает сам `pop_start_level` — на части
|
||||
уровней это не старт, а пройденный чекпойнт. Esc за кнопку продолжения не
|
||||
считается: он открывает pause menu. Логика ожидания живёт в банке
|
||||
(`pop_dead_prompt`, pop_status.c) — резидент W1 переполнен.
|
||||
|
||||
**Зачем.** Решение пользователя, 2026-08-25: возврат к title после каждой
|
||||
смерти в отладочной сборке съедает всё время прохода, а прежний вариант
|
||||
(авто-респавн через 400 кадров либо стрелка вверх) не объяснял игроку, чего
|
||||
от него ждут.
|
||||
|
||||
**Чем платим.** Двумя вещами. Первое: смерть больше не заканчивает
|
||||
партию — счёт попыток фактически бесконечен, тогда как оригинал через 24
|
||||
секунды бездействия отправляет в title. Второе: любая клавиша вместо
|
||||
Enter/Shift означает, что случайное нажатие (например, ещё не отпущенная
|
||||
после боя клавиша) перезапустит уровень — отсюда требование сперва отпустить
|
||||
всё. Когда дойдёт до «настоящей» игры, обе развилки придётся выбирать
|
||||
заново: вернуть таймер на 288 тиков со start_game и сузить клавиши до
|
||||
Enter/Shift — либо оставить как есть уже осознанно.
|
||||
|
||||
**Что проверять при регрессе.** Нажатие принимается только после того, как
|
||||
отпущено ВСЁ, что игрок держал в момент смерти (иначе зажатая при падении
|
||||
стрелка перезапускает уровень мгновенно), и не раньше `RESPAWN_SETTLE`
|
||||
кадров — труп должен успеть лечь.
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
* Left: turn or run left
|
||||
* Right: turn or run right
|
||||
* Up: jump or climb up
|
||||
* Down: crouch or climb down
|
||||
* Down+Left/Right: hop
|
||||
* Shift: pick up things
|
||||
* Shift+Left/Right: careful step
|
||||
* Home or Up+Left: jump left
|
||||
* Page Up or Up+Right: jump right
|
||||
* Up while running: running jump
|
||||
* Shift while falling: grab onto ledge
|
||||
|
||||
* Left/Right: walk (advance or retreat)
|
||||
* Shift: strike (attack)
|
||||
* Up: block (defend)
|
||||
* Down: put sword away; press Shift to draw your sword again.
|
||||
|
||||
===
|
||||
|
||||
* Esc: Pause game.
|
||||
* Space: Show how much time is left.
|
||||
* Ctrl+A: Restart level.
|
||||
|
||||
* Ctrl+R: Return to intro.
|
||||
* Ctrl+S: Sound on/off.
|
||||
* Ctrl+M: Music on/off.
|
||||
* Ctrl+V: Show version of SprPoP.
|
||||
* Ctrl+Q: Quit game.
|
||||
|
||||
* F6: Quicksave: Save the exact state of the game.
|
||||
* F9: Quickload: Load what the last quicksave saved.
|
||||
* F12: Save a screenshot to the screenshots folder.
|
||||
* Backspace: Display the in-game menu. (Esc will also display the menu by default, but you can turn that off.)
|
||||
|
||||
* Shift+L: Go to next level.
|
||||
* -: Decrease remaining time by one minute.
|
||||
* +: Increase remaining time by one minute.
|
||||
* R: Resurrect kid.
|
||||
* K: Kill guard.
|
||||
* Shift+I: Flip the screen upside down.
|
||||
* Shift+W: Slow falling.
|
||||
* Shift+S: Restore a lost hit-point. (Like a small red potion.)
|
||||
* Shift+T: Give more hit-points. (Like a big red potion.)
|
||||
|
||||
===
|
||||
|
||||
* H: Look at the room to the left.
|
||||
* J: Look at the room to the right.
|
||||
* U: Look at the room above.
|
||||
* N: Look at the room below.
|
||||
* Ctrl+B: Go back to the room where the prince is. (Undo H,J,U,N.)
|
||||
|
||||
===
|
||||
|
||||
* [: Shift kid 1 pixel to the left.
|
||||
* ]: Shift kid 1 pixel to the right.
|
||||
* T: Toggle display of timer (remaining minutes:seconds:ticks). Also shows the total elapsed ticks during playback.
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Pause menu и Settings для Sprinter PoP
|
||||
|
||||
Статус: **согласованный план, код не начат** (2026-08-21).
|
||||
Статус: **MS0, MS2 и MS4–MS8 выполнены** (2026-08-23). Pause menu, CFG,
|
||||
Settings, диалоги, Controls и build screen находятся в bank 9. Решение о
|
||||
рендеринге и затемнении — §10.
|
||||
|
||||
Связанные документы:
|
||||
|
||||
@@ -39,12 +41,12 @@ acceleration, scaling, aspect ratio, rumble. UI берёт структуру SD
|
||||
|
||||
```text
|
||||
RESUME
|
||||
QUICKSAVE
|
||||
QUICKLOAD
|
||||
QUICKSAVE (F6)
|
||||
QUICKLOAD (F9)
|
||||
RESTART LEVEL
|
||||
SETTINGS
|
||||
RESTART GAME
|
||||
QUIT
|
||||
QUIT GAME
|
||||
```
|
||||
|
||||
Поведение:
|
||||
@@ -54,7 +56,13 @@ QUIT
|
||||
- QuickSave/QuickLoad только взводят запрос, фактическая операция идёт на
|
||||
безопасной границе кадра;
|
||||
- QuickLoad disabled/показывает `NO QUICKLOAD`, если нет валидных SAV/BAK;
|
||||
- Restart Level и Restart Game требуют подтверждения;
|
||||
- перед QuickLoad из меню лёгкий probe проверяет заголовок и checksum обоих
|
||||
файлов: валидный `POP.BAK` при отсутствующем/битом `POP.SAV` требует
|
||||
отдельного `LOAD BACKUP?`, а не загружается молча;
|
||||
- Restart Level и Restart Game выполняются сразу, БЕЗ подтверждения
|
||||
(2026-08-22): Restart Level перечитывает уровень, Restart Game завершает
|
||||
gameplay и возвращает к первому экрану title/intro; новая игра создаётся
|
||||
общим LEVEL_LOAD только после skip/attract;
|
||||
- Quit требует подтверждения и закрывает файлы/каналы штатным путём;
|
||||
- меню недоступно в demo, cutscene, time-expired и ending;
|
||||
- отдельная debug-комбинация немедленного выхода может остаться только в
|
||||
@@ -82,6 +90,10 @@ BACK
|
||||
`Gameplay profile: VANILLA` показывается read-only: место в модели уже есть,
|
||||
но пользователь не может выбрать ещё не реализованный ENHANCED.
|
||||
|
||||
Изменения применяются немедленно к скорости, читам и звуку, но `POP.CFG`
|
||||
записывается один раз при Back/Esc. На экране есть итог `SETTINGS SAVED` или
|
||||
`SAVE ERROR`; во втором случае runtime-значения остаются рабочими.
|
||||
|
||||
Отладочные параметры `ROOMNAV`, border profiling, stop-frame и переключение
|
||||
double buffering не являются пользовательскими Settings. Они остаются
|
||||
compile-time/debug функциями и скрываются из release UI.
|
||||
@@ -210,10 +222,11 @@ Restart Level:
|
||||
|
||||
Restart Game:
|
||||
|
||||
- подтверждение;
|
||||
- завершить текущий gameplay session;
|
||||
- освободить level/atlas/temporary EMM;
|
||||
- создать новую игру с уровня 1 и новым глобальным таймером;
|
||||
- выполняется сразу, без подтверждения;
|
||||
- завершить текущий gameplay session и вернуть автомат в TITLE;
|
||||
- начать title/intro с самого первого экрана;
|
||||
- создать новую игру с `FIRST_LEVEL` и новым глобальным таймером только
|
||||
после пользовательского skip либо ввода в attract-demo;
|
||||
- настройки оставить;
|
||||
- QuickSave не удалять.
|
||||
|
||||
@@ -230,29 +243,217 @@ Restart Game:
|
||||
- F6/F9 QuickSave/QuickLoad;
|
||||
- Ctrl+S sound;
|
||||
- P speed;
|
||||
- доступные cheats, только если они включены.
|
||||
- доступные cheats, только если они включены: K/Kill Guard, I/Immortal,
|
||||
Shift+L/Next Level, U/Flip Screen и F7/F8/Time −/+ на отдельных понятных
|
||||
строках. Нижней подсказки `Esc or Enter: Back` нет.
|
||||
|
||||
## 10. UI renderer и ввод
|
||||
|
||||
Меню использует общую с title текстовую подсистему:
|
||||
### 10.1. Выбор способа отрисовки: текст против спрайт-атласов
|
||||
|
||||
- фиксированный bitmap font без `_gfx_font_buf` на 2 КБ в W2;
|
||||
- фон/рамка и выделенная строка;
|
||||
- вертикальная навигация, left/right для значения, Enter, Esc;
|
||||
- edge-triggered клавиши поверх существующего `kbd_raw`;
|
||||
- двойная буферизация либо один заранее сохранённый фон menu;
|
||||
- строки и холодный код — в отдельном банке, постоянное состояние — в W2.
|
||||
Ограничение платформы: стандартный текстовый вывод libbgi (`outtextxy`)
|
||||
не годится — он тянет системный знакогенератор в `_gfx_font_buf` (2 КБ
|
||||
статики в W2) плюс жирный резидентный код, а W1/W2 забиты игрой
|
||||
(тот же вывод зафиксирован комментарием в `roomtest_cold.c`, где отладочный
|
||||
борд рисуется палочками именно поэтому). Значит, любой вариант требует
|
||||
СВОЕЙ реализации вывода меню, живущей в отдельном банке (память на банк
|
||||
есть; скорость не критична — меню работает на паузе).
|
||||
|
||||
Рассматривались два подхода.
|
||||
|
||||
**Вариант A — текстовые строки + собственный растровый рендерер.**
|
||||
|
||||
Плюсы:
|
||||
- минимальные данные: шрифт 2–4 КБ + таблицы строк по сотни байт на язык;
|
||||
- весь динамический текст бесплатно: значения опций (ON/OFF,
|
||||
NORMAL/FAST/FASTEST), сообщения (`QUICKSAVED`, `INCOMPATIBLE SAVE`),
|
||||
диалоги (`LOAD BACKUP?`), экран Controls, будущий ввод инициалов
|
||||
Hall of Fame — без текстового движка HoF вообще не сделать;
|
||||
- правка формулировки = правка C-строки, мгновенные итерации;
|
||||
- локализация = вторая таблица строк (+ вторая половина глифов);
|
||||
- **решающий аргумент: так сделано в самом SDLPoP** — см. §10.2.
|
||||
|
||||
Минусы:
|
||||
- надо написать рендерер (блиттер глифа + строка + центрирование +
|
||||
подсветка) — небольшой, но свой;
|
||||
- вид определяется качеством шрифта-ассета.
|
||||
|
||||
**Вариант B — готовые спрайт-атласы** (атлас главного меню с активными/
|
||||
неактивными пунктами, атлас вложенного меню, атлас каждой опции
|
||||
On/Off и т.д.).
|
||||
|
||||
Плюсы:
|
||||
- аутентичный вид: любая типографика/декор запекаются при упаковке;
|
||||
- вывод = существующий блит атласов, текстовый движок не нужен;
|
||||
- язык = другой файл атласа с диска, ноль логики.
|
||||
|
||||
Минусы:
|
||||
- комбинаторика ассетов: 7 пунктов × состояния + вложенные меню + значения
|
||||
всех опций + все сообщения + все диалоги ≈ десятки КБ raw на язык до RLE;
|
||||
второй язык удваивает;
|
||||
- любая правка текста = перегенерация ассетов + перекладка ресурсов;
|
||||
- динамический текст (HoF initials) всё равно потребует шрифтового движка —
|
||||
получили бы ОБЕ системы сразу.
|
||||
|
||||
**Решение (2026-08-22): Вариант A**, шрифт — ассет. Спрайты остаются только
|
||||
для нетекстового декора (рамка/фон меню, маркер выделения — как arrowheads
|
||||
в SDLPoP). Титульный экран — полноэкранная картинка, тема `full_game_plan.md`.
|
||||
|
||||
### 10.2. Референс: как устроено меню в SDLPoP
|
||||
|
||||
`SDLPoP/src/menu.c` + текстовый движок `seg009` — источник структуры:
|
||||
|
||||
- **Текстовые строки + встроенный пропорциональный bitmap-шрифт**
|
||||
`hc_small_font_data[]` (menu.c:2488): символы 32..126, каждый глиф —
|
||||
монохромное изображение переменной ширины; `font_type`
|
||||
{first_char, last_char, space_between_chars, height_above_baseline, chtab}.
|
||||
Никаких per-item атласов, хотя SDL_ttf доступен.
|
||||
- Вывод — портированный движок оригинального DOS PoP (seg009):
|
||||
`draw_text_character` → `method_3_blit_mono(image, x, y, textblit,
|
||||
textcolor)`; `get_line_width` для центрирования; перенос по словам.
|
||||
Тем же движком рисуются in-game тексты и copy protection.
|
||||
- Пункты меню — data-driven C-структуры `{id, previous, next, required,
|
||||
char text[32]}` + таблицы `pause_menu_items[]` / `settings_menu_items[]`;
|
||||
`required` — указатель на флаг disabled, такие пункты пропускаются при
|
||||
навигации (prev/next пересчитываются).
|
||||
- Выделенный пункт = смена цвета текста (bright-white против обычного) +
|
||||
рамка-контур `draw_rect_contours(selection_box, lightgray)`; НЕ отдельный
|
||||
спрайт «активного пункта».
|
||||
- Фон меню — затемнение замороженного игрового кадра:
|
||||
`draw_rect_with_alpha(black, alpha=120)`, внизу просвечивает «GAME PAUSED».
|
||||
- Settings — декларативная таблица `setting_type` со стилями TOGGLE / NUMBER /
|
||||
TEXT_ONLY / KEY, геттером/сеттером/increase/decrease значения, строкой-
|
||||
explanation внизу экрана, скроллом длинных списков и фокусом «левая половина
|
||||
(список) / правая половина (значения)».
|
||||
- Диалоги — один общий `draw_confirmation_dialog(text)` + обработчик
|
||||
результата; диалог возвращает решение автомату меню.
|
||||
- Мини-спрайты только для декора значений (arrowheads up/down/left/right).
|
||||
- Навигация озвучена (menu tick), ввод клавиатура+мышь, hover по прямоугольникам.
|
||||
|
||||
### 10.3. Наша реализация
|
||||
|
||||
- Банк 9: код рендерера,
|
||||
шрифт, таблицы строк, автомат меню. Резидентно — только request-flag и
|
||||
вызов процесса на границе кадра (паттерн pop_qsave_io).
|
||||
- Рендерер повторяет минимальный контракт seg009: пропорциональные глифы,
|
||||
baseline, `draw_string` и центрирование по сумме advance. Блит идёт через
|
||||
W0-атлас, в `GFX_BANK_SPRITE`: `0xFF` в атласе пропускается, а UI временный
|
||||
и не портит теневую копию игрового фона. Перед каждым кадром UI `gfx_copy_page` переносит чистый
|
||||
shadow видимой страницы в скрытую, затем готовый кадр показывается только
|
||||
на следующем фронте. При выходе чистый фон тем же способом возвращается на
|
||||
обе страницы и восстанавливается исходная visible-страница. Поэтому
|
||||
перемещение выделения не показывает поэтапную перерисовку и не оставляет
|
||||
следов на back buffer.
|
||||
- Шрифт — АССЕТ из **оригинальных** `hc_small_font_data[]` и
|
||||
`hc_font_data[]` SDLPoP, не системный ZG и не TTF. Паковщик
|
||||
`toolchain/pop_extract_font.py` делает `FONT\\font.atl`: 95 ASCII-глифов
|
||||
малого и 95 крупного шрифта (7667 Б). Номер ленты вычисляется из ASCII,
|
||||
поэтому это один текстовый движок, а не атлас готовых надписей.
|
||||
- Двуязычность (eng/rus): строки храним в CP866 — латиница и кириллица одним
|
||||
байтовым порядком, одна кодировка на оба алфавита. Локаль = пара
|
||||
(указатель на таблицу строк, файл шрифта); переключатель — одна настройка.
|
||||
Русские строки длиннее английских ~10–15% — раскладку экранов и ширину
|
||||
колонок закладывать по русской. Второй язык можно добавить позже без
|
||||
переделки: сначала eng.
|
||||
- Визуальная композиция MS4 следует SDLPoP: замороженная сцена остаётся
|
||||
открытой, поверх неё компактный центрированный список без чёрной карточки,
|
||||
выбранная строка обведена тонким светло-серым контуром, а крупное
|
||||
`GAME PAUSED` лежит в нижнем борту. Цвета текста и контура берутся из
|
||||
стабильного диапазона палитры 0x37..0x3F.
|
||||
- Фон открытого меню: снимок текущей палитры, затемнение всех слотов кроме
|
||||
UI 0x37..0x3F и точное восстановление при выходе. Снимок хранится в
|
||||
свободном хвосте EMM-страницы шрифта, не в W2.
|
||||
- Навигация MS4: вверх/вниз, Enter/Esc, edge-triggered поверх `kbd_raw`.
|
||||
Left/right и menu tick добавляются вместе с настройками на MS5.
|
||||
|
||||
Первый UI может быть визуально простым. Критично отсутствие потери клавиш,
|
||||
предсказуемая пауза и отсутствие повреждения игрового back buffer.
|
||||
|
||||
### 10.4. Затенение экрана под меню — решение MS4
|
||||
|
||||
Режим меню виден сразу: bank 9 делает динамический снимок palette 0,
|
||||
затемняет RGB-каналы вдвое и пишет одинаковый результат в обе экранные
|
||||
палитры. Девять стабильных UI-слотов 0x37..0x3F не гасятся. При Resume/Enter
|
||||
палитра восстанавливается из EMM-снимка. Это выбранный вариант Б ниже;
|
||||
ступенчатый fade для роликов пока не нужен и остаётся отдельной будущей
|
||||
задачей, а не причиной раздувать MS4.
|
||||
|
||||
**Как сделано в SDLPoP** (`seg009.c`):
|
||||
|
||||
- Меню: `draw_rect_with_alpha(&screen_rect, color_0_black, pause_menu_alpha)`
|
||||
(menu.c:1364) — альфа-заливка чёрным поверх замороженного кадра средствами
|
||||
SDL; нижняя полоса рисуется с alpha=0, чтобы сквозь неё просвечивало
|
||||
«GAME PAUSED». Прямого аналога на Sprinter НЕТ (альфа-блендинг в железе
|
||||
отсутствует) — это SDL-специфика, переносить нечего.
|
||||
- Ролики/переходы: `fade_in_2/fade_out_2(rows)` (seg009.c:3947+, вызовы из
|
||||
seg000.c) — ПОШАГОВОЕ затухание ПАЛИТРЫ к чёрному и обратно: палитра
|
||||
копируется, каждая строка по 16 цветов гасится за несколько кадров
|
||||
(`which_rows` маской выбирает, какие строки участвуют: 0x800/0x1000/...).
|
||||
Вот этот механизм на Sprinter воспроизводим один в один.
|
||||
|
||||
Отсюда рабочая гипотеза: наш примитив = «снимок текущей палитры → ступенчатое
|
||||
приближение к затемнённой копии (кроме резервного блока для UI)», статично для
|
||||
меню и анимированно для роликов/переходов. Варианты:
|
||||
|
||||
**Вариант А — единая основная палитра (глобальный рефакторинг палитры).**
|
||||
|
||||
1. Собрать ВСЕ палитры игры (уровневые наборы `pal_env*`, kid.pal, палитра
|
||||
Тени и пр.) в одну общую 256-цветную; использовать её целиком всегда.
|
||||
Сейчас переиспользования цветов НЕТ — каждая загрузка ассетов перезаписывает
|
||||
слоты (см. pop_boot: kid.pal затирает тайловые цвета, приходится
|
||||
восстанавливать `pop_bg_pal_apply`/`pop_shadow_pal_apply`).
|
||||
2. Для затенения — затемнённая копия основной палитры, КРОМЕ зарезервированного
|
||||
блока из 16 цветов для самого меню (кандидат — стандартные 16 цветов VGA).
|
||||
3. Выход из меню — возврат к полной основной палитре.
|
||||
|
||||
Плюс: решает попутно существующую боль с перезаписью палитр при загрузках.
|
||||
Минус: большой разовый рефакторинг упаковщиков и всех загрузчиков атласов;
|
||||
нужен аудит, что все цвета всех уровней влезают в 256. **Против говорит
|
||||
план перевода камней подземелья на цвета VGA-версии PoP: там ряд уровней
|
||||
несёт ДРУГУЮ палитру, отличную от SDLPoP (VDUNGEON/VPALACE каскад,
|
||||
levels_plan.md), — единая палитра этому прямо противоречит.**
|
||||
|
||||
**Вариант Б — динамический снимок текущей палитры (сейчас выглядит
|
||||
предпочтительным).**
|
||||
|
||||
1. При открытии меню прочитать всю текущую палитру, сохранить.
|
||||
2. Записать затемнённую копию (кроме зарезервированного блока для меню).
|
||||
3. При выходе — восстановить сохранённую.
|
||||
|
||||
Плюс: локальная правка внутри меню, ничего в пайплайне ассетов не меняется;
|
||||
работает при любой текущей палитре автоматически — включая будущие
|
||||
уровне-специфичные палитры VGA-камней; тот же примитив ступенями даёт
|
||||
fade-out/fade-in для роликов и переходов между уровнями (как fade_*_2 в
|
||||
SDLPoP). Минус: чтение/запись 256 записей палитры при входе/выходе (раз на
|
||||
открытие — дёшево); затемнение «на глаз» может по-разному выглядеть на разных
|
||||
уровнях.
|
||||
|
||||
Резервный блок 16 цветов нужен в ОБОИХ вариантах; текущий диапазон 0x37..0x3F
|
||||
(стабильный, проверен) даёт 9 цветов — этого может не хватить на
|
||||
текст+подсветку+рамку, тогда резервировать отдельный блок.
|
||||
|
||||
**Следствие для архитектуры:** работа с цветом/палитрой должна собраться в
|
||||
ОДИН модуль (сейчас она разбросана: gfx_pal_* вызовы в boot, pop_bg_pal_apply,
|
||||
pop_shadow_pal_apply, вспышки урона в roomtest.c и т.д.). Модуль палитры —
|
||||
единственный владелец записи в палитру и предоставляет примитивы, которые
|
||||
понадобятся и меню, и роликам:
|
||||
|
||||
```text
|
||||
pal_snapshot()/pal_restore() — снимок/восстановление всей палитры
|
||||
pal_dim(step) / pal_undim(step) — ступени затемнения (кроме резервного блока)
|
||||
pal_fade_out(rows)/pal_fade_in(rows) — анимированное затухание по строкам
|
||||
(порт fade_out_2/fade_in_2, seg009)
|
||||
```
|
||||
|
||||
Меню уже использует snapshot+dim локально в bank 9. Когда появятся ролики,
|
||||
выделить из него общий palette/fade-модуль; вспышка урона сможет переехать
|
||||
туда же после отдельного аудита.
|
||||
|
||||
## 11. Диалоги
|
||||
|
||||
Общий диалог подтверждения:
|
||||
|
||||
```text
|
||||
RESTART LEVEL?
|
||||
RESTART GAME?
|
||||
QUIT GAME?
|
||||
RESTORE DEFAULTS?
|
||||
LOAD BACKUP?
|
||||
@@ -262,6 +463,10 @@ YES / NO
|
||||
|
||||
Диалог не выполняет действие напрямую: он возвращает решение автомату меню,
|
||||
который формирует команду приложению. Так UI не зависит от gameplay-модулей.
|
||||
По умолчанию выбран `NO`; Up/Down/Left/Right меняют ответ, Enter подтверждает,
|
||||
Esc отменяет. Реализованы все три вопроса: Quit, Restore defaults и backup
|
||||
QuickLoad. В Quit-dialog вопрос и `YES / [NO]` заключены в общую рамку;
|
||||
отдельная строка `Enter: Select Esc: Cancel` не выводится.
|
||||
|
||||
## 12. Будущий ENHANCED
|
||||
|
||||
@@ -282,15 +487,15 @@ YES / NO
|
||||
|
||||
| этап | результат | критерий приёмки |
|
||||
|---|---|---|
|
||||
| **MS0** | определить команды app/menu и структуру settings | UI не вызывает gameplay internals напрямую |
|
||||
| **MS0** ✓ | определить команды app/menu и структуру settings | UI возвращает команду главному циклу; прямых gameplay-вызовов нет |
|
||||
| **MS1** | проверить запись/rename/copy на HDD DSS | crash/power-loss сценарий не теряет обе копии save |
|
||||
| **MS2** | `POP.CFG`: defaults, load, validate, save | повреждённый CFG безопасно даёт defaults |
|
||||
| **MS2** ✓ | `POP.CFG`: defaults, load, validate, save | v1 codec, будущий хвост, checksum; повреждённый CFG даёт defaults |
|
||||
| **MS3** | QuickSave hotkeys + POP.SAV/BAK | полный критерий `quicksave_plan.md` |
|
||||
| **MS4** | минимальный pause menu | все семь пунктов доступны и корректно паузят игру |
|
||||
| **MS5** | General/Gameplay Settings | значения применяются и переживают рестарт |
|
||||
| **MS6** | dialogs + backup recovery | подтверждения и fallback на POP.BAK |
|
||||
| **MS7** | Controls help | полная актуальная раскладка на экране |
|
||||
| **MS8** | интеграция с title/build info | CFG применяется до первого экрана |
|
||||
| **MS4** ✓ | текстовый рендерер + два шрифта (малый для пунктов, крупный для сообщений) + минимальный pause menu | SDLPoP fonts в одном W0-atlas, центрирование, dim/restore palette и tear-free page flip; все семь пунктов видимы, навигация и Resume/QuickSave работают в MAME |
|
||||
| **MS5** ✓ | General/Gameplay Settings | значения применяются сразу и после Back/Esc записываются в POP.CFG |
|
||||
| **MS6** ✓ | dialogs + backup recovery | подтверждения default-NO; QuickLoad спрашивает перед валидным POP.BAK |
|
||||
| **MS7** ✓ | Controls help | показаны движение, action, menu, save/load, звук, speed и conditional cheats; MAME проверил отдельные K/I и Shift+L/U и возврат Esc ровно на один уровень |
|
||||
| **MS8** ✓ | build info | CFG читается до первого показа; включаемый build screen получает ID и дату из Make/git |
|
||||
|
||||
QuickSave (`MS1/MS3`) можно реализовать раньше визуального menu: сначала
|
||||
F6/F9 и сообщения, затем подключить те же команды к пунктам UI.
|
||||
@@ -298,12 +503,18 @@ F6/F9 и сообщения, затем подключить те же кома
|
||||
## 14. Тесты
|
||||
|
||||
- Host: CFG round-trip, defaults, bad magic/version/size/checksum.
|
||||
- Host: menu navigation, disabled items, confirmations, команды приложению.
|
||||
- Host: меню navigation, disabled items, confirmations, команды приложению.
|
||||
- Host: рендерер строк — вывод глифов обеих локалей, центрирование,
|
||||
ширина строки для малого и крупного шрифта.
|
||||
- Host: SAV invalid -> BAK valid; оба invalid -> NO QUICKLOAD.
|
||||
- MAME: F6, изменение сцены, F9; затем рестарт программы и повторный F9.
|
||||
- MAME: прервать запись/испортить SAV — BAK остаётся загружаемым.
|
||||
- MAME: pause на бое/падении, Resume не меняет состояние и таймер.
|
||||
- MAME: pause на бое/падении, Resume не меняет состояние и таймер; смена
|
||||
выбранного пункта не показывает промежуточный кадр и после закрытия не
|
||||
оставляет меню на второй странице.
|
||||
- MAME: Settings сохраняются после полного выхода и запуска с HDD.
|
||||
- MAME: включить Show Sprinter screen, перезапустить `roomtest`, увидеть
|
||||
build ID/date до первого игрового кадра и пропустить экран Esc/Enter/Space.
|
||||
- Проверка лимита 8 DSS handles на каждом error path.
|
||||
- `make size-check`; menu/text строки не должны съесть резидентный бюджет.
|
||||
|
||||
@@ -316,4 +527,3 @@ F6/F9 и сообщения, затем подключить те же кома
|
||||
- key rebinding;
|
||||
- SDL visual/controller options;
|
||||
- фактическая реализация ENHANCED и individual fix switches.
|
||||
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
# План: консолидация работы с палитрами + переход уровня через fade
|
||||
|
||||
Статус: **этапы A и B реализованы; визуальная приёмка полного маршрута ещё
|
||||
идёт** (2026-08-24). Палитры выделены в bank 10, а renderer cutscene/intro —
|
||||
в bank 11, чтобы не переполнять bank 9 оболочки.
|
||||
Обсуждение велось вокруг `roomtest/` (банк 9 — оболочка, fade из `pop_ui.c`).
|
||||
|
||||
---
|
||||
|
||||
## 1. Текущее состояние: карта палитры
|
||||
|
||||
Палитра Sprinter — 256 записей по 4 байта (B, G, R, 0) = 1 КБ на страницу.
|
||||
У страниц дабл-буфера ДВЕ раздельные палитры (`gfx_pal_load(0,…)` и
|
||||
`gfx_pal_load(1,…)` — почти всегда парой). BIOS читает буферы только из
|
||||
#4000–#BFFF: банковую rodata напрямую отдавать нельзя (копия в стек/W2),
|
||||
см. грабли `pop_guard_set_palette` и `bg_load_tile_pal`.
|
||||
|
||||
### 1.1 Игровая палитра `KID\kid.pal` — раскладка слотов
|
||||
|
||||
Собирается `toolchain/pop_pack_kid.py build_palette()`, грузится одним
|
||||
`gfx_pal_fload` (перезаписывает все 256 записей). Атласы запекались под эти
|
||||
индексы — менять раскладку нельзя без перепаковки ассетов.
|
||||
|
||||
| Слоты | Назначение | Источник | Динамика |
|
||||
|---|---|---|---|
|
||||
| 0x00 | Цвет фона + **вспышка молнии** (подмена записи 0, `flash_bg` ← do_flash/set_bg_attr SDLPoP) | — | меняется в игре |
|
||||
| 0x01–0x2F | Не закреплены (нули) | — | свободно |
|
||||
| 0x30–0x3F | VGA16 — базовые 16 цветов для mono-блитов: пламя факелов, пузырьки зелья (+12 красный «лечение», +10 зелёный, +9 синий), кровь чомпера (12), дворцовая кладка mono (+6) | `VGA16[]` | статично |
|
||||
| ↳ 0x37–0x3F | Поддиапазон **UI**: текст/рамка меню; единственное, что `keep_ui` не затемняет (`MENU_BORDER`=0x37) | — | — |
|
||||
| 0x40–0x4F | chtab_1 пламя/зелья (`POT_PAL_BASE`) | VDUNGEON res150.pal | статично |
|
||||
| 0x50–0x5F | **ENV фон тайлсета** (`POP_PAL_ENV`) | res200.pal набора | **меняется при смене тайлсета** |
|
||||
| 0x60–0x6F | **WALL тайлсета** (`POP_PAL_WALL`) | res360.pal набора | **меняется при смене тайлсета** |
|
||||
| 0x70–0x7F | Kid (`PAL_BASE`) | KID res400.pal | статично |
|
||||
| 0x80–0x8F | Меч chtab_0 (`SWORD_PAL_BASE`) | POT res700.pal | статично |
|
||||
| 0x90–0x9F | Страж chtab_5 (`GUARD_PAL_BASE`) | res10.bin guard_palettes | **меняется по КОМНАТАМ** |
|
||||
| 0xA0–0xAF | Тень (`POP_SHADOW_PAL_BASE`) | RGB-сетка pop_pack_shadow.py | статично |
|
||||
| 0xB0–0xFF | Свободны (5 слотов) | — | — |
|
||||
|
||||
Итого динамических зон три: запись 0 (молния), env+wall (тип здания),
|
||||
стражи (per-room). Всё остальное одинаково всю игру.
|
||||
|
||||
### 1.2 Полноэкранные палитры заставок
|
||||
|
||||
Каждая перезаписывает ВСЕ 256 записей:
|
||||
|
||||
| Файл | Где используется |
|
||||
|---|---|
|
||||
| `KID\kid.pal` (+ fallback `a:\kid.pal`) | BOOT и возврат в игру после заставок |
|
||||
| `TITLE\title.pal` | экран TITLE |
|
||||
| `PV\story.pal` | INTRO и HALL_OF_FAME (одна палитра на обе фазы) |
|
||||
|
||||
### 1.3 Тайлсеты: подземелье ↔ дворец
|
||||
|
||||
Оба набора используют ОДНИ И ТЕ ЖЕ слоты 0x50–0x5F/0x60–0x6F, заполняя их
|
||||
разными цветами (атласы обоих наборов запекались под эти индексы).
|
||||
Переключение = загрузка 64 байт (32 записи env+wall) в обе страницы
|
||||
(`bg_load_tile_pal`); остальные 224 записи не трогаются.
|
||||
|
||||
Какие уровни дворец — `tbl_level_type` (`pop_level_cold.c:44`):
|
||||
**4, 5, 6, 10, 11, 14**; остальные подземелье.
|
||||
|
||||
Палитра дворца `pal_tile.pal` (расшифровка, формат записи B,G,R):
|
||||
|
||||
ENV 0x50–0x5F (пол, ковры, факелы, ворота, пики, арки):
|
||||
|
||||
| Слот | RGB | | Слот | RGB |
|
||||
|---|---|---|---|---|
|
||||
| 50 | 0,0,0 чёрный | | 58 | 202,190,178 серо-бежевый |
|
||||
| 51 | 121,89,60 коричневый | | 59 | 153,133,129 серо-лиловый |
|
||||
| 52 | 161,121,76 светло-коричневый | | 5A | 76,64,56 тёмный серо-бурый |
|
||||
| 53 | 194,149,89 песочный | | 5B | 153,97,89 кирпично-красный |
|
||||
| 54 | 230,178,113 яркий песок | | 5C | 137,80,72 тёмный кирпич |
|
||||
| 55 | 246,202,125 кремовый | | 5D | 48,125,125 бирюзовый |
|
||||
| 56 | 255,234,170 бледно-кремовый | | 5E | 12,56,89 тёмно-синий |
|
||||
| 57 | 255,255,255 белый | | 5F | 202,56,28 красно-оранжевый |
|
||||
|
||||
WALL 0x60–0x6F (вся палитра песочная): 61=(218,170,89), 62=(226,165,93),
|
||||
63=(226,170,97), 64=(218,161,85), 65=белый, 66=(226,165,93), 67=(218,165,89),
|
||||
68=(226,170,89), 69=(218,170,97), 6A=(255,210,137), 6B=(255,218,149),
|
||||
6C=(255,210,137), 6D=(255,218,145), 6E=(194,153,80 тёмный песок),
|
||||
6F=(238,186,117).
|
||||
|
||||
Чем рисуется во дворце:
|
||||
- **Тело стены — НЕ спрайты**, а сплошные заливки; цвет разыгрывается на
|
||||
комнату prandom'ом (`gen_palace_wall_colors`, `pop_bg.c:140`, порт
|
||||
seg000:1942): подряды 1 и 3 берут случайный из 0x61–0x64, подряды 0 и 2 —
|
||||
из 0x66–0x69; соседи по горизонтали не повторяются.
|
||||
- Декор стен id 3–17 — mono-силуэт цветом VGA16+6 (0x36).
|
||||
- Верх дверных проёмов дворца — спец-id 78–84 + полоса 145 («полоса под
|
||||
окнами», pop_room.c:478).
|
||||
- Остальное (пол, ковры, порталы-факелы, ворота, пики) — env-куски
|
||||
pal_env*.atl с ENV-таблицей выше.
|
||||
|
||||
### 1.4 Стражи (0x90–0x9F)
|
||||
|
||||
Цвет задаётся на КОМНАТУ (`level.guards_color[room-1]`), при входе в
|
||||
комнату зовётся `pop_guard_set_palette(color)` ДО отрисовки (слоты общие
|
||||
на экран — смена посреди кадра дала бы стража в новой палитре с полосой HP
|
||||
в старой). Только для обычных стражей (`tbl_guard_type == 0`): скелет и
|
||||
Джафар имеют собственную палитру, зашитую в kid.pal; им зовётся с color=0
|
||||
(не трогать — иначе Джафар на ур.13 покрасился бы в цвет стража своей
|
||||
комнаты). Внутри одного уровня слоты могут перезаписываться многократно.
|
||||
|
||||
## 2. Текущее состояние: механика fade
|
||||
|
||||
### 2.1 Наша реализация (`pop_ui.c`, банк 9)
|
||||
|
||||
- `pop_ui_palette_snapshot()` — снимок всех 256 записей через
|
||||
`gfx_pal_get` по 4 чанкам × 64; хранится в хвосте страницы шрифта
|
||||
FONT.ATL ([0x3C00,0x4000)), map/unmap W0. Требует `font_ready`.
|
||||
- `pop_ui_palette_dim(step, keep_ui)` — готовит ОБЕ экранные палитры из
|
||||
снимка. Шкала без умножений (только сдвиги):
|
||||
|
||||
| Шаг | Формула на канал | Яркость |
|
||||
|---|---|---|
|
||||
| 0 | x | оригинал |
|
||||
| 1 | `(x>>1)+(x>>2)` | ≈3/4 |
|
||||
| 2 | `x>>1` | 1/2 |
|
||||
| 3 | `x>>2` | 1/4 |
|
||||
| 4 | 0 | чёрный |
|
||||
|
||||
`keep_ui` пропускает 0x37–0x3F (меню остаётся ярким).
|
||||
- `pop_ui_fade_out/in(steps)` — проигрывание ступеней за `steps` кадров
|
||||
vsync (`step = i*4/steps`, целочисленно): steps=4 — канонический (по кадру
|
||||
на ступень), steps<4 — перескакивает ступени, steps>4 — повторяет (плавнее),
|
||||
steps=0 у fade_in — мгновенный restore.
|
||||
- Контракт map/unmap: обращения к EMM/W0 и BIOS-палитре строго после unmap.
|
||||
|
||||
Стоимость одного dim ≈ 15–25 тыс. тактов (~4–7 мс при 3.5 МГц) —
|
||||
укладывается в кадр vsync, на практике лагов нет.
|
||||
|
||||
### 2.2 Как сделано в SDLPoP (seg009.c, USE_FADE/gmMcgaVga)
|
||||
|
||||
- fade_out: каждый кадр КАЖДЫЙ ненулевой канал каждой записи −1; до нуля.
|
||||
- fade_in: `fade_pos` от 0x40 вниз; канал +1, пока меньше оригинала.
|
||||
- Уровней затемнения до 63–64 (VGA-канал 6 бит), полный фейд ~63 кадра ×
|
||||
wait_time=2 тика — медленно и кинематографично.
|
||||
- `which_rows` — битовая маска групп по 16 записей: можно фейдить часть
|
||||
палитры (в оригинале используется).
|
||||
- По завершении принудительно восстанавливается оригинал; после out экран
|
||||
заливается чёрным.
|
||||
|
||||
Это осознанное расхождение (скорость/такты vs плавность) — ЗАПИСАТЬ в
|
||||
`docs/impl_diff.md` (сейчас записи нет).
|
||||
|
||||
## 3. Зафиксированные решения
|
||||
|
||||
1. **Ступени затемнения: остаются 4.** Вариант 8 ступеней той же сдвиговой
|
||||
техникой — рассмотреть отдельно, сейчас не внедрять.
|
||||
2. **Предрасчёт fade-вариантов палитры отклонён.** Аргументы: чтение файла
|
||||
с диска на порядок дороже вычисления; 3–7 КБ постоянной RAM при
|
||||
MEMORY=small непозволительны; предрасчёт привязан к конкретным палитрам,
|
||||
а снимок работает с любой текущей автоматически; keep_ui удвоил бы набор.
|
||||
3. **Считать на лету**, хранить один снимок (уже есть, бесплатно в хвосте
|
||||
страницы шрифта).
|
||||
4. **Буферы на стеке**, не статика (W1/W2 мало) и не 1 КБ: обнулить 64/256
|
||||
байт дешевле, чем держать килобайт резидентно.
|
||||
5. **Контракт `gfx_pal_load(pal, start, count, data)`**: count — число
|
||||
СЛОТОВ, буфер обязан быть `count*4` байт; count=0 означает «все 256».
|
||||
6. **Leaf-applеры остаются на месте** (`pop_bg_pal_apply` — банк 7 со своими
|
||||
таблицами, `pop_shadow_pal_apply`, `pop_guard_set_palette`): банковая
|
||||
rodata чужого банка не видна, перенос сломал бы доступ к данным.
|
||||
7. **Молния (`flash_bg` в roomtest.c) не переносится** — игровой эффект
|
||||
записи 0; после вспышки восстановление записи 0 из снимка ложится на API.
|
||||
8. Модель состояния: разделены «какая палитра логически загружена» (load_*)
|
||||
и «с какой яркостью показана» (apply/fade). Любой load_* обновляет снимок;
|
||||
apply/fade показывает его с нужной глубиной. Это позволяет грузить новую
|
||||
палитру «в темноте» (экран остаётся чёрным, пока не позвали apply/fade_in).
|
||||
|
||||
## 4. Целевой API `pop_pal.c/.h` (банк 9)
|
||||
|
||||
```c
|
||||
/* сброс */
|
||||
void pop_pal_black(void) __banked;
|
||||
/* все 256 записей ОБЕИХ страниц = 0. Стековый buf[256], обнуление циклом,
|
||||
* 8 вызовов gfx_pal_load (4 чанка × 2 страницы, паттерн как в dim).
|
||||
* Зовётся СРАЗУ ПОСЛЕ initgraph в pop_boot (раньше нельзя — нет гарантий
|
||||
* состояния графического режима): закрывает кейс «мусор/палитра предыдущей
|
||||
* программы при включении графики». СНИМОК НЕ ТРОГАЕТ (контракт:
|
||||
* чёрный экран без изменения логической палитры). */
|
||||
|
||||
/* загрузка (пишет полную палитру в обе страницы + refresh снимка;
|
||||
* видимую яркость НЕ трогают — экран меняется только по apply/fade) */
|
||||
void pop_pal_file_load(const char *name) __banked;
|
||||
/* gfx_pal_fload + fallback "a:\" + gfx_pal_sync (fallback сегодня
|
||||
* скопирован в каждом из ~6 мест вызова) */
|
||||
|
||||
void pop_pal_game_load(void) __banked;
|
||||
/* file_load("KID\kid.pal") + pop_bg_pal_apply + pop_shadow_pal_apply.
|
||||
* Сегодня тройка скопирована 3 раза (roomtest_cold ~958, pop_title ~88,
|
||||
* pop_intro ~183). Единое место инварианта «kid.pal затирает слоты
|
||||
* тайлсета 0x50..0x6F и тени 0xA0..0xAF». */
|
||||
|
||||
void pop_pal_level_load(uint8_t full) __banked;
|
||||
/* палитра уровня: kid.pal/shadow + tileset 0x50..0x6F если набор сменился
|
||||
* (сравнение через pop_level_type()). full=1 — ПРИНУДИТЕЛЬНО перечитать
|
||||
* kid.pal/shadow (один экспорт с флагом, не две функции — меньше банковых
|
||||
* точек входа). СТРАЖЕЙ (0x90..0x9F) НЕ включает: это компетенция входа
|
||||
* в комнату (pop_guard_set_palette до первого draw). */
|
||||
|
||||
void pop_pal_story_load(void) __banked; /* PV\story.pal (INTRO и HOF — файл один, функция одна) */
|
||||
void pop_pal_title_load(void) __banked; /* TITLE\title.pal */
|
||||
|
||||
/* отображение */
|
||||
void pop_pal_snapshot(void) __banked; /* переезд из pop_ui, тело то же */
|
||||
void pop_pal_apply(uint8_t fade) __banked; /* = dim(fade, 0), 0..4 */
|
||||
void pop_pal_fade_in(uint8_t steps) __banked; /* переезд из pop_ui */
|
||||
void pop_pal_fade_out(uint8_t steps) __banked;
|
||||
|
||||
/* меню продолжает звать низкоуровневый dim(step, keep_ui=1) — отдельный
|
||||
* тонкий экспорт, чтобы не тащить флаг в горячий apply. Старые имена
|
||||
* pop_ui_palette_* / pop_ui_fade_* УДАЛЯЮТСЯ (без алиасов — меньше
|
||||
* экспорта банка). */
|
||||
```
|
||||
|
||||
Соответствие старое→новое: snapshot→snapshot, restore→apply(0),
|
||||
fade_out/in→fade_out/in, тройка kid.pal×3→game_load, fload+fallback+sync×6→file_load.
|
||||
|
||||
## 5. Этап A: рефакторинг — выполнен (2026-08-24)
|
||||
|
||||
1. Создан `roomtest/pop_pal.c/.h` в **bank 10**, добавлен в Makefile.
|
||||
Он владеет политикой `load logical palette → snapshot → apply brightness`.
|
||||
Низкоуровневые snapshot/dim/fade остаются физически в `pop_ui.c`: там
|
||||
владелец страницы FONT.ATL, где лежит снимок; наружу они доступны только
|
||||
через `pop_pal`.
|
||||
2. Заменены call-sites:
|
||||
- `roomtest_cold.c` ~958: black → game_load вместо тройки;
|
||||
- `pop_title.c` title_restore_game_palette → game_load; загрузка title.pal → title_load;
|
||||
- `pop_intro.c` intro_load/intro_restore → story_load/game_load;
|
||||
- `pop_hof.c` (2 × story.pal) → story_load;
|
||||
- `pop_menu.c`: fade/dim → новые имена (dim с keep_ui — низкоуровневый экспорт);
|
||||
- `roomtest.c` demo-start (snapshot+dim(4,0)+fade_in(4)) → новый API.
|
||||
3. Старые вызовы не остаются в коде приложения; внутренние функции `pop_ui`
|
||||
сохранены как реализации одного владельца памяти снимка.
|
||||
4. Сборка и host-тесты пройдены. `make size-check` неприменим: меняется
|
||||
приложение, а не libc/libbgi.
|
||||
5. MAME smoke-тест полного цикла смен палитр: boot → title (title.pal +
|
||||
fade) → intro (story/kid) → demo fade-in → игра → HOF (story.pal).
|
||||
Проверить: отсутствие мусора при включении графики (эффект black),
|
||||
меню с keep_ui остаётся ярким при затемнении, молния (запись 0)
|
||||
восстанавливается.
|
||||
|
||||
## 6. Этап B: переход уровня через fade — реализован, ждёт визуальной приёмки
|
||||
|
||||
Сценарий (обсуждён, детали уточнить по SDLPoP перед реализацией — как
|
||||
оригинал делает смену уровня, есть ли там fade в DOS-версии):
|
||||
|
||||
```
|
||||
fade_out // последний кадр уровня N темнеет
|
||||
рисуем комнату 1 уровня N+1 // во ВТОРУЮ страницу, в темноте
|
||||
pop_pal_level_load(full=0) // новая палитра: железо+снимок обновлены,
|
||||
// экран всё ещё чёрный
|
||||
флип + копия второй страницы обратно в первую
|
||||
fade_in // = анимированный apply 3→2→1→0
|
||||
```
|
||||
|
||||
Экономия: реально переезжают только 32 записи (env/wall) при смене набора
|
||||
dungeon↔palace; guards_color обновит вход в комнату. Kid/shadow не меняются
|
||||
— потому full=0.
|
||||
|
||||
Реализация находится в `roomtest.c` / `roomtest_cold.c`: последний кадр
|
||||
уровня N темнеет, `pop_level_switch()` подготавливает первый кадр N+1 и
|
||||
обновляет логический источник через `pop_pal_level_load(1)`, затем главный
|
||||
цикл показывает кадр только через fade-in. Восемь ступеней и отдельная
|
||||
анимация смерти не входят в этот этап.
|
||||
|
||||
**Этап B закрывает два открытых бага** (разборы — `roomtest/BUGS_OPEN.md`):
|
||||
- [PAL-L1-AFTER-INTRO] — вход в игру на уровень 1 после интро с чёрным
|
||||
экраном (маршрут demo_new_game; корень не установлен, воспроизведение
|
||||
нестабильно);
|
||||
- [PAL-DUNGEON-STALE] — переход 3→4 оставляет подземную палитру (корень
|
||||
ясен: fade_in восстанавливает из снимка, снятого ДО загрузки тайлсета
|
||||
дворца; быстрый фикс `fade_in_pending` 2026-08-23 сам же и проявляет этот
|
||||
дефект модели).
|
||||
|
||||
Быстрый фикс 2026-08-23 (маршрут CUTSCENE → LEVEL_LOAD → PLAYING,
|
||||
`fade_in_pending` + `pop_ui_fade_in(4)` после `pop_level_switch`) закрыл
|
||||
чёрный экран на переходах с pre-cutscene внутри подземелья (1→2), но модель
|
||||
«кто и когда меняет яркость» остаётся разношёрстной — её и приводит в
|
||||
порядок этап B.
|
||||
|
||||
## 7. Этап C: документирование
|
||||
|
||||
- Запись в `docs/impl_diff.md`: наши 4 ступени vs SDLPoP ~64 (что делает
|
||||
оригинал, что делаем мы — сдвиговая шкала ради тактов, чем платим —
|
||||
грубее градации, что проверять при регрессе).
|
||||
- После этапа B — дополнить запись про сам переход.
|
||||
|
||||
## 8. Не трогаем
|
||||
|
||||
- Молнию (`flash_bg`, roomtest.c) — включая обход SDCC-бага
|
||||
`gfx_pal_set(0,0,0,0,0)` → ручные `gfx_pal_set(0/1, 0, r,g,b)`;
|
||||
- leaf-applеры: `pop_bg_pal_apply` (банк 7), `pop_shadow_pal_apply`,
|
||||
`pop_guard_set_palette` (данные своих модулей);
|
||||
- хранилище снимка в хвосте страницы шрифта FONT.ATL (бесплатное место,
|
||||
guard `font_ready`);
|
||||
- раскладку слотов 0x00–0xAF (зафиксирована атласами).
|
||||
@@ -1,6 +1,8 @@
|
||||
# QuickSave / QuickLoad — разбор оригинала и план реализации
|
||||
|
||||
Статус: **план, код не начат** (2026-08-17). Задача на доске —
|
||||
Статус: **РЕАЛИЗОВАНО и проверено в MAME** (2026-08-22; F6/F9, POP.SAV +
|
||||
POP.BAK — см. коммит `v0.6-pop-quicksave`). Документ оставлен как
|
||||
справочник по формату снимка и разбору. Задача на доске —
|
||||
[`../roomtest/TASKS_OPEN.md#qsave`](../roomtest/TASKS_OPEN.md#qsave).
|
||||
|
||||
---
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Звук в порте PoP — разбор и план
|
||||
|
||||
Дата: 2026-08-20. Статус: **разбор, кода нет.**
|
||||
Дата: 2026-08-20, музыка дописана 2026-08-25. Статус: **PCM-эффекты
|
||||
реализованы; музыка — путь C (PCM через CBL), первый трек играет.**
|
||||
|
||||
Задача пользователя: добавить звук. Приоритет — эффекты; музыку, если
|
||||
найдётся способ. Эффекты — **обязательно WAV, а не PC-спикер**
|
||||
@@ -351,6 +352,7 @@ IBM PC 1989 года: один квадратный голос. Он у нас
|
||||
| 4/5/6/7 | ворота: закрываются / открываются / рухнули / стоп | `pop_trob.c` | seg007 |
|
||||
| 8 | удар о стену | `pop_map.c` bumped_fall/bumped_floor + seqtbl | seg004/seg006 |
|
||||
| 9 | зацеп за карниз | `pop_map.c` check_grab | seg006 |
|
||||
| 10 | клинок о клинок | `roomtest.c` после `check_sword_hurt`, если один из бойцов в кадре 167 | seg000:1353 |
|
||||
| 11 | свист клинка мимо | `guards.c` check_hurting | seg002:0DAE |
|
||||
| 12/13 | ранен соперник / Кид | `guards.c` hurt_by_sword | seg002:0C1F |
|
||||
| 13 | Кид ранен зельем | `pop_map.c` ветка «злого» зелья | seg006:1894 |
|
||||
@@ -368,7 +370,8 @@ IBM PC 1989 года: один квадратный голос. Он у нас
|
||||
| 48 | напоролся на пики | `pop_map.c` | seg005 |
|
||||
| 49 | пики пошли | `pop_map.c` start_anim_spike | seg007:08F6 |
|
||||
|
||||
Что осталось не разведено — только МУЗЫКА (25 презентация, 26 объятия,
|
||||
Что осталось не разведено — только МУЗЫКА (24/28 смерть, 25 презентация,
|
||||
26 объятия,
|
||||
27/35/40 заставки, 29 встреча Джафара, 30/33 зелья, 32/41 конец уровня,
|
||||
36 время вышло, 37 победа, 43 смерть Джафара, 50/52/53 сюжетные вставки)
|
||||
и 51 (дверь принцессы, тоже из заставки). Их черёд — фаза «музыка».
|
||||
@@ -641,3 +644,320 @@ play_next_sound() seg000:1304 — раз в кадр решает, зап
|
||||
нужно сходить Кидом на левую кнопку и вернуться. Это поведение оригинала
|
||||
(trob челюстей создаётся событием), а не наш дефект — учитывать при
|
||||
постановке автотестов.
|
||||
|
||||
## 13. Повторный аудит игрового звука (2026-08-24)
|
||||
|
||||
Проверены три независимых слоя: содержимое атласов, места вызова и живой
|
||||
тракт `play -> tick -> CBL`.
|
||||
|
||||
- В восьми `SND*.ATL` есть все 31 оцифрованных ресурса: `0..23`, `44..49`
|
||||
и `51`; ненулевая страница/длина есть у каждой записи таблицы.
|
||||
- Для всех 30 PCM-эффектов, которые могут возникать непосредственно в игре,
|
||||
есть место вызова. Последним пропуском был звук 10 при столкновении
|
||||
клинков; условие перенесено буквально из `check_sword_vs_sword` SDLPoP.
|
||||
Звук 51 относится к сцене с принцессой, а не к игровому циклу.
|
||||
- Звук 19 «Кид достал меч» проверен в MAME брейкпоинтами. В момент вызова
|
||||
предыдущий PCM уже закончился (`sfx_left=0`), номинация дошла до
|
||||
`pop_sfx_tick`, после чего курсор получил id 19, страницу 5, смещение
|
||||
`0x1500` и длину 2816 байт. В этом прогоне его не подавляли решётка,
|
||||
плиты, шаги или приоритеты. Если он всё ещё субъективно не слышен, искать
|
||||
надо после выбора эффекта — в непрерывности CBL/громкости самого сэмпла.
|
||||
- После продолжительного прогона title/intro/demo/menu CBL обслужил 3303
|
||||
блока и сообщил 0 программных недоливов (`cbl_requests=0x0CE7`,
|
||||
`cbl_underruns=0`). Это исключает возврат `fill=0`, но само по себе не
|
||||
измеряет запоздание прерывания внутри слишком длинной секции `DI`.
|
||||
- Регрессия full-game на входе в Level 1 оказалась именно запозданием CBL:
|
||||
`pop_level_switch` открывал его ДО блокирующего BIOS fade-in. Demo был
|
||||
чистым, потому что включал палитру без fade. Теперь загрузчик оставляет
|
||||
CBL закрытым, а caller открывает его после окончательной палитры/QuickLoad;
|
||||
скрежет на Level 1 исчез в MAME. Однократный щелчок самого первого
|
||||
`cbl_open` за всю MAME-сессию остаётся отдельной низкоприоритетной задачей.
|
||||
- Открытие pause menu у SDLPoP беззвучно; движение играет 21, вход/выход
|
||||
из подменю — 22, изменение настройки — 10. Эти вызовы перенесены. На
|
||||
время полного копирования страницы и файловых операций CBL закрывается,
|
||||
после flip открывается снова: аппаратная половина не должна повторять
|
||||
старые данные и давать «скрежет».
|
||||
|
||||
Отдельно остаётся игровая музыка и сигнальные мелодии без PCM: смерть
|
||||
(`24/28`), начало/появление Shadow (`25`), встреча Jaffar (`29`), большое
|
||||
и малое зелья (`30/33`), Shadow (`32`), победа/меч (`37`), перо (`39`),
|
||||
конец уровня (`41`) и победа над Jaffar (`43`). Вызовы и AY-проигрыватель
|
||||
для них ещё не реализованы; наличие всех PCM-эффектов эту задачу не закрывает.
|
||||
|
||||
|
||||
## 4. МУЗЫКА: решение пересмотрено (2026-08-25) — путь C вместо A
|
||||
|
||||
В §1б первым заходом был выбран **путь A** (ноты PC-спикера на AY), а путь
|
||||
C (запись -> WAV -> CBL) стоил дорого из-за строки «нужен синтезатор на
|
||||
хосте». Это обстоятельство отпало: у пользователя есть **готовые записи
|
||||
DOS-версии** — `applications/PoP/PoP1_DOS_music` (flac/mp3/ogg/ogg_MT-32,
|
||||
22 трека). Синтезировать нечего, остаётся `ffmpeg -ac 1 -ar 10937 -f u8`.
|
||||
|
||||
### Что померено по этим записям
|
||||
|
||||
| группа | длительность | PCM 10 937,5 Гц |
|
||||
|---|---:|---:|
|
||||
| всё вместе (22 трека) | 339 с | **3 625 КБ = 227 EMM-страниц** |
|
||||
| игровые джинглы (10) | 57 с | 608 КБ |
|
||||
| заставки и титры | 282 с | 3 017 КБ |
|
||||
| финальный `won` один | 115 с | 1 233 КБ |
|
||||
|
||||
Свободной EMM на старте ~3 440 КБ, так что вся музыка разом в память не
|
||||
влезает и не должна: трек грузится под сцену и освобождается после.
|
||||
|
||||
### Что сделано
|
||||
|
||||
`toolchain/pop_pack_music.py` -> один файл `MUS\m<id>.bin` на трек
|
||||
(читается порциями по 16 КБ из одного открытого fd) + каталог
|
||||
`pop_music_tbl.h`. Длина трека хранится **порциями по 128 байт**,
|
||||
а не байтами: 169 КБ в uint16 не влезает, 1350 блоков — легко.
|
||||
|
||||
`pop_music.c` (банк 9) грузит трек в EMM и ставит курсор; насос
|
||||
`pop_sfx_fill` (резидент) получил **третий источник**: эффект важнее
|
||||
музыки, музыка важнее тишины. Эффект музыку не сбрасывает — её курсор
|
||||
стоит, пока эффект доигрывает, и она продолжается с места.
|
||||
|
||||
Проверено в MAME записью звука: трек `story_1_absence` на первом экране
|
||||
истории, корреляция огибающих с эталоном **0,836**, RMS 22,6 против 18,3
|
||||
(разница — 8-битное квантование). Эффекты двери в PV-сцене после него
|
||||
звучат как прежде, то есть освобождение страниц и возврат к набору
|
||||
эффектов работают.
|
||||
|
||||
### Цена и что осталось
|
||||
|
||||
* CBL один: пока играет музыка, эффектов нет. Для заставок это не важно
|
||||
(их там не бывает), для игровых джинглов — открытый вопрос §1б.4.
|
||||
* Резидент вырос на 83 байта (третий источник в насосе) плюс 25 байт
|
||||
данных под курсор и таблицу страниц; запас W2 — 151 байт. Кучи в
|
||||
приложении нет (`malloc` не слинкован), так что это чистый запас роста.
|
||||
* Банк 9 занят на 88 %. Следующий модуль туда уже не влезет — либо
|
||||
переносить, либо заводить банк 12.
|
||||
* `won` (77 страниц) в POP_MUS_PAGES=20 не помещается: финал придётся либо
|
||||
резать, либо стримить кусками по ходу.
|
||||
|
||||
|
||||
## 5. СКРЕЖЕТ ПРИ BIOS-ВЫЗОВАХ: причина и решение (2026-08-25)
|
||||
|
||||
Симптом: во время затемнения (fade) звук хрипел — одинаково с играющей
|
||||
музыкой и в тишине. Пользователь заметил ключевое: **повтор тишины обязан
|
||||
звучать тишиной**, значит дело не в недоливе буфера.
|
||||
|
||||
### Как искали
|
||||
|
||||
Отладочные клавиши, каждая делает ровно один кусок fade:
|
||||
|
||||
| клавиша | что делала | результат |
|
||||
|---|---|---|
|
||||
| H | только ожидание 8 кадров | чисто |
|
||||
| V | только чтение палитры | скрежет |
|
||||
| G | затемнение целиком | скрежет |
|
||||
| J | только запись палитры | скрежет |
|
||||
| L | 512 раз `bios_get_place()` — видео не трогает | **скрежет** |
|
||||
|
||||
`L` и решил вопрос: виновата не палитра, а **любой вызов BIOS**.
|
||||
|
||||
### Причина
|
||||
|
||||
Вход в BIOS — это `rst 8`, то есть `out ($7C),a`. В драйвере MAME он
|
||||
правит `m_rom_sys` и вызывает `update_memory()`, которая перестраивает
|
||||
**окно 0**: `m_pages[0]` + `m_bank_view0.select(1)`. ПЗУ ложится ПОВЕРХ
|
||||
страничного регистра.
|
||||
|
||||
Насос CBL брал окно взаймы именно у W0 (`_io_page_w0 = phys` + `OTIR` по
|
||||
адресу < 0x4000). Пока BIOS работает, запись в порт `0x82` ничего не
|
||||
меняет, и `OTIR` вычитывает ПЗУ, отдавая его в звук.
|
||||
|
||||
Побочно выяснилось, почему `DI` вокруг BIOS помогал лишь иногда: он не
|
||||
даёт войти в ISR (тогда блок просто пропускается, что неслышно), но в
|
||||
обработчиках BIOS есть `EI`, так что защита негарантированная.
|
||||
|
||||
### Решение
|
||||
|
||||
Насос переведён на **W3** (идея пользователя): это окно управляется только
|
||||
портом `0xE2`, подмену из прерывания никто не перекрывает, а BIOS во время
|
||||
нашего ISR не исполняется — окно возвращается до выхода.
|
||||
|
||||
```c
|
||||
saved = _io_page_w3;
|
||||
_io_page_w3 = phys;
|
||||
cbl_push_otir((const void *)(0xC000u + ptr), n);
|
||||
_io_page_w3 = saved;
|
||||
```
|
||||
|
||||
После этого BIOS безопасен везде: и палитра, и любые другие функции.
|
||||
Временный обход палитры мимо BIOS (`gfx_pal_write`) стал не нужен — он
|
||||
остался в libbgi как более быстрый примитив (2,5 тыс. тактов на 64 цвета
|
||||
против 10,8 тыс. у BIOS), но игра его не зовёт.
|
||||
|
||||
Бонус: в W3 нет стаба восстановления окна, который в W0 занимал начало
|
||||
страницы, — звуковые страницы можно использовать целиком.
|
||||
|
||||
## 6. ВСЯ ЗАСТАВКА ОЗВУЧЕНА (2026-08-25)
|
||||
|
||||
К `story_1_absence` добавлены остальные четыре трека заставки:
|
||||
**54** intro_theme (титры), **50** story_2_princess, **53**
|
||||
story_3_Jaffar_enters, **52** story_4_Jaffar_leaves (сцена с принцессой).
|
||||
`MUS_IDS` в Makefile — 50 52 53 54 55, всего 1056 КБ на образе.
|
||||
|
||||
### Два слота вместо одного
|
||||
|
||||
Реплики оригинала идут ВСТЫК: следующая начинается там, где кончилась
|
||||
предыдущая, паузы под загрузку нет. Поэтому `pop_music` держит два слота
|
||||
EMM: `pop_music_load*` всегда пишет в НЕ играющий, `pop_music_play`
|
||||
подменяет резидентную таблицу страниц и отпускает прошлый слот. Своей
|
||||
копии таблицы слот не хранит — её и так держит блок EMM, `mem_get_page`
|
||||
отдаёт номер по индексу (иначе −40 байт W2 у игры, а там их нет).
|
||||
|
||||
Плюс **постраничная загрузка**: `pop_music_load_begin` / `_load_step`
|
||||
читают по одной странице за вызов. Страница стоит 33 мс — четверть
|
||||
логического кадра заставки (133 мс), поэтому подкачка следующей реплики
|
||||
прямо посреди анимации не видна. Кто может позволить себе паузу (титры,
|
||||
чёрный экран между сценами) — зовёт прежний `pop_music_load`.
|
||||
|
||||
### Тайминги приведены к шкале оригинала
|
||||
|
||||
Все длительности сцен взяты из SDLPoP в его тиках (60 Гц), а ждём мы
|
||||
кадрами луча (~50 Гц). Пока сцены были немыми, разбег в 20 % не был
|
||||
виден; с музыкой он слышен сразу — реплика кончается раньше картинки.
|
||||
Введён `POP_T60(t)` (pop_cutscene.h), и на него переведены титры,
|
||||
`intro_before_pv`, хвост после PV и пейсинг самой PV-сцены (счётчик
|
||||
потраченных кадров луча против `POP_T60(tick)`, вместо прежних жёстких
|
||||
четырёх кадров на логический).
|
||||
|
||||
**Паузы-реплики.** Там, где оригинал ждёт конца сэмпла, у нас теперь
|
||||
стоит реальная длина нашей записи: m50 — 831 тик, m53 — 985. Отсюда
|
||||
новая шкала PV: конец m50 на 846, вход Джафара (m53) на 1046, уход
|
||||
(m52) на 2073, конец сцены 2500 тиков (было 1959).
|
||||
|
||||
**Fade перед PV** (вопрос пользователя: наш fade короче, 4 ступени против
|
||||
64). Совпасть должен момент полной темноты, считая от пуска m55:
|
||||
у SDLPoP это 80 (transition) + 600 (wait) + 128 (fade_out_2: 0x40 шагов
|
||||
по 2 тика); у нас переход занимает 80 кадров луча = 96 тиков, а fade —
|
||||
5 тиков. Отсюда `WAIT = 80 + 600 + 128 − 96 − 5 = 707` тиков. Дальше и
|
||||
там и там экран уже чёрный, а трек доигрывает: этой паузой заставка и
|
||||
стыкуется с PV.
|
||||
|
||||
**Музыка переживает смену сцен.** m54 звучит с титров и до первого
|
||||
экрана истории (`pop_intro_show` больше не глушит CBL на входе), m52
|
||||
начинается в PV и доигрывает уже на экране «свадьбы» — как seg000:2051.
|
||||
|
||||
Проверено в MAME: цепочка 54 → 55 → 50 → 53 → 52 отыгрывается целиком,
|
||||
курсор `pop_mus_id` меняется ровно на своих кадрах, интро доходит до
|
||||
демо-режима. Слуховая проверка (нет ли хрипа от диска при играющей
|
||||
музыке) — за пользователем.
|
||||
|
||||
## 7. МУЗЫКА ПО ХОДУ ИГРЫ (2026-08-26)
|
||||
|
||||
Звуки 24..43 в наборе оцифровки ПУСТЫЕ — в оригинале это Adlib-музыка, и
|
||||
в digisnd её нет вовсе. На этом и построено подключение: `pop_sfx_play`
|
||||
для звука с нулевой длиной не пытается его играть, а кладёт НОМЕР в
|
||||
`pop_mus_req` (один байт). Заявку разбирает `pop_music_service()` — один
|
||||
вызов на кадр из любого цикла (игрового, интро, катсцены); всё чтение с
|
||||
диска живёт там.
|
||||
|
||||
**Стриминг вместо загрузки.** Ждать полной загрузки джингла нельзя — это
|
||||
фриз на треть секунды посреди игры. `pop_music_stream` читает ПЕРВУЮ
|
||||
страницу (33 мс) и сразу пускает трек: она звучит 1,5 с, а следующая
|
||||
читается те же 33 мс — запас сорокакратный. Остальные доливаются по
|
||||
одной за кадр, пока `pop_music_loading()`. Номера страниц известны сразу
|
||||
после `mem_alloc_pages`, поэтому таблица для насоса заполняется целиком —
|
||||
данные появятся раньше, чем насос до них дойдёт.
|
||||
|
||||
**Что и где играет** (номера и места — из SDLPoP):
|
||||
|
||||
| трек | событие | место у нас |
|
||||
|------|---------|-------------|
|
||||
| 24 / 28 / 32 | смерть: обычная / в бою / от руки тени | `ctrl_kid_death` |
|
||||
| 25 | вступление 1-го уровня (Кид сидит), тень 6-го | `control_crouched`, `guards.c` |
|
||||
| — | НА ДЕМО-УРОВНЕ музыки нет вовсе: там одни эффекты | гейт в `pop_music_service` |
|
||||
| 27 / 35 / 40 | сцены перед 2/4/6/12, 8/9, «времени мало» | `pop_pre_cutscene_show` |
|
||||
| 29 | встреча с Джафаром | `pop_meet_jaffar` |
|
||||
| 30 / 33 | большая склянка / малая | `pop_proc_get_object` |
|
||||
| 36 | время вышло | `pop_time_expired_show` |
|
||||
| 37 / 43 | меч найден, страж убит / смерть Джафара | `pop_proc_get_object`, `on_guard_killed` |
|
||||
| 39 | перо (медленное падение) | `pop_proc_get_object` |
|
||||
| 41 / 32 | конец уровня / конец 4-го (тень) | опкод SND_LEVEL в `play_seq` |
|
||||
| 26 | встреча с принцессой | `cut_ending` |
|
||||
|
||||
**Вступление первого уровня — автомат, а не «звук при приседе»** (seg005:02EB).
|
||||
Пока `need_level1_music` не ноль, `control_crouched` НЕ ЧИТАЕТ управление:
|
||||
Кид сидит, тема играет, и лишь когда она смолкла, он может встать. Наша
|
||||
первая версия просто играла трек при первом приседе — и тема догоняла
|
||||
игрока посреди уровня (пробежал, спрыгнул, присел — заиграла). Признак
|
||||
«ещё звучит» берём у курсора насоса `pop_mus_left`: он резидентный, и если
|
||||
музыка выключена, курсор остаётся нулём — Кид просто встаёт.
|
||||
|
||||
Темы, которые звучат один раз за заход на уровень (вступление 1-го, тень
|
||||
6-го), сбрасывает `pop_music_level_start()` из `pop_start_level`. Оригинал
|
||||
для этого портит переменную двери (`leveldoor_open = 0x4D`) — у нас на это
|
||||
есть свои два байта.
|
||||
|
||||
**ГДЕ КОНЧАЕТСЯ МУЗЫКА СЦЕНЫ** (уточнено 2026-08-26). Сначала мы отдали
|
||||
трек «доигрывать в игре»: у оригинала load_intro после сцены просто гасит
|
||||
экран и возвращает управление. На слух оказалось хуже, чем в оригинале —
|
||||
музыка спотыкается: загрузка уровня (ESTEX плюс сборка комнаты) не даёт
|
||||
насосу долить блок вовремя. У DOS-версии этой проблемы нет, там звук
|
||||
живёт своей жизнью на аппаратуре.
|
||||
|
||||
Поэтому дослушиваем ПОД ЧЁРНЫМ ЭКРАНОМ, до отрисовки уровня: следующий
|
||||
load_intro у оригинала и так начинается с ожидания тишины (seg001:681), то
|
||||
есть к новому уровню трек в любом случае смолкает. Пропуск сцены обрывает
|
||||
и музыку — игрок нажал клавишу, чтобы идти дальше.
|
||||
|
||||
**ДЛИТЕЛЬНОСТЬ FADE.** fade_in_1/fade_out_1 — это 64 шага палитры по два
|
||||
тика, 128 тиков = 2,13 с; сцена перед уровнем 2 занимает с ними около семи
|
||||
секунд. Наши четыре ступени укладывались в восемь сотых секунды, и сцена
|
||||
выходила втрое короче. Теперь `INTRO_FADE = POP_T60(128)`, а ступеней в
|
||||
`pop_ui_palette_dim` тридцать две вместо четырёх: на четырёх растянутых
|
||||
ступенях затемнение выглядело бы скачками. Половина от оригинальных 64 —
|
||||
на глаз от них не отличается (ступень каждые 66 мс), а вот шестнадцать уже
|
||||
видно. Сумма «fade in + сцена + fade out» при этом совпадает с оригиналом
|
||||
сама собой: длительность каждого fade та же, что у fade_*_1.
|
||||
|
||||
**Цена ступени** (замеры в MAME 2026-08-26, такты 21 МГц; кадр 430 000):
|
||||
|
||||
| версия | такты | что изменилось |
|
||||
|--------|-------|----------------|
|
||||
| исходная | 2 440 000 | снимок копировался побайтовым циклом на C |
|
||||
| + таблица яркости на стеке | 1 250 000 | 768 умножений uint16 заменены 256 сложениями |
|
||||
| + memcpy для снимка | 487 000 | LDIR вместо цикла — главный выигрыш |
|
||||
|
||||
Из оставшихся 487 тысяч 136 тысяч — заливка палитры через BIOS (8 вызовов
|
||||
`gfx_pal_load` по 17 000). Дальше можно было бы хранить готовые таблицы
|
||||
яркости файлом, но при 1,2 мс на построение это уже незаметно.
|
||||
|
||||
ВАЖНО: ступень пересчитывается только когда она СМЕНИЛАСЬ. Наивный цикл
|
||||
«ступень на каждый кадр» звал пересчёт сто раз и растягивал fade до
|
||||
десяти с лишним секунд.
|
||||
|
||||
## 8. ПОТОКОВЫЙ ТРЕК: ФИНАЛЬНАЯ ТЕМА (2026-08-26)
|
||||
|
||||
`won` (56) — 115 с, 1,2 МБ, 78 страниц EMM. В память он не влезает ни при
|
||||
каком бюджете, поэтому играется КОЛЬЦОМ из шести страниц (96 КБ = 9 с): насос
|
||||
идёт по кругу, а `pop_music_service` дочитывает файл в те страницы, которые
|
||||
насос уже прошёл.
|
||||
|
||||
**Кто кого догоняет.** Страница звучит 1,5 с, а читается 33 мс — запас
|
||||
сорокакратный. Дистанция считается без отдельных счётчиков: страница ровно
|
||||
128 блоков насоса, поэтому проигранных страниц = (всего блоков − осталось)
|
||||
/ 128. Пока прочитано меньше, чем проиграно плюс размер кольца, в кольце
|
||||
есть свободный слот. Файл читается ПОСЛЕДОВАТЕЛЬНО, без `lseek`.
|
||||
|
||||
**Что пришлось учесть.**
|
||||
|
||||
* Насос заворачивает страницу только при `pop_mus_ring != 0`; конец трека
|
||||
по-прежнему определяет `left`. Обычный трек этой ветки не касается.
|
||||
* Кольцо обязано сниматься при любом обычном запуске (`pop_music_play`,
|
||||
`_stream`, `_load_begin`): иначе следующий трек играет по кругу первых
|
||||
шести страниц — поймано на титрах сразу после победы.
|
||||
* Живые сцены комнаты принцессы открывают CBL сами (`cut_begin`):
|
||||
`pop_ending_show` глушит звук первым действием, и «arrived to princess»
|
||||
(26) не звучал вовсе.
|
||||
* Тема дослушивается до конца (прерывается клавишей), как `while
|
||||
(check_sound_playing() && !key_test_quit())` в seg001:637. У оригинала
|
||||
между титрами и этим ожиданием стоит ввод имени в таблицу рекордов —
|
||||
когда он появится у нас, ожидание переедет за него (задача HOF-ENTRY).
|
||||
|
||||
Проверено в MAME на сборке `LEVEL=14`: после встречи с принцессой звучит
|
||||
тема победы (`pop_mus_id` = 56, `pop_mus_ring` = 6), курсор уходит далеко
|
||||
за размер кольца — то есть подкачка успевает.
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
# Текст в нижней статус-строке (строке HP) — полная инвентаризация SDLPoP
|
||||
|
||||
Разбор `SDLPoP/src/` на 2026-08-25. Цель — знать ВЕСЬ набор сообщений,
|
||||
которые оригинал печатает в ту же полосу, где нарисованы деления HP,
|
||||
и правила их появления/исчезновения. Это входные данные для порта:
|
||||
у нас пока туда пишется только `GAME PAUSED`.
|
||||
|
||||
---
|
||||
|
||||
## 1. Геометрия: одна полоса на HP и на текст
|
||||
|
||||
```
|
||||
rect_bottom_text = { top 193, left 70, bottom 202, right 250 } // data.h:217
|
||||
display_text_bottom: draw_rect(чёрным) + show_text(halign_center, valign_bottom)
|
||||
```
|
||||
|
||||
* Деления HP **Кида** — от `x = 0` вправо, шаг 7, максимум 10 → занимают `x 0..69`.
|
||||
* Деления HP **стража** — от `x = 314` влево, шаг 7, максимум 10 → занимают `x 245..320`.
|
||||
* Текст живёт РОВНО в промежутке `x 70..250` и по X с делениями не пересекается.
|
||||
* По Y деления на `y = 194..200`, текст (`valign_bottom` к 202) — на `y = 195..201`,
|
||||
то есть на строку ниже. Именно поэтому в оригинале текст выглядит «сидящим»
|
||||
чуть ниже стрелок HP.
|
||||
|
||||
**У нас**: `POP_HP_Y = 194` (`pop_cdraw.h`), экран сдвинут на `POP_YOFF = 28`,
|
||||
базовая линия крупного шрифта `POP_YOFF + POP_HP_Y + 8 = 230` — силуэт
|
||||
ложится на `223..229`, то есть та же картинка.
|
||||
|
||||
## 2. Два примитива и два таймера
|
||||
|
||||
| Имя | Что делает |
|
||||
|-----|------------|
|
||||
| `display_text_bottom(text)` (seg008:2644) | стереть прямоугольник цветом 0 и напечатать текст по центру |
|
||||
| `erase_bottom_text(arg)` (seg008:266D) | стереть прямоугольник; при `arg != 0` ещё и обнулить оба таймера |
|
||||
| `text_time_remaining` | сколько игровых тиков сообщение ещё висит; 0 — ничего не висит |
|
||||
| `text_time_total` | **идентификатор сообщения**, а не только его длительность |
|
||||
|
||||
Обработка тика — в `draw_game_frame`/`idle` (seg000:956). Комментарий в
|
||||
оригинале прямой: *«Note: texts are identified by their total time!»* Значения
|
||||
`text_time_total`, у которых есть особое поведение:
|
||||
|
||||
| `total` | Смысл | Что происходит по истечении |
|
||||
|---------|-------|------------------------------|
|
||||
| 12 | «1 SECOND LEFT» | обычное стирание |
|
||||
| 24 | обычное короткое сообщение | обычное стирание |
|
||||
| 36 | смерть на демо-уровне (0) или на уровне зелий (15) — **текста нет** | `start_game()` — рестарт игры |
|
||||
| 288 | «Press Button to Continue» | `start_game()` — рестарт игры |
|
||||
| 1188 | защита от копирования (уровень 15) | **не убывает и не исчезает** |
|
||||
|
||||
Мигание: при `total == 288` и `remaining < 72` сообщение мигает с периодом 12
|
||||
тиков — 4 тика видно (`blink_frame <= 3`), 8 нет; в кадре `blink_frame == 3`
|
||||
заново печатается текст и играет звук 38 (`sound_38_blink`).
|
||||
|
||||
Сброс: `init_game()` (seg003:32) обнуляет оба таймера и `is_show_time` — то есть
|
||||
любое сообщение умирает на старте уровня.
|
||||
|
||||
---
|
||||
|
||||
## 3. Полный список сообщений
|
||||
|
||||
### 3.1. Состояние программы
|
||||
|
||||
| Текст | Где | Таймер |
|
||||
|-------|-----|--------|
|
||||
| `GAME PAUSED` | seg000:1769, пока `is_paused` | **без таймера**: печатается на входе в паузу, `erase_bottom_text(1)` на выходе (seg000:1784) |
|
||||
|
||||
### 3.2. Уровень и оставшееся время (`show_level` / `show_time`, seg008)
|
||||
|
||||
| Текст | Условие | `total` |
|
||||
|-------|---------|---------|
|
||||
| `LEVEL %d` | `show_level()` при старте уровня; только `1..12` (`hide_level_number_from_level = 14`), не при `seamless`; уровень 13 показывается как **12** (`level_13_level_number`) | 24, дальше сразу `is_show_time = 1` |
|
||||
| `%d MINUTES LEFT` | каждая минута, кратная 5, и каждая из последних 5 | 24 |
|
||||
| `%d SECONDS LEFT` | последняя минута, раз в 12 тиков | 24 |
|
||||
| `1 SECOND LEFT` | остался 1 с | **12** |
|
||||
| `TIME HAS EXPIRED!` | `rem_min == 0` | 24 |
|
||||
| `%d MINUTES PASSED` / `1 MINUTE PASSED` | только SDLPoP (`ALLOW_INFINITE_TIME`), при отрицательном таймере | 24 |
|
||||
|
||||
Что взводит `is_show_time` (все → следующий кадр печатает время):
|
||||
|
||||
* **Space** — seg000:612, штатная клавиша оригинала «сколько осталось»;
|
||||
* читы **`-`/`+` numpad** (изменение времени) — seg000:762 / 777, при этом
|
||||
таймеры сообщения обнуляются, чтобы новое напечаталось немедленно;
|
||||
* **смерть Джафара** — `on_guard_killed()` seg006:1936, уровень 13
|
||||
(`jaffar_victory_level`): вспышка + показать время;
|
||||
* истечение очередной минуты — seg008:1796;
|
||||
* сразу после `show_level()`.
|
||||
|
||||
Обнуляет `is_show_time`: `play_kid()` при смерти (seg006:1365) и
|
||||
`show_copyprot(1)` (seg000:2385).
|
||||
|
||||
### 3.3. Смерть Кида
|
||||
|
||||
| Текст | Где | `total` |
|
||||
|-------|-----|---------|
|
||||
| `Press Button to Continue` | `play_kid()` seg006:1383 — умер на обычном уровне | **288** (мигает, затем рестарт игры) |
|
||||
| *(без текста)* | тот же код, но уровень 0 (демо) или 15 (зелья) | **36** (тихая пауза, затем рестарт игры) |
|
||||
|
||||
Стирается: `fell_out()` (seg006:1342, упал из комнаты 0) и чит **R**
|
||||
(воскрешение, seg000:783) — оба зовут `erase_bottom_text(1)`.
|
||||
|
||||
### 3.4. Сохранение и загрузка
|
||||
|
||||
| Текст | Клавиша | `total` |
|
||||
|-------|---------|---------|
|
||||
| `GAME SAVED` / `UNABLE TO SAVE GAME` | Ctrl+G (`save_game`, seg000:2211) | `total` не ставится, `remaining = 24` |
|
||||
| `QUICKSAVE` / `NO QUICKSAVE` | F6 (расширение SDLPoP, seg000:497) | 24 |
|
||||
| `QUICKLOAD` / `NO QUICKLOAD` | F9 (расширение SDLPoP, seg000:514) | 24 |
|
||||
|
||||
### 3.5. Ответы на клавиши (`answer_text` → `need_show_text`, все `total = 24`)
|
||||
|
||||
| Текст | Клавиша |
|
||||
|-------|---------|
|
||||
| `SOUND ON` / `SOUND OFF` | Ctrl+S |
|
||||
| `KEYBOARD MODE` | Ctrl+K |
|
||||
| `JOYSTICK MODE` / `JOYSTICK NOT FOUND` / `JOYSTICK UNAVAILABLE` | Ctrl+J |
|
||||
| `PRINCE OF PERSIA V1.0` (в SDLPoP заменено на `SDLPoP v%s`) | Ctrl+V |
|
||||
| `SDL COMP v… LINK v…` | Ctrl+C — только SDLPoP |
|
||||
|
||||
### 3.6. Отладочные читы (`cheats_enabled`, `total = 24`)
|
||||
|
||||
| Текст | Клавиша | Смысл |
|
||||
|-------|---------|-------|
|
||||
| `S%d L%d R%d A%d B%d` | `C` | номер отрисованной комнаты и её соседей L/R/A/B |
|
||||
| `AL%d AR%d BL%d BR%d` | Shift+`C` | диагональные соседи |
|
||||
|
||||
### 3.7. Защита от копирования (только уровень 15)
|
||||
|
||||
| Текст | Где | `total` |
|
||||
|-------|-----|---------|
|
||||
| `WORD %d LINE %d PAGE %d` | `show_copyprot(1)` seg000:2389 | **1188** — висит, пока не сменится уровень |
|
||||
|
||||
### 3.8. Только SDLPoP, в оригинале 1989 отсутствует
|
||||
|
||||
| Текст | Где |
|
||||
|-------|-----|
|
||||
| `RECORDING`, `REPLAY SAVED`, `REPLAY CANCELED` | replay.c:599/626/628 |
|
||||
| имя файла скриншота | screenshot.c:62 |
|
||||
|
||||
---
|
||||
|
||||
## 4. Что из этого касается нашего порта
|
||||
|
||||
Реализовано (`pop_status.c`, таймер 24 тика как у оригинала):
|
||||
|
||||
* `GAME PAUSED` — без таймера, рисует само меню (`pop_menu.c`);
|
||||
* `QUICKSAVE` / `NO QUICKSAVE`, `QUICKLOAD` / `NO QUICKLOAD` — заявка стоит
|
||||
в `pop_qsave_process`, то есть в единственном месте, где известно, что
|
||||
именно делали. Лейбл печатается ДО дисковой операции — осознанное
|
||||
расхождение, см. `impl_diff.md`;
|
||||
* `SOUND ON` / `SOUND OFF` — Ctrl+S;
|
||||
* `LEVEL %d` — порт `show_level()` целиком: демо-уровень 0 и номера от 14
|
||||
молчат, тринадцатый показывается двенадцатым, бесшовный переход 12→13
|
||||
пропускается и гасит флаг за собой.
|
||||
|
||||
* вся группа времени — `N MINUTES LEFT`, `N SECONDS LEFT`, `1 SECOND LEFT`,
|
||||
`TIME HAS EXPIRED!`. Флаг `pop_show_time` (порт `is_show_time`) взводит
|
||||
само ядро таймера на круглых пятёрках и каждую секунду последней минуты,
|
||||
а также старт уровня и читы времени; значение 2 означает «перебить
|
||||
текущую строку», как оригинал делает в последнюю минуту;
|
||||
* `Press Button to Continue` — висит бессрочно (`MSG_HOLD`), уровень
|
||||
перезапускает кнопка. Расхождение с оригиналом, см. `impl_diff.md`.
|
||||
|
||||
Пока НЕ печатается:
|
||||
|
||||
* номера комнат (`C`/Shift+`C`) — у нас отдельная отладочная строка;
|
||||
* copy protection и SDLPoP-расширения (replay, скриншоты) — не нужны.
|
||||
|
||||
Отладочная строка вдобавок показывает оставшееся время `##:##` у правого
|
||||
края. На табло уходит `minutes-1`: у оригинала `rem_min` — это НОМЕР идущей
|
||||
минуты, а не остаток целых (старт 60 при `rem_tick` 719 = «почти 60:00»).
|
||||
Секунды считаются делением раз в 12 кадров, а не каждый кадр.
|
||||
|
||||
Нам не нужно: copy protection (уровень 15 исключён из порта — см.
|
||||
`full_game_plan.md`), joystick-режимы, replay, скриншоты.
|
||||
|
||||
Механика, которую придётся портировать целиком, если брать группу времени:
|
||||
пара таймеров `text_time_total`/`text_time_remaining` с семантикой
|
||||
«идентификатор сообщения» — иначе не воспроизвести ни мигание, ни рестарт по
|
||||
истечении 36/288.
|
||||
|
||||
---
|
||||
|
||||
## 5. Цена вывода и что делать, если упрёмся
|
||||
|
||||
Блит одного глифа стоит ~4,6 тыс. тактов почти независимо от размера — это
|
||||
цена вызова, а не пикселей (memory `blit_cost_model`). Полсотни символов =
|
||||
полкадра. Что уже сделано в `pop_status.c` / `pop_ui.c`:
|
||||
|
||||
* **change-driven**: пока показанное не изменилось, не рисуем вовсе;
|
||||
* **по полям**: смена комнаты — две цифры (~9 тыс. тактов, 2% кадра), а не
|
||||
вся строка; подписи рисуются только при полной инвалидации;
|
||||
* **пробелы не блитятся**: их глиф целиком прозрачен, а стоит как буква —
|
||||
на полной отладочной строке это девять сэкономленных блитов;
|
||||
* **заливка только поля** при входе в комнату (`pop_screen_fill_field`):
|
||||
борта от комнаты к комнате не меняются, это и четверть заливки, и то, что
|
||||
обе полосы вход переживают.
|
||||
|
||||
Запас, если бюджета всё же не хватит (идеи пользователя, 2026-08-25):
|
||||
|
||||
1. **Растянуть вывод на несколько кадров, не показывая полуготовую строку.**
|
||||
Печатать по нескольку букв за кадр, держа цвет шрифта чёрным (отдельный
|
||||
индекс палитры), а по готовности подменить этот индекс на белый — строка
|
||||
появится целиком и мгновенно. Стоит ноль байт памяти и укладывается в
|
||||
нашу же технику «два разных чёрных» (`POP_COL_OUTSIDE`).
|
||||
2. **Собирать строку в один спрайт** в свободном хвосте страницы шрифта и
|
||||
блитить одним вызовом. Дороже по подготовке (~35 тыс. тактов на
|
||||
копирование), но выгодно там, где строка ЦЕЛИКОМ меняется каждый раз.
|
||||
Для меню этот путь уже рассматривался и был отвергнут; для статус-строк
|
||||
он имеет смысл только вместе с п.1.
|
||||
|
||||
Про QuickSave/QuickLoad оптимизация не нужна вовсе: там игра и так стоит на
|
||||
время дисковой операции.
|
||||
|
||||
## 6. Ловушка: свисающие глифы
|
||||
|
||||
Зона стирания текста обязана захватывать строку НИЖЕ базовой линии. В малом
|
||||
шрифте `'p'` имеет высоту 7 при ascent 5, `','` — 6: они свисают под базовую
|
||||
линию. Стирание ровно до неё оставляло от хвоста «p» в «Speed:» одинокую
|
||||
точку (поймано в MAME 2026-08-25).
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user