Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ea8efdb0fd | |||
| 623199337e | |||
| 602c3a20fa | |||
| 4ac3584bf9 | |||
| 3bf28da8ee | |||
| 770b946a36 | |||
| 89e9663753 | |||
| ea07a8d7b0 | |||
| d23126983e | |||
| 50570881f5 | |||
| f25ed37d85 | |||
| 3449f6f8c9 | |||
| 6dabe9b4b1 | |||
| a05970cd36 | |||
| 2176c12cc5 |
@@ -9,3 +9,21 @@ args = [
|
||||
"mcp<2",
|
||||
"/Volumes/SAM8/Projects/DIY/Z80/Sprinter/C-Compiler/mame/sources/MAME/src/mame_mcp.py",
|
||||
]
|
||||
|
||||
[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"
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
# # EXTRA_SRCS := helper.c util.c # additional .c files in this dir
|
||||
# # EXTRA_FLAGS := --crt0=minimal # passed through to sprinter-cc
|
||||
# # EXTRA_DATA := test.txt # extra files to add to `make floppy`
|
||||
# # HDD_DEST_DIR := games/myapp # общий каталог файлов в `make hdd`
|
||||
#
|
||||
# include $(PROJ_ROOT)/app.mk
|
||||
#
|
||||
@@ -40,7 +41,7 @@ FLOPPY_IMG := $(MAME_DIR)/IMG/mc.img
|
||||
# образ у себя и связывает его с 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.
|
||||
@@ -67,6 +68,9 @@ SOURCES := $(MAIN_SRC) $(EXTRA_SRCS)
|
||||
# Аргументы упаковщика HDD. Обычно это exe и EXTRA_DATA; приложение со
|
||||
# своей раскладкой каталогов может переопределить переменную до include.
|
||||
HDD_PACK_ARGS ?= $(EXE) $(EXTRA_DATA)
|
||||
# Общий каталог назначения внутри HDD. Пустое значение сохраняет прежнюю
|
||||
# укладку в корень; вложенные КАТАЛОГ:файл считаются относительно него.
|
||||
HDD_DEST_DIR ?=
|
||||
|
||||
CC_FLAGS := --memory $(MEMORY)
|
||||
ifneq ($(STACK_SIZE),)
|
||||
@@ -125,7 +129,7 @@ run: floppy
|
||||
# используется MCP-мостом к MAME (run_bridge.sh). После пересборки образа
|
||||
# MAME ОБЯЗАН полный рестарт (chdman -f = новый inode; см. memory).
|
||||
hdd: $(EXE)
|
||||
$(MAKE_HDD) $(HDD_IMG) $(HDD_PACK_ARGS)
|
||||
$(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) — образ пересобран."
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
PROJ_ROOT := $(abspath $(CURDIR)/../../..)
|
||||
EXAMPLE := roomtest
|
||||
HDD_IMG := $(CURDIR)/build/hdd/roomtest.chd
|
||||
|
||||
# small: W1+W2 как единое 32КБ-пространство, DATA сразу за CODE (иначе
|
||||
# CODE ~23КБ переполняет W1 и лезет в фиксированный DATA@0x8000 в huge).
|
||||
MEMORY ?= huge # small-раскладка + банки кода в W3 (стражи)
|
||||
@@ -88,7 +90,7 @@ EXTRA_FLAGS ?= --gfx 256 -I $(CURDIR)/../poc/res/bg --max-allocs $(ALLOCS) \
|
||||
# умолчанию, пока идёт работа с производительностью; make PROF=0 выключает.
|
||||
# Каждая фаза кадра красит бордюр в свой цвет, высота полосы на скриншоте
|
||||
# MAME = время фазы в растровых строках.
|
||||
PROF ?= 1
|
||||
PROF ?= 0
|
||||
PROF_FLAGS := -DPROF_BORDER=$(PROF)
|
||||
# Стартовый уровень (отладка): make LEVEL=9 — начать сразу с девятого.
|
||||
# Дефолт держим на отлаживаемом сейчас уровне; для «настоящей» игры — LEVEL=1.
|
||||
|
||||
@@ -1,187 +0,0 @@
|
||||
# Точка входа для следующей сессии (обновлено 2026-08-12, ночь)
|
||||
|
||||
Файл для старта с чистого контекста: где всё стоит, что делать первым, какие
|
||||
грабли уже собраны. Читать целиком — он короткий. Дальше по ссылкам:
|
||||
`TASKS_OPEN.md` (доска), `BUGS_OPEN.md` (открытые баги), `CLAUDE.md`.
|
||||
|
||||
---
|
||||
|
||||
## 1. СЛЕДУЮЩАЯ ЗАДАЧА: smoke-прогон уровня 9 и регресс 1-8
|
||||
|
||||
Переворот (зелье инверсии) **доведён и принят пользователем 2026-08-12**:
|
||||
фон, персонажи, клинок, полоса HP, факелы, окклюзия передних граней и
|
||||
переходы между комнатами работают зеркально; чит `U` и зелёное зелье идут
|
||||
одним путём. Осталось пройти сам уровень 9 живьём и убедиться, что на
|
||||
уровнях 1-8 ничего не поехало (весь слой фона теперь спрашивает `pop_upside`).
|
||||
|
||||
Что смотреть на прогоне в первую очередь:
|
||||
|
||||
- **вис на кромке и спуск в перевёрнутом виде** — это новый срез `clip_char`
|
||||
(он режет кадр СНИЗУ вместо верха); тело должно уходить за кладку, а не
|
||||
ложиться поверх неё;
|
||||
- **бой со стражем перевёрнутым** — у соперника свой зеркальный набор
|
||||
(`g*_v.atl`), а тень рисуется атласами Кида, отдельных файлов ей не надо;
|
||||
- **зелье в другой комнате** — переворот перерисовывает не комнату, а чистит
|
||||
факелы и отражает страницу; проверить, что после смены комнаты и возврата
|
||||
фон остаётся чистым.
|
||||
|
||||
Открытый риск (пока не проявился, но проверить осознанно): имена зеркальных
|
||||
атласов `kid10_v.atl`…`kid27_v.atl` — **9 символов до точки, а DSS работает
|
||||
по 8.3**. Если часть кадров Кида в перевёрнутом виде рисуется обычными
|
||||
(неперевёрнутыми) — это оно; лечится переименованием в `pop_pack_kid.py`
|
||||
(например `k10v.atl`) плюс те же имена в `pop_kboot.c`.
|
||||
|
||||
Дальше по плану — уровни 10/11 (по анализу нового в них нет) и
|
||||
[MEM-COLD2](TASKS_OPEN.md#mem-cold2) п.3, если снова понадобится место.
|
||||
|
||||
Ощущенческий хвост прежний — [GRAB-KBD-TIMING](BUGS_OPEN.md#grab-kbd-timing),
|
||||
разбирается после готовности всех уровней (решение пользователя).
|
||||
|
||||
## 2. Состояние репозитория
|
||||
|
||||
Всё закоммичено, `main` чист. `tests-host`: **6 наборов** проходят
|
||||
(`[char] 65`, `[geom] 3144`, `[grab] 55`, `[mouse] 17`, `[phys] 1727`,
|
||||
`[wall] 1`).
|
||||
|
||||
Коммиты 2026-08-12 (сверху — свежие):
|
||||
|
||||
| хеш | что |
|
||||
|---|---|
|
||||
| `7f5c99b` | чистка факелов при перевороте — в СТАРОЙ системе координат |
|
||||
| `c9aa6d9` | переворот: не перерисовывать комнату, стирать только запечённое пламя |
|
||||
| `948d8f0` | переворот: перерисовка вместо отражения, переключение на границе кадра, `clip_char` |
|
||||
| `5b6664f` | зеркальные кадры персонажей — готовыми файлами `*_v.atl` |
|
||||
| `c4dd2c7` | отсев тайлов по fore-окну — в ЛОГИЧЕСКИХ координатах |
|
||||
| `7551906` | L9-INVERT II.4: персонажи рисуются перевёрнутыми |
|
||||
| `055d6d7` | чтение файлов нельзя выносить в банк (код банка сам живёт в W3) |
|
||||
| `147cb18` | резидент: `pop_kid`/`pop_guard`/`main` расколоты по частоте вызова |
|
||||
| `5d31ed0` | резидент: `pop_redraw` + холодная половина `pop_level` в банки |
|
||||
| `f541c0a` | клип поля при перевороте (мусор в бортах) |
|
||||
|
||||
Уровни 1-8 проходятся, уровень 9 ждёт прогона.
|
||||
|
||||
## 3. Раскладка кода после разгрузки резидента
|
||||
|
||||
Куча выросла **902 → 5795 Б** (`_CODE` 25 628 → 20 569). Кандидатов выбирали
|
||||
не по размеру, а по тому, КТО зовёт: если единственный вызывающий уже в
|
||||
банке, код едет к нему и трамплин не появляется вовсе.
|
||||
|
||||
| банк | модули |
|
||||
|---|---|
|
||||
| 1 | `guards.c` |
|
||||
| 2 | `pop_bg.c` |
|
||||
| 3 | `pop_map.c` |
|
||||
| 4 | `pop_cdraw.c` + **`pop_kdraw.c`** (клинок, блит по id — прямые вызовы) |
|
||||
| 5 | `pop_ctrl.c` |
|
||||
| 6 | `pop_trob.c` |
|
||||
| 7 | `pop_room.c` + **`pop_redraw.c`** |
|
||||
| 8 | `roomtest_cold.c` + **`pop_level_cold.c`** + **`pop_kboot.c`** + **`pop_guard_cold.c`** |
|
||||
|
||||
Внутренние контракты: `_pop_level.h`, `_pop_kid.h`, `_pop_kdraw.h`. В них же
|
||||
записаны два правила, которые линкер НЕ проверяет:
|
||||
|
||||
1. **Банковый модуль не маппит W3** — он в этом окне и исполняется (через W0
|
||||
можно). Поэтому `pop_level_read_file` и `pop_kid_data_load` остались в
|
||||
резиденте: первый же вызов из банка улетал в halt по мусору.
|
||||
2. **Прямые вызовы между модулями одного банка** корректны только пока они
|
||||
действительно в одном банке; меняешь раскладку в Makefile — проверь пары.
|
||||
|
||||
## 4. Переворот (L9-INVERT) — как он устроен
|
||||
|
||||
- **Флаги:** `pop_upside_want` (пишут зелье, смерть Кида, чит `U`) и
|
||||
`pop_upside` (читают все слои). Переключение — ОДНО место, начало кадра,
|
||||
вместе с `pop_flip_screen`. Оригиналу этого не нужно (он рисует в offscreen
|
||||
неперевёрнутым и зеркалит на выводе) — расхождение записано в
|
||||
`../docs/impl_diff.md`.
|
||||
- **Сам переворот:** чистка запечённого пламени (`pop_torch_wipe`, 0..4
|
||||
тайла) + отражение страницы акселератором + прямая копия во вторую.
|
||||
**904 247 тактов = 2.1 кадра.** Вариант с полной перерисовкой комнаты
|
||||
(как `redraw_screen` в оригинале) стоил 6 245 477 = 14.5 кадра — отвергнут.
|
||||
- **Спрайты персонажей:** column-major, вертикальное зеркало для них не
|
||||
бесплатно, поэтому второй набор файлов делает упаковщик (`vflip_cols` в
|
||||
`pop_pack_kid.py`). Загрузка одной страницы с диска — 970 003 такта,
|
||||
разворот той же страницы в рантайме — 12 645 582; поэтому файлы.
|
||||
`pop_vflip_load_all()` идемпотентна и зовётся откуда угодно (сейчас — старт
|
||||
и смена уровня, если это `POP_UPSIDE_LEVEL`; задел под загрузку в интро).
|
||||
- **Координаты:** у окна fore-клипа ДВЕ пары границ — экранная
|
||||
(`pop_t_fclip_y0/y1`, по ней режется блит) и логическая
|
||||
(`pop_t_fclip_ly0/ly1`, по ней идут ВСЕ отсевы тайлов). Смешать их — и
|
||||
передние грани перестают перекрывать персонажа.
|
||||
|
||||
## 5. Состояние окружения
|
||||
|
||||
- **MAME запущена с мостом**, на HDD — roomtest с `LEVEL=9`. Стартовый
|
||||
уровень теперь **параметр сборки**: `make LEVEL=9` (дефолт 9; для «настоящей»
|
||||
игры — `LEVEL=1`), константа объявлена в `pop_tune.h`.
|
||||
- **ПРАВИЛО ПОЛЬЗОВАТЕЛЯ: перед сборкой образа — `make clean`, потом `make`,
|
||||
потом `make hdd`** (memory `mame_build_clean_rule`). После пересборки
|
||||
образа MAME **обязана** пройти полный рестарт (memory
|
||||
`mame_hdd_rebuild_restart`): остановка через `exit` в дебаггере, затем
|
||||
`run_bridge.sh`.
|
||||
- **Тайминги моста** (правило пользователя, длиннее не ждать; они же в
|
||||
`docs/mame-autotest.md` §10): старт `run_bridge.sh` → **6 с** → `go` →
|
||||
**8 с** → `keyseq d:{ENTER}` + `keyseq roomtest{ENTER}` → **5 с** →
|
||||
программа работает. Весь цикл перезапуска ~20 секунд.
|
||||
- **MCP-инструменты `mame-z80` работают** (в отличие от прошлой сессии):
|
||||
`press_key`, `screenshot`, `read_logical_memory`, `debugger_command`.
|
||||
Профилирование — `bpset <addr>` + `print totalcycles`, затем `out` и снова
|
||||
`print totalcycles`; кадр = 430 000 тактов.
|
||||
- Адреса состояния смещаются после КАЖДОЙ пересборки — брать из
|
||||
`.sprinter-cc-roomtest/roomtest.map` заново. Банковые функции лежат по
|
||||
`0x0008XXXX`; для брейкпоинта брать младшие 16 бит (`0xC912` и т.п.).
|
||||
- **SDLPoP собран с отладочной информацией** (`-O0 -g3`).
|
||||
- **НЕ ПРИБРАНО:** в `SDLPoP/src/seg008.c` диагностический `fprintf` с меткой
|
||||
`DBGMIRROR` в начале `add_objtable`.
|
||||
|
||||
## 6. Остальное открытое
|
||||
|
||||
- **[FORE-DUP](BUGS_OPEN.md#fore-dup)** — передний слой тайла рисуется дважды
|
||||
при перекрытии объектов. Картинку не портит, тратит такты. Сначала
|
||||
замерить, потом чинить.
|
||||
- **TORCH-ANIM-RIGHT** — под запечённым пламенем могут застыть пики/меч/зелье
|
||||
справа от факела.
|
||||
- **[GATE-FORE-KID](BUGS_OPEN.md#gate-fore-kid)** — Кид в проёме ворот виден
|
||||
поверх решётки; портирован только шовный случай.
|
||||
- Долгие: `BUG-SPIKE-1`, `BUG-CHOMP-JUMP-1`, `L3-COLOR`, `L1-SPEED`, `TUNE-1`.
|
||||
- **Вид тени (OR+XOR) отложен** до готовности всех уровней —
|
||||
[`../docs/shadow_render.md`](../docs/shadow_render.md).
|
||||
|
||||
## 7. Грабли, собранные 2026-08-12 (вечер/ночь)
|
||||
|
||||
- **`return 1;` из ветки у SDCC 4.5 может не положить единицу в A** — функция
|
||||
возвращает мусор. Годами маскировался (в A случайно оставался ненулевой
|
||||
байт) и вылез ровно на `__banked`. Репро — `docs/bugs/sdcc-z80-ret-const-lost/`,
|
||||
обход — один выход через переменную. Аудит всех `.asm` roomtest: других
|
||||
мест нет.
|
||||
- **Банковый модуль не может маппить W3** (см. §3). Симптом — halt по мусору
|
||||
в 0xC4xx на первом же вызове.
|
||||
- **Логические и экранные координаты при перевороте — разные системы.** Три
|
||||
бага подряд из одного корня: отсев тайлов по fore-окну, `clip_char`, чистка
|
||||
факелов. Правило: всё, что считается ИЗ РЯДА комнаты (`63*row + …`), —
|
||||
логическое; всё, что режет блит, — экранное.
|
||||
- **Пламя факела ЗАПЕКАЕТСЯ в ОЗУ-копию** (heal'а у него нет, следующий кадр
|
||||
накрывает предыдущий непрозрачно). Любое отражение/копирование страницы
|
||||
тащит его с собой — перед такими операциями звать `pop_torch_wipe`.
|
||||
- **`pop_upside` переключать в середине кадра нельзя**: остаток кадра
|
||||
рисуется зеркально поверх ещё неперевёрнутой картинки, и запечённые
|
||||
транзиенты остаются в комнате навсегда.
|
||||
- **Перенос константы между файлами — потерянное значение.** При выносе
|
||||
`FIRST_LEVEL` в `pop_tune.h` записал дефолт 1, и игра молча стартовала с
|
||||
первого уровня вместо девятого; заметил пользователь. Теперь это параметр
|
||||
сборки (`make LEVEL=N`).
|
||||
- **`make -C tests-host` не пересобирает по изменению заголовков** — при
|
||||
правке прототипов (особенно `__banked`) обязателен `make -C tests-host
|
||||
clean`, иначе стаб и вызывающий разъезжаются по ABI и тест врёт.
|
||||
|
||||
## Грабли 2026-08-10/11
|
||||
|
||||
- **Не оценивать железо по своей же memory-заметке.** Дважды поправлял
|
||||
пользователь: accel умеет не только копирование (есть AND/OR/XOR) и умеет
|
||||
работать с обычной RAM, а не только с видео (`docs/part2/accelerator_doc.txt`:
|
||||
«supports data blocks transfer of RAM and Video-RAM», не поддерживает только
|
||||
ROM и FastRAM). Проверять по драйверу MAME и докам, а не по памяти.
|
||||
- **lldb через FIFO — плохая идея.** Работает надёжно: `fprintf(stderr, …)`
|
||||
прямо в SDLPoP, пересборка (секунды) и чтение stdout.
|
||||
- **Доска отставала от кода на три задачи.** Перед планированием — сверять
|
||||
`TASKS_OPEN.md` с кодом грепом, а не верить записи.
|
||||
- Диапазон `obj_x` = **−416..695** (посчитан из `kid_data.bin`).
|
||||
|
Before Width: | Height: | Size: 482 KiB |
|
Before Width: | Height: | Size: 426 KiB |
|
Before Width: | Height: | Size: 532 KiB |
|
Before Width: | Height: | Size: 413 KiB |
|
Before Width: | Height: | Size: 462 KiB |
|
Before Width: | Height: | Size: 825 KiB |
|
Before Width: | Height: | Size: 825 KiB |
|
Before Width: | Height: | Size: 825 KiB |
@@ -1,6 +0,0 @@
|
||||
Уровень 5, комната 2 - с разбега чуть раньше - иногда попадаем в стену (некорректное отображение падения)
|
||||
|
||||
Уровень 7, комната 9 - падает плита -1,1 - звук потери HP у Кида (причем раз даже когда Кид просто бъет по плите стоя на 0,0)
|
||||
|
||||
/// Уровень 10, комната 1 - когда Кид стоя на кнопке 1,8 роняет плиту 0,8 на кнопку 1,8 - кнопка ломается (превращается в щебень)
|
||||
/// но двери которые она должна открыть остаются закрытыми
|
||||
@@ -91,7 +91,7 @@ BANK9_SRCS := pop_menu.c pop_status.c pop_music.c pop_app.c pop_timer.c \
|
||||
# стоит 51 мс, так что трамплин банк→банк на их фоне не существует.
|
||||
# Отдельный банк для этого не нужен: в десятом лежал один pop_pal.c на
|
||||
# 485 байт, то есть 3% страницы.
|
||||
BANK10_SRCS := pop_pal.c pop_config.c pop_hof.c
|
||||
BANK10_SRCS := pop_pal.c pop_config.c pop_hof.c pop_path_bank.c
|
||||
BANK11_SRCS := pop_cutscene.c pop_intro.c
|
||||
BANK_NUMS := 1 2 3 4 5 6 7 8 9 10 11
|
||||
BANKED_SRCS := $(foreach n,$(BANK_NUMS),$(BANK$(n)_SRCS))
|
||||
@@ -220,7 +220,7 @@ DISK := README_E.TXT README_R.TXT \
|
||||
# список — это и зависимость exe, и то, что умеет чинить себя само.
|
||||
GEN_ARC_H := bg_arc.h kid_arc.h guard_arc.h skel_arc.h vizier_arc.h \
|
||||
shadow_arc.h title_arc.h pv_arc.h
|
||||
GEN_H := $(addprefix gen/,$(GEN_ARC_H) pop_bg_atlas.h kid_atlas.h kid_data.h \
|
||||
GEN_H := $(addprefix gen/,$(GEN_ARC_H) pop_bg_atlas.h kid_data.h \
|
||||
pop_guard_pal.h pop_shadow_atlas.h pop_sound_tbl.h \
|
||||
pop_music_tbl.h pop_music_ticks.h pop_font.h)
|
||||
|
||||
@@ -228,8 +228,9 @@ PACKED_FILES := $(addprefix $(PACKED)/,$(DISK))
|
||||
STAGED := $(addprefix $(BUILD_DIR)/,$(DISK))
|
||||
|
||||
EXTRA_DATA := $(STAGED)
|
||||
# Запись вида КАТАЛОГ:файл кладёт файл в подкаталог образа, голый путь — в
|
||||
# корень. Проверка на слэш ОБЯЗАТЕЛЬНА: без неё `README_E.TXT` превращался
|
||||
# Запись вида КАТАЛОГ:файл кладёт файл в подкаталог HDD_DEST_DIR, голый путь —
|
||||
# непосредственно в HDD_DEST_DIR. Проверка на слэш ОБЯЗАТЕЛЬНА: без неё
|
||||
# `README_E.TXT` превращался
|
||||
# в `README_E.TXT:build/README_E.TXT`, то есть упаковщик заводил каталог с
|
||||
# именем файла и клал файл внутрь него (поймано пользователем на готовом
|
||||
# образе).
|
||||
@@ -237,6 +238,9 @@ HDD_PACK_ARGS := $(BUILD_DIR)/$(EXAMPLE).exe \
|
||||
$(foreach f,$(DISK),$(if $(findstring /,$(f)),\
|
||||
$(word 1,$(subst /, ,$(f))):$(BUILD_DIR)/$(f),\
|
||||
$(BUILD_DIR)/$(f)))
|
||||
# Всё дерево SprPoP лежит рядом с EXE, но не меняет дефолт глобального
|
||||
# упаковщика: остальные приложения по-прежнему попадают в корень HDD.
|
||||
HDD_DEST_DIR := games/sprpop
|
||||
# Образ живёт в build/, а не в mame/v306/IMG — приложение автономно. Связь с
|
||||
# MAME — одна символьная ссылка, ставится однократно: make mame-link.
|
||||
HDD_IMG := $(CURDIR)/$(BUILD_DIR)/hdd/$(EXAMPLE).chd
|
||||
@@ -361,7 +365,7 @@ $(1): $(STAMP)/$(2)
|
||||
@test -f $$@ || { $(MAKE) GENERATE=1 -B $(STAMP)/$(2); test -f $$@; }
|
||||
endef
|
||||
$(eval $(call from_stamp,$(BG_ATL) $(ATL)/bg/pop_tile.pal $(ATL)/bg/pal_tile.pal gen/pop_bg_atlas.h,bg))
|
||||
$(eval $(call from_stamp,$(KID_ATL) $(ATL)/kid/kid.pal gen/kid_atlas.h,kid))
|
||||
$(eval $(call from_stamp,$(KID_ATL) $(ATL)/kid/kid.pal,kid))
|
||||
$(eval $(call from_stamp,$(ATL)/kid/kid.ani gen/kid_data.h,kid-bin))
|
||||
$(eval $(call from_stamp,$(GUARD_ATL) gen/pop_guard_pal.h,guard))
|
||||
$(eval $(call from_stamp,$(SKEL_ATL),skel))
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
| [T-1](#t-1) | пики перерисовываются безусловно | оптимизация | открыт |
|
||||
| [BUG-SPIKE-1](#bug-spike-1) | пики залипают выдвинутыми рядом с Кидом | **низкий** | маловоспроизводим: ни сценарием, ни попиксельной подгонкой X не поднимается |
|
||||
| [BUG-CHOMP-JUMP-1](#bug-chomp-jump-1) | прыжок с места вплотную к чомперу: кадр с отступом назад | **низкий** | маловоспроизводим: на повторе не поднялся; на пререлиз |
|
||||
| [GRAB-KBD-TIMING](#grab-kbd-timing) | зацеп в падении срабатывает менее стабильно, чем в оригинале | **после всех уровней** | открыт: подозрение на клавиатурный модуль, не на физику |
|
||||
| [GRAB-KBD-TIMING](#grab-kbd-timing) | зацеп в падении срабатывает менее стабильно, чем в оригинале | **после всех уровней** | подозрение на клавиатуру ПОДТВЕРДИЛОСЬ частично: кража байт DSS устранена 2026-08-28, перепроверить |
|
||||
| [GUARD-ENTRY-DEATH](#guard-entry-death) | вход в комнату вплотную к стражу = мгновенная смерть (не портирован `bump_into_opponent`) | порт | **фикс есть, ждёт проверки в MAME** |
|
||||
| [BUG-SHADOW-SET](#bug-shadow-set) | Тень на 12-м уровне дерётся спрайтами СТРАЖА: SHADOW.DAT у нас нет вовсе | порт | открыт, разбор готов |
|
||||
| [PAL-L1-AFTER-INTRO](#pal-l1-after-intro) | вход в игру на уровень 1 после интро — игровая палитра не установлена (чёрный экран) | палитра/fade | открыт: воспроизведение нестабильно, корень не установлен |
|
||||
@@ -32,11 +32,11 @@
|
||||
| [MOB-CLIP-RIGHT](#mob-clip-right) | окна и узор кладки рисуются ПОВЕРХ падающих плит | окклюзия | открыт: корень найден 2026-08-13 |
|
||||
| [MID-OVERLAY-LAYER](#mid-overlay-layer) | оверлей кромки всегда поверх плит и персонажей (у оригинала — midtable с сортировкой) | слои | открыт: нужен разбор архитектуры |
|
||||
| [GUARD-FALLOUT-VICTORY](#guard-fallout-victory) | страж, выпавший из комнаты, не засчитывается убитым | порт | открыт: две строки |
|
||||
| [KBD-STUCK-WAIT](#kbd-stuck-wait) | потерянный break-код клавиши вешает игру НАСМЕРТЬ в межуровневой заставке | клавиатура/клин | открыт: диагноз полный, чинить не сейчас (решение пользователя 2026-08-24) |
|
||||
| [KBD-STUCK-WAIT](#kbd-stuck-wait) | потерянный break-код клавиши вешает игру НАСМЕРТЬ в межуровневой заставке | клавиатура/клин | **корень найден и устранён** 2026-08-28 (байты крал KEYSCAN DSS); ждёт полевой проверки |
|
||||
| [L10-BUTTON-DEBRIS](#l10-button-debris) | ур.10 к.1: плита упала на кнопку (1,8) — щебень есть, а решётки закрыты | кнопки/ворота | открыт: **не воспроизводится**, проверено многое (см. разбор) |
|
||||
| [LOOSE-SEAM-ANIM](#loose-seam-anim) | плита в шве (col −1), уроненная в прошлой комнате, исчезает без анимации падения | **мелкий** | открыт по решению пользователя: «пока пусть будет так» |
|
||||
| [SND-PACE-DEAD](#snd-pace-dead) | пейсинг сцен по насосу CBL не включается: признак «часы идут» вычисляется двумя чтениями подряд | **тайминг/звук** | открыт: диагноз полный, симптом обойдён подгонкой (2026-08-27) |
|
||||
| [PV-RENDER-BOUND](#pv-render-bound) | сцена с принцессой рисуется дороже бюджета: ~49 тиков/с вместо 60, музыка уезжает от картинки | производительность | открыт: замер есть, лечение — удешевить кадр |
|
||||
| [SND-PACE-DEAD](#snd-pace-dead) | пейсинг сцен по насосу CBL не включается: признак «часы идут» вычисляется двумя чтениями подряд | **тайминг/звук** | **снят 2026-08-28**: ветка удалена, сцена на единых часах по лучу |
|
||||
| [PV-RENDER-BOUND](#pv-render-bound) | сцена с принцессой рисуется дороже бюджета: ~49 тиков/с вместо 60, музыка уезжает от картинки | производительность | **исправлено 2026-08-28**: кадр разложен на блоки по интервалу, удешевлять не понадобилось |
|
||||
| [MUS-LEFT-TEAR](#mus-left-tear) | `pop_mus_left` (16 бит, пишет прерывание) читается из главного цикла неатомарно — возможен ложный «трек кончился» | **потенциальный** | открыт: хазард показан рассуждением, в прогоне не проявился |
|
||||
|
||||
---
|
||||
@@ -98,8 +98,67 @@ POS=18`, MAME): Кид стоит на кнопке -> решётка `(2,7)` о
|
||||
<a id="kbd-stuck-wait"></a>
|
||||
## KBD-STUCK-WAIT. Потерянный break-код вешает игру насмерть в межуровневой заставке
|
||||
|
||||
> **Решение пользователя (2026-08-24): не чинить сейчас.** Записан разбор и
|
||||
> варианты; вернуться, когда дойдут руки до клавиатурного слоя.
|
||||
> **КОРЕНЬ НАЙДЕН И УСТРАНЁН 2026-08-28.** Байты у нас крал сам DSS — см.
|
||||
> раздел «Корень» ниже. Правка в `libc/irq/_irq_tramp.c`. Запись остаётся
|
||||
> открытой до полевой проверки и решения по страховке-таймауту.
|
||||
|
||||
## Корень (2026-08-28): скан-коды воровал KEYSCAN DSS
|
||||
|
||||
Пришёл из чтения исходников DSS (`docs/sources/Estex-DSS/DSS/`, подсказка
|
||||
пользователя). Обработчик прерывания DSS живёт в IM1 по `0x0038`
|
||||
(`DSS-MAIN.ASM`), и его `INTx38_Handler` ПЕРВЫМ ДЕЛОМ делает `CALL KEYSCAN`,
|
||||
а `KEYSCAN.RESCAN` (`KEYINTER.ASM`) вычерпывает FIFO SIO досуха.
|
||||
|
||||
Наш трамплин проверял «есть ли клавиатурный байт» ОДИН раз, на входе в
|
||||
прерывание, а хвост кадрового пути уходил в DSS (`jp 0x0038`). Значит
|
||||
скан-код, прилетевший ПОЗЖЕ этой проверки, доставался DSS и уезжал в его
|
||||
буфер — при этом **Rx-overrun не взводится**, потому что байт не потерян
|
||||
железом, он просто прочитан не тем владельцем. Отсюда и загадка
|
||||
исходного диагноза: бит залип, а `_kbdraw_overrun == 0`.
|
||||
|
||||
**Измерено в MAME (сессия 2026-08-28, брейки + `totalcycles`):**
|
||||
|
||||
| Что | Значение |
|
||||
|---|---|
|
||||
| окно от входа трамплина до чтения байта DSS | **738 тактов ≈ 34 мкс**, на КАЖДОМ кадровом прерывании (50/с) |
|
||||
| из них пролог самого DSS (`jp 0x38` → `.Handler` → `call INTx38_Handler` → 11 `push` → `call KEYSCAN`) | 481 такт ≈ 22 мкс |
|
||||
| цепочка кадровых хендлеров у SprPoP | пуста (`_irq_chain_n = 0`) — окно уже минимальное |
|
||||
| расчётная частота на железе при удержании клавиши (typematic 30 Гц = 60 байт/с) | потеря байта раз в ~10 с |
|
||||
|
||||
Последняя строка сходится с полевым наблюдением пользователя: залипание
|
||||
стрелки ловилось на ур. 14, когда Кид долго бежит влево через несколько
|
||||
комнат.
|
||||
|
||||
**Пойманный случай (MAME, старая сборка).** Стоп на `IN A,($18)` внутри
|
||||
`KEYSCAN` при `_kbdraw_active = 1`: DSS прочитал `A = 0x74` — make стрелки
|
||||
«вправо», при том что `_kbdraw_pending = 2` (EXT), то есть префикс `E0` уже
|
||||
разобрали МЫ. Посылка `E0 74` разорвана пополам между двумя владельцами
|
||||
канала. Злейший подвид — кража `E0` у break'а `E0 F0 6B`: тогда мы гасим
|
||||
бит в PLAIN-половине, а взведённый бит в EXT остаётся навсегда, и это ровно
|
||||
залипшая стрелка `0x6B`/`0x72` из исходного разбора.
|
||||
|
||||
**Правка.** Пока raw-канал открыт, кадровый путь трамплина НЕ вызывает
|
||||
обработчик DSS вообще (приватный `RETI` вместо `jp 0x0038`) — окно ровно
|
||||
ноль. Промежуточный вариант «проверить FIFO перед chain'ом» пробовали и
|
||||
отвергли замером: он снимает лишь свою треть окна (738 → 481 такт), кражи
|
||||
продолжались. Цена принятого варианта: пока открыт raw, у DSS замирает
|
||||
кадровое обслуживание — опрос мыши и мигание текстового курсора (PoP не
|
||||
нужно ни то, ни другое; зафиксировано в `<kbd_raw.h>`).
|
||||
|
||||
**Проверка (MAME 2026-08-28).** До правки брейк на входе `KEYSCAN` с
|
||||
условием «страница точно DSS + наш raw открыт» срабатывал мгновенно (50/с);
|
||||
после правки молчит, а положительный контроль — брейк на нашем приватном
|
||||
`RETI` — срабатывает сразу. ВАЖНО про методику: первый брейк без проверки
|
||||
сигнатуры байтов дал ЛОЖНОЕ срабатывание — в W0 периодически лежит
|
||||
драйверная страница DSS, и по адресу `0x0570` там своя команда.
|
||||
|
||||
**Что осталось открытым.** Кража устранена, но потерять break всё ещё
|
||||
может настоящий Rx-overrun SIO (редкий: FIFO вычерпывает `kbd_raw_poll` из
|
||||
главного цикла). Значит вариант A из таблицы ниже — таймаут в циклах
|
||||
«ждём отпускания всех клавиш» — остаётся разумной страховкой от КЛАССА
|
||||
отказа «вечный клин», и его стоит сделать отдельно.
|
||||
|
||||
## Исходный разбор (2026-08-24)
|
||||
|
||||
**Наблюдение (пользователь, 2026-08-24).** Нажат Shift+L (чит «следующий
|
||||
уровень») — игра встала намертво: картинка прежнего уровня, реакции нет.
|
||||
@@ -169,6 +228,13 @@ Rx-overrun не срабатывала. Гашение бита отладчи
|
||||
> **Решение пользователя (2026-08-12): отложено до готовности всех уровней.**
|
||||
> Механика работает, это вопрос ощущения, а не проходимости.
|
||||
|
||||
> **Обновление 2026-08-28.** Гипотеза «дело в клавиатурном модуле, а не в
|
||||
> физике» подтвердилась ЧАСТИЧНО: найдена и устранена кража скан-кодов
|
||||
> обработчиком DSS (разбор — [KBD-STUCK-WAIT](#kbd-stuck-wait)). Украденный
|
||||
> make Shift'а или стрелки — это ровно «нажал, а не сработало», и на
|
||||
> железе при удержании клавиши такое случалось раз в ~10 с. Перепроверить
|
||||
> ощущение зацепа на новой сборке ДО того, как лезть в физику.
|
||||
|
||||
**Наблюдение (пользователь).** Вход на уровень 7: Кид влетает в комнату сверху
|
||||
и, пролетая мимо края пола, МОЖЕТ за него зацепиться — но получается заметно
|
||||
реже, чем в оригинале. «Похоже, это проблемы нашего клавиатурного модуля».
|
||||
@@ -1140,7 +1206,15 @@ fade_in по построению пишет все 256 записей из сн
|
||||
---
|
||||
|
||||
<a id="snd-pace-dead"></a>
|
||||
## SND-PACE-DEAD. Пейсинг сцен по насосу CBL не включается
|
||||
## SND-PACE-DEAD. Пейсинг сцен по насосу CBL не включается — СНЯТ 2026-08-28
|
||||
|
||||
> **Закрыт удалением самой ветки.** Сцена с Джафаром переведена на ЕДИНЫЕ
|
||||
> часы — кадры луча (`intro_pv_animated`): вторая шкала по насосу и гонка,
|
||||
> которая её выбирала, убраны целиком. Насос как часы был нужен потому,
|
||||
> что кадр сцены рисовался дольше своего интервала; теперь отрисовка
|
||||
> разложена по интервалам (`pv_restore_bg`), и счёт кадров честен.
|
||||
> Побочный эффект гонки — «кода реплики каждый прогон в другом месте» —
|
||||
> исчез; проверено пользователем. Ниже — исходный разбор.
|
||||
|
||||
**Замер (MAME, 2026-08-27).** В сцене `intro_pv_scene()` зонды показали
|
||||
`snd_done == 0` на всём прогоне — то есть ветка `if (snd_pace)`, ради
|
||||
@@ -1171,7 +1245,17 @@ snd_pace = (uint8_t)(pop_snd_tick != snd_prev); /* часы реально и
|
||||
---
|
||||
|
||||
<a id="pv-render-bound"></a>
|
||||
## PV-RENDER-BOUND. Сцена с принцессой не укладывается в свой бюджет кадра
|
||||
## PV-RENDER-BOUND. Сцена с принцессой не укладывается в свой бюджет кадра — ИСПРАВЛЕНО 2026-08-28
|
||||
|
||||
> **Причина оказалась не в цене кадра, а в том, ГДЕ отсчитывался интервал.**
|
||||
> Кадр рисовался целиком, и только потом ждали vsync и ещё четыре — то есть
|
||||
> отрисовка ПРИБАВЛЯЛАСЬ к делителю. Теперь она разложена на блоки, каждый
|
||||
> из которых влезает в кадровый интервал, и после каждого ждём vsync
|
||||
> (`pv_restore_bg` + раскладка кадра в `cut_run` / `pre_room_animated` /
|
||||
> `intro_pv_draw_frame`); фон восстанавливаем только по картинке, 200 строк
|
||||
> вместо 256. Удешевлять сцену не понадобилось: отрисовка ~2 кадра при
|
||||
> бюджете 5. Подгонка `PV_MAGIC_LEAD` снята (0) — проверено пользователем:
|
||||
> кода попадает в нужный кадр. Ниже — исходный замер.
|
||||
|
||||
**Замер (MAME, 2026-08-27).** Реплика Джафара `m53` — 1403 порции насоса,
|
||||
то есть 16,42 с реального времени. За это время шкала сцены прошла
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
| [`palette_plan.md`](palette_plan.md) | Карта всех 256 слотов палитры + механика fade |
|
||||
| [`status_line_text.md`](status_line_text.md) | Строка HP как статус-строка: инвентаризация ВСЕХ текстов SDLPoP |
|
||||
| [`sound_plan.md`](sound_plan.md) | Звук через CBL: разбор и архитектура |
|
||||
| [`music_runtime_index_plan.md`](music_runtime_index_plan.md) | Будущее улучшение: runtime-индексы музыки и PCM-эффектов, смена аудионаборов без перелинковки EXE |
|
||||
| [`shadow_render.md`](shadow_render.md) | Вид Тени (OR+XOR) — отложено: почему XOR несовместим с прозрачностью `#FF` |
|
||||
| [`roomnav_skip.md`](roomnav_skip.md) | Комнаты для отладочного телепорта `+`/`−`: какие пропускать и почему |
|
||||
| [`host_tests_plan.md`](host_tests_plan.md) | Модульные тесты движка под ucsim_z80: два шва, регрессии, дифф против SDLPoP |
|
||||
|
||||
@@ -89,3 +89,47 @@ raw-клавиатура (`<kbd_raw.h>`), которую мы и так заби
|
||||
после выхода — заметный. Делать после того, как закроем стражей и
|
||||
займёмся бюджетом всерьёз (там же, где батчинг кроссбанковых вызовов и
|
||||
возможный возврат `pop_bg` в резидент `--w3`).
|
||||
|
||||
## Готовить следующий уровень, пока играет мелодия конца текущего
|
||||
|
||||
**САМАЯ ДАЛЬНЯЯ ВЕРСИЯ.** Не полишинг и не порт: это улучшение ПРОТИВ
|
||||
оригинала. Планируем, но не раньше, чем закроем уровни и полишинг.
|
||||
|
||||
**Идея (пользователь, 2026-08-28).** Пока звучит мелодия конца уровня (и
|
||||
трек заставки между уровнями), экран не меняется — значит в это время можно
|
||||
успеть прочитать с диска следующий уровень, чтобы после музыки он появлялся
|
||||
сразу, а не через секунду-другую загрузки.
|
||||
|
||||
**Что УЖЕ сделано и мерено (2026-08-28).** Половина этого уже работает:
|
||||
трек заставки играет ПОВЕРХ загрузки уровня — так же, как в оригинале
|
||||
(`seg003:68-108`: `load_intro` возвращает управление, `load_level()` идёт под
|
||||
музыку, и только потом `while (check_sound_playing())` на чёрном экране).
|
||||
Насос это переживает: 500 подряд вызовов `pop_sfx_fill` через всю загрузку —
|
||||
максимальный зазор 245 832 такта при дедлайне 251 000, ни одной пропущенной
|
||||
порции. Осталась вторая половина: мелодия конца уровня досиживается на
|
||||
ЖИВОМ экране пройденного уровня (как в оригинале, `play_level_2`), и только
|
||||
после неё начинается загрузка.
|
||||
|
||||
**Оговорки, найденные при разборе.**
|
||||
|
||||
1. **Загрузку придётся разрезать надвое.** Сейчас `pop_level_switch` мешает
|
||||
дисковую работу (файл уровня в EMM-страницу, атласы стража и тайлсета) с
|
||||
экранно-палитровой (`pop_bg_load` переписывает записи палитры и атласы,
|
||||
`pop_guard_load` физически применяет цветовые слоты, обе страницы
|
||||
заливаются, `pop_pal_black`). Вторую половину НЕЛЬЗЯ выполнять, пока на
|
||||
экране ещё живёт пройденный уровень — иначе палитра поедет прямо на
|
||||
картинке. То есть в «музыкальное окно» можно вынести только диск, а всё,
|
||||
что трогает палитру и VRAM, остаётся после.
|
||||
2. **Бюджет кадра.** Ожидание мелодии идёт в игровом цикле (гейт
|
||||
`pop_endmus_left` + `pop_music_busy()`), кадр там обычный, 80-100 мс.
|
||||
Шаг подкачки должен быть такого же размера, как музыкальный — полстраницы
|
||||
(~16 мс), а не страница целиком; иначе кадры на экране пройденного уровня
|
||||
начнут дёргаться.
|
||||
3. **Память.** Страницы следующего уровня придётся держать одновременно со
|
||||
страницами текущего — проверить EMM-бюджет (memory `sprinter_emm_budget`),
|
||||
тяжёлые тут не файл уровня, а атласы тайлсета и стража.
|
||||
4. **Риск невелик, но он в самом хрупком месте.** Палитро-атласный обмен —
|
||||
источник уже пойманных багов (`PAL-DUNGEON-STALE`, `PAL-L1-AFTER-INTRO`),
|
||||
и трогать его ради экономии секунды стоит только на спокойную голову.
|
||||
|
||||
**Выигрыш.** Одна-две секунды один раз на уровень.
|
||||
|
||||
@@ -0,0 +1,573 @@
|
||||
# Аудио без перелинковки: runtime-индексы музыки и эффектов
|
||||
|
||||
> Статус: план будущего улучшения, 2026-08-30. Код пока не менялся.
|
||||
|
||||
## 1. Цель
|
||||
|
||||
Один и тот же `sprpop.exe` должен корректно работать с любым штатным набором
|
||||
музыки (`flac`, `mp3`, `ogg`, `mt32`) без перекомпиляции и перелинковки.
|
||||
Замена набора должна сводиться к замене файлов в `MUS/` вместе с описывающим
|
||||
их индексом.
|
||||
|
||||
Сейчас это невозможно: упаковщик печатает зависящие от набора значения в
|
||||
`gen/pop_music_tbl.h` и `gen/pop_music_ticks.h`, после чего они становятся
|
||||
частью EXE. Если заменить только `MUS/mNN.bin`, загрузчик продолжает верить
|
||||
размерам старого набора, а сценарии — его длительностям.
|
||||
|
||||
## 2. Подтверждённое текущее состояние
|
||||
|
||||
После преобразования в PCM наборы `flac`, `mp3` и `ogg` имеют одинаковые
|
||||
длины. Исполнение MT-32 отличается. Значимые примеры:
|
||||
|
||||
| трек | FLAC/MP3/OGG | MT-32 |
|
||||
|---|---:|---:|
|
||||
| m32, конец уровня 4 | 438 тиков | 449 тиков |
|
||||
| m41, обычный конец уровня | 732 | 685 |
|
||||
| m50, принцесса ждёт | 831 | 867 |
|
||||
| m53, реплика Джафара | 985 | 1044 |
|
||||
| m56, финальная тема | 9865 блоков / 78 страниц | 10462 / 82 |
|
||||
|
||||
Все обычные треки каждого из четырёх наборов по-прежнему помещаются в
|
||||
`POP_MUS_PAGES == 20`. Только m56 требует кольцевого проигрывателя.
|
||||
`uint16_t` достаточно и для самого длинного MT-32-трека: 10462 блока.
|
||||
|
||||
Compile-time длительности используются не повсюду:
|
||||
|
||||
- `pop_music.c` читает из `pop_music_tbl.h` наличие, число страниц и блоков;
|
||||
- `pop_intro.c` использует длительности m50 и m53 для шкалы PV-сцены;
|
||||
- `pop_kid.c` использует длительности m32 и m41 для паузы конца уровня,
|
||||
включая режим с выключенной музыкой;
|
||||
- остальные ожидания уже опираются на `pop_music_busy()` и автоматически
|
||||
заработают с правильным runtime-числом блоков.
|
||||
|
||||
## 3. Что должно остаться константами программы
|
||||
|
||||
В индекс переносятся только свойства конкретного набора. Инварианты формата
|
||||
остаются в коде, а заголовок IDX лишь подтверждает их:
|
||||
|
||||
- PCM unsigned 8-bit mono;
|
||||
- частота `CBL_FREQ_10K9`;
|
||||
- один блок насоса — 128 байт;
|
||||
- пространство оригинальных sound id — 0..56;
|
||||
- `POP_MUS_PAGES == 20` для обычного загрузчика;
|
||||
- размер кольца потокового проигрывателя.
|
||||
|
||||
Это важно: IDX не должен обещать программе другой формат PCM, который насос
|
||||
физически не умеет воспроизводить.
|
||||
|
||||
## 4. Предлагаемый файл `MUS/MUSIC.IDX`
|
||||
|
||||
Предпочтителен отдельный индекс набора, а не заголовок в каждом треке:
|
||||
|
||||
- все сценарные длительности доступны сразу после старта;
|
||||
- `mNN.bin` остаются простыми сырыми PCM-потоками;
|
||||
- загрузчик трека меняется минимально;
|
||||
- один индекс легко заменить вместе с набором;
|
||||
- формат помещается в один 512-байтовый сектор DSS.
|
||||
|
||||
Предлагаемая версия `PMI1`:
|
||||
|
||||
```text
|
||||
0..3 "PMI1" magic и версия
|
||||
4 57 число плотных записей (id 0..56)
|
||||
5 4 размер записи
|
||||
6 7 log2 размера блока: 1 << 7 = 128
|
||||
7 1 формат PCM: u8 mono, CBL_FREQ_10K9
|
||||
8..15 0 резерв будущих версий
|
||||
|
||||
16.. 57 записей по 4 байта:
|
||||
+0..1 uint16 blocks длина PCM в блоках по 128 байт
|
||||
+2..3 uint16 ticks60 длительность ожидания в тиках оригинала
|
||||
|
||||
244..511 нулевой резерв до одного сектора
|
||||
```
|
||||
|
||||
Нулевые `blocks` и `ticks60` означают, что трека с таким id в наборе нет.
|
||||
|
||||
`pages` в IDX не хранится: это производная величина, и её дублирование может
|
||||
разойтись с `blocks`:
|
||||
|
||||
```c
|
||||
pages = blocks / 128 + ((blocks & 127) != 0);
|
||||
```
|
||||
|
||||
По умолчанию упаковщик вычисляет тики той же формулой, что сейчас:
|
||||
|
||||
```text
|
||||
ticks60 = round(blocks * 40 / 57)
|
||||
```
|
||||
|
||||
Хранить `ticks60` отдельно всё же полезно: в будущем момент окончания
|
||||
сценарной реплики можно будет уточнить независимо от технического хвоста PCM.
|
||||
Генератор обязан печатать предупреждение, если явно заданные тики заметно
|
||||
отличаются от длительности файла.
|
||||
|
||||
## 5. Размещение индекса в памяти
|
||||
|
||||
Загружать таблицу в обычный изменяемый `static` нельзя. Данные банковых
|
||||
модулей сейчас попадают в общий `_DATA`/W2, а свободная куча составляет около
|
||||
238 байт. Даже таблица из 228 байт практически уничтожит этот запас.
|
||||
|
||||
Индекс следует держать в одной EMM-странице:
|
||||
|
||||
1. `pop_music_init()` выделяет блок из одной страницы;
|
||||
2. загружает туда первые 512 байт `MUS\MUSIC.IDX`;
|
||||
3. проверяет magic, версию, размеры полей и формат PCM;
|
||||
4. готовит страницу для безопасного временного отображения в W0;
|
||||
5. сохраняет в W2 только номер блока/страницы и флаг готовности.
|
||||
|
||||
Потеря 16 КБ EMM ради маленькой таблицы допустима: EMM у игры с запасом, а
|
||||
W1/W2 — самый дефицитный ресурс. Не следует ради экономии страницы прятать
|
||||
индекс в хвост `kid.ani` или звукового набора: это создаст ненужную связь
|
||||
между независимыми ресурсами.
|
||||
|
||||
Доступ к записи предоставляет банковая функция наподобие:
|
||||
|
||||
```c
|
||||
int8_t pop_music_info(uint8_t id, pop_music_info_t *out) __banked;
|
||||
```
|
||||
|
||||
Она на короткое время отображает страницу IDX в W0, копирует четыре байта в
|
||||
буфер вызывающего и сразу восстанавливает окно. Наружу указатель на EMM не
|
||||
выдаётся.
|
||||
|
||||
Индекс живёт до выхода из программы. `pop_music_free()`, который вызывается
|
||||
между сценами, освобождать его не должен; для полного завершения нужен
|
||||
отдельный shutdown либо освобождение в общем маршруте выхода.
|
||||
|
||||
## 6. Изменения загрузчика музыки
|
||||
|
||||
`pop_music_load_begin(id)` должен получать из runtime-индекса:
|
||||
|
||||
- наличие трека;
|
||||
- число блоков;
|
||||
- вычисленное число EMM-страниц.
|
||||
|
||||
Далее существующая архитектура почти не меняется:
|
||||
|
||||
- `slot_blocks[]` уже хранит runtime-длину загруженного трека;
|
||||
- `pop_mus_left` получает её при `play`/`stream`;
|
||||
- насос сам останавливается на правильном блоке;
|
||||
- `pop_music_busy()` автоматически отражает фактический конец;
|
||||
- кольцевой проигрыватель получает правильные 82 страницы MT-32 m56 вместо
|
||||
78 страниц FLAC.
|
||||
|
||||
При каждом открытии `mNN.bin` надо без дополнительного `open` проверить
|
||||
фактический размер:
|
||||
|
||||
1. `lseek(fd, 0, SEEK_END)`;
|
||||
2. убедиться, что размер положительный и кратен 128;
|
||||
3. сравнить его с `blocks * 128` из IDX;
|
||||
4. вернуть позицию через `lseek(fd, 0, SEEK_SET)`.
|
||||
|
||||
`lseek(SEEK_END)` уже реализован в libc поверх DSS `MOVE_FP $15` и
|
||||
используется самой игрой для `POP.CFG`.
|
||||
|
||||
Рекомендуемая политика несовпадения:
|
||||
|
||||
- вывести диагностическое сообщение;
|
||||
- считать фактический размер файла главным;
|
||||
- пересчитать `blocks/pages/ticks60` в загруженной EMM-копии индекса;
|
||||
- продолжить работу, если размер проходит ограничения.
|
||||
|
||||
Так случайно забытый старый IDX не приведёт к чтению чужой EMM-страницы или
|
||||
обрыву трека. При этом штатная поставка обязана всегда включать согласованные
|
||||
IDX и PCM.
|
||||
|
||||
## 7. Runtime-тики без 32-битного переполнения
|
||||
|
||||
Если тики приходится восстанавливать из фактического размера, прямое
|
||||
`blocks * 40` может переполнить `uint16_t`. Та же формула считается только
|
||||
16-битной арифметикой:
|
||||
|
||||
```c
|
||||
ticks = (blocks / 57) * 40
|
||||
+ ((blocks % 57) * 40 + 28) / 57;
|
||||
```
|
||||
|
||||
Даже при `blocks == 65535` промежуточные значения остаются в `uint16_t`.
|
||||
Это холодный путь, поэтому небольшая цена деления допустима.
|
||||
|
||||
## 8. Перевод сценариев с compile-time на runtime
|
||||
|
||||
### PV-сцена (`pop_intro.c`)
|
||||
|
||||
Текущий большой `enum` смешивает два класса величин:
|
||||
|
||||
- неизменные интервалы сценария;
|
||||
- абсолютные точки, сдвигаемые длительностями m50 и m53.
|
||||
|
||||
Надо сохранить относительные сценарные константы, а при входе в PV-сцену
|
||||
один раз собрать локальную структуру `pv_timing_t`:
|
||||
|
||||
```text
|
||||
m50_end = MUS_2_START + ticks(m50)
|
||||
wait_end = m50_end + 40
|
||||
...
|
||||
dialog1_start = предыдущая фиксированная цепочка
|
||||
exit_start = dialog1_start + ticks(m53)
|
||||
anim_end = exit_start + 469
|
||||
```
|
||||
|
||||
Структуру лучше держать на стеке `intro_pv_animated()` и передавать нужным
|
||||
helper-функциям указателем. File-scope изменяемая таблица снова попала бы в
|
||||
W2.
|
||||
|
||||
m50 уже загружен до начала PV. m53 начинает подгружаться задолго до своего
|
||||
старта, поэтому метаданные обоих треков к моменту использования доступны.
|
||||
|
||||
### Конец уровня (`pop_kid.c`)
|
||||
|
||||
Вместо `POP_MUS_TICKS_32/41` редкое событие окончания уровня вызывает
|
||||
банковый accessor IDX и вычисляет `pop_endmus_left` из runtime-тиков.
|
||||
|
||||
Индекс используется даже при выключенной музыке: оригинал выдерживает эту
|
||||
паузу молча. Поэтому нельзя заменять её одним `pop_music_busy()`.
|
||||
|
||||
## 9. Генератор и сборка
|
||||
|
||||
`tools/pop_pack_music.py` должен:
|
||||
|
||||
1. как сейчас преобразовать выбранный набор в `mNN.bin`;
|
||||
2. проверить кратность каждого результата 128 байтам;
|
||||
3. собрать плотные записи 0..56;
|
||||
4. записать `MUS/MUSIC.IDX` в формате `PMI1`;
|
||||
5. больше не генерировать `pop_music_tbl.h` и `pop_music_ticks.h`.
|
||||
|
||||
Makefile должен:
|
||||
|
||||
- добавить `MUS/MUSIC.IDX` в `DISK` и staging;
|
||||
- убрать музыкальные generated-header'ы из `GEN_H`;
|
||||
- убрать их из зависимостей EXE;
|
||||
- оставить `MUSIC_FMT` зависимостью только музыкальных ресурсов;
|
||||
- гарантировать, что `make music-flac/mp3/ogg/mt32` заменяет и PCM, и IDX,
|
||||
но не перелинковывает EXE.
|
||||
|
||||
Критерий архитектуры: SHA/дата `sprpop.exe` не меняется при переключении
|
||||
между четырьмя музыкальными целями.
|
||||
|
||||
## 10. Ошибки и совместимость
|
||||
|
||||
Игра не должна падать из-за необязательной музыки:
|
||||
|
||||
- нет IDX — музыка отключена, игра продолжает работать;
|
||||
- неверная magic/версия/формат — музыка отключена с диагностикой;
|
||||
- записи нет — конкретный трек считается отсутствующим;
|
||||
- файла нет — запрос трека завершается молча/с диагностикой, как сейчас;
|
||||
- обычный трек требует больше 20 страниц — не загружать обычным путём;
|
||||
- потоковый трек имеет больше 255 страниц — отвергнуть, потому что текущие
|
||||
счётчики страниц восьмибитные;
|
||||
- IDX и файл расходятся — применить политику §6;
|
||||
- все файловые операции выполнять через `POP_PATH_CALL`, чтобы сохранить
|
||||
работу на старых DSS с повреждением текущего каталога.
|
||||
|
||||
Для отсутствующего/повреждённого IDX остаётся выбрать поведение немой паузы
|
||||
конца уровня: нулевая пауза либо небольшой канонический fallback. Это не
|
||||
мешает основной архитектуре, но решение надо принять до реализации.
|
||||
|
||||
## 11. Этапы реализации
|
||||
|
||||
### MI0 — формат и host-тест
|
||||
|
||||
- вынести writer/reader `PMI1` в тестируемый код упаковщика;
|
||||
- проверить magic, размеры, LE-поля, нулевые записи и padding;
|
||||
- для всех четырёх наборов сверить `blocks/ticks` с фактическими PCM;
|
||||
- зафиксировать тестом значения MT-32 m50/m53/m56 как отличающиеся от FLAC.
|
||||
|
||||
### MI1 — runtime-загрузка IDX
|
||||
|
||||
- `pop_music_init()`/shutdown;
|
||||
- одна EMM-страница, загрузка и валидация;
|
||||
- accessor одной записи;
|
||||
- отказ без порчи W0/W3, EMM и файловых дескрипторов.
|
||||
|
||||
### MI2 — loader/pump
|
||||
|
||||
- заменить `pop_mus_tbl[]` runtime-записью;
|
||||
- проверять реальный размер файла;
|
||||
- обычный, немедленный и кольцевой пути должны использовать одну метаинформацию;
|
||||
- удалить `pop_music_tbl.h`.
|
||||
|
||||
### MI3 — runtime-шкала сцен
|
||||
|
||||
- перевести m50/m53 в `pop_intro.c` на локальную runtime-шкалу;
|
||||
- перевести m32/m41 в `pop_kid.c`;
|
||||
- удалить `pop_music_ticks.h`.
|
||||
|
||||
### MI4 — сборка и образы
|
||||
|
||||
- добавить IDX в каждый музыкальный набор;
|
||||
- исключить музыку из зависимостей линковки;
|
||||
- собрать четыре HDD-варианта с одним EXE.
|
||||
|
||||
### MI5 — приёмка в MAME/на железе
|
||||
|
||||
- FLAC: title -> story -> PV, обычный конец уровня, конец уровня 4, ending;
|
||||
- MT-32: те же маршруты, особенно m50/m53 и полный m56;
|
||||
- музыка выключена: пауза конца уровня берётся из IDX;
|
||||
- замена набора без пересборки EXE;
|
||||
- повреждённый IDX, отсутствующий трек, несовпадающий размер;
|
||||
- контроль, что не запущено более одного MAME и предыдущий экземпляр закрыт
|
||||
перед новым прогоном.
|
||||
|
||||
## 12. Критерии готовности
|
||||
|
||||
- один бинарник запускается со всеми четырьмя наборами;
|
||||
- ни один музыкальный generated-header не входит в сборку C;
|
||||
- начало следующей сцены/уровня соответствует фактической записи;
|
||||
- MT-32 m56 проигрывает все 82 страницы и не обрывается как FLAC-вариант;
|
||||
- обычные треки не читают за пределами выделенного EMM-блока;
|
||||
- IDX не расходует сотни байт W2;
|
||||
- ошибочный набор отключает музыку безопасно;
|
||||
- переключение `music-*` не меняет EXE;
|
||||
- старые DSS продолжают работать через восстановление каталога приложения.
|
||||
|
||||
---
|
||||
|
||||
# Часть II. Звуковые эффекты без перелинковки
|
||||
|
||||
## 13. Цель и отличие от музыки
|
||||
|
||||
Один и тот же `sprpop.exe` должен работать с разными наборами PCM-эффектов,
|
||||
если в них сохранена исходная нумерация sound id 0..56. Замена набора должна
|
||||
сводиться к замене согласованной пары `SND/SOUND.IDX` + `SND/SND.ARC`, без
|
||||
генерации C-заголовка и перелинковки EXE.
|
||||
|
||||
Набор состоит из двух согласованных файлов:
|
||||
|
||||
- `SND/SND.ARC` — обычный архив PBA1 с PCM-страницами;
|
||||
- `SND/SOUND.IDX` — описание раскладки эффектов внутри этих страниц.
|
||||
|
||||
Отдельный IDX предпочтительнее расширения заголовка `snd.arc`: PBA1 остаётся
|
||||
универсальным и не получает специального варианта только для звука, а
|
||||
формат индекса можно независимо версионировать и проверять тем же способом,
|
||||
что будущий `MUS/MUSIC.IDX`.
|
||||
|
||||
Существующий формат записи менять не требуется:
|
||||
|
||||
```c
|
||||
typedef struct {
|
||||
uint8_t page;
|
||||
uint16_t off;
|
||||
uint16_t len;
|
||||
} pop_snd_ent_t;
|
||||
```
|
||||
|
||||
Это ровно необходимые проигрывателю номер логической страницы, смещение в
|
||||
ней и длина PCM. В текущем ABI SDCC/z80 запись занимает 5 байт; дисковый
|
||||
формат обязан описывать эти пять байт явно (`uint16` little-endian), а код
|
||||
должен проверять `sizeof(pop_snd_ent_t) == 5` на этапе сборки.
|
||||
|
||||
## 14. Подтверждённое текущее состояние
|
||||
|
||||
Сейчас `tools/pop_pack_sound.py` печатает раскладку в
|
||||
`gen/pop_sound_tbl.h`, а `pop_sfx.c` включает её как
|
||||
`static const pop_snd_tbl[57]`. Поэтому конкретные `page/off/len` становятся
|
||||
частью EXE.
|
||||
|
||||
В текущей сборке таблица занимает 285 байт в `_CODE`, по адресам
|
||||
`0x52D8..0x53F4`. Это общий резидентный диапазон игры: при huge-модели
|
||||
`_CODE`, `_DATA`, heap и stack совместно используют плоские 32 КБ
|
||||
`0x4000..0xBFFF`; граница W1/W2 отдельного бюджета здесь не создаёт.
|
||||
|
||||
Проверены оба имеющихся исходных набора DIGISND:
|
||||
|
||||
| набор | PCM-эффектов | страниц после упаковки | отличия раскладки |
|
||||
|---|---:|---:|---:|
|
||||
| MSDOS 1.3/1.4 | 31 из 57 | 10 | эталон текущей сборки |
|
||||
| SDLPoP 1.0/1.1 | 31 из 57 | 9 | отличаются 8 id |
|
||||
|
||||
Максимальный эффект занимает 17 664 байта, или 138 блоков по 128 байт.
|
||||
У SDLPoP эффект 48 содержит только 7 исходных сэмплов; это свойство самого
|
||||
набора, а не ошибка runtime-индекса.
|
||||
|
||||
## 15. Файл `SND/SOUND.IDX`
|
||||
|
||||
IDX занимает один 512-байтовый сектор и после короткого заголовка является
|
||||
точным дисковым дампом 57 записей `pop_snd_tbl`:
|
||||
|
||||
```text
|
||||
0..3 "PSI1" magic и версия индекса эффектов
|
||||
4 57 число плотных записей, id 0..56
|
||||
5 5 размер записи
|
||||
6 7 log2 размера блока: 1 << 7 = 128
|
||||
7 1 формат: unsigned 8-bit mono, CBL_FREQ_10K9
|
||||
8 pages фактическое число PCM-страниц в snd.arc
|
||||
9..15 0 резерв
|
||||
|
||||
16..300 57 записей по 5 байт:
|
||||
+0 uint8 page
|
||||
+1..2 uint16 off, little-endian
|
||||
+3..4 uint16 len, little-endian
|
||||
|
||||
301..511 0 резерв до одного сектора
|
||||
```
|
||||
|
||||
`pages` — фактический размер конкретного набора, например 10 для MSDOS или
|
||||
9 для SDLPoP. Его не следует называть или трактовать как
|
||||
`POP_SND_MAX_PAGES`: максимум 16 является compile-time-вместимостью
|
||||
загрузчика, а IDX сообщает число реально нужных страниц и обязан укладываться
|
||||
в этот предел.
|
||||
|
||||
Загрузчик сверяет `pages` с числом элементов внешнего PBA1 и не доверяет
|
||||
расходящимся файлам. `off` и `len` обязаны быть кратны 128; нулевая длина
|
||||
означает отсутствие PCM для данного id.
|
||||
|
||||
IDX загружается прямо в резидентный `pop_snd_tbl`, поэтому отдельной страницы
|
||||
EMM для него не требуется. Цена решения — одно дополнительное открытие и
|
||||
чтение 512 байт при старте звука; на фоне загрузки 9–10 страниц PCM это
|
||||
приемлемый холодный расход.
|
||||
|
||||
## 16. Размещение runtime-таблицы в памяти
|
||||
|
||||
`pop_snd_tbl` становится обычным изменяемым внутренним объектом:
|
||||
|
||||
```c
|
||||
pop_snd_ent_t pop_snd_tbl[POP_SND_COUNT];
|
||||
```
|
||||
|
||||
Его следует определить в отдельном internal data-модуле и объявить через
|
||||
`extern` в `_pop_sfx.h`, потому что таблицу заполняет холодный загрузчик из
|
||||
банка 8, а читает резидентный `pop_sfx.c`.
|
||||
|
||||
После снятия `const` SDCC перенесёт эти 285 байт из `_CODE` в `_DATA`. Это
|
||||
не добавляет 285 байт к общему резидентному расходу: `_CODE` одновременно
|
||||
уменьшается на тот же размер, а `_CODE` и `_DATA` последовательно лежат в
|
||||
одном диапазоне `0x4000..0xBFFF`. Возможна лишь небольшая разница из-за
|
||||
выравнивания, которую надо проверить итоговой map-картой и size-check.
|
||||
|
||||
Банк 8 может читать индекс напрямую в этот буфер: весь диапазон
|
||||
`0x4000..0xBFFF` остаётся доступен, пока банковый код исполняется в W3.
|
||||
Горячие обращения `pop_snd_tbl[id].page/off/len` и ISR не требуют новых
|
||||
маппингов или accessor-функций.
|
||||
|
||||
## 17. Фактическое число страниц
|
||||
|
||||
Инвариантами программы остаются:
|
||||
|
||||
```c
|
||||
#define POP_SND_MAX_PAGES 16
|
||||
#define POP_SND_COUNT 57
|
||||
#define POP_SND_BLOCK 128
|
||||
```
|
||||
|
||||
`POP_SND_PAGES == 10` больше не должен означать размер конкретного набора.
|
||||
Вместо него появляется runtime-состояние:
|
||||
|
||||
```c
|
||||
uint8_t pop_snd_pages;
|
||||
uint8_t pop_snd_page[POP_SND_MAX_PAGES];
|
||||
```
|
||||
|
||||
Загрузчик выделяет `mem_alloc_pages(pop_snd_pages)` и заполняет только
|
||||
фактическое число элементов массива. MSDOS-набор займёт 10 страниц,
|
||||
SDLPoP-набор — 9. Предел 16 оставляет запас будущим наборам без
|
||||
перекомпиляции и увеличивает постоянный массив лишь на 6 байт относительно
|
||||
текущего; ещё один байт занимает `pop_snd_pages`.
|
||||
|
||||
Насос при переходе длинного эффекта через границу страницы сравнивает
|
||||
`sfx_pg + 1` с `pop_snd_pages`, а не с compile-time-константой.
|
||||
|
||||
## 18. Загрузка и валидация
|
||||
|
||||
`pop_sfx_init()` должен выполнять следующую последовательность:
|
||||
|
||||
1. открыть `SND/SOUND.IDX` через существующий путь `POP_PATH_CALL`;
|
||||
2. прочитать 16-байтовый заголовок, затем 285 байт записей прямо в
|
||||
`pop_snd_tbl`, и проверить полный размер IDX;
|
||||
3. проверить magic, версию, число/размер записей, формат и размер блока;
|
||||
4. получить из IDX фактическое число страниц и проверить диапазон 1..16;
|
||||
5. открыть `SND/SND.ARC` через `POP_PATH_CALL`;
|
||||
6. прочитать обычную таблицу PBA1 с ёмкостью `POP_SND_MAX_PAGES`;
|
||||
7. сверить `PSI1.pages` с числом элементов PBA1;
|
||||
8. проверить каждую ненулевую запись;
|
||||
9. только после этого выделить фактическое число EMM-страниц и загрузить их.
|
||||
|
||||
Для каждой записи проверяются:
|
||||
|
||||
- `page < pop_snd_pages`;
|
||||
- `off < 16384` и `off % POP_SND_BLOCK == 0`;
|
||||
- `len % POP_SND_BLOCK == 0`;
|
||||
- диапазон `page/off/len` не выходит за загруженный набор;
|
||||
- переход длинного эффекта на следующие страницы не превышает
|
||||
`pop_snd_pages`.
|
||||
|
||||
При любой ошибке блок EMM и файловый дескриптор освобождаются, таблица не
|
||||
используется, эффекты остаются выключенными. Набор без `SOUND.IDX` считается
|
||||
неполным; хранить в EXE старую таблицу как fallback не следует, иначе
|
||||
зависимость бинарника от конкретного набора останется.
|
||||
|
||||
## 19. Генератор и сборка
|
||||
|
||||
`tools/pop_pack_sound.py` должен:
|
||||
|
||||
1. как сейчас разобрать DIGISND, привести PCM к CBL_FREQ_10K9 и выровнять
|
||||
начала/длины на 128 байт;
|
||||
2. вычислить фактическое число страниц и отвергнуть набор больше 16;
|
||||
3. сформировать плотные 57 записей `page/off/len`;
|
||||
4. записать `SND/SOUND.IDX` в формате `PSI1`, включая фактическое число
|
||||
страниц;
|
||||
5. собрать обычный `SND/SND.ARC` из полученных страниц;
|
||||
6. больше не генерировать `gen/pop_sound_tbl.h`.
|
||||
|
||||
Чтобы число страниц не было захардкожено списком `s0.bin`..`s9.bin` в
|
||||
Makefile, упаковщик звука предпочтительно должен сразу формировать
|
||||
`assets/packed/SND/snd.arc` либо передавать упаковщику архивов динамический
|
||||
список результатов без сохранения устаревшей десятой страницы.
|
||||
|
||||
Стабильные тип и константы переносятся в обычный internal-заголовок. EXE не
|
||||
должен зависеть от результата упаковки звука: цель выбора набора меняет
|
||||
согласованную пару `SOUND.IDX` + `SND.ARC`.
|
||||
|
||||
## 20. Этапы реализации эффектов
|
||||
|
||||
### SI0 — формат и host-тест
|
||||
|
||||
- writer/reader отдельного `SOUND.IDX` в формате `PSI1`;
|
||||
- проверка точного пятибайтового LE-формата записи;
|
||||
- сборка и разбор обоих имеющихся DIGISND-наборов;
|
||||
- проверки 10 страниц MSDOS, 9 страниц SDLPoP и отличающихся записей.
|
||||
|
||||
### SI1 — runtime-таблица
|
||||
|
||||
- изменяемый `pop_snd_tbl` в общем резидентном CODE/DATA-диапазоне;
|
||||
- загрузка и полная валидация `PSI1`;
|
||||
- удаление generated `pop_sound_tbl.h`;
|
||||
- проверка map-карты: перенос CODE -> DATA не должен съесть heap.
|
||||
|
||||
### SI2 — гибкое выделение страниц
|
||||
|
||||
- `POP_SND_MAX_PAGES == 16` и runtime `pop_snd_pages`;
|
||||
- фактический размер стека таблицы PBA1;
|
||||
- выделение/освобождение 9, 10 и тестовых 16 страниц;
|
||||
- runtime-гард перехода длинного эффекта между страницами.
|
||||
|
||||
### SI3 — сборка без перелинковки
|
||||
|
||||
- убрать фиксированный `SND_ATL` из десяти имён;
|
||||
- сделать `SOUND.IDX` и `SND.ARC` согласованными результатами выбора набора;
|
||||
- проверить неизменность SHA/даты `sprpop.exe` при смене набора.
|
||||
|
||||
### SI4 — приёмка
|
||||
|
||||
- проиграть короткий, обычный и переходящий страницу эффекты;
|
||||
- проверить приоритеты и перебивание звуков;
|
||||
- проверить музыку поверх общего CBL после runtime-загрузки эффектов;
|
||||
- отсутствующий IDX и повреждённые magic/count/pages/page/off/len должны
|
||||
безопасно отключать эффекты;
|
||||
- перед каждым MAME-прогоном завершать предыдущий экземпляр и никогда не
|
||||
запускать две копии одновременно.
|
||||
|
||||
## 21. Критерии готовности эффектов
|
||||
|
||||
- один `sprpop.exe` работает с MSDOS- и SDLPoP-наборами;
|
||||
- замена согласованной пары `SND/SOUND.IDX` + `SND/SND.ARC` не требует
|
||||
компиляции C и не меняет EXE;
|
||||
- `pop_snd_tbl` больше не генерируется как C-код;
|
||||
- загружается фактическое число страниц в диапазоне 1..16;
|
||||
- таблица не расходует дополнительную страницу EMM и не увеличивает
|
||||
суммарный резидентный CODE+DATA на свои 285 байт;
|
||||
- горячий путь и ISR используют прежние прямые `page/off/len`;
|
||||
- повреждённый индекс не приводит к чтению за пределами EMM-блока;
|
||||
- файловые операции сохраняют совместимость со старыми DSS через
|
||||
восстановление каталога приложения.
|
||||
@@ -1,41 +0,0 @@
|
||||
/* kid_atlas.h — раскладка атласов Kid. Сгенерировано pop_pack_kid.py. */
|
||||
#ifndef KID_ATLAS_H
|
||||
#define KID_ATLAS_H
|
||||
#define KID_SHIFT 3
|
||||
#define KID_MASK 7
|
||||
#define KID_PAGES 28
|
||||
#define KID_PAL 0x70
|
||||
static const char *const kid_atl[KID_PAGES] = {
|
||||
"kid0.atl",
|
||||
"kid1.atl",
|
||||
"kid2.atl",
|
||||
"kid3.atl",
|
||||
"kid4.atl",
|
||||
"kid5.atl",
|
||||
"kid6.atl",
|
||||
"kid7.atl",
|
||||
"kid8.atl",
|
||||
"kid9.atl",
|
||||
"kid10.atl",
|
||||
"kid11.atl",
|
||||
"kid12.atl",
|
||||
"kid13.atl",
|
||||
"kid14.atl",
|
||||
"kid15.atl",
|
||||
"kid16.atl",
|
||||
"kid17.atl",
|
||||
"kid18.atl",
|
||||
"kid19.atl",
|
||||
"kid20.atl",
|
||||
"kid21.atl",
|
||||
"kid22.atl",
|
||||
"kid23.atl",
|
||||
"kid24.atl",
|
||||
"kid25.atl",
|
||||
"kid26.atl",
|
||||
"kid27.atl",
|
||||
};
|
||||
#define KID_PAL_FILE "kid.pal"
|
||||
#define KID_SWORD_ATL "sword.atl" /* chtab_0: меч в руке */
|
||||
#define KID_SWORD_ID0 0 /* индекс в атласе = sword_tbl.id - ID0 */
|
||||
#endif
|
||||
@@ -1,5 +1,5 @@
|
||||
/* pop_bg_atlas.h — раскладка атласов статического фона PoP.
|
||||
* Сгенерировано toolchain/pop_pack_bg.py — НЕ править вручную.
|
||||
* Сгенерировано tools/pop_pack_bg.py — НЕ править вручную.
|
||||
*
|
||||
* Прямая адресация (ноль remap-таблиц в W2):
|
||||
* ENV фон id N -> atlas env_bg[N>>4], idx N&15
|
||||
@@ -23,25 +23,15 @@
|
||||
#define POP_PAL_ENV 0x50
|
||||
#define POP_PAL_WALL 0x60
|
||||
|
||||
/* Имена файлов атласов по тайлсету (грузятся atlas_load). Таблицы
|
||||
* видны только тому, кто попросил POP_BG_ATLAS_NAMES: иначе копия
|
||||
* строк уедет в каждый включивший заголовок модуль. */
|
||||
/* Палитры пока остаются отдельными файлами рядом с bg.arc. Имена
|
||||
* видит только модуль загрузки, чтобы не размножать строки. */
|
||||
#ifdef POP_BG_ATLAS_NAMES
|
||||
static const char *const pop_env_atl[POP_TILESETS][POP_ENV_PAGES] = {
|
||||
{ "pop_env0.atl", "pop_env1.atl", "pop_env2.atl", "pop_env3.atl", "pop_env4.atl", "pop_env5.atl", "pop_env6.atl", "pop_env7.atl", "pop_env8.atl", "pop_env9.atl" },
|
||||
{ "pal_env0.atl", "pal_env1.atl", "pal_env2.atl", "pal_env3.atl", "pal_env4.atl", "pal_env5.atl", "pal_env6.atl", "pal_env7.atl", "pal_env8.atl", "pal_env9.atl" },
|
||||
};
|
||||
static const char *const pop_wall_atl[POP_TILESETS] = { "pop_wall.atl", "pal_wall.atl" };
|
||||
static const char *const pop_fore_atl[POP_TILESETS] = { "pop_fore.atl", "pal_fore.atl" };
|
||||
static const char *const pop_tile_pal[POP_TILESETS] = { "pop_tile.pal", "pal_tile.pal" };
|
||||
#endif /* POP_BG_ATLAS_NAMES */
|
||||
#define POP_POT_ATL "pop_pot.atl" /* chtab_1: зелья, от набора не зависит */
|
||||
#define POP_PAL_POT 0x40
|
||||
/* Пузырёк зелья: красный набор = id 16..22 (кадры оригинала),
|
||||
зелёный (перо/переворот) и синий (вред/открыть) = те же кадры
|
||||
под id 30..36 и 40..46 (draw_tile_anim, seg008:652). */
|
||||
#define POP_POT_BUBB_GREEN 30
|
||||
#define POP_POT_BUBB_BLUE 40
|
||||
#define POP_BG_PAL "pop_bg.pal"
|
||||
|
||||
#endif
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* pop_shadow_atlas.h — раскладка атласа ТЕНИ.
|
||||
* СГЕНЕРЕНО toolchain/pop_pack_shadow.py — НЕ править вручную.
|
||||
* СГЕНЕРЕНО tools/pop_pack_shadow.py — НЕ править вручную.
|
||||
*
|
||||
* Две половины, потому что и у оригинала их две (см. шапку
|
||||
* упаковщика): кадры вне боя адресуют chtab_2 (спрайты Кида),
|
||||
@@ -25,44 +25,6 @@
|
||||
/* Половина «в бою» — SHADOW.DAT (chtab_5, кадры 150..189). */
|
||||
#define POP_SHADOW_FGT_PAGES 4
|
||||
|
||||
static const char *const pop_shadow_kid_atl[POP_SHADOW_KID_PAGES] = {
|
||||
"SHADOW\\sk0.atl",
|
||||
"SHADOW\\sk1.atl",
|
||||
"SHADOW\\sk2.atl",
|
||||
"SHADOW\\sk3.atl",
|
||||
"SHADOW\\sk4.atl",
|
||||
"SHADOW\\sk5.atl",
|
||||
"SHADOW\\sk6.atl",
|
||||
"SHADOW\\sk7.atl",
|
||||
"SHADOW\\sk8.atl",
|
||||
"SHADOW\\sk9.atl",
|
||||
"SHADOW\\sk10.atl",
|
||||
"SHADOW\\sk11.atl",
|
||||
"SHADOW\\sk12.atl",
|
||||
"SHADOW\\sk13.atl",
|
||||
"SHADOW\\sk14.atl",
|
||||
"SHADOW\\sk15.atl",
|
||||
"SHADOW\\sk16.atl",
|
||||
"SHADOW\\sk17.atl",
|
||||
"SHADOW\\sk18.atl",
|
||||
"SHADOW\\sk19.atl",
|
||||
"SHADOW\\sk20.atl",
|
||||
"SHADOW\\sk21.atl",
|
||||
"SHADOW\\sk22.atl",
|
||||
"SHADOW\\sk23.atl",
|
||||
"SHADOW\\sk24.atl",
|
||||
"SHADOW\\sk25.atl",
|
||||
"SHADOW\\sk26.atl",
|
||||
"SHADOW\\sk27.atl",
|
||||
};
|
||||
|
||||
static const char *const pop_shadow_fgt_atl[POP_SHADOW_FGT_PAGES] = {
|
||||
"SHADOW\\sf0.atl",
|
||||
"SHADOW\\sf1.atl",
|
||||
"SHADOW\\sf2.atl",
|
||||
"SHADOW\\sf3.atl",
|
||||
};
|
||||
|
||||
/* 16 цветов, запись 4 байта (B, G, R, 0) — формат gfx_pal_load. */
|
||||
static const uint8_t pop_shadow_pal[64] = {
|
||||
0x00,0x00,0x00,0x00,0xD8,0xFC,0xFC,0x00,0x6C,0x80,0xDC,0x00,0x78,0x44,0x24,0x00,0xA0,0xB8,0xD8,0x00,0xB5,0x7C,0x21,0x00,0x00,0x90,0xB8,0x00,0x5D,0x6D,0xAB,0x00,0x0C,0xF0,0x6C,0x00,0xCC,0x38,0x05,0x00,0x38,0xCC,0xC0,0x00,0x58,0xDB,0xA7,0x00,0xBB,0x9F,0x54,0x00,0x5D,0x18,0x60,0x00,0x37,0xD7,0x37,0x00,0x2F,0x17,0xD2,0x00,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* pop_sound_tbl.h — раскладка оцифрованных эффектов PoP.
|
||||
* СГЕНЕРЕНО toolchain/pop_pack_sound.py — НЕ править вручную.
|
||||
* СГЕНЕРЕНО tools/pop_pack_sound.py — НЕ править вручную.
|
||||
*
|
||||
* Данные: 8 бит беззнаковые, МОНО, 10937.5 Гц (CBL_FREQ_10K9) — формат
|
||||
* CBL один в один, конвертировать в рантайме нечего.
|
||||
@@ -28,6 +28,8 @@
|
||||
#define POP_SND_SILENCE_PAGE 0
|
||||
#define POP_SND_SILENCE_OFF 0x0000
|
||||
|
||||
/* Полная таблица нужна только горячей половине звука. */
|
||||
#ifdef POP_SND_TABLE
|
||||
typedef struct { uint8_t page; uint16_t off; uint16_t len; } pop_snd_ent_t;
|
||||
|
||||
/* len == 0 — у звука нет оцифровки (это музыка, см. sound_plan.md). */
|
||||
@@ -90,24 +92,6 @@ static const pop_snd_ent_t pop_snd_tbl[POP_SND_COUNT] = {
|
||||
{ 0, 0x0000, 0 }, /* 55 */
|
||||
{ 0, 0x0000, 0 }, /* 56 */
|
||||
};
|
||||
|
||||
/* Имена файлов видит ТОЛЬКО тот, кто попросил POP_SND_FILES (холодная
|
||||
* половина): иначе копия строк уезжает в каждый включивший заголовок
|
||||
* модуль, а в резиденте W1/W2 каждая сотня байт на счету. Тот же
|
||||
* приём, что у POP_BG_ATLAS_NAMES в pop_bg_atlas.h. */
|
||||
#ifdef POP_SND_FILES
|
||||
static const char *const pop_snd_files[POP_SND_PAGES] = {
|
||||
"SND\\s0.bin",
|
||||
"SND\\s1.bin",
|
||||
"SND\\s2.bin",
|
||||
"SND\\s3.bin",
|
||||
"SND\\s4.bin",
|
||||
"SND\\s5.bin",
|
||||
"SND\\s6.bin",
|
||||
"SND\\s7.bin",
|
||||
"SND\\s8.bin",
|
||||
"SND\\s9.bin",
|
||||
};
|
||||
#endif
|
||||
#endif /* POP_SND_TABLE */
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* _pop_file.h — внутренний файловый слой SprPoP для старых версий DSS.
|
||||
*
|
||||
* DSS до исправления относительных путей оставляет рабочий каталог внутри
|
||||
* последнего подкаталога. POP_PATH_CALL восстанавливает абсолютный каталог
|
||||
* приложения до и после операции. Это макрос намеренно: код исполняется в
|
||||
* банке вызывающего, где доступен переданный указатель, и не расходует W1.
|
||||
*/
|
||||
#ifndef _POP_FILE_H
|
||||
#define _POP_FILE_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <errno.h>
|
||||
|
||||
/* BANK10: получить каталог EXE через APPINFO B=1 и сделать его текущим. */
|
||||
int8_t pop_path_init(void) __banked;
|
||||
int8_t pop_chdir_home(void) __banked;
|
||||
|
||||
/* expression вычисляется только после успешного chdir. errno файловой
|
||||
* операции сохраняется через завершающее восстановление каталога. */
|
||||
#define POP_PATH_CALL(result, expression) do { \
|
||||
if (pop_chdir_home() != 0) { \
|
||||
(result) = -1; \
|
||||
} else { \
|
||||
int _pop_path_errno; \
|
||||
(result) = (expression); \
|
||||
_pop_path_errno = errno; \
|
||||
(void)pop_chdir_home(); \
|
||||
errno = _pop_path_errno; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
#endif
|
||||
@@ -1,14 +1,14 @@
|
||||
/*
|
||||
* _pop_sfx.h — внутренний контракт между половинами звука (НЕ публичный,
|
||||
* публичный — pop_sfx.h). Горячая половина живёт в резиденте
|
||||
* (pop_sfx.c), холодная в банке (pop_sfx_cold.c); делят они таблицу
|
||||
* звуков, массив физических страниц и насос.
|
||||
* (pop_sfx.c), холодная в банке (pop_sfx_cold.c); делят они массив
|
||||
* физических страниц и насос.
|
||||
*/
|
||||
#ifndef _POP_SFX_INTERNAL_H
|
||||
#define _POP_SFX_INTERNAL_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include "pop_sound_tbl.h" /* pop_snd_tbl, POP_SND_* — генерит упаковщик */
|
||||
#include "pop_sound_tbl.h" /* POP_SND_* и, по запросу, таблица эффектов */
|
||||
|
||||
/* Физические страницы набора, по индексу из pop_snd_tbl[].page. */
|
||||
extern uint8_t pop_snd_page[POP_SND_PAGES];
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <string.h>
|
||||
#include <sprinter_mem.h>
|
||||
#include "pop_arc.h"
|
||||
#include "_pop_file.h"
|
||||
|
||||
/* Пути 8.3, как требует DSS. Держим их ЗДЕСЬ: строка обязана лежать в том
|
||||
* же банке, что и код, который её читает (см. pop_arc.h). */
|
||||
@@ -42,7 +43,7 @@ int8_t pop_arc_open(pop_arc_t *a, const char *path,
|
||||
|
||||
a->fd = -1;
|
||||
a->count = 0;
|
||||
fd = open(path, O_RDONLY);
|
||||
POP_PATH_CALL(fd, open(path, O_RDONLY));
|
||||
if (fd < 0) return -1;
|
||||
if (read(fd, head, sizeof(head)) != (int)sizeof(head) ||
|
||||
memcmp(head, "PBA1", 4) != 0 ||
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
#include "pop_cheat.h"
|
||||
#include "pop_sfx.h"
|
||||
#include "_pop_config.h"
|
||||
#include "_pop_file.h"
|
||||
|
||||
#define CFG_BASE 0x100
|
||||
|
||||
@@ -151,7 +152,7 @@ static uint8_t cfg_read_file(uint8_t *buf, uint16_t *size)
|
||||
int fd, got;
|
||||
long n;
|
||||
|
||||
fd = open("POP.CFG", O_RDONLY);
|
||||
POP_PATH_CALL(fd, open("POP.CFG", O_RDONLY));
|
||||
if (fd < 0) return 0;
|
||||
n = lseek(fd, 0, SEEK_END);
|
||||
if (n < POP_CFG_MIN_SIZE || n > POP_CFG_MAX_SIZE ||
|
||||
@@ -172,7 +173,7 @@ int8_t pop_cfg_load(void) __banked
|
||||
|
||||
if (!cfg_read_file(buf, &size) || !pop_cfg_decode(buf, size)) return 0;
|
||||
if (buf[9] != POP_PROFILE_VANILLA)
|
||||
printf("POP.CFG: ENHANCED profile -> VANILLA\n");
|
||||
puts("POP.CFG: ENHANCED profile -> VANILLA\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -192,7 +193,7 @@ int8_t pop_cfg_save(void) __banked
|
||||
gfx_w0_unmap();
|
||||
/* Не unlink: bank_save_file сам делает O_TRUNC и не создаёт лишнего
|
||||
* окна, когда POP.CFG вообще отсутствует после ошибки unlink. */
|
||||
rc = bank_save_file(page, CFG_BASE, "POP.CFG", sizeof(buf));
|
||||
POP_PATH_CALL(rc, bank_save_file(page, CFG_BASE, "POP.CFG", sizeof(buf)));
|
||||
mem_free_block(blk);
|
||||
return (rc == sizeof(buf)) ? 1 : 0;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
#include "pop_title.h" /* титульная картинка после ввода имени */
|
||||
#include "pop_music.h" /* подкачка потоковой темы победы */
|
||||
#include "pop_hof.h"
|
||||
#include "_pop_file.h"
|
||||
|
||||
/* MAX_HOF_COUNT оригинала — шесть строк. */
|
||||
#define HOF_COUNT 6
|
||||
@@ -162,7 +163,7 @@ static void hof_load(void)
|
||||
if (!blk) return;
|
||||
page = mem_get_page(blk, 0);
|
||||
gfx_w0_page_prepare(page);
|
||||
got = bank_load_file(page, HOF_BASE, "POP.HOF", HOF_SIZE);
|
||||
POP_PATH_CALL(got, bank_load_file(page, HOF_BASE, "POP.HOF", HOF_SIZE));
|
||||
if (got == HOF_SIZE) {
|
||||
gfx_w0_map(page);
|
||||
for (i = 0; i < HOF_SIZE; i++)
|
||||
@@ -187,7 +188,11 @@ static void hof_save(void)
|
||||
gfx_w0_unmap();
|
||||
/* Неудача записи не меняет только что показанную таблицу: следующее
|
||||
* открытие разумно начнёт с прежнего файла или с пустого списка. */
|
||||
bank_save_file(page, HOF_BASE, "POP.HOF", HOF_SIZE);
|
||||
{
|
||||
int saved;
|
||||
POP_PATH_CALL(saved,
|
||||
bank_save_file(page, HOF_BASE, "POP.HOF", HOF_SIZE));
|
||||
}
|
||||
mem_free_block(blk);
|
||||
}
|
||||
|
||||
|
||||
@@ -83,26 +83,24 @@ enum {
|
||||
PV_MUS_4_LOAD = PV_DIALOG1_START + 54, /* подкачка m52 */
|
||||
PV_WALK2_START = PV_DIALOG1_START + 176,
|
||||
PV_DIALOG2_START = PV_DIALOG1_START + 416,
|
||||
/* ЗАКЛИНАНИЕ СДВИНУТО РАНЬШЕ СЦЕНАРИЯ — ПОДГОНКА ПОД МУЗЫКУ.
|
||||
/* ПОДГОНКА ЗАКЛИНАНИЯ ПОД МУЗЫКУ СНЯТА (2026-08-28): теперь 0.
|
||||
*
|
||||
* Сцена render-bound: кадр рисуется дороже своего бюджета, кадровая
|
||||
* ветка пейсинга догона не имеет, и анимация идёт ~49 тиков в секунду
|
||||
* вместо 60. Реплика Джафара при этом играет по РЕАЛЬНОМУ времени и
|
||||
* успевает кончиться, когда шкала стоит на 1854 вместо 2028 (замер в
|
||||
* MAME 2026-08-27 зондом на конец трека: длина насосу отдана полная,
|
||||
* 1403 порции, курсор дошёл до последней страницы — трек доигрывает
|
||||
* целиком, отстаёт именно картинка).
|
||||
* История: сцена была render-bound — кадр рисовался дороже своего
|
||||
* бюджета, кадровая ветка пейсинга догона не имела, и анимация шла
|
||||
* ~49 тиков в секунду вместо 60 (замер 2026-08-27). Реплика Джафара
|
||||
* при этом играет по РЕАЛЬНОМУ времени, поэтому кода приходила
|
||||
* примерно на секунду раньше молнии, и жест целиком (замах, шаг
|
||||
* назад, вспышка) сдвигали раньше сценария на подобранные на слух
|
||||
* 100 тиков.
|
||||
*
|
||||
* Из-за этого кода реплики приходила примерно на секунду раньше
|
||||
* молнии. Честное лечение — удешевить кадр сцены; до тех пор двигаем
|
||||
* жест целиком (замах, шаг назад, вспышка), чтобы он не сломался
|
||||
* внутри себя. Величина подобрана НА СЛУХ и уточняется прогоном:
|
||||
* расчётные полсотни тиков (секунда отставания при ~49 тиках/с) на
|
||||
* слух почти не изменили картины, поэтому взято вдвое больше.
|
||||
*
|
||||
* Эффектов дверей это не касается: у них и звук, и картинка идут по
|
||||
* одной и той же шкале и разъехаться не могут. */
|
||||
PV_MAGIC_LEAD = 100,
|
||||
* Причину убрали: отрисовка разложена по кадровым интервалам
|
||||
* (pv_restore_bg), сцена идёт по единственным часам — кадрам луча, —
|
||||
* и подкачка трека оплачивается интервалом (pop_music_load_step).
|
||||
* После этого подгонка стала вредной: молния била больше чем на
|
||||
* секунду раньше коды (проверка пользователем 2026-08-28). Константу
|
||||
* оставляем на месте — если запись другого набора (mt32/ogg) снова
|
||||
* разъедется, крутить надо её, а не тайминги сценария. */
|
||||
PV_MAGIC_LEAD = 0,
|
||||
PV_RAISE_START = PV_DIALOG1_START + 696 - PV_MAGIC_LEAD,
|
||||
PV_STEPBACK_START = PV_DIALOG1_START + 703 - PV_MAGIC_LEAD,
|
||||
PV_MAGIC_START = PV_DIALOG1_START + 822 - PV_MAGIC_LEAD,
|
||||
@@ -430,6 +428,44 @@ static int intro_pv_draw_base(void)
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ВОССТАНОВЛЕНИЕ ФОНА ЗАДНЕЙ СТРАНИЦЫ — ДВУМЯ БЛОКАМИ ПО КАДРОВОМУ ИНТЕРВАЛУ.
|
||||
*
|
||||
* Персонажи и живой фон рисуются банком спрайтов и в ОЗУ-копию не оседают,
|
||||
* поэтому «стереть прошлый кадр» = скопировать чистый фон из теневой копии
|
||||
* соседней страницы. Раньше это была ОДНА копия на весь экран, и она стоила
|
||||
* ~547 000 тактов = 1,27 кадра луча (модель blit_cost_model). Хвост кадра
|
||||
* сцены ждал vsync ПОСЛЕ отрисовки, так что эти 1,27 кадра ПРИБАВЛЯЛИСЬ к
|
||||
* пяти интервалам делителя: сцена шла 6+ кадров вместо 5, то есть ~8,2 fps
|
||||
* вместо десяти (замер 49/60 тиков — docs/BUGS_OPEN.md PV-RENDER-BOUND).
|
||||
*
|
||||
* Лечение — не «удешевить», а РАЗЛОЖИТЬ по интервалам: каждый блок заведомо
|
||||
* влезает в кадр, и после него честно ждём vsync. Тогда отрисовка живёт
|
||||
* ВНУТРИ делителя, а не сверх него.
|
||||
* - копируем только картинку (200 строк с POP_YOFF), а не все 256:
|
||||
* сверху и снизу чёрная рамка, туда никто не рисует;
|
||||
* - двумя половинами по 100 строк: ~219 000 тактов каждая, чуть больше
|
||||
* половины кадра — запас двукратный.
|
||||
*
|
||||
* ТРАТИТ РОВНО ДВА кадровых интервала (по gfx_wait_vsync на блок) — это
|
||||
* учтено в раскладке кадра у всех трёх сцен (см. вызовы). */
|
||||
#define PV_BG_Y0 POP_YOFF
|
||||
#define PV_BG_H 200
|
||||
|
||||
static void pv_restore_bg(void)
|
||||
{
|
||||
gfx_rect_t r;
|
||||
|
||||
r.x = 0;
|
||||
r.w = GFX_WIDTH;
|
||||
r.h = PV_BG_H / 2;
|
||||
r.y = PV_BG_Y0;
|
||||
gfx_copy_page(&r, GFX_COPY_DIRECT);
|
||||
gfx_wait_vsync();
|
||||
r.y = PV_BG_Y0 + PV_BG_H / 2;
|
||||
gfx_copy_page(&r, GFX_COPY_DIRECT);
|
||||
gfx_wait_vsync();
|
||||
}
|
||||
|
||||
static void pv_blit_floor(const atlas_t *at, uint8_t frame, int x, int floor)
|
||||
{
|
||||
const uint8_t *img;
|
||||
@@ -701,12 +737,10 @@ static uint8_t intro_pv_draw_frame(const atlas_t *princess, const atlas_t *j0,
|
||||
uint8_t sand_frame, uint8_t flash,
|
||||
uint16_t tick)
|
||||
{
|
||||
gfx_rect_t full;
|
||||
uint8_t back = (uint8_t)(gfx_get_visible_page() ^ 1);
|
||||
|
||||
gfx_set_draw_page(back);
|
||||
full.x = 0; full.y = 0; full.w = GFX_WIDTH; full.h = GFX_HEIGHT;
|
||||
gfx_copy_page(&full, GFX_COPY_DIRECT);
|
||||
pv_restore_bg(); /* блоки 1-2: фон, два интервала */
|
||||
gfx_set_bank(GFX_BANK_SPRITE);
|
||||
/* y=116 в princess_room_torch — нижняя строка, не верх спрайта. */
|
||||
pv_blit_floor(torch, torch_left, 93, 116);
|
||||
@@ -730,7 +764,7 @@ static uint8_t intro_pv_draw_frame(const atlas_t *princess, const atlas_t *j0,
|
||||
* его надо повторить ПОСЛЕ актёров, иначе Jaffar проходит перед
|
||||
* правой передней колонной. */
|
||||
pv_blit_floor(hourglass, 0, 240, 167);
|
||||
gfx_wait_vsync();
|
||||
gfx_wait_vsync(); /* блок 3: актёры и декорации */
|
||||
gfx_set_visible_page(back);
|
||||
if (flash) {
|
||||
uint8_t pulse;
|
||||
@@ -743,9 +777,9 @@ static uint8_t intro_pv_draw_frame(const atlas_t *princess, const atlas_t *j0,
|
||||
pop_pal_flash_white(0);
|
||||
gfx_wait_vsync();
|
||||
}
|
||||
return 11;
|
||||
return 13; /* три интервала кадра + десять на молнию */
|
||||
}
|
||||
return 1;
|
||||
return 3; /* сколько кадровых интервалов съели */
|
||||
}
|
||||
|
||||
/* Сцена pv_scene() SDLPoP: ожидание Princess с факелами/звёздами, поворот,
|
||||
@@ -759,12 +793,7 @@ static int intro_pv_animated(void)
|
||||
uint16_t tick;
|
||||
uint16_t frame_end;
|
||||
uint16_t render_tick;
|
||||
uint16_t beam = 0; /* запасной путь: кадров луча ждали */
|
||||
uint16_t snd_done = 0; /* тиков насоса прошло с начала сцены */
|
||||
uint16_t snd_want = 0; /* сколько их должно было пройти */
|
||||
uint16_t snd_acc = 0; /* дробный остаток пересчёта 57/40 */
|
||||
uint8_t snd_prev = 0;
|
||||
uint8_t snd_pace = 0;
|
||||
uint16_t beam = 0; /* кадров луча потрачено с начала сцены */
|
||||
uint8_t period;
|
||||
uint8_t anim_step = 0;
|
||||
uint8_t torch_left = 1;
|
||||
@@ -813,18 +842,22 @@ static int intro_pv_animated(void)
|
||||
* Полноэкранная accel-копия кадра насосу не мешает: leaf режет DI
|
||||
* бандами по 16 строк. */
|
||||
(void)pop_sfx_start();
|
||||
/* ЧЕМ МЕРИТЬ ВРЕМЯ. Пока сцена была немой, кадры просто считали
|
||||
* ожидания vsync — и этого хватало. С музыкой выяснилось, что так
|
||||
* считаются НАШИ ожидания, а не прошедшее время: полноэкранная копия
|
||||
* страницы (~0,55 млн тактов) съедает больше кадра луча, и сцена
|
||||
* незаметно отстаёт — в MAME молния била уже ПОСЛЕ конца реплики.
|
||||
* Часы берём у насоса CBL: он идёт от расхода буфера железом, 85,4 Гц,
|
||||
* и ему безразлично, чем занят главный цикл. Если звук выключен,
|
||||
* часы стоят — тогда работает прежний путь по кадрам луча. */
|
||||
snd_prev = pop_snd_tick;
|
||||
gfx_wait_vsync();
|
||||
snd_pace = (uint8_t)(pop_snd_tick != snd_prev); /* часы реально идут? */
|
||||
snd_prev = pop_snd_tick;
|
||||
/* ЧЕМ МЕРИТЬ ВРЕМЯ — ЕДИНЫЕ ЧАСЫ: КАДРЫ ЛУЧА.
|
||||
*
|
||||
* Была вторая шкала — тики насоса CBL, и выбор между ними делался
|
||||
* гонкой на старте сцены (одна выборка `pop_snd_tick` через кадр:
|
||||
* успел ли диск раскрутить звук). Значит от прогона к прогону сцена
|
||||
* шла то по одним часам, то по другим, и кода реплики приходилась
|
||||
* каждый раз на другое место картинки (наблюдение пользователя
|
||||
* 2026-08-28; баг SND-PACE-DEAD). Насос как часы был нужен потому,
|
||||
* что кадр сцены рисовался ДОЛЬШЕ своего интервала и счёт наших
|
||||
* ожиданий врал; теперь отрисовка разложена на блоки по кадровому
|
||||
* интервалу (см. pv_restore_bg), и счёт кадров луча честен.
|
||||
*
|
||||
* Осталось учесть единственную работу, которая в интервал не влезала
|
||||
* сама, — подкачку страницы трека: шаг читает ПОЛСТРАНИЦЫ (~16 мс,
|
||||
* pop_music_load_step) и оплачивается ровно одним интервалом, как
|
||||
* любой другой блок кадра. */
|
||||
result = 0;
|
||||
princess_actor.x = 120;
|
||||
princess_actor.frame = 11; /* Pstand, facing left */
|
||||
@@ -892,59 +925,42 @@ static int intro_pv_animated(void)
|
||||
torch_right, sand_frame, flash,
|
||||
render_tick);
|
||||
pop_sfx_tick(); /* как в оригинале — в конце отрисовки кадра */
|
||||
|
||||
/* ГАРАНТИРОВАННЫЙ ШАГ ПОДКАЧКИ, а не «если останется время».
|
||||
*
|
||||
* Ниже страница следующей реплики читается в ПАУЗЕ кадра, и это
|
||||
* Раньше страница следующей реплики читалась в ПАУЗЕ кадра, и это
|
||||
* верно ровно до тех пор, пока пауза есть. В этой сцене её почти
|
||||
* нет: кадр дорогой, шкала работает по принципу «долг не догоняем»
|
||||
* (snd_want подтягивается к snd_done), и цикл ожидания выходит
|
||||
* сразу. Замер в MAME 2026-08-26: ОДНА страница за семь секунд,
|
||||
* то есть одиннадцать страниц m53 не успевали за всю сцену —
|
||||
* pop_music_play() на своём тике не находил готового трека, и
|
||||
* реплики Джафара пропадали совсем.
|
||||
* не было: кадр дорогой, шкала работает по принципу «долг не
|
||||
* догоняем», и цикл ожидания выходил сразу. Замер в MAME
|
||||
* 2026-08-26: ОДНА страница за семь секунд, то есть одиннадцать
|
||||
* страниц m53 не успевали за всю сцену — pop_music_play() на своём
|
||||
* тике не находил готового трека, и реплики Джафара пропадали
|
||||
* совсем (memory pv_music_stall_regression).
|
||||
*
|
||||
* Страница стоит 33 мс против кадра сцены в 100-133 мс, поэтому
|
||||
* один безусловный шаг в кадр сцену не ломает: он лишь тратит
|
||||
* часть той же паузы, а если паузы не было — добавляет к кадру
|
||||
* четверть его длины, и шкала это учтёт сама. */
|
||||
if (pop_music_loading()) (void)pop_music_load_step();
|
||||
* Поэтому шаг безусловный — и оплачен как обычный блок кадра: он
|
||||
* читает полстраницы (~16 мс), а следующий gfx_wait_vsync ставит
|
||||
* нас на край интервала, который мы этим и заняли. */
|
||||
if (pop_music_loading()) {
|
||||
(void)pop_music_load_step();
|
||||
gfx_wait_vsync();
|
||||
beam++;
|
||||
}
|
||||
/* Первый кадр собран под чёрной палитрой — теперь проявляем его
|
||||
* целиком, вместе с принцессой и факелами. */
|
||||
if (!faded_in) {
|
||||
faded_in = 1;
|
||||
if (pop_pal_fade_in_skip(INTRO_FADE)) { result = 1; break; }
|
||||
}
|
||||
/* ПЕЙСИНГ ПО ОРИГИНАЛЬНОЙ ШКАЛЕ. Кадр cutscene — 6/8/7 тиков по
|
||||
* 1/60 с; один тик = 85,4/60 = 1,424 тика насоса, и 57/40 = 1,425
|
||||
* повторяет это с точностью 0,07 % (30 мс на всю сцену) без
|
||||
* деления в кадре и без 32-битной арифметики. */
|
||||
if (snd_pace) {
|
||||
snd_acc += (uint16_t)period * 57u;
|
||||
while (snd_acc >= 40u) { snd_acc -= 40u; snd_want++; }
|
||||
for (;;) {
|
||||
uint8_t now = pop_snd_tick;
|
||||
|
||||
/* Разность в uint8_t: счётчик переполняется каждые 3 с, а
|
||||
* заглядываем мы в него не реже раза в кадр. */
|
||||
snd_done += (uint8_t)(now - snd_prev);
|
||||
snd_prev = now;
|
||||
if (snd_done >= snd_want) break;
|
||||
/* Страницу следующей реплики читаем ЗДЕСЬ, в паузе кадра:
|
||||
* 33 мс диска укладываются в ожидание (кадр сцены — 133 мс),
|
||||
* и подкачка не добавляется к длительности кадра, а
|
||||
* растворяется в ней. */
|
||||
if (pop_music_loading()) (void)pop_music_load_step();
|
||||
else gfx_wait_vsync();
|
||||
}
|
||||
/* ДОЛГ НЕ ДОГОНЯЕМ. Подкачка следующей реплики (страница — 33 мс)
|
||||
* и любой тяжёлый кадр оставляют snd_done впереди snd_want, и
|
||||
* дальше цикл гнал кадры без единого ожидания: пламя факелов
|
||||
* мелькало, а события соседних тиков (створка ворот и дверь
|
||||
* покоев) слипались в один кадр — заявка перезаписывала заявку, и
|
||||
* один из звуков пропадал. Тот же принцип, что у pop_pace_end:
|
||||
* якорь ставится ПО ФАКТУ, пропущенное время не наверстывается. */
|
||||
if (snd_done > snd_want) snd_want = snd_done;
|
||||
} else {
|
||||
/* ПЕЙСИНГ ПО ОРИГИНАЛЬНОЙ ШКАЛЕ. Кадр сцены — 6/8/7 тиков по
|
||||
* 1/60 с (pv_seq_period), в кадры луча переводит POP_T60, и цель
|
||||
* КУМУЛЯТИВНА: нецелые 6,67 и 5,83 не копят ошибку.
|
||||
*
|
||||
* ДОЛГ НЕ ДОГОНЯЕМ: если кадр вышел дороже своего бюджета, цикл
|
||||
* ожидания просто не сработает, но beam уже учёл потраченное — и
|
||||
* следующая цель считается от той же общей шкалы. Наверстывать
|
||||
* пропущенное нельзя: события соседних тиков (створка ворот, дверь
|
||||
* покоев) слиплись бы в один кадр, и один из звуков пропал бы. */
|
||||
{
|
||||
uint16_t target = POP_T60(tick);
|
||||
|
||||
while (beam < target) { gfx_wait_vsync(); beam++; }
|
||||
@@ -995,7 +1011,6 @@ static uint8_t pre_hourglass_state(void)
|
||||
static int pre_room_animated(uint8_t picture, uint8_t state, uint8_t frames)
|
||||
{
|
||||
atlas_t hourglass, torch;
|
||||
gfx_rect_t full;
|
||||
uint8_t torch_left = 1, torch_right = 6, sand = 0, frame;
|
||||
uint8_t star_phase[6] = { 1, 1, 1, 1, 1, 1 };
|
||||
intro_skip_t skip;
|
||||
@@ -1025,7 +1040,6 @@ static int pre_room_animated(uint8_t picture, uint8_t state, uint8_t frames)
|
||||
|
||||
pop_screen_present(1);
|
||||
result = 0;
|
||||
full.x = 0; full.y = 0; full.w = GFX_WIDTH; full.h = GFX_HEIGHT;
|
||||
intro_skip_begin(&skip);
|
||||
for (frame = 0; frame < frames; frame++) {
|
||||
uint8_t back, hold, star;
|
||||
@@ -1033,7 +1047,7 @@ static int pre_room_animated(uint8_t picture, uint8_t state, uint8_t frames)
|
||||
if (intro_skip_requested(&skip)) { result = 1; break; }
|
||||
back = (uint8_t)(gfx_get_visible_page() ^ 1);
|
||||
gfx_set_draw_page(back);
|
||||
gfx_copy_page(&full, GFX_COPY_DIRECT);
|
||||
pv_restore_bg(); /* блоки 1-2: фон, два интервала */
|
||||
gfx_set_bank(GFX_BANK_SPRITE);
|
||||
pv_blit_floor(&torch, torch_left, 93, 116);
|
||||
pv_blit_floor(&torch, torch_right, 211, 116);
|
||||
@@ -1046,10 +1060,11 @@ static int pre_room_animated(uint8_t picture, uint8_t state, uint8_t frames)
|
||||
pv_blit_floor(&hourglass, (uint8_t)(8 + sand), 160, 164);
|
||||
}
|
||||
pv_blit_floor(&hourglass, 0, 240, 167); /* передняя колонна */
|
||||
gfx_wait_vsync();
|
||||
gfx_wait_vsync(); /* блок 3: живой фон */
|
||||
gfx_set_visible_page(back);
|
||||
/* Первый тик уже потрачен на commit: ещё четыре = 5/50 с. */
|
||||
for (hold = 1; hold < 5; hold++) gfx_wait_vsync();
|
||||
/* Три интервала уже потрачены на блоки отрисовки — добираем до
|
||||
* пяти (5/50 с = кадр катсцены оригинала). */
|
||||
for (hold = 3; hold < 5; hold++) gfx_wait_vsync();
|
||||
|
||||
/* Проявляем ПОСЛЕ первого кадра: иначе две секунды fade идут по
|
||||
* пустой комнате, а часы и пламя возникают уже на свету. */
|
||||
@@ -1128,8 +1143,14 @@ static void intro_restore_game_palette(void)
|
||||
/* Набор PV2 (res901..930) не влезает в одну EMM: image 0..16 в a1,
|
||||
* 17..29 в a2. Номер кадра при этом остаётся исходным image-id. */
|
||||
#define CUT_PV2_SPLIT 17
|
||||
/* Один логический кадр катсцены = 5 vsync, как у остальных PV-сцен
|
||||
* (cutscene_frame_time SDLPoP; см. pre_room_animated). */
|
||||
/* Один логический кадр катсцены = 5 vsync, как у остальных PV-сцен: у
|
||||
* оригинала `cutscene_frame_time = 6` тиков по 1/60 с (reset_cutscene,
|
||||
* seg001:527, и его зовёт load_intro прямо перед сценой) = 100 мс, у нас
|
||||
* 5 кадров луча по 50 Гц = те же 100 мс.
|
||||
*
|
||||
* РАСКЛАДКА ПО ИНТЕРВАЛАМ (иначе отрисовка прибавлялась к делителю, а не
|
||||
* жила внутри него — см. pv_restore_bg): фон половинами 1-2, актёры и
|
||||
* декорации 3, служебный блок (звук/подкачка музыки) 4-5. */
|
||||
#define CUT_FRAME_VSYNC 5
|
||||
|
||||
static atlas_t cut_pv1, cut_pv2a, cut_pv2b, cut_hg, cut_torch;
|
||||
@@ -1290,11 +1311,9 @@ static void cut_shad_init(uint8_t x, uint8_t y, uint8_t seq)
|
||||
* прервал сцену (любое НОВОЕ нажатие). */
|
||||
static uint8_t cut_run(uint16_t frames, uint8_t kid_visible)
|
||||
{
|
||||
gfx_rect_t full;
|
||||
uint16_t i;
|
||||
uint8_t hold;
|
||||
|
||||
full.x = 0; full.y = 0; full.w = GFX_WIDTH; full.h = GFX_HEIGHT;
|
||||
for (i = 0; i < frames; i++) {
|
||||
uint8_t back;
|
||||
kframe kid_fr, pr_fr;
|
||||
@@ -1316,8 +1335,9 @@ static uint8_t cut_run(uint16_t frames, uint8_t kid_visible)
|
||||
back = (uint8_t)(gfx_get_visible_page() ^ 1);
|
||||
gfx_set_draw_page(back);
|
||||
/* Чистый фон обеих страниц лежит в shadow-копии; персонажи рисуются
|
||||
* банком спрайтов и потому в неё не оседают. */
|
||||
gfx_copy_page(&full, GFX_COPY_DIRECT);
|
||||
* банком спрайтов и потому в неё не оседают. Двумя блоками по
|
||||
* кадровому интервалу — см. pv_restore_bg. */
|
||||
pv_restore_bg(); /* блоки 1-2 */
|
||||
gfx_set_bank(GFX_BANK_SPRITE);
|
||||
pv_blit_floor(&cut_torch, cut_torch_l, 93, 116);
|
||||
pv_blit_floor(&cut_torch, cut_torch_r, 211, 116);
|
||||
@@ -1339,12 +1359,17 @@ static uint8_t cut_run(uint16_t frames, uint8_t kid_visible)
|
||||
* сквозь неё — поймано пользователем 2026-08-25). */
|
||||
pv_blit_floor(&cut_hg, 0, 240, 167);
|
||||
gfx_set_bank(GFX_BANK_NORMAL);
|
||||
gfx_wait_vsync();
|
||||
gfx_wait_vsync(); /* блок 3: актёры и декорации */
|
||||
gfx_set_visible_page(back);
|
||||
for (hold = 1; hold < CUT_FRAME_VSYNC; hold++) gfx_wait_vsync();
|
||||
|
||||
/* Блок 4 — служебный, и он НЕ добавляется к кадру, а живёт внутри
|
||||
* оставшихся интервалов: подкачка страницы трека стоит ~33 мс, то
|
||||
* есть меньше двух интервалов, которые всё равно надо переждать.
|
||||
* Порядок как в сцене с Джафаром: шаг подкачки — безусловный (см.
|
||||
* memory pv_music_stall_regression). */
|
||||
pop_sfx_tick();
|
||||
pop_music_service(); /* «встреча с принцессой» — трек, а не эффект */
|
||||
for (hold = 3; hold < CUT_FRAME_VSYNC; hold++) gfx_wait_vsync();
|
||||
cut_torch_l = (uint8_t)((cut_torch_l + 1) % 9);
|
||||
cut_torch_r = (uint8_t)((cut_torch_r + 1) % 9);
|
||||
cut_sand = (uint8_t)((cut_sand + 1) % 3);
|
||||
@@ -1662,7 +1687,9 @@ uint8_t pop_intro_show(void) __banked
|
||||
/* Первая реплика PV читается ЗДЕСЬ, под чёрным экраном и в тишине:
|
||||
* дальше внутри сцены остаётся только пуск, а следующие две реплики
|
||||
* подкачиваются по странице за кадр. */
|
||||
pop_sfx_pause();
|
||||
/* Глушение снято 2026-08-28: массовое чтение страниц насос не морит
|
||||
* (замер на загрузке уровня — ни одной пропущенной порции), а трек
|
||||
* титров может и должен доигрывать. */
|
||||
(void)pop_music_load(POP_MUS_STORY_2);
|
||||
pv_result = intro_pv_animated();
|
||||
if (pv_result > 0) {
|
||||
@@ -1740,15 +1767,21 @@ uint8_t pop_intro_show(void) __banked
|
||||
* дальше, а не чтобы досидеть до конца трека. */
|
||||
static void pre_cut_finish(uint8_t aborted)
|
||||
{
|
||||
if (!aborted) {
|
||||
intro_skip_t tail;
|
||||
|
||||
intro_skip_begin(&tail);
|
||||
while (pop_music_busy() && !intro_skip_requested(&tail))
|
||||
pop_wait_edge();
|
||||
/* ТРЕК ПЕРЕЖИВАЕТ СЦЕНУ — как в оригинале. load_intro (seg001:690)
|
||||
* ничего не ждёт: гасит экран и возвращает управление, а звук играет
|
||||
* дальше поверх загрузки уровня и первых секунд игры. Мы вместо этого
|
||||
* ДОСИЖИВАЛИ трек на чёрном экране (27 — 10,7 с против 2,6 с картинки),
|
||||
* то есть дарили игроку восемь секунд пустого экрана, и только потом
|
||||
* глушили. ЗАМЕР 2026-08-28: снято вместе с глушением на загрузке
|
||||
* уровня; если загрузка не укладывается в дедлайн насоса (251 000
|
||||
* тактов), вернуть оба места разом.
|
||||
*
|
||||
* Пропуск сцены по-прежнему обрывает и музыку: игрок нажал клавишу,
|
||||
* чтобы идти дальше, а не чтобы дослушать. */
|
||||
if (aborted) {
|
||||
pop_music_free();
|
||||
pop_sfx_pause();
|
||||
}
|
||||
pop_music_free();
|
||||
pop_sfx_pause();
|
||||
intro_restore_game_palette();
|
||||
}
|
||||
|
||||
@@ -1756,7 +1789,7 @@ void pop_pre_cutscene_show(pop_pre_cutscene_t scene) __banked
|
||||
{
|
||||
const pop_cs_cmd_t *script = 0;
|
||||
|
||||
pop_sfx_pause();
|
||||
/* Глушение перед чтением трека снято 2026-08-28 (см. замер насоса). */
|
||||
/* МУЗЫКА ПЕРЕЖИВАЕТ СЦЕНУ. Треки заставок между уровнями длиннее самих
|
||||
* сцен (27 — 10,7 с против 2,6 с картинки), и оригинал их не обрывает:
|
||||
* load_intro просто гасит экран и возвращает управление, а звук
|
||||
@@ -1824,7 +1857,7 @@ int pop_intro_hof_bg(uint8_t page) __banked
|
||||
|
||||
void pop_time_expired_show(void) __banked
|
||||
{
|
||||
pop_sfx_pause();
|
||||
/* Глушение снято 2026-08-28 (см. замер насоса). */
|
||||
/* time_expired (seg001:04D3) начинается с play_sound(sound_36). */
|
||||
if (pop_music_load(36) == 0) {
|
||||
(void)pop_sfx_start();
|
||||
@@ -1853,7 +1886,7 @@ void pop_time_expired_show(void) __banked
|
||||
|
||||
void pop_ending_show(void) __banked
|
||||
{
|
||||
pop_sfx_pause();
|
||||
/* Глушение снято 2026-08-28 (см. замер насоса). */
|
||||
/* Экран гасим ПЕРВЫМ действием — как load_intro оригинала, который
|
||||
* начинается с draw_rect(&screen_rect, color_0_black). Иначе загрузка
|
||||
* ресурсов сцены идёт поверх ЖИВОЙ комнаты уровня 14, и она «мелькает»
|
||||
|
||||
@@ -18,18 +18,14 @@
|
||||
#include "pop_guard.h" /* pop_guard_vflip_load — зеркала соперника */
|
||||
#include "pop_arc.h"
|
||||
#include "kid_arc.h"
|
||||
#include "_pop_file.h"
|
||||
|
||||
/* Серии в архиве обязаны лежать подряд — так их кладёт список в Makefile,
|
||||
* и только поэтому индекс считается сложением. Если порядок собьётся,
|
||||
* сборка встанет здесь, а не рассыплется картинкой на экране. */
|
||||
#if (ARC_KID_KID27 - ARC_KID_KID0) != 27
|
||||
#error "kid0..kid27 в архиве не подряд"
|
||||
#if (ARC_KID_SWORD - ARC_KID_KID0_V) != KID_PAGES
|
||||
#error "обычная и зеркальная серии Kid должны иметь одинаковую длину"
|
||||
#endif
|
||||
#if (ARC_KID_KID27_V - ARC_KID_KID0_V) != 27
|
||||
#error "kid0_v..kid27_v в архиве не подряд"
|
||||
#endif
|
||||
|
||||
#define KID_MAXPAGES 28
|
||||
|
||||
/* Таблица архива живёт на стеке вызывающего (4 байта на запись, здесь 232)
|
||||
* — держать её постоянно незачем, а W2 у игры и без того тесный. */
|
||||
@@ -39,7 +35,7 @@ int pop_kid_load(uint8_t npages) __banked
|
||||
pop_arc_t arc;
|
||||
uint8_t i;
|
||||
|
||||
if (npages > KID_MAXPAGES) npages = KID_MAXPAGES;
|
||||
if (npages > KID_PAGES) npages = KID_PAGES;
|
||||
if (pop_arc_open_id(&arc, POP_ARC_KID, tbl, ARC_KID_COUNT) < 0) return -1;
|
||||
for (i = 0; i < npages; i++)
|
||||
if (pop_arc_atlas(&arc, tbl, (uint8_t)(ARC_KID_KID0 + i),
|
||||
@@ -75,7 +71,8 @@ int pop_kid_data_load(const char *path) __banked
|
||||
blk=mem_alloc_pages(1);
|
||||
if (!blk) return -1;
|
||||
page=mem_get_page(blk,0);
|
||||
n=bank_load_file(page,KD_DATA_OFF,path,16384-KD_DATA_OFF);
|
||||
POP_PATH_CALL(n, bank_load_file(page,KD_DATA_OFF,path,
|
||||
16384-KD_DATA_OFF));
|
||||
if (n < (int)(KID_BIN_SEQTBL_OFF + KID_SEQTBL_LEN)) {
|
||||
mem_free_block(blk); return -1;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
#include "pop_map.h" /* pop_clip_char_top (clip_char) */
|
||||
#include "pop_geom.h"
|
||||
#include "pop_level.h" /* POP_LEVEL_LAST — level 15 исключён */
|
||||
#include "pop_cutscene.h" /* POP_T60 — тики оригинала в кадры луча */
|
||||
#include "pop_music_ticks.h" /* длина мелодии конца уровня */
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <sprinter_mem.h>
|
||||
@@ -36,14 +38,21 @@ int8_t knock;
|
||||
* оригинале, чтобы читы «следующий/предыдущий уровень» (levels_plan.md §4)
|
||||
* легли сюда же без переделки. */
|
||||
uint8_t pop_next_level;
|
||||
/* СКОЛЬКО ЛОГИЧЕСКИХ КАДРОВ ЕЩЁ ДЕРЖАТЬ УРОВЕНЬ, пока играет мелодия конца.
|
||||
* У оригинала (seg003:387, play_level_2) уровень не сменяется, пока
|
||||
* `check_sound_playing()`: экран пройденного уровня живёт с анимацией
|
||||
* факелов ровно столько, сколько звучит трек. У нас признак «звучит» —
|
||||
* pop_music_busy(), но он ложен при ВЫКЛЮЧЕННОЙ музыке, а оригинал
|
||||
* выдерживает паузу и молча. Поэтому здесь взводится счётчик по длине
|
||||
* записи (gen/pop_music_ticks.h), а главный цикл ждёт большего из двух. */
|
||||
uint8_t pop_endmus_left;
|
||||
|
||||
|
||||
/* ---- Атласы Kid (прямая адресация image>>3) ------------------------ *
|
||||
* НЕ static: набор спрайтов слота POP_CH_KID общей отрисовки персонажей
|
||||
* (pop_cdraw.c, банк 4). Отдан как есть, а не через аксессор: читается раз
|
||||
* в кадр из другого банка, и вызов стоил бы дороже самого чтения. */
|
||||
#define KID_MAXPAGES 28
|
||||
atlas_t kidp[KID_MAXPAGES];
|
||||
atlas_t kidp[KID_PAGES];
|
||||
uint8_t kid_npages;
|
||||
|
||||
/* Таблица «seq_id -> адрес в seqtbl» (original_seqtbl_offsets). Определена
|
||||
@@ -358,9 +367,17 @@ void play_seq(void)
|
||||
else if (ev == 4) {
|
||||
/* seg006:646. На четвёртом уровне уход за дверь — это
|
||||
* встреча с тенью, и трек там свой; на 13-м (Джафар) и
|
||||
* 15-м оригинал молчит. */
|
||||
if (pop_current_level == 4) pop_sfx_play(32);
|
||||
else if (pop_current_level != 13) pop_sfx_play(41);
|
||||
* 15-м оригинал молчит. Вместе с треком взводим
|
||||
* задержку смены уровня (см. pop_endmus_left): кадров
|
||||
* = длина записи в тиках -> кадры луча -> делённое на
|
||||
* длину логического кадра (4 кадра луча вне боя). */
|
||||
if (pop_current_level == 4) {
|
||||
pop_sfx_play(32);
|
||||
pop_endmus_left = (uint8_t)(POP_T60(POP_MUS_TICKS_32) / 4);
|
||||
} else if (pop_current_level != 13) {
|
||||
pop_sfx_play(41);
|
||||
pop_endmus_left = (uint8_t)(POP_T60(POP_MUS_TICKS_41) / 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -13,8 +13,13 @@
|
||||
#include <stdint.h>
|
||||
#include "pop_char.h"
|
||||
#include "kid_data.h" /* kframe — тип кадра для pop_sword_draw */
|
||||
#include "kid_arc.h" /* границы серий атласов в kid.arc */
|
||||
#include <sprite.h> /* atlas_t — набор спрайтов Кида (см. kidp ниже) */
|
||||
|
||||
/* Число страниц Кида берём из метаданных архива: зеркальная серия начинается
|
||||
* сразу после обычной. Не дублировать литералом в загрузчике/хранилище. */
|
||||
#define KID_PAGES (ARC_KID_KID0_V - ARC_KID_KID0)
|
||||
|
||||
/* Состояние персонажа — общий тип для Кида, стража и Char (pop_char.h). */
|
||||
extern pop_char_t Kid;
|
||||
|
||||
@@ -26,6 +31,8 @@ extern int8_t knock;
|
||||
* пройден». Читает и обнуляет главный цикл. Пока уровень один, поэтому
|
||||
* там рестарт того же уровня; загрузка следующего — levels_plan.md. */
|
||||
extern uint8_t pop_next_level;
|
||||
/* Задержка смены уровня на время мелодии конца (см. pop_kid.c). */
|
||||
extern uint8_t pop_endmus_left;
|
||||
|
||||
/* Загрузить npages атласов Kid (kid0..kid{npages-1}.atl). ДО initgraph.
|
||||
* 0 — OK, -1 — ошибка. */
|
||||
|
||||
@@ -45,7 +45,7 @@ extern uint8_t pop_current_level;
|
||||
void pop_level_qsave(pop_qs_io_t *io);
|
||||
|
||||
/* Загрузить уровень НОМЕРОМ: освободить страницу прошлого, взять
|
||||
* `LEVELS\res20NN.bin` (fallback `a:\res20NN.bin`) и выставить
|
||||
* `LEVELS\res20NN.bin` относительно каталога EXE и выставить
|
||||
* pop_current_level. 0 — OK, -1 — файла нет / не влез (тогда
|
||||
* pop_current_level и страница НЕ тронуты — играем дальше на старом).
|
||||
* Порт связки load_lev_spr + load_level (seg000:1098/1169). */
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include "pop_level.h"
|
||||
#include "_pop_level.h"
|
||||
#include "pop_geom.h" /* pop_x_bump / FIRST_ONSCREEN_COLUMN — pos_guards */
|
||||
#include "_pop_file.h"
|
||||
|
||||
/* Низкоуровневое чтение через W3 выполняет резидентный bank_load_file();
|
||||
* остальная одноразовая подготовка уровня безопасно остаётся в банке 8. */
|
||||
@@ -58,7 +59,8 @@ static int level_read_file(const char *path)
|
||||
blk=mem_alloc_pages(1);
|
||||
if (!blk) return -1;
|
||||
page=mem_get_page(blk,0);
|
||||
n=bank_load_file(page,LVL_DATA_OFF,path,16384-LVL_DATA_OFF);
|
||||
POP_PATH_CALL(n, bank_load_file(page,LVL_DATA_OFF,path,
|
||||
16384-LVL_DATA_OFF));
|
||||
if (n < (int)MIN_SIZE) { mem_free_block(blk); return -1; }
|
||||
gfx_w0_page_prepare(page);
|
||||
gfx_w0_map(page);
|
||||
@@ -80,18 +82,17 @@ int pop_level_load_num(uint8_t n) __banked
|
||||
/* Имена патчим на месте, а не через sprintf: printf-семейство тянет в
|
||||
* резидент W1 форматтер, которого здесь больше негде применить. */
|
||||
static char pri[] = "LEVELS\\res2000.bin"; /* цифры — [12],[13] */
|
||||
static char alt[] = "a:\\res2000.bin"; /* цифры — [8],[9] */
|
||||
uint8_t saved_blk = pop_lvl_blk, saved_page = pop_lvl_page, saved_ok = pop_lvl_ok;
|
||||
|
||||
if (n > POP_LEVEL_LAST) return -1;
|
||||
pri[12] = alt[8] = (char)('0' + n / 10);
|
||||
pri[13] = alt[9] = (char)('0' + n % 10);
|
||||
pri[12] = (char)('0' + n / 10);
|
||||
pri[13] = (char)('0' + n % 10);
|
||||
|
||||
/* Старую страницу отпускаем ТОЛЬКО после успешной загрузки новой:
|
||||
* level_read_file сам аллоцирует страницу, и если файла нет — остаёмся
|
||||
* на текущем уровне вместо падения в пустой уровень. */
|
||||
pop_lvl_ok = 0; /* чтобы level_load_path не мешался */
|
||||
if (level_read_file(pri) != 0 && level_read_file(alt) != 0) {
|
||||
if (level_read_file(pri) != 0) {
|
||||
pop_lvl_blk = saved_blk; pop_lvl_page = saved_page; pop_lvl_ok = saved_ok;
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
#include "pop_settings.h"
|
||||
#include "pop_cfg.h"
|
||||
#include "pop_qsave.h"
|
||||
#include "pop_music.h" /* pop_music_service — подкачка кольца, см. ui_wait_frame */
|
||||
#include "pop_status.h"
|
||||
#include "pop_sfx.h"
|
||||
|
||||
@@ -196,10 +197,21 @@ static uint8_t ui_begin(void)
|
||||
gfx_rect_t full;
|
||||
uint8_t back = (uint8_t)(gfx_get_visible_page() ^ 1);
|
||||
|
||||
/* Полностраничная accel-копия держит DI дольше половины CBL-буфера.
|
||||
* Закрываем вывод, иначе железо повторяет старые 128 байт (скрежет).
|
||||
* ui_end откроет его уже с заново заполненной тишиной. */
|
||||
pop_sfx_pause();
|
||||
/* ЗВУК ЗДЕСЬ БОЛЬШЕ НЕ ГЛУШИМ (снято 2026-08-28).
|
||||
*
|
||||
* Был вызов pop_sfx_pause() с обоснованием «полностраничная accel-копия
|
||||
* держит DI дольше половины CBL-буфера, иначе железо повторяет старые
|
||||
* 128 байт». Обоснование устарело: лист, который зовёт gfx_copy_page
|
||||
* (_bgi_scroll_rows_raw), режет DI бандами по 16 строк — это ~1,6 мс
|
||||
* против дедлайна насоса 11,7 мс, и окно прерываний между бандами есть.
|
||||
* Тот же полностраничный копир каждый кадр делают игровой цикл и
|
||||
* катсцены, и звук там не рвётся.
|
||||
*
|
||||
* А цена была слышимой: pause закрывает CBL целиком (cbl_close), курсор
|
||||
* трека остаётся на месте, и короткая мелодия начала уровня замирала
|
||||
* ровно на время перерисовки меню — «тормозится на кусочек» (жалоба
|
||||
* пользователя 2026-08-28). У оригинала такого нет и быть не может:
|
||||
* там нет насоса, который надо кормить руками. */
|
||||
gfx_set_draw_page(back);
|
||||
full.x = 0; full.y = 0; full.w = GFX_WIDTH; full.h = GFX_HEIGHT;
|
||||
gfx_copy_page(&full, GFX_COPY_DIRECT);
|
||||
@@ -207,10 +219,32 @@ static uint8_t ui_begin(void)
|
||||
return back;
|
||||
}
|
||||
|
||||
/* КАДР МЕНЮ: перед ожиданием фронта — обслужить музыку.
|
||||
*
|
||||
* Насос CBL живёт в прерывании и главный цикл ему не нужен, НО потоковый
|
||||
* трек (титульная тема играется КОЛЬЦОМ, pop_music.c) дочитывается с диска
|
||||
* именно из pop_music_service: очередная страница попадает в слот, который
|
||||
* насос уже прошёл. Пока открыто меню, главный цикл стоит, и без этого
|
||||
* вызова кольцо опустошалось за несколько секунд — музыка замолкала до
|
||||
* закрытия меню, а на перемещениях по пунктам это слышно как паузы
|
||||
* (жалоба пользователя 2026-08-28).
|
||||
*
|
||||
* Место — рядом с ожиданием кадра: получается тот же темп 50 раз в
|
||||
* секунду, что и в игровом цикле, и подкачка не чаще, чем нужна. */
|
||||
static void ui_wait_frame(void)
|
||||
{
|
||||
pop_music_service();
|
||||
pop_wait_edge();
|
||||
}
|
||||
|
||||
static void ui_end(uint8_t back)
|
||||
{
|
||||
pop_wait_edge(); /* готовый кадр показать только на фронте */
|
||||
ui_wait_frame(); /* готовый кадр показать только на фронте */
|
||||
gfx_set_visible_page(back);
|
||||
/* Парный к снятому pop_sfx_pause (см. ui_begin) вызов оставлен: он
|
||||
* идемпотентен (открытый CBL возвращает 0 сразу) и чинит случай, когда
|
||||
* в меню вошли при закрытом выводе — например сразу после загрузки
|
||||
* уровня, которая глушит звук намеренно. */
|
||||
(void)pop_sfx_start();
|
||||
}
|
||||
|
||||
@@ -227,7 +261,7 @@ static void ui_wait_release(void)
|
||||
kbd_raw_down(KBD_SPACE) ||
|
||||
kbd_raw_down(KBD_UP) || kbd_raw_down(KBD_DOWN) ||
|
||||
kbd_raw_down(KBD_LEFT) || kbd_raw_down(KBD_RIGHT))
|
||||
pop_wait_edge();
|
||||
ui_wait_frame();
|
||||
}
|
||||
|
||||
/* Действие = Enter ИЛИ Space: в оригинале выбор подтверждается обеими, и
|
||||
@@ -250,7 +284,7 @@ static void ui_wait_ack(void)
|
||||
return;
|
||||
}
|
||||
esc_prev = esc; act_prev = act;
|
||||
pop_wait_edge();
|
||||
ui_wait_frame();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -478,7 +512,7 @@ static uint8_t ui_confirm(const char *question)
|
||||
confirm_draw(question, sel);
|
||||
menu_sound(MENU_SND_MOVE);
|
||||
}
|
||||
pop_wait_edge();
|
||||
ui_wait_frame();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -522,10 +556,12 @@ static uint8_t settings_finish(uint8_t changed)
|
||||
int8_t rc;
|
||||
if (!changed) return 0;
|
||||
pop_settings_apply(1);
|
||||
/* ESTEX может уйти в диск на много CBL-периодов. */
|
||||
pop_sfx_pause();
|
||||
/* Звук не глушим: POP.CFG — десятки байт, а на практике HOF пишется
|
||||
* под звучащий «won» и насос это переживает (проверка пользователя
|
||||
* 2026-08-28). Прежняя осторожность «ESTEX может уйти в диск на много
|
||||
* CBL-периодов» относилась к загрузке НАБОРА страниц, а не к записи
|
||||
* одного мелкого файла. */
|
||||
rc = pop_cfg_save();
|
||||
(void)pop_sfx_start();
|
||||
ui_notice(rc > 0 ? "SETTINGS SAVED" : "SAVE ERROR");
|
||||
return 1;
|
||||
}
|
||||
@@ -609,17 +645,16 @@ static uint8_t settings_process(void)
|
||||
settings_draw(sel);
|
||||
menu_sound(sound);
|
||||
}
|
||||
pop_wait_edge();
|
||||
ui_wait_frame();
|
||||
}
|
||||
}
|
||||
|
||||
static int8_t menu_quickload(void)
|
||||
{
|
||||
uint8_t slots;
|
||||
/* probe делает open/close на D:, поэтому насос в это время выключен. */
|
||||
pop_sfx_pause();
|
||||
/* probe — это только open/close на D:, данных не читает; глушить насос
|
||||
* ради него незачем (см. settings_finish). */
|
||||
slots = pop_qsave_probe();
|
||||
(void)pop_sfx_start();
|
||||
if (slots & POP_QS_SLOT_SAVE) return POP_MENU_QUICKLOAD;
|
||||
if ((slots & POP_QS_SLOT_BACKUP) && ui_confirm("LOAD BACKUP?"))
|
||||
return POP_MENU_QUICKLOAD_BACKUP;
|
||||
@@ -641,14 +676,14 @@ static void menu_erase(uint8_t entry_page)
|
||||
/* Из shadow видимой страницы берём кадр без текстов. */
|
||||
gfx_set_draw_page(hidden);
|
||||
gfx_copy_page(&full, GFX_COPY_DIRECT);
|
||||
pop_wait_edge();
|
||||
ui_wait_frame();
|
||||
gfx_set_visible_page(hidden);
|
||||
|
||||
/* Теперь чистая hidden — источник для второй страницы. */
|
||||
gfx_set_draw_page(visible);
|
||||
gfx_copy_page(&full, GFX_COPY_DIRECT);
|
||||
if (entry_page != hidden) {
|
||||
pop_wait_edge();
|
||||
ui_wait_frame();
|
||||
gfx_set_visible_page(entry_page);
|
||||
}
|
||||
}
|
||||
@@ -679,13 +714,18 @@ int8_t pop_menu_process(void) __banked
|
||||
|
||||
/* Открыли по НАЖАТИЮ Esc: дождаться отпускания, иначе то же нажатие
|
||||
* закроет меню раньше первого кадра. pop_wait_edge опрашивает FIFO. */
|
||||
while (kbd_raw_down(KBD_ESC)) pop_wait_edge();
|
||||
while (kbd_raw_down(KBD_ESC)) ui_wait_frame();
|
||||
|
||||
entry_page = gfx_get_visible_page();
|
||||
saved_page = gfx_get_draw_page();
|
||||
saved_bank = gfx_get_bank();
|
||||
getfillsettings(&saved_fill);
|
||||
pop_sfx_pause(); /* snapshot/dim + первый full redraw */
|
||||
/* Звук НЕ глушим (снято 2026-08-28, вторая половина той же заплатки —
|
||||
* см. ui_begin). Здесь под ней были снимок палитры, затемнение фона и
|
||||
* первая полная перерисовка: копия режет DI бандами по 16 строк, а
|
||||
* палитровое затемнение идёт и в титрах с катсценами, где музыку оно не
|
||||
* рвёт. Цена глушения была слышна как пауза в мелодии ровно на
|
||||
* появление меню. */
|
||||
pop_pal_snapshot();
|
||||
/* 50 % яркости; диапазон UI остаётся ярким. Долю берём от шкалы, а не
|
||||
* числом: ступеней стало 32 вместо четырёх, и прежняя «2» означала бы
|
||||
@@ -742,11 +782,12 @@ int8_t pop_menu_process(void) __banked
|
||||
menu_draw(sel);
|
||||
menu_sound(sound);
|
||||
}
|
||||
pop_wait_edge();
|
||||
ui_wait_frame();
|
||||
pop_pace_end(pop_pace_n(0)); /* игра стоит — обычный темп */
|
||||
}
|
||||
|
||||
pop_sfx_pause(); /* две full-page копии menu_erase */
|
||||
/* И на закрытии не глушим: menu_erase — те же две полностраничные
|
||||
* копии, что и в ui_begin, с DI бандами по 16 строк. */
|
||||
menu_erase(entry_page);
|
||||
/* menu_erase вернул обе страницы из тени, снятой ДО меню: всё, что
|
||||
* статус-полосы нарисовали за это время (или сообщение, дотикавшее в
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include "pop_music_tbl.h"
|
||||
#include "pop_sfx.h"
|
||||
#include "pop_app.h" /* POP_APP_DEMO — в демке музыки нет */
|
||||
#include "_pop_file.h"
|
||||
|
||||
uint8_t pop_mus_page[POP_MUS_PAGES];
|
||||
|
||||
@@ -49,6 +50,7 @@ static uint8_t ld_busy;
|
||||
static int ld_fd;
|
||||
static uint8_t ld_slot;
|
||||
static uint8_t ld_next; /* следующая страница к чтению */
|
||||
static uint8_t ld_half; /* 0 = впереди первая половина страницы */
|
||||
static uint8_t ld_pages;
|
||||
static uint8_t ld_id;
|
||||
|
||||
@@ -110,13 +112,14 @@ void pop_music_free(void) __banked
|
||||
* ПОСТРАНИЧНО, А НЕ ЦЕЛИКОМ. Реплики сцены с принцессой идут встык, и
|
||||
* следующую надо читать, пока звучит текущая. Трек — до 320 КБ, целиком
|
||||
* это 0,7 с диска: на 7,5 кадрах в секунду сцена встала бы на пять кадров.
|
||||
* Одна страница стоит 33 мс — меньше четверти кадра, и подкачка растворя-
|
||||
* ется в анимации. Кто может позволить себе паузу (титры, чёрный экран
|
||||
* Шаг стоит 16 мс (полстраницы, см. pop_music_load_step) — меньше одного
|
||||
* кадрового интервала, и подкачка растворяется в анимации. Кто может позволить себе паузу (титры, чёрный экран
|
||||
* между сценами), зовёт pop_music_load — тот же цикл, но целиком. */
|
||||
static void load_abort(void)
|
||||
{
|
||||
if (ld_busy) close(ld_fd);
|
||||
ld_busy = 0;
|
||||
ld_half = 0;
|
||||
slot_release(ld_slot);
|
||||
}
|
||||
|
||||
@@ -139,30 +142,42 @@ int8_t pop_music_load_begin(uint8_t id) __banked
|
||||
/* Трек лежит ОДНИМ файлом, и fd остаётся открытым до последней
|
||||
* страницы: `open` стоит дороже самого чтения (51,4 мс против 32,6 мс
|
||||
* на 16 КБ), и платить его на каждую страницу — чистые потери. */
|
||||
ld_fd = open(name, O_RDONLY);
|
||||
POP_PATH_CALL(ld_fd, open(name, O_RDONLY));
|
||||
if (ld_fd < 0) return -1;
|
||||
slot_blk[n] = mem_alloc_pages(pages);
|
||||
if (!slot_blk[n]) { close(ld_fd); return -1; }
|
||||
ld_busy = 1;
|
||||
ld_slot = n;
|
||||
ld_next = 0;
|
||||
ld_half = 0;
|
||||
ld_pages = pages;
|
||||
ld_id = id;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ШАГ = ПОЛСТРАНИЦЫ (8 КБ, ~16 мс), а не страница (~33 мс).
|
||||
*
|
||||
* Шаг вызывают раз в кадр из циклов, где кадр разложен на блоки по одному
|
||||
* кадровому интервалу (20 мс): катсцены после перехода на блочную
|
||||
* отрисовку, см. pop_intro.c/pv_restore_bg. Целая страница в интервал не
|
||||
* влезала и незаметно растягивала кадр сцены; половина влезает, а цена —
|
||||
* вдвое больше вызовов (11 страниц реплики = 22 кадра вместо 11, то есть
|
||||
* меньше полусекунды). Игровому циклу это безразлично: там логический
|
||||
* кадр 80-100 мс. */
|
||||
int8_t pop_music_load_step(void) __banked
|
||||
{
|
||||
uint8_t p;
|
||||
|
||||
if (!ld_busy) return 1; /* нечего грузить */
|
||||
p = mem_get_page(slot_blk[ld_slot], ld_next);
|
||||
/* Последняя страница короче — читаем сколько есть; ошибку ловим по
|
||||
* отрицательному коду, а не по «не 16384». */
|
||||
if (!p || bank_read_page(ld_fd, p, 0, 16384) < 0) {
|
||||
/* Последняя страница короче — читаем сколько есть (вторая половина
|
||||
* может дать и ноль байт); ошибку ловим по отрицательному коду. */
|
||||
if (!p || bank_read_page(ld_fd, p, ld_half ? 8192 : 0, 8192) < 0) {
|
||||
load_abort();
|
||||
return -1;
|
||||
}
|
||||
if (!ld_half) { ld_half = 1; return 0; } /* дочитаем в следующий раз */
|
||||
ld_half = 0;
|
||||
if (++ld_next < ld_pages) return 0;
|
||||
|
||||
close(ld_fd);
|
||||
@@ -274,7 +289,7 @@ int8_t pop_music_ring_start(uint8_t id) __banked
|
||||
pop_music_free(); /* обычные слоты кольцу не нужны */
|
||||
pop_music_ring_stop();
|
||||
mus_name(name, id);
|
||||
ring_fd = open(name, O_RDONLY);
|
||||
POP_PATH_CALL(ring_fd, open(name, O_RDONLY));
|
||||
if (ring_fd < 0) return -1;
|
||||
ring_fd_open = 1;
|
||||
ring_blk = mem_alloc_pages(RING_PAGES);
|
||||
|
||||
@@ -14,10 +14,12 @@
|
||||
#include "pop_guard.h"
|
||||
#include "pop_pal.h"
|
||||
#include "pop_ui.h" /* POP_COL_OUTSIDE */
|
||||
#include "_pop_file.h"
|
||||
|
||||
static void pal_file_load(const char *path, const char *fallback)
|
||||
static void pal_file_load(const char *path)
|
||||
{
|
||||
if (gfx_pal_fload(0, path) < 0) gfx_pal_fload(0, fallback);
|
||||
int rc;
|
||||
POP_PATH_CALL(rc, gfx_pal_fload(0, path));
|
||||
gfx_pal_sync();
|
||||
pop_ui_palette_snapshot();
|
||||
}
|
||||
@@ -84,7 +86,7 @@ void pop_pal_menu_dim(uint8_t fade) __banked
|
||||
|
||||
void pop_pal_game_load(void) __banked
|
||||
{
|
||||
pal_file_load("KID\\kid.pal", "a:\\kid.pal");
|
||||
pal_file_load("KID\\kid.pal");
|
||||
/* kid.pal содержит все 256 записей и намеренно затирает эти три
|
||||
* динамических диапазона (тайлсет, Тень, набор соперника).
|
||||
* Восстановить их надо ДО snapshot. */
|
||||
@@ -110,7 +112,7 @@ void pop_pal_level_load(uint8_t full) __banked
|
||||
|
||||
void pop_pal_title_load(void) __banked
|
||||
{
|
||||
pal_file_load("TITLE\\title.pal", "a:\\title.pal");
|
||||
pal_file_load("TITLE\\title.pal");
|
||||
}
|
||||
|
||||
/* ЕДИНСТВЕННОЕ ОТЛИЧИЕ ФИНАЛА ОТ ИНТРО — цвет фона текстовой рамки.
|
||||
@@ -120,7 +122,7 @@ void pop_pal_title_load(void) __banked
|
||||
* файла и до нового снимка — как тайлсет/Тень в pop_pal_game_load. */
|
||||
void pop_pal_story_load(uint8_t ending) __banked
|
||||
{
|
||||
pal_file_load("PV\\story.pal", "a:\\story.pal");
|
||||
pal_file_load("PV\\story.pal");
|
||||
if (!ending) return;
|
||||
gfx_pal_set(0, POP_PAL_STORY_BG, 0x80, 0, 0);
|
||||
gfx_pal_set(1, POP_PAL_STORY_BG, 0x80, 0, 0);
|
||||
@@ -129,5 +131,5 @@ void pop_pal_story_load(uint8_t ending) __banked
|
||||
|
||||
void pop_pal_pv_load(void) __banked
|
||||
{
|
||||
pal_file_load("PV\\pv.pal", "a:\\pv.pal");
|
||||
pal_file_load("PV\\pv.pal");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* pop_path_bank.c — каталог приложения и восстановление cwd, BANK10.
|
||||
*
|
||||
* APPINFO B=1 возвращает абсолютный каталог запущенного EXE вместе с диском.
|
||||
* Получаем его один раз при старте. Буфер объявлен const намеренно: у
|
||||
* Sprinter банковые страницы — RAM, а --constseg кладёт объект в BANK10,
|
||||
* не расходуя дефицитную резидентную _DATA в W2. Записывает его только
|
||||
* ESTEX во время pop_path_init().
|
||||
*
|
||||
* ABI: публичные функции __banked; их вызывает POP_PATH_CALL из банка
|
||||
* файлового клиента. После возврата трамплин восстанавливает прежнюю W3,
|
||||
* поэтому переданный самой файловой операции указатель снова доступен.
|
||||
*/
|
||||
#include <stdint.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "_pop_file.h"
|
||||
|
||||
static const char pop_app_dir[256] = { 0 };
|
||||
|
||||
/* ESTEX APPINFO ($47), B=1: HL=буфер, CF/A=ошибка. */
|
||||
static int appinfo_dir(const char *buf) __naked
|
||||
{
|
||||
(void)buf;
|
||||
__asm
|
||||
push ix
|
||||
ld b, #1
|
||||
ld c, #0x47
|
||||
rst #0x10
|
||||
pop ix
|
||||
jr c, _paid_err
|
||||
ld de, #0
|
||||
ret
|
||||
_paid_err:
|
||||
call __errno_set
|
||||
ld de, #-1
|
||||
ret
|
||||
__endasm;
|
||||
}
|
||||
|
||||
int8_t pop_path_init(void) __banked
|
||||
{
|
||||
const char *dir = pop_app_dir;
|
||||
|
||||
if (appinfo_dir(dir) != 0 || !dir[0]) return -1;
|
||||
return (int8_t)chdir(dir);
|
||||
}
|
||||
|
||||
int8_t pop_chdir_home(void) __banked
|
||||
{
|
||||
return (int8_t)chdir((const char *)pop_app_dir);
|
||||
}
|
||||
@@ -22,6 +22,7 @@
|
||||
#include "pop_pace.h"
|
||||
#include "pop_timer.h"
|
||||
#include "sprpop_cold.h"
|
||||
#include "_pop_file.h"
|
||||
|
||||
#define QS_VERSION 4
|
||||
#define QS_SAVE 1
|
||||
@@ -154,6 +155,7 @@ static int8_t qs_save(uint8_t page)
|
||||
uint8_t tail[4];
|
||||
const uint8_t h[7]={'P','O','P','Q',QS_VERSION,pop_current_level,0};
|
||||
uint16_t size;
|
||||
int rc;
|
||||
qs_init(&io,page,QS_LIMIT,0,0);
|
||||
pop_qs_raw_put(&io,h,sizeof(h));
|
||||
qs_payload(&io);
|
||||
@@ -162,14 +164,18 @@ static int8_t qs_save(uint8_t page)
|
||||
tail[2]=(uint8_t)io.checksum; tail[3]=(uint8_t)(io.checksum >> 8);
|
||||
pop_qs_raw_put(&io,tail,4); size=(uint16_t)(io.pos-QS_BASE);
|
||||
if (io.failed) return -1;
|
||||
unlink("POP.NEW");
|
||||
if (bank_save_file(page,QS_BASE,"POP.NEW",size) != size) {
|
||||
unlink("POP.NEW"); return -1;
|
||||
POP_PATH_CALL(rc, unlink("POP.NEW"));
|
||||
POP_PATH_CALL(rc, bank_save_file(page,QS_BASE,"POP.NEW",size));
|
||||
if (rc != size) {
|
||||
POP_PATH_CALL(rc, unlink("POP.NEW"));
|
||||
return -1;
|
||||
}
|
||||
unlink("POP.BAK");
|
||||
(void)rename("POP.SAV", "POP.BAK");
|
||||
if (rename("POP.NEW", "POP.SAV") != 0) {
|
||||
(void)rename("POP.BAK", "POP.SAV"); return -1;
|
||||
POP_PATH_CALL(rc, unlink("POP.BAK"));
|
||||
POP_PATH_CALL(rc, rename("POP.SAV", "POP.BAK"));
|
||||
POP_PATH_CALL(rc, rename("POP.NEW", "POP.SAV"));
|
||||
if (rc != 0) {
|
||||
POP_PATH_CALL(rc, rename("POP.BAK", "POP.SAV"));
|
||||
return -1;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
@@ -178,7 +184,8 @@ static int8_t qs_load_one(const char *name, uint8_t page)
|
||||
{
|
||||
uint8_t level;
|
||||
uint16_t payload_size;
|
||||
int size=bank_load_file(page,QS_BASE,name,QS_LIMIT);
|
||||
int size;
|
||||
POP_PATH_CALL(size, bank_load_file(page,QS_BASE,name,QS_LIMIT));
|
||||
if (size <= 0 || !qs_validate_page(page,(uint16_t)size,&level,
|
||||
&payload_size)) return -1;
|
||||
if (level != pop_current_level) {
|
||||
@@ -205,11 +212,11 @@ uint8_t pop_qsave_probe(void) __banked
|
||||
page = mem_get_page(blk, 0);
|
||||
gfx_w0_page_prepare(page);
|
||||
|
||||
size = bank_load_file(page, QS_BASE, "POP.SAV", QS_LIMIT);
|
||||
POP_PATH_CALL(size, bank_load_file(page, QS_BASE, "POP.SAV", QS_LIMIT));
|
||||
if (size > 0 && qs_validate_page(page, (uint16_t)size, &level,
|
||||
&payload_size))
|
||||
slots |= POP_QS_SLOT_SAVE;
|
||||
size = bank_load_file(page, QS_BASE, "POP.BAK", QS_LIMIT);
|
||||
POP_PATH_CALL(size, bank_load_file(page, QS_BASE, "POP.BAK", QS_LIMIT));
|
||||
if (size > 0 && qs_validate_page(page, (uint16_t)size, &level,
|
||||
&payload_size))
|
||||
slots |= POP_QS_SLOT_BACKUP;
|
||||
@@ -234,14 +241,16 @@ int8_t pop_qsave_process(void) __banked
|
||||
if (!blk) return -1;
|
||||
page=mem_get_page(blk,0);
|
||||
gfx_w0_page_prepare(page);
|
||||
pop_sfx_pause();
|
||||
/* Звук не глушим (2026-08-28). Единственное место из снятых, где по
|
||||
* диску реально едут 16 КБ (~33 мс, три периода насоса): если на F6/F9
|
||||
* появится скрежет — вернуть сюда pop_sfx_pause/pop_sfx_start, это
|
||||
* лечится точечно и только здесь. */
|
||||
if (request == QS_SAVE) rc = qs_save(page);
|
||||
else if (request == QS_LOAD_BACKUP) rc = qs_load_one("POP.BAK", page);
|
||||
else {
|
||||
rc = qs_load_one("POP.SAV",page);
|
||||
if (rc < 0) rc = qs_load_one("POP.BAK",page);
|
||||
}
|
||||
pop_sfx_start();
|
||||
mem_free_block(blk);
|
||||
/* Успех ничего не меняет: нужная строка висит с начала операции.
|
||||
* Отказ переписывает её на NO QUICKSAVE/NO QUICKLOAD — обычной
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
#include "pop_kid.h" /* Kid.curr_col/curr_row — порядок оверлеев */
|
||||
#include "pop_cdraw.h" /* pop_cd — габарит кадра Кида (y-порядок плиты) */
|
||||
#include "pop_map.h" /* pop_upside — переворот (зелье инверсии) */
|
||||
#include "_pop_file.h"
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
|
||||
@@ -83,7 +84,8 @@ static const char *bg_path(const char *name)
|
||||
static int bg_load_tile_pal(const char *name)
|
||||
{
|
||||
uint8_t buf[64];
|
||||
int fd = open(bg_path(name), O_RDONLY);
|
||||
int fd;
|
||||
POP_PATH_CALL(fd, open(bg_path(name), O_RDONLY));
|
||||
if (fd < 0) return -1;
|
||||
if (read(fd, buf, 64) != 64) { close(fd); return -1; }
|
||||
gfx_pal_load(0, POP_PAL_ENV, 16, buf);
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include <irq.h> /* IRQ_DISABLE/IRQ_ENABLE вокруг курсора */
|
||||
#include "pop_sfx.h"
|
||||
#include "pop_music.h"
|
||||
#define POP_SND_TABLE /* таблица нужна только горячей половине */
|
||||
#include "_pop_sfx.h"
|
||||
|
||||
/* Физические страницы набора; заполняет холодная половина. */
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
#include <cbl.h>
|
||||
#include "pop_sfx.h"
|
||||
#include "pop_arc.h"
|
||||
#define POP_SND_FILES /* имена файлов нужны ТОЛЬКО здесь, см. pop_sound_tbl.h */
|
||||
#include "_pop_sfx.h"
|
||||
|
||||
static uint8_t snd_blk; /* блок EMM под весь набор */
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
#include "pop_arc.h"
|
||||
#include "pop_ui.h"
|
||||
#include "pop_status.h"
|
||||
#include "pop_music.h" /* pop_music_service — подкачка кольца в долгих фейдах */
|
||||
#include "_pop_file.h"
|
||||
|
||||
typedef struct {
|
||||
uint8_t base;
|
||||
@@ -53,8 +55,10 @@ static uint8_t text_index(const pop_font_desc_t *font, uint8_t ch)
|
||||
|
||||
int pop_ui_init(void) __banked
|
||||
{
|
||||
int rc;
|
||||
if (font_ready) return 0;
|
||||
if (atlas_load(&font_at, "FONT\\font.atl") != 0) return -1;
|
||||
POP_PATH_CALL(rc, atlas_load(&font_at, "FONT\\font.atl"));
|
||||
if (rc != 0) return -1;
|
||||
font_ready = 1;
|
||||
return 0;
|
||||
}
|
||||
@@ -241,8 +245,10 @@ void pop_ui_palette_black(void) __banked
|
||||
uint8_t *black = pop_pal_buf;
|
||||
|
||||
/* BIOS принимает максимум 64 записи за вызов; одним буфером покрываем
|
||||
* все четыре четверти палитры, не занимая постоянную память. */
|
||||
for (i = 0; i < sizeof(black); i++) black[i] = 0;
|
||||
* все четыре четверти палитры, не занимая постоянную память.
|
||||
* sizeof(black) здесь нельзя: black — 16-битный указатель, и тогда
|
||||
* обнулятся лишь первые два байта, а цвет заливки останется случайным. */
|
||||
for (i = 0; i < sizeof(pop_pal_buf); i++) black[i] = 0;
|
||||
for (chunk = 0; chunk < 4; chunk++) {
|
||||
uint8_t start = (uint8_t)(chunk << 6);
|
||||
gfx_pal_load(0, start, 64, black);
|
||||
@@ -292,6 +298,11 @@ static uint8_t fade_run(uint8_t frames, uint8_t up, uint8_t skip)
|
||||
pop_ui_palette_dim(step, 0);
|
||||
shown = step;
|
||||
}
|
||||
/* Fade — это 2,13 с, и всё это время главный цикл стоит. Кольцо
|
||||
* потокового трека дочитывает pop_music_service, иначе музыка
|
||||
* замолкает прямо посреди перехода (та же причина, что у меню:
|
||||
* ui_wait_frame в pop_menu.c). */
|
||||
pop_music_service();
|
||||
gfx_wait_vsync();
|
||||
if (skip) {
|
||||
kbd_raw_sync();
|
||||
@@ -429,6 +440,7 @@ uint8_t pop_screen_present_ltr(void) __banked
|
||||
stripe.x = x;
|
||||
stripe.w = (uint16_t)((GFX_WIDTH - x < 4) ? GFX_WIDTH - x : 4);
|
||||
gfx_copy_page(&stripe, GFX_COPY_DIRECT);
|
||||
pop_music_service(); /* 80 кадров перехода — см. fade_run */
|
||||
gfx_wait_vsync();
|
||||
}
|
||||
return 0;
|
||||
@@ -461,8 +473,10 @@ int pop_screen_blit_atlas_strips(const char * const *parts,
|
||||
gfx_set_bank(GFX_BANK_NORMAL);
|
||||
for (part = 0; part < count; part++) {
|
||||
atlas_t at;
|
||||
int rc;
|
||||
|
||||
if (atlas_load(&at, parts[part]) != 0) return -1;
|
||||
POP_PATH_CALL(rc, atlas_load(&at, parts[part]));
|
||||
if (rc != 0) return -1;
|
||||
strip_blit(&at, x, &y);
|
||||
}
|
||||
return 0;
|
||||
|
||||
@@ -40,15 +40,6 @@ int pop_vflip_take(const atlas_t *orig, const atlas_t *mirror)
|
||||
return 0;
|
||||
}
|
||||
|
||||
int pop_vflip_add(const atlas_t *orig, const char *path)
|
||||
{
|
||||
atlas_t m;
|
||||
|
||||
if (!orig || vf_n >= VF_MAX) return -1;
|
||||
if (atlas_load(&m, path) != 0) return -1;
|
||||
return pop_vflip_take(orig, &m);
|
||||
}
|
||||
|
||||
uint8_t pop_vflip_page(uint8_t src)
|
||||
{
|
||||
uint8_t i;
|
||||
|
||||
@@ -26,11 +26,7 @@
|
||||
#include <stdint.h>
|
||||
#include <sprite.h>
|
||||
|
||||
/* Загрузить зеркальный файл path и связать его с атласом orig.
|
||||
* 0 — OK, -1 — файла нет / не хватило EMM / таблица полна. */
|
||||
int pop_vflip_add(const atlas_t *orig, const char *path);
|
||||
|
||||
/* То же, но зеркало УЖЕ загружено (например, прочитано из архива): таблица
|
||||
/* Зеркало УЖЕ загружено из архива: таблица
|
||||
* забирает его себе, и дальше он освобождается вместе с остальными. */
|
||||
int pop_vflip_take(const atlas_t *orig, const atlas_t *mirror);
|
||||
|
||||
|
||||