Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 81e8ed4676 | |||
| 8490288d79 | |||
| 31d0075090 | |||
| 6176f6da31 | |||
| 7377bf6a2c | |||
| b34997073e | |||
| 63bfd997a9 | |||
| decbec79de | |||
| b0e7130d0b | |||
| 4e12aa50d1 | |||
| c218e8b983 | |||
| a3aaa30e53 | |||
| 90e304f071 | |||
| b55d4d11e3 | |||
| ca67895667 | |||
| a5252b1c60 | |||
| 3a0e847353 | |||
| df5071a967 | |||
| 2349481b86 | |||
| 589894c50d | |||
| ea8efdb0fd | |||
| 623199337e | |||
| 602c3a20fa | |||
| 4ac3584bf9 | |||
| 3bf28da8ee | |||
| 770b946a36 | |||
| 89e9663753 | |||
| ea07a8d7b0 | |||
| d23126983e | |||
| 50570881f5 | |||
| f25ed37d85 | |||
| 3449f6f8c9 | |||
| 6dabe9b4b1 | |||
| a05970cd36 | |||
| 2176c12cc5 |
@@ -9,3 +9,21 @@ args = [
|
|||||||
"mcp<2",
|
"mcp<2",
|
||||||
"/Volumes/SAM8/Projects/DIY/Z80/Sprinter/C-Compiler/mame/sources/MAME/src/mame_mcp.py",
|
"/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"
|
||||||
|
|||||||
@@ -132,15 +132,9 @@ applications/PoP/PoP1_DOS_music/
|
|||||||
# не идёт (сама roomtest и есть версируемая ветка разработки).
|
# не идёт (сама roomtest и есть версируемая ветка разработки).
|
||||||
applications/PoP/R1/
|
applications/PoP/R1/
|
||||||
|
|
||||||
# SprPoP — автономное приложение. В репозиторий идут исходники, генерируемые
|
# SprPoP — автономное приложение, и правила игнора у него СВОИ:
|
||||||
# заголовки (gen/) и упакованные ресурсы (assets/packed/), потому что без них
|
# applications/SprPoP/.gitignore. Он написан так, чтобы стать корневым
|
||||||
# из чистого клона ничего не собрать. НЕ идут:
|
# .gitignore, когда SprPoP выделят в отдельный репозиторий, — поэтому
|
||||||
# assets/orig/ оригинальные данные игры — копирайт (SDLPoP, MSDOS,
|
# здесь его содержимое НЕ дублируется (иначе разъедется). Коротко: в
|
||||||
# записи музыки DOS-версии); откуда их взять,
|
# репозиторий не идут assets/orig/ (чужие данные — их выкачивает
|
||||||
# написано в assets/orig/README.md
|
# `make fetch`) и assets/packed/LEVELS/ (уровни из оригинала как есть).
|
||||||
# assets/packed/LEVELS/ уровни res20xx.bin ложатся на диск прямо из
|
|
||||||
# оригинала, конвертации нет — то есть это тот же
|
|
||||||
# копирайтный материал, только в раскладке диска
|
|
||||||
# build/ уже покрыт общим правилом build/ выше
|
|
||||||
applications/SprPoP/assets/orig/
|
|
||||||
applications/SprPoP/assets/packed/LEVELS/
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
# # EXTRA_SRCS := helper.c util.c # additional .c files in this dir
|
# # EXTRA_SRCS := helper.c util.c # additional .c files in this dir
|
||||||
# # EXTRA_FLAGS := --crt0=minimal # passed through to sprinter-cc
|
# # EXTRA_FLAGS := --crt0=minimal # passed through to sprinter-cc
|
||||||
# # EXTRA_DATA := test.txt # extra files to add to `make floppy`
|
# # EXTRA_DATA := test.txt # extra files to add to `make floppy`
|
||||||
|
# # HDD_DEST_DIR := games/myapp # общий каталог файлов в `make hdd`
|
||||||
#
|
#
|
||||||
# include $(PROJ_ROOT)/app.mk
|
# include $(PROJ_ROOT)/app.mk
|
||||||
#
|
#
|
||||||
@@ -40,7 +41,7 @@ FLOPPY_IMG := $(MAME_DIR)/IMG/mc.img
|
|||||||
# образ у себя и связывает его с MAME символьной ссылкой.
|
# образ у себя и связывает его с MAME символьной ссылкой.
|
||||||
HDD_IMG ?= $(MAME_DIR)/IMG/test_hdd.chd
|
HDD_IMG ?= $(MAME_DIR)/IMG/test_hdd.chd
|
||||||
MAKE_DISK := $(MAME_DIR)/make_disk.py
|
MAKE_DISK := $(MAME_DIR)/make_disk.py
|
||||||
MAKE_HDD := $(PROJ_ROOT)/toolchain/make_hdd.sh
|
MAKE_HDD ?= $(PROJ_ROOT)/toolchain/make_hdd.sh
|
||||||
RUN_MAME := $(MAME_DIR)/run_mame.sh
|
RUN_MAME := $(MAME_DIR)/run_mame.sh
|
||||||
|
|
||||||
# Optional knobs — see top of file.
|
# Optional knobs — see top of file.
|
||||||
@@ -67,6 +68,9 @@ SOURCES := $(MAIN_SRC) $(EXTRA_SRCS)
|
|||||||
# Аргументы упаковщика HDD. Обычно это exe и EXTRA_DATA; приложение со
|
# Аргументы упаковщика HDD. Обычно это exe и EXTRA_DATA; приложение со
|
||||||
# своей раскладкой каталогов может переопределить переменную до include.
|
# своей раскладкой каталогов может переопределить переменную до include.
|
||||||
HDD_PACK_ARGS ?= $(EXE) $(EXTRA_DATA)
|
HDD_PACK_ARGS ?= $(EXE) $(EXTRA_DATA)
|
||||||
|
# Общий каталог назначения внутри HDD. Пустое значение сохраняет прежнюю
|
||||||
|
# укладку в корень; вложенные КАТАЛОГ:файл считаются относительно него.
|
||||||
|
HDD_DEST_DIR ?=
|
||||||
|
|
||||||
CC_FLAGS := --memory $(MEMORY)
|
CC_FLAGS := --memory $(MEMORY)
|
||||||
ifneq ($(STACK_SIZE),)
|
ifneq ($(STACK_SIZE),)
|
||||||
@@ -125,7 +129,7 @@ run: floppy
|
|||||||
# используется MCP-мостом к MAME (run_bridge.sh). После пересборки образа
|
# используется MCP-мостом к MAME (run_bridge.sh). После пересборки образа
|
||||||
# MAME ОБЯЗАН полный рестарт (chdman -f = новый inode; см. memory).
|
# MAME ОБЯЗАН полный рестарт (chdman -f = новый inode; см. memory).
|
||||||
hdd: $(EXE)
|
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
|
||||||
@echo "HDD (D:) ready: $(HDD_IMG) (with $(EXAMPLE).exe$(if $(EXTRA_DATA), + $(EXTRA_DATA)))"
|
@echo "HDD (D:) ready: $(HDD_IMG) (with $(EXAMPLE).exe$(if $(EXTRA_DATA), + $(EXTRA_DATA)))"
|
||||||
@echo "ВНИМАНИЕ: перезапусти MAME (run_bridge.sh) — образ пересобран."
|
@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)/../../..)
|
PROJ_ROOT := $(abspath $(CURDIR)/../../..)
|
||||||
EXAMPLE := roomtest
|
EXAMPLE := roomtest
|
||||||
|
HDD_IMG := $(CURDIR)/build/hdd/roomtest.chd
|
||||||
|
|
||||||
# small: W1+W2 как единое 32КБ-пространство, DATA сразу за CODE (иначе
|
# small: W1+W2 как единое 32КБ-пространство, DATA сразу за CODE (иначе
|
||||||
# CODE ~23КБ переполняет W1 и лезет в фиксированный DATA@0x8000 в huge).
|
# CODE ~23КБ переполняет W1 и лезет в фиксированный DATA@0x8000 в huge).
|
||||||
MEMORY ?= huge # small-раскладка + банки кода в W3 (стражи)
|
MEMORY ?= huge # small-раскладка + банки кода в W3 (стражи)
|
||||||
@@ -88,7 +90,7 @@ EXTRA_FLAGS ?= --gfx 256 -I $(CURDIR)/../poc/res/bg --max-allocs $(ALLOCS) \
|
|||||||
# умолчанию, пока идёт работа с производительностью; make PROF=0 выключает.
|
# умолчанию, пока идёт работа с производительностью; make PROF=0 выключает.
|
||||||
# Каждая фаза кадра красит бордюр в свой цвет, высота полосы на скриншоте
|
# Каждая фаза кадра красит бордюр в свой цвет, высота полосы на скриншоте
|
||||||
# MAME = время фазы в растровых строках.
|
# MAME = время фазы в растровых строках.
|
||||||
PROF ?= 1
|
PROF ?= 0
|
||||||
PROF_FLAGS := -DPROF_BORDER=$(PROF)
|
PROF_FLAGS := -DPROF_BORDER=$(PROF)
|
||||||
# Стартовый уровень (отладка): make LEVEL=9 — начать сразу с девятого.
|
# Стартовый уровень (отладка): make LEVEL=9 — начать сразу с девятого.
|
||||||
# Дефолт держим на отлаживаемом сейчас уровне; для «настоящей» игры — LEVEL=1.
|
# Дефолт держим на отлаживаемом сейчас уровне; для «настоящей» игры — 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 - кнопка ломается (превращается в щебень)
|
|
||||||
/// но двери которые она должна открыть остаются закрытыми
|
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
# ===========================================================================
|
||||||
|
# SprPoP — .gitignore приложения
|
||||||
|
# ===========================================================================
|
||||||
|
# Пока SprPoP лежит внутри репозитория C-Compiler, git читает этот файл как
|
||||||
|
# правила ДЛЯ ЭТОГО ПОДДЕРЕВА, и он дополняет корневой .gitignore тулчейна.
|
||||||
|
# Когда SprPoP выделят в самостоятельный проект, файл станет КОРНЕВЫМ и
|
||||||
|
# продолжит работать как есть: все пути написаны от корня SprPoP и ничего
|
||||||
|
# не знают ни про applications/, ни про раскладку тулчейна.
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# ЧУЖИЕ ДАННЫЕ — в репозиторий не идут никогда
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# assets/orig/ — оригинальные данные игры (копирайт). В репозитории вместо
|
||||||
|
# них лежит ЗНАНИЕ, откуда их взять: tools/fetch_orig.py (`make fetch`)
|
||||||
|
# качает SDLPoP и записи саундтрека, MSDOS/ пользователь кладёт сам (он
|
||||||
|
# необязателен). Скачанные архивы кэшируются в assets/orig/.cache/.
|
||||||
|
#
|
||||||
|
# Шаблон со ЗВЁЗДОЧКОЙ, а не `assets/orig/`: git не заходит внутрь
|
||||||
|
# игнорируемого каталога, и исключение ниже иначе не сработало бы, а
|
||||||
|
# README.md — единственное, что отсюда ОБЯЗАНО быть в репозитории: без
|
||||||
|
# него в чистом клоне не написано, откуда брать данные.
|
||||||
|
assets/orig/*
|
||||||
|
!assets/orig/README.md
|
||||||
|
|
||||||
|
# Уровни res20xx.bin ложатся на диск ПРЯМО из оригинала, конвертации нет —
|
||||||
|
# то есть это тот же копирайтный материал, только в раскладке диска.
|
||||||
|
# Остальное содержимое assets/packed/ версионируется намеренно: без него из
|
||||||
|
# чистого клона не собрать игру, а исходников (assets/orig/) может не быть.
|
||||||
|
assets/packed/LEVELS/
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# ВЫХОД СБОРКИ
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# build/ — .exe, раскладка диска, hdd/, промежуточные атласы, stamp'ы.
|
||||||
|
# Правило без слэша спереди намеренно: под ним и tests/host/build/.
|
||||||
|
build/
|
||||||
|
|
||||||
|
# Промежуточный каталог sprinter-cc.
|
||||||
|
.sprinter-cc-*/
|
||||||
|
|
||||||
|
# Артефакты компиляции, которые SDCC кладёт рядом с целью — в корень
|
||||||
|
# приложения. Только корень: единственный версионируемый ассемблерный
|
||||||
|
# файл (tests/host/bank_stub.s) лежит глубже и под правило не попадает.
|
||||||
|
/*.exe
|
||||||
|
/*.asm
|
||||||
|
/*.lst
|
||||||
|
/*.lk
|
||||||
|
/*.ihx
|
||||||
|
/*.noi
|
||||||
|
/*.sym
|
||||||
|
/*.map
|
||||||
|
/*.rel
|
||||||
|
/*.cdb
|
||||||
|
/*.mem
|
||||||
|
/*.rst
|
||||||
|
|
||||||
|
# Хостовые тесты и утилиты собираются нативным cc.
|
||||||
|
*.o
|
||||||
|
*.obj
|
||||||
|
*.dSYM/
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# ОС / редакторы / локальные настройки
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
.DS_Store
|
||||||
|
._*
|
||||||
|
*~
|
||||||
|
*.swp
|
||||||
|
*.bak
|
||||||
|
*.orig
|
||||||
|
*.rej
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
.kilo/
|
||||||
|
.claude/
|
||||||
@@ -17,8 +17,9 @@ sprinter-cc / libc / libbgi. Действуют правила корневог
|
|||||||
## Главное правило: SDLPoP — источник истины. Сначала читай, потом кодь
|
## Главное правило: SDLPoP — источник истины. Сначала читай, потом кодь
|
||||||
|
|
||||||
**`assets/orig/SDLPoP/src/` (github.com/NagyD/SDLPoP, GPLv3) — ЕДИНСТВЕННЫЙ
|
**`assets/orig/SDLPoP/src/` (github.com/NagyD/SDLPoP, GPLv3) — ЕДИНСТВЕННЫЙ
|
||||||
авторитетный источник того, как оригинальный движок это делает.** Правило
|
авторитетный источник того, как оригинальный движок это делает.** В
|
||||||
без исключений:
|
репозитории его нет: заполняется `make fetch` (`make fetch-check` — что уже
|
||||||
|
на месте). Правило без исключений:
|
||||||
|
|
||||||
1. **Перед реализацией ЛЮБОЙ функции** (движение, коллизия, окклюзия,
|
1. **Перед реализацией ЛЮБОЙ функции** (движение, коллизия, окклюзия,
|
||||||
падение, loose-полы, стражники, отрисовка, тайминги, любые числовые
|
падение, loose-полы, стражники, отрисовка, тайминги, любые числовые
|
||||||
@@ -69,6 +70,8 @@ make собрать build/sprpop.exe и разложить build
|
|||||||
make hdd + образ build/hdd/sprpop.chd
|
make hdd + образ build/hdd/sprpop.chd
|
||||||
make mame-link однократно: подставить образ в MAME (см. ниже)
|
make mame-link однократно: подставить образ в MAME (см. ниже)
|
||||||
make -C tests/host модульные тесты движка под ucsim_z80 (секунды, без MAME)
|
make -C tests/host модульные тесты движка под ucsim_z80 (секунды, без MAME)
|
||||||
|
make test-tools тесты упаковщиков на хосте (форматы, которые читает Z80)
|
||||||
|
make fetch скачать внешние данные в assets/orig/ (SDLPoP + музыка)
|
||||||
make resources перегенерировать ресурсы из assets/orig/
|
make resources перегенерировать ресурсы из assets/orig/
|
||||||
make music-mp3 музыка из другого набора (flac|mp3|ogg|mt32)
|
make music-mp3 музыка из другого набора (flac|mp3|ogg|mt32)
|
||||||
make clean снести build/ (ассеты не трогает)
|
make clean снести build/ (ассеты не трогает)
|
||||||
@@ -93,13 +96,19 @@ make distclean clean + снести assets/packed/ (вернуть — mak
|
|||||||
|---|---|
|
|---|---|
|
||||||
| `src/` | рукописный C: главный цикл `sprpop.c`, холодная половина `sprpop_cold.c`, движок `pop_*.c` |
|
| `src/` | рукописный C: главный цикл `sprpop.c`, холодная половина `sprpop_cold.c`, движок `pop_*.c` |
|
||||||
| `gen/` | генерируемые заголовки (индексы архивов, таблицы кадров, шрифт, палитры). **Руками не править** — их печатают упаковщики; лежат в репозитории, потому что без них `src/` не собрать |
|
| `gen/` | генерируемые заголовки (индексы архивов, таблицы кадров, шрифт, палитры). **Руками не править** — их печатают упаковщики; лежат в репозитории, потому что без них `src/` не собрать |
|
||||||
| `assets/orig/` | оригинальные данные (SDLPoP, MSDOS, записи музыки). Вне репозитория — копирайт; что и откуда взять, написано в `assets/orig/README.md` |
|
| `assets/orig/` | оригинальные данные (SDLPoP, записи музыки; MSDOS — необязательно). Вне репозитория — копирайт; вместо них в репозитории лежит `tools/fetch_orig.py`, который их качает (`make fetch`). Правила — в `assets/orig/README.md` |
|
||||||
| `assets/packed/` | то, что ложится на диск игры, уже в раскладке диска (`BG/`, `KID/`, …). В репозитории — иначе из чистого клона не собрать |
|
| `assets/packed/` | то, что ложится на диск игры, уже в раскладке диска (`BG/`, `KID/`, …). В репозитории — иначе из чистого клона не собрать |
|
||||||
| `tools/` | конверторы ресурсов. Раскладку путей знает ОДИН файл — `tools/paths.py`; менять пути нужно там, а не в отдельных упаковщиках |
|
| `tools/` | конверторы ресурсов. Раскладку путей знает ОДИН файл — `tools/paths.py`; менять пути нужно там, а не в отдельных упаковщиках. Адреса ВНЕШНИХ источников — так же в одном: `tools/fetch_orig.py` |
|
||||||
| `docs/` | планы, доски, справочники; `docs/PoP/` — форматы ресурсов оригинала |
|
| `docs/` | планы, доски, справочники; `docs/PoP/` — форматы ресурсов оригинала |
|
||||||
| `tests/host/` | модульные тесты движка под ucsim_z80 |
|
| `tests/host/` | модульные тесты движка под ucsim_z80 |
|
||||||
| `build/` | выход: `sprpop.exe`, каталоги ресурсов, `hdd/`, промежуточные `atl/` |
|
| `build/` | выход: `sprpop.exe`, каталоги ресурсов, `hdd/`, промежуточные `atl/` |
|
||||||
|
|
||||||
|
Звуковые ЭФФЕКТЫ по умолчанию берутся из SDLPoP (`SND_SRC=sdlpop`) — сборка
|
||||||
|
обязана работать без оригинального дистрибутива DOS. У кого лежит
|
||||||
|
`assets/orig/MSDOS/`, включает его набор явно (`make SND_SRC=msdos`): там
|
||||||
|
оцифровка полнее — в SDLPoP пуст звук 48 `spiked` (насаживание на пики).
|
||||||
|
Разбор — `docs/sound_plan.md`.
|
||||||
|
|
||||||
Музыка собирается из одного из четырёх наборов записей
|
Музыка собирается из одного из четырёх наборов записей
|
||||||
(`assets/orig/PoP1_DOS_music/`): `flac` по умолчанию, плюс `mp3`, `ogg` и
|
(`assets/orig/PoP1_DOS_music/`): `flac` по умолчанию, плюс `mp3`, `ogg` и
|
||||||
`mt32` (исполнение Roland MT-32 — звучит иначе, длина треков другая). Набор
|
`mt32` (исполнение Roland MT-32 — звучит иначе, длина треков другая). Набор
|
||||||
@@ -153,6 +162,9 @@ Makefile склеивает архивы прямо в `assets/packed/<КАТА
|
|||||||
| `pop_kid.c/.h` | Анимация/движение: интерпретатор seqtbl `play_seq` + frame_table (`seg006`), окна `Char` (loadkid/loadshad), атласы Кида. |
|
| `pop_kid.c/.h` | Анимация/движение: интерпретатор seqtbl `play_seq` + frame_table (`seg006`), окна `Char` (loadkid/loadshad), атласы Кида. |
|
||||||
| `pop_cdraw.c/.h` | ОТРИСОВКА персонажей — одна на всех Char (порт `add_kid_to_objtable`/`add_guard_to_objtable`): спрайт, clip_char, брызги, клинок, heal; полоса HP и палитры соперника. Там же ПРОПУСК неизменившегося кадра (`pop_char_skip_mask`). |
|
| `pop_cdraw.c/.h` | ОТРИСОВКА персонажей — одна на всех Char (порт `add_kid_to_objtable`/`add_guard_to_objtable`): спрайт, clip_char, брызги, клинок, heal; полоса HP и палитры соперника. Там же ПРОПУСК неизменившегося кадра (`pop_char_skip_mask`). |
|
||||||
| `pop_ctrl.c/.h` | Управление: диспетчер `control()` (`seg005`) + ввод `read_user_control` (`seg006`) поверх `<kbd_raw.h>`. |
|
| `pop_ctrl.c/.h` | Управление: диспетчер `control()` (`seg005`) + ввод `read_user_control` (`seg006`) поверх `<kbd_raw.h>`. |
|
||||||
|
| `pop_sfx.c` + `pop_sfx_cold.c` | Звук: насос CBL в резиденте + холодная загрузка набора. Раскладка эффектов НЕ компилируется в EXE — читается с диска (`SND/snd.idx`, формат `PSI1`); см. `docs/sound_plan.md` §10. |
|
||||||
|
| `pop_music.c` | Музыка (банк 9): загрузка треков, кольцо для финальной темы. Длины и длительности — из `MUS/mus.idx` (EMM-страница + accessor `pop_music_info`), в EXE их нет; см. `docs/sound_plan.md` §11. |
|
||||||
|
| `pop_snd_tbl.h` + `pop_snd_data.c` | Тип записи набора и инварианты (рукописный заголовок) + резидентные `pop_snd_tbl`/`pop_snd_page`/`pop_snd_pages`. Размер записи 5 байт — часть дискового контракта, проверяется статически. |
|
||||||
| `pop_map.c/.h` | Коллизия с картой + физика падения/приземления/стены + loose-полы (`seg005/006/007`). |
|
| `pop_map.c/.h` | Коллизия с картой + физика падения/приземления/стены + loose-полы (`seg005/006/007`). |
|
||||||
|
|
||||||
## Банки: две мины, которых компилятор не видит
|
## Банки: две мины, которых компилятор не видит
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ BANK9_SRCS := pop_menu.c pop_status.c pop_music.c pop_app.c pop_timer.c \
|
|||||||
# стоит 51 мс, так что трамплин банк→банк на их фоне не существует.
|
# стоит 51 мс, так что трамплин банк→банк на их фоне не существует.
|
||||||
# Отдельный банк для этого не нужен: в десятом лежал один pop_pal.c на
|
# Отдельный банк для этого не нужен: в десятом лежал один pop_pal.c на
|
||||||
# 485 байт, то есть 3% страницы.
|
# 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
|
BANK11_SRCS := pop_cutscene.c pop_intro.c
|
||||||
BANK_NUMS := 1 2 3 4 5 6 7 8 9 10 11
|
BANK_NUMS := 1 2 3 4 5 6 7 8 9 10 11
|
||||||
BANKED_SRCS := $(foreach n,$(BANK_NUMS),$(BANK$(n)_SRCS))
|
BANKED_SRCS := $(foreach n,$(BANK_NUMS),$(BANK$(n)_SRCS))
|
||||||
@@ -100,6 +100,7 @@ BANK_FLAGS := $(foreach n,$(BANK_NUMS),\
|
|||||||
|
|
||||||
# Модули резидента (кроме sprpop.c — его подставляет app.mk).
|
# Модули резидента (кроме sprpop.c — его подставляет app.mk).
|
||||||
EXTRA_SRCS := $(addprefix $(SRC_DIR)/,pop_vflip.c pop_pace.c pop_sfx.c pop_state.c \
|
EXTRA_SRCS := $(addprefix $(SRC_DIR)/,pop_vflip.c pop_pace.c pop_sfx.c pop_state.c \
|
||||||
|
pop_snd_data.c \
|
||||||
pop_draw.c pop_tile.c pop_kid.c pop_level.c pop_geom.c pop_guard.c \
|
pop_draw.c pop_tile.c pop_kid.c pop_level.c pop_geom.c pop_guard.c \
|
||||||
pop_qsave_io.c)
|
pop_qsave_io.c)
|
||||||
|
|
||||||
@@ -166,7 +167,10 @@ SHADOW_ATL := $(foreach n,$(KID_N),$(ATL)/shadow/sk$(n).atl) \
|
|||||||
# Звук: оригинальная оцифровка на частоте CBL (10 937,5 Гц, 8 бит моно).
|
# Звук: оригинальная оцифровка на частоте CBL (10 937,5 Гц, 8 бит моно).
|
||||||
# Число файлов задаёт упаковщик (POP_SND_PAGES в gen/pop_sound_tbl.h) —
|
# Число файлов задаёт упаковщик (POP_SND_PAGES в gen/pop_sound_tbl.h) —
|
||||||
# список ОБЯЗАН совпадать: недостающий файл это молча пропавшие эффекты.
|
# список ОБЯЗАН совпадать: недостающий файл это молча пропавшие эффекты.
|
||||||
SND_ATL := $(foreach n,0 1 2 3 4 5 6 7 8 9,$(ATL)/sound/s$(n).bin)
|
# Страниц звука СТАТИЧЕСКИ НЕ ПОСЧИТАТЬ: их число зависит от источника
|
||||||
|
# (SDLPoP даёт 9, MSDOS — 10, см. SND_SRC). Поэтому списка s0..sN здесь нет,
|
||||||
|
# а архив зависит от stamp'а упаковщика и берёт страницы из каталога уже
|
||||||
|
# после того, как тот отработал (см. правило SND/snd.arc ниже).
|
||||||
TITLE_ATL := $(foreach s,0 1 2 3,$(foreach p,0 1 2 3 4,$(ATL)/title/t$(s)_$(p).atl))
|
TITLE_ATL := $(foreach s,0 1 2 3,$(foreach p,0 1 2 3 4,$(ATL)/title/t$(s)_$(p).atl))
|
||||||
# PV/INTRO: story и стадии режутся на полосы; у настоящего PV intro есть ещё
|
# PV/INTRO: story и стадии режутся на полосы; у настоящего PV intro есть ещё
|
||||||
# чистый фон b0_* и шесть <=16K actor-атласов — они остаются в EMM на время
|
# чистый фон b0_* и шесть <=16K actor-атласов — они остаются в EMM на время
|
||||||
@@ -189,6 +193,15 @@ MUS_IDS := 24 25 26 27 28 29 30 32 33 35 36 37 39 40 41 43 50 52 53 54 55 56
|
|||||||
# Формат входит в имя stamp'а, поэтому смена MUSIC_FMT сама по себе делает
|
# Формат входит в имя stamp'а, поэтому смена MUSIC_FMT сама по себе делает
|
||||||
# музыку устаревшей — принудительная пересборка не нужна.
|
# музыку устаревшей — принудительная пересборка не нужна.
|
||||||
MUSIC_FMT ?= flac
|
MUSIC_FMT ?= flac
|
||||||
|
# Источник оцифрованных ЭФФЕКТОВ (digisnd1..3): sdlpop (умолчание) или msdos.
|
||||||
|
# Умолчание — SDLPoP, потому что он выкачивается автоматически (`make fetch`)
|
||||||
|
# и сборка обязана работать вообще без оригинального дистрибутива DOS.
|
||||||
|
# Наборы НЕ одинаковы: в MSDOS оцифровка полнее (в SDLPoP `spiked` (48)
|
||||||
|
# фактически пустой, `sword_vs_sword` (10) короче — разбор в
|
||||||
|
# docs/sound_plan.md). У кого дистрибутив есть — `make SND_SRC=msdos`.
|
||||||
|
# Источник входит в имя stamp'а, поэтому смена SND_SRC сама делает звук
|
||||||
|
# устаревшим, как и смена MUSIC_FMT.
|
||||||
|
SND_SRC ?= sdlpop
|
||||||
# res2000 — attract-demo; res2001..res2014 — штатная игра. res2015 (copy
|
# res2000 — attract-demo; res2001..res2014 — штатная игра. res2015 (copy
|
||||||
# protection) намеренно не пакуется и не существует в маршруте загрузчика.
|
# protection) намеренно не пакуется и не существует в маршруте загрузчика.
|
||||||
LVL_NUMS := 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14
|
LVL_NUMS := 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14
|
||||||
@@ -209,8 +222,8 @@ DISK := README_E.TXT README_R.TXT \
|
|||||||
BG/bg.arc BG/pop_tile.pal BG/pal_tile.pal \
|
BG/bg.arc BG/pop_tile.pal BG/pal_tile.pal \
|
||||||
KID/kid.arc KID/kid.pal KID/kid.ani \
|
KID/kid.arc KID/kid.pal KID/kid.ani \
|
||||||
GUARD/guard.arc SKEL/skel.arc VIZIER/vizier.arc SHADOW/shadow.arc \
|
GUARD/guard.arc SKEL/skel.arc VIZIER/vizier.arc SHADOW/shadow.arc \
|
||||||
SND/snd.arc \
|
SND/snd.arc SND/snd.idx \
|
||||||
$(foreach i,$(MUS_IDS),MUS/m$(i).bin) \
|
$(foreach i,$(MUS_IDS),MUS/m$(i).bin) MUS/mus.idx \
|
||||||
FONT/font.atl \
|
FONT/font.atl \
|
||||||
TITLE/title.arc TITLE/title.pal \
|
TITLE/title.arc TITLE/title.pal \
|
||||||
PV/pv.arc PV/story.pal PV/pv.pal \
|
PV/pv.arc PV/story.pal PV/pv.pal \
|
||||||
@@ -220,16 +233,22 @@ DISK := README_E.TXT README_R.TXT \
|
|||||||
# список — это и зависимость exe, и то, что умеет чинить себя само.
|
# список — это и зависимость exe, и то, что умеет чинить себя само.
|
||||||
GEN_ARC_H := bg_arc.h kid_arc.h guard_arc.h skel_arc.h vizier_arc.h \
|
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
|
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 \
|
# pop_sound_tbl.h здесь БОЛЬШЕ НЕТ: раскладка эффектов уехала на диск
|
||||||
pop_guard_pal.h pop_shadow_atlas.h pop_sound_tbl.h \
|
# (SND/snd.idx), и EXE от звукового набора не зависит — пару
|
||||||
pop_music_tbl.h pop_music_ticks.h pop_font.h)
|
# snd.idx + snd.arc можно менять без перекомпиляции.
|
||||||
|
# Ни звукового, ни музыкальных заголовков здесь БОЛЬШЕ НЕТ: раскладка
|
||||||
|
# эффектов и длины треков уехали на диск (SND/snd.idx, MUS/mus.idx), и EXE
|
||||||
|
# не зависит ни от звукового набора, ни от набора записей.
|
||||||
|
GEN_H := $(addprefix gen/,$(GEN_ARC_H) pop_bg_atlas.h kid_data.h \
|
||||||
|
pop_guard_pal.h pop_shadow_atlas.h pop_font.h)
|
||||||
|
|
||||||
PACKED_FILES := $(addprefix $(PACKED)/,$(DISK))
|
PACKED_FILES := $(addprefix $(PACKED)/,$(DISK))
|
||||||
STAGED := $(addprefix $(BUILD_DIR)/,$(DISK))
|
STAGED := $(addprefix $(BUILD_DIR)/,$(DISK))
|
||||||
|
|
||||||
EXTRA_DATA := $(STAGED)
|
EXTRA_DATA := $(STAGED)
|
||||||
# Запись вида КАТАЛОГ:файл кладёт файл в подкаталог образа, голый путь — в
|
# Запись вида КАТАЛОГ:файл кладёт файл в подкаталог HDD_DEST_DIR, голый путь —
|
||||||
# корень. Проверка на слэш ОБЯЗАТЕЛЬНА: без неё `README_E.TXT` превращался
|
# непосредственно в HDD_DEST_DIR. Проверка на слэш ОБЯЗАТЕЛЬНА: без неё
|
||||||
|
# `README_E.TXT` превращался
|
||||||
# в `README_E.TXT:build/README_E.TXT`, то есть упаковщик заводил каталог с
|
# в `README_E.TXT:build/README_E.TXT`, то есть упаковщик заводил каталог с
|
||||||
# именем файла и клал файл внутрь него (поймано пользователем на готовом
|
# именем файла и клал файл внутрь него (поймано пользователем на готовом
|
||||||
# образе).
|
# образе).
|
||||||
@@ -237,6 +256,9 @@ HDD_PACK_ARGS := $(BUILD_DIR)/$(EXAMPLE).exe \
|
|||||||
$(foreach f,$(DISK),$(if $(findstring /,$(f)),\
|
$(foreach f,$(DISK),$(if $(findstring /,$(f)),\
|
||||||
$(word 1,$(subst /, ,$(f))):$(BUILD_DIR)/$(f),\
|
$(word 1,$(subst /, ,$(f))):$(BUILD_DIR)/$(f),\
|
||||||
$(BUILD_DIR)/$(f)))
|
$(BUILD_DIR)/$(f)))
|
||||||
|
# Всё дерево SprPoP лежит рядом с EXE, но не меняет дефолт глобального
|
||||||
|
# упаковщика: остальные приложения по-прежнему попадают в корень HDD.
|
||||||
|
HDD_DEST_DIR := games/sprpop
|
||||||
# Образ живёт в build/, а не в mame/v306/IMG — приложение автономно. Связь с
|
# Образ живёт в build/, а не в mame/v306/IMG — приложение автономно. Связь с
|
||||||
# MAME — одна символьная ссылка, ставится однократно: make mame-link.
|
# MAME — одна символьная ссылка, ставится однократно: make mame-link.
|
||||||
HDD_IMG := $(CURDIR)/$(BUILD_DIR)/hdd/$(EXAMPLE).chd
|
HDD_IMG := $(CURDIR)/$(BUILD_DIR)/hdd/$(EXAMPLE).chd
|
||||||
@@ -339,6 +361,28 @@ $(STAMP)/$(1): $(2) | $(STAMP) check-orig
|
|||||||
@touch $$@
|
@touch $$@
|
||||||
endef
|
endef
|
||||||
|
|
||||||
|
# ВАРИАНТЫ РЕСУРСА (источник звука SND_SRC, набор записей MUSIC_FMT). Оба
|
||||||
|
# варианта пишут в ОДИН выход — build/atl/sound, build/atl/music, — поэтому
|
||||||
|
# одного stamp'а на вариант мало: вернувшись к прежнему варианту, make видит
|
||||||
|
# его stamp свежим и не делает НИЧЕГО, а в выходе лежит чужое (поймано на
|
||||||
|
# sdlpop -> msdos -> sdlpop: заголовок остался с 10 страницами вместо 9).
|
||||||
|
# Лечится файлом с именем текущего варианта: он переписывается только при
|
||||||
|
# смене (cmp), а его свежесть делает stamp устаревшим.
|
||||||
|
.PHONY: variant-check
|
||||||
|
variant-check: ;
|
||||||
|
# СМЕНА ВАРИАНТА СНОСИТ STAMP'Ы СВОЕГО СЕМЕЙСТВА, а не полагается на то,
|
||||||
|
# что make заметит свежесть variant-файла. Причина — секундная
|
||||||
|
# гранулярность mtime: упаковка укладывается в одну секунду с записью
|
||||||
|
# variant, и stamp того же имени считается актуальным. Возврат к прежнему
|
||||||
|
# варианту (`sdlpop -> msdos -> sdlpop`) тогда ТИХО оставлял чужой набор.
|
||||||
|
# Нет файла — правило обязано выполниться, и время тут ни при чём.
|
||||||
|
define variant_rule
|
||||||
|
$(STAMP)/$(1).variant: variant-check | $(STAMP)
|
||||||
|
@echo "$(2)" | cmp -s - $$@ || { echo "$(2)" > $$@; rm -f $(STAMP)/$(1)-*; }
|
||||||
|
endef
|
||||||
|
$(eval $(call variant_rule,sound,$(SND_SRC)))
|
||||||
|
$(eval $(call variant_rule,music,$(MUSIC_FMT)))
|
||||||
|
|
||||||
$(eval $(call stamp_rule,bg,$(TOOLS)/pop_pack_bg.py $(TOOLS)/render_room.py $(TOOLS)/paths.py,pop_pack_bg.py dungeon && $(PY) pop_pack_bg.py palace))
|
$(eval $(call stamp_rule,bg,$(TOOLS)/pop_pack_bg.py $(TOOLS)/render_room.py $(TOOLS)/paths.py,pop_pack_bg.py dungeon && $(PY) pop_pack_bg.py palace))
|
||||||
$(eval $(call stamp_rule,kid,$(TOOLS)/pop_pack_kid.py $(TOOLS)/pop_pack_bg.py $(TOOLS)/paths.py,pop_pack_kid.py))
|
$(eval $(call stamp_rule,kid,$(TOOLS)/pop_pack_kid.py $(TOOLS)/pop_pack_bg.py $(TOOLS)/paths.py,pop_pack_kid.py))
|
||||||
$(eval $(call stamp_rule,kid-bin,$(TOOLS)/pop_extract_kid_data.py $(TOOLS)/paths.py,pop_extract_kid_data.py))
|
$(eval $(call stamp_rule,kid-bin,$(TOOLS)/pop_extract_kid_data.py $(TOOLS)/paths.py,pop_extract_kid_data.py))
|
||||||
@@ -346,8 +390,25 @@ $(eval $(call stamp_rule,guard,$(TOOLS)/pop_pack_guard.py $(TOOLS)/paths.py,pop_
|
|||||||
$(eval $(call stamp_rule,skel,$(TOOLS)/pop_pack_guard.py $(TOOLS)/paths.py,pop_pack_guard.py SKEL))
|
$(eval $(call stamp_rule,skel,$(TOOLS)/pop_pack_guard.py $(TOOLS)/paths.py,pop_pack_guard.py SKEL))
|
||||||
$(eval $(call stamp_rule,vizier,$(TOOLS)/pop_pack_guard.py $(TOOLS)/paths.py,pop_pack_guard.py VIZIER))
|
$(eval $(call stamp_rule,vizier,$(TOOLS)/pop_pack_guard.py $(TOOLS)/paths.py,pop_pack_guard.py VIZIER))
|
||||||
$(eval $(call stamp_rule,shadow,$(TOOLS)/pop_pack_shadow.py $(TOOLS)/pop_pack_bg.py $(TOOLS)/pop_pack_kid.py $(TOOLS)/paths.py,pop_pack_shadow.py))
|
$(eval $(call stamp_rule,shadow,$(TOOLS)/pop_pack_shadow.py $(TOOLS)/pop_pack_bg.py $(TOOLS)/pop_pack_kid.py $(TOOLS)/paths.py,pop_pack_shadow.py))
|
||||||
$(eval $(call stamp_rule,sound,$(TOOLS)/pop_pack_sound.py $(TOOLS)/paths.py,pop_pack_sound.py))
|
# ЗВУК — ОСОБЫЙ СЛУЧАЙ: упаковка, сборка архива и копия индекса делаются
|
||||||
$(eval $(call stamp_rule,music-$(MUSIC_FMT),$(TOOLS)/pop_pack_music.py $(TOOLS)/paths.py,pop_pack_music.py --format $(MUSIC_FMT) $(MUS_IDS)))
|
# ОДНИМ рецептом, а не тремя правилами через таймстемпы.
|
||||||
|
#
|
||||||
|
# Почему. Упаковщик и копирование укладываются в одну секунду, а make
|
||||||
|
# сравнивает mtime с секундной гранулярностью: цель, сделанная в ту же
|
||||||
|
# секунду, что и её зависимость, считается актуальной. При смене SND_SRC
|
||||||
|
# это давало ТИХО НЕВЕРНЫЙ результат — `sdlpop -> msdos -> sdlpop`
|
||||||
|
# перепаковывал build/atl, но в assets/packed оставался прежний набор
|
||||||
|
# (10 страниц вместо 9; поймано 2026-08-31, с паузой в секунду не
|
||||||
|
# воспроизводится). Внутри одного рецепта сравнивать нечего.
|
||||||
|
$(STAMP)/sound-$(SND_SRC): $(TOOLS)/pop_pack_sound.py $(TOOLS)/pop_idx.py \
|
||||||
|
$(TOOLS)/paths.py $(TOOLS)/pop_pack_arc.py \
|
||||||
|
$(STAMP)/sound.variant | $(STAMP) check-orig
|
||||||
|
cd $(TOOLS) && $(PY) pop_pack_sound.py --source $(SND_SRC)
|
||||||
|
@mkdir -p $(PACKED)/SND
|
||||||
|
$(ARC) $(PACKED)/SND/snd.arc $$(ls $(ATL)/sound/s*.bin | sort -V)
|
||||||
|
@cp $(ATL)/sound/snd.idx $(PACKED)/SND/snd.idx
|
||||||
|
@touch $@
|
||||||
|
$(eval $(call stamp_rule,music-$(MUSIC_FMT),$(TOOLS)/pop_pack_music.py $(TOOLS)/pop_idx.py $(TOOLS)/paths.py $(STAMP)/music.variant,pop_pack_music.py --format $(MUSIC_FMT) $(MUS_IDS)))
|
||||||
$(eval $(call stamp_rule,font,$(TOOLS)/pop_extract_font.py $(TOOLS)/paths.py,pop_extract_font.py))
|
$(eval $(call stamp_rule,font,$(TOOLS)/pop_extract_font.py $(TOOLS)/paths.py,pop_extract_font.py))
|
||||||
$(eval $(call stamp_rule,title,$(TOOLS)/pop_pack_title.py $(TOOLS)/paths.py,pop_pack_title.py))
|
$(eval $(call stamp_rule,title,$(TOOLS)/pop_pack_title.py $(TOOLS)/paths.py,pop_pack_title.py))
|
||||||
$(eval $(call stamp_rule,pv,$(TOOLS)/pop_pack_intro.py $(TOOLS)/paths.py,pop_pack_intro.py))
|
$(eval $(call stamp_rule,pv,$(TOOLS)/pop_pack_intro.py $(TOOLS)/paths.py,pop_pack_intro.py))
|
||||||
@@ -361,14 +422,13 @@ $(1): $(STAMP)/$(2)
|
|||||||
@test -f $$@ || { $(MAKE) GENERATE=1 -B $(STAMP)/$(2); test -f $$@; }
|
@test -f $$@ || { $(MAKE) GENERATE=1 -B $(STAMP)/$(2); test -f $$@; }
|
||||||
endef
|
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,$(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,$(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,$(GUARD_ATL) gen/pop_guard_pal.h,guard))
|
||||||
$(eval $(call from_stamp,$(SKEL_ATL),skel))
|
$(eval $(call from_stamp,$(SKEL_ATL),skel))
|
||||||
$(eval $(call from_stamp,$(VIZIER_ATL),vizier))
|
$(eval $(call from_stamp,$(VIZIER_ATL),vizier))
|
||||||
$(eval $(call from_stamp,$(SHADOW_ATL) gen/pop_shadow_atlas.h,shadow))
|
$(eval $(call from_stamp,$(SHADOW_ATL) gen/pop_shadow_atlas.h,shadow))
|
||||||
$(eval $(call from_stamp,$(SND_ATL) gen/pop_sound_tbl.h,sound))
|
$(eval $(call from_stamp,$(foreach i,$(MUS_IDS),$(ATL)/music/m$(i).bin) $(ATL)/music/mus.idx,music-$(MUSIC_FMT)))
|
||||||
$(eval $(call from_stamp,$(foreach i,$(MUS_IDS),$(ATL)/music/m$(i).bin) gen/pop_music_tbl.h gen/pop_music_ticks.h,music-$(MUSIC_FMT)))
|
|
||||||
$(eval $(call from_stamp,$(ATL)/font/font.atl gen/pop_font.h,font))
|
$(eval $(call from_stamp,$(ATL)/font/font.atl gen/pop_font.h,font))
|
||||||
$(eval $(call from_stamp,$(TITLE_ATL) $(ATL)/title/title.pal,title))
|
$(eval $(call from_stamp,$(TITLE_ATL) $(ATL)/title/title.pal,title))
|
||||||
$(eval $(call from_stamp,$(PV_ATL) $(ATL)/pv/story.pal $(ATL)/pv/pv.pal,pv))
|
$(eval $(call from_stamp,$(PV_ATL) $(ATL)/pv/story.pal $(ATL)/pv/pv.pal,pv))
|
||||||
@@ -392,9 +452,15 @@ $(eval $(call arc_rule,TITLE,title,title,$(TITLE_ATL),TITLE))
|
|||||||
$(eval $(call arc_rule,PV,pv,pv,$(PV_ATL),PV))
|
$(eval $(call arc_rule,PV,pv,pv,$(PV_ATL),PV))
|
||||||
# У звука своего *_arc.h нет: содержимое адресуется таблицей pop_sound_tbl.h,
|
# У звука своего *_arc.h нет: содержимое адресуется таблицей pop_sound_tbl.h,
|
||||||
# которую печатает сам упаковщик звука.
|
# которую печатает сам упаковщик звука.
|
||||||
$(PACKED)/SND/snd.arc: $(SND_ATL) $(TOOLS)/pop_pack_arc.py
|
# Зависимость на stamp, а не на список страниц: сколько их будет, известно
|
||||||
@mkdir -p $(dir $@)
|
# только после запуска упаковщика (источник задаёт число). Порядок страниц
|
||||||
$(ARC) $@ $(SND_ATL)
|
# в архиве обязан быть ЧИСЛОВЫМ — pop_snd_tbl адресует их индексом, а голый
|
||||||
|
# ls даёт s10 перед s2. sort -V, а не `-t s -k n`: буква s есть и в пути.
|
||||||
|
# Пара snd.arc + snd.idx согласована по построению: её делает ОДИН рецепт
|
||||||
|
# stamp'а выше (см. там же, почему не раздельными правилами). Здесь
|
||||||
|
# только восстановление, если файл снесли руками.
|
||||||
|
$(PACKED)/SND/snd.arc $(PACKED)/SND/snd.idx: $(STAMP)/sound-$(SND_SRC)
|
||||||
|
@test -f $@ || { $(MAKE) GENERATE=1 -B $(STAMP)/sound-$(SND_SRC); test -f $@; }
|
||||||
|
|
||||||
# Файлы, которые ложатся на диск как есть, — простое копирование из staging.
|
# Файлы, которые ложатся на диск как есть, — простое копирование из staging.
|
||||||
define copy_rule
|
define copy_rule
|
||||||
@@ -411,10 +477,12 @@ $(eval $(call copy_rule,TITLE/title.pal,$(ATL)/title/title.pal))
|
|||||||
$(eval $(call copy_rule,PV/story.pal,$(ATL)/pv/story.pal))
|
$(eval $(call copy_rule,PV/story.pal,$(ATL)/pv/story.pal))
|
||||||
$(eval $(call copy_rule,PV/pv.pal,$(ATL)/pv/pv.pal))
|
$(eval $(call copy_rule,PV/pv.pal,$(ATL)/pv/pv.pal))
|
||||||
$(foreach i,$(MUS_IDS),$(eval $(call copy_rule,MUS/m$(i).bin,$(ATL)/music/m$(i).bin)))
|
$(foreach i,$(MUS_IDS),$(eval $(call copy_rule,MUS/m$(i).bin,$(ATL)/music/m$(i).bin)))
|
||||||
|
$(eval $(call copy_rule,MUS/mus.idx,$(ATL)/music/mus.idx))
|
||||||
# Уровни идут на диск ПРЯМО из оригинала: формат res20xx.bin наш загрузчик
|
# Уровни идут на диск ПРЯМО из оригинала: формат res20xx.bin наш загрузчик
|
||||||
# читает как есть, упаковщика для них нет. check-orig здесь тоже order-only —
|
# читает как есть, упаковщика для них нет. check-orig здесь тоже order-only —
|
||||||
# без оригиналов взять уровни неоткуда, и сказать об этом надо внятно.
|
# без оригиналов взять уровни неоткуда, и сказать об этом надо внятно.
|
||||||
LVL_ORIG := assets/orig/SDLPoP/data/LEVELS
|
LVL_ORIG := assets/orig/SDLPoP/data/LEVELS
|
||||||
|
SND_ORIG_MSDOS := assets/orig/MSDOS
|
||||||
define lvl_rule
|
define lvl_rule
|
||||||
$(PACKED)/LEVELS/res20$(1).bin: $(LVL_ORIG)/res20$(1).bin | check-orig
|
$(PACKED)/LEVELS/res20$(1).bin: $(LVL_ORIG)/res20$(1).bin | check-orig
|
||||||
@mkdir -p $$(dir $$@)
|
@mkdir -p $$(dir $$@)
|
||||||
@@ -443,8 +511,8 @@ resources-bg: $(PACKED)/BG/bg.arc $(PACKED)/BG/pop_tile.pal $(PACKED)/BG/pal
|
|||||||
resources-kid: $(PACKED)/KID/kid.arc $(PACKED)/KID/kid.pal $(PACKED)/KID/kid.ani
|
resources-kid: $(PACKED)/KID/kid.arc $(PACKED)/KID/kid.pal $(PACKED)/KID/kid.ani
|
||||||
resources-actors: $(PACKED)/GUARD/guard.arc $(PACKED)/SKEL/skel.arc \
|
resources-actors: $(PACKED)/GUARD/guard.arc $(PACKED)/SKEL/skel.arc \
|
||||||
$(PACKED)/VIZIER/vizier.arc $(PACKED)/SHADOW/shadow.arc
|
$(PACKED)/VIZIER/vizier.arc $(PACKED)/SHADOW/shadow.arc
|
||||||
resources-sound: $(PACKED)/SND/snd.arc
|
resources-sound: $(PACKED)/SND/snd.arc $(PACKED)/SND/snd.idx
|
||||||
resources-music: $(foreach i,$(MUS_IDS),$(PACKED)/MUS/m$(i).bin)
|
resources-music: $(foreach i,$(MUS_IDS),$(PACKED)/MUS/m$(i).bin) $(PACKED)/MUS/mus.idx
|
||||||
resources-font: $(PACKED)/FONT/font.atl
|
resources-font: $(PACKED)/FONT/font.atl
|
||||||
resources-title: $(PACKED)/TITLE/title.arc $(PACKED)/TITLE/title.pal
|
resources-title: $(PACKED)/TITLE/title.arc $(PACKED)/TITLE/title.pal
|
||||||
resources-pv: $(PACKED)/PV/pv.arc $(PACKED)/PV/story.pal $(PACKED)/PV/pv.pal
|
resources-pv: $(PACKED)/PV/pv.arc $(PACKED)/PV/story.pal $(PACKED)/PV/pv.pal
|
||||||
@@ -455,8 +523,12 @@ resources: resources-bg resources-kid resources-actors resources-sound \
|
|||||||
|
|
||||||
check-orig:
|
check-orig:
|
||||||
@test -d $(LVL_ORIG) || { \
|
@test -d $(LVL_ORIG) || { \
|
||||||
echo "SprPoP: нет assets/orig/ — генерировать ресурсы не из чего." >&2; \
|
echo "SprPoP: нет assets/orig/SDLPoP — генерировать ресурсы не из чего." >&2; \
|
||||||
echo " Что и откуда положить, написано в assets/orig/README.md" >&2; false; }
|
echo " Скачать: make fetch (подробности — assets/orig/README.md)" >&2; false; }
|
||||||
|
@test "$(SND_SRC)" != msdos || test -d $(SND_ORIG_MSDOS) || { \
|
||||||
|
echo "SprPoP: SND_SRC=msdos, но нет $(SND_ORIG_MSDOS)." >&2; \
|
||||||
|
echo " Дистрибутив DOS не качается — положить руками, либо собирать" >&2; \
|
||||||
|
echo " со звуком из SDLPoP: make SND_SRC=sdlpop" >&2; false; }
|
||||||
|
|
||||||
else
|
else
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
@@ -506,6 +578,33 @@ $(MUSIC_TARGETS):
|
|||||||
@$(MAKE) MUSIC_FMT=$(patsubst music-%,%,$@) resources-music
|
@$(MAKE) MUSIC_FMT=$(patsubst music-%,%,$@) resources-music
|
||||||
@$(MAKE) MUSIC_FMT=$(patsubst music-%,%,$@) stage
|
@$(MAKE) MUSIC_FMT=$(patsubst music-%,%,$@) stage
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Внешние исходные данные (assets/orig/)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# В репозитории чужих данных нет — есть tools/fetch_orig.py, который знает,
|
||||||
|
# откуда их взять, и таблица источников внутри него. Качаем только то,
|
||||||
|
# чего нет; `FETCH_ARGS=--force` перекачивает заново, `FETCH_ARGS=--ref v1.23`
|
||||||
|
# берёт конкретный тег SDLPoP.
|
||||||
|
# make fetch SDLPoP + записи музыки
|
||||||
|
# make fetch-sdlpop только SDLPoP (data/ + src/-эталон)
|
||||||
|
# make fetch-music только записи саундтрека (43 МБ)
|
||||||
|
# make fetch-check что уже на месте
|
||||||
|
# make fetch-list таблица источников
|
||||||
|
# MSDOS/ не качается — это копирайтный дистрибутив; он НЕОБЯЗАТЕЛЕН, всё
|
||||||
|
# нужное (уровни, оцифровка) есть в SDLPoP. См. SND_SRC выше.
|
||||||
|
FETCH := $(PY) $(TOOLS)/fetch_orig.py
|
||||||
|
fetch: ; $(FETCH) all $(FETCH_ARGS)
|
||||||
|
fetch-sdlpop: ; $(FETCH) sdlpop $(FETCH_ARGS)
|
||||||
|
fetch-music: ; $(FETCH) music $(FETCH_ARGS)
|
||||||
|
fetch-check: ; @$(FETCH) --check
|
||||||
|
fetch-list: ; @$(FETCH) --list
|
||||||
|
FETCH_TARGETS := fetch fetch-sdlpop fetch-music fetch-check fetch-list
|
||||||
|
|
||||||
|
# Тесты инструментов (хост, питон): форматы, которые читает Z80. Секунды,
|
||||||
|
# ни эмулятора, ни оригиналов не нужно.
|
||||||
|
test-tools:
|
||||||
|
@cd $(CURDIR) && $(PY) -m unittest discover -s tools/tests
|
||||||
|
|
||||||
# Перелинковка приложения без пересборки библиотек и ресурсов.
|
# Перелинковка приложения без пересборки библиотек и ресурсов.
|
||||||
relink:
|
relink:
|
||||||
$(MAKE) -W Makefile $(EXE)
|
$(MAKE) -W Makefile $(EXE)
|
||||||
@@ -525,4 +624,4 @@ distclean: clean
|
|||||||
.PHONY: stage check-orig resources resources-rebuild resources-bg resources-kid \
|
.PHONY: stage check-orig resources resources-rebuild resources-bg resources-kid \
|
||||||
resources-actors resources-sound resources-music resources-font \
|
resources-actors resources-sound resources-music resources-font \
|
||||||
resources-title resources-pv resources-levels $(MUSIC_TARGETS) \
|
resources-title resources-pv resources-levels $(MUSIC_TARGETS) \
|
||||||
relink distclean mame-link
|
$(FETCH_TARGETS) test-tools relink distclean mame-link
|
||||||
|
|||||||
@@ -13,12 +13,19 @@ make # build/sprpop.exe + ресурсы
|
|||||||
make hdd # + образ build/hdd/sprpop.chd
|
make hdd # + образ build/hdd/sprpop.chd
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Чужих данных в репозитории нет — есть адреса, откуда их взять. Чтобы
|
||||||
|
перегенерировать ресурсы (или получить эталон механики), сначала:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make fetch # SDLPoP + записи саундтрека DOS-версии в assets/orig/
|
||||||
|
```
|
||||||
|
|
||||||
## Что где
|
## Что где
|
||||||
|
|
||||||
```
|
```
|
||||||
src/ рукописный C: главный цикл sprpop.c + движок pop_*.c
|
src/ рукописный C: главный цикл sprpop.c + движок pop_*.c
|
||||||
gen/ генерируемые заголовки — руками не править
|
gen/ генерируемые заголовки — руками не править
|
||||||
assets/orig/ оригинальные данные игры (вне репозитория, см. там README)
|
assets/orig/ оригинальные данные игры (вне репозитория, качает `make fetch`)
|
||||||
assets/packed/ то, что ложится на диск игры, в раскладке диска
|
assets/packed/ то, что ложится на диск игры, в раскладке диска
|
||||||
tools/ конверторы ресурсов; все пути — в tools/paths.py
|
tools/ конверторы ресурсов; все пути — в tools/paths.py
|
||||||
docs/ планы, доски задач и багов, справочники (начинать с docs/README.md)
|
docs/ планы, доски задач и багов, справочники (начинать с docs/README.md)
|
||||||
@@ -33,9 +40,12 @@ build/ выход: sprpop.exe, каталоги ресурсов, hdd/,
|
|||||||
| `make` | собрать `build/sprpop.exe` и разложить ресурсы в `build/` |
|
| `make` | собрать `build/sprpop.exe` и разложить ресурсы в `build/` |
|
||||||
| `make hdd` | + образ жёсткого диска `build/hdd/sprpop.chd` |
|
| `make hdd` | + образ жёсткого диска `build/hdd/sprpop.chd` |
|
||||||
| `make mame-link` | однократно подставить образ в MAME (символьная ссылка на `IMG/test_hdd.chd`) |
|
| `make mame-link` | однократно подставить образ в MAME (символьная ссылка на `IMG/test_hdd.chd`) |
|
||||||
|
| `make fetch` | скачать внешние исходные данные в `assets/orig/`: `fetch-sdlpop`, `fetch-music`, `fetch-check`, `fetch-list` |
|
||||||
| `make resources` | перегенерировать ресурсы из `assets/orig/` |
|
| `make resources` | перегенерировать ресурсы из `assets/orig/` |
|
||||||
| `make music-mp3` | музыка из другого набора записей: `music-flac` (умолчание), `music-mp3`, `music-ogg`, `music-mt32` |
|
| `make music-mp3` | музыка из другого набора записей: `music-flac` (умолчание), `music-mp3`, `music-ogg`, `music-mt32` |
|
||||||
|
| `make SND_SRC=msdos` | оцифровка эффектов из дистрибутива DOS вместо SDLPoP (нужен свой `assets/orig/MSDOS/`) |
|
||||||
| `make -C tests/host` | модульные тесты движка (секунды, без эмулятора) |
|
| `make -C tests/host` | модульные тесты движка (секунды, без эмулятора) |
|
||||||
|
| `make test-tools` | тесты упаковщиков на хосте: форматы файлов, которые читает Z80 |
|
||||||
| `make clean` | снести `build/` (ассеты не трогает) |
|
| `make clean` | снести `build/` (ассеты не трогает) |
|
||||||
| `make distclean` | `clean` + снести упакованные ассеты `assets/packed/` |
|
| `make distclean` | `clean` + снести упакованные ассеты `assets/packed/` |
|
||||||
|
|
||||||
@@ -54,7 +64,14 @@ build/ выход: sprpop.exe, каталоги ресурсов, hdd/,
|
|||||||
|
|
||||||
`assets/orig/` нужен только чтобы ресурсы **перегенерировать** (`make
|
`assets/orig/` нужен только чтобы ресурсы **перегенерировать** (`make
|
||||||
resources`) и как **эталон механики**: любая правка поведения сверяется с
|
resources`) и как **эталон механики**: любая правка поведения сверяется с
|
||||||
`assets/orig/SDLPoP/src/` до кодинга, см. `CLAUDE.md`.
|
`assets/orig/SDLPoP/src/` до кодинга, см. `CLAUDE.md`. Заполняется он одной
|
||||||
|
командой `make fetch` — качается SDLPoP (ресурсы + исходники-эталон) и записи
|
||||||
|
саундтрека; адрес архива музыки скрипт берёт из самого SDLPoP.
|
||||||
|
|
||||||
|
Оригинальный дистрибутив DOS (`assets/orig/MSDOS/`) **не нужен**: и уровни, и
|
||||||
|
оцифровка звука берутся из SDLPoP. Он даёт ровно одно — более полный набор
|
||||||
|
эффектов (в SDLPoP пуст звук `spiked`, 48); у кого дистрибутив есть, включает
|
||||||
|
его явно: `make SND_SRC=msdos`. Подробности — `assets/orig/README.md`.
|
||||||
|
|
||||||
## Происхождение
|
## Происхождение
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
# assets/orig — оригинальные данные
|
||||||
|
|
||||||
|
Здесь лежат **исходные** данные Prince of Persia, из которых `make resources`
|
||||||
|
делает ресурсы Sprinter-версии. В репозиторий этот каталог не идёт —
|
||||||
|
материал копирайтный (см. `.gitignore`). Вместо данных в репозитории лежит
|
||||||
|
**знание, откуда они берутся**: `tools/fetch_orig.py` и таблица источников
|
||||||
|
внутри него.
|
||||||
|
|
||||||
|
## Как заполнить
|
||||||
|
|
||||||
|
```
|
||||||
|
make fetch # SDLPoP + записи саундтрека (всё, что качается)
|
||||||
|
make fetch-check # что уже на месте
|
||||||
|
make fetch-list # таблица источников с адресами
|
||||||
|
```
|
||||||
|
|
||||||
|
Тонкости:
|
||||||
|
|
||||||
|
```
|
||||||
|
make fetch-sdlpop только SDLPoP
|
||||||
|
make fetch-music только музыка (43 МБ)
|
||||||
|
make FETCH_ARGS=--force fetch перекачать заново
|
||||||
|
make FETCH_ARGS="--ref v1.23" fetch-sdlpop конкретный тег SDLPoP
|
||||||
|
```
|
||||||
|
|
||||||
|
Скачанные архивы кэшируются в `assets/orig/.cache/` — повторный `make fetch`
|
||||||
|
сеть не трогает. Версия SDLPoP записывается в `assets/orig/SDLPoP/.fetch.json`
|
||||||
|
(ветка/тег, sha, дата), и следующий `make fetch` по ней понимает, последняя
|
||||||
|
у нас версия или нет.
|
||||||
|
|
||||||
|
Собрать игру без `assets/orig/` **можно**: упакованные ресурсы лежат в
|
||||||
|
`../packed/`, а генерируемые заголовки — в `../../gen/`. `assets/orig` нужен
|
||||||
|
только чтобы ресурсы **перегенерировать**.
|
||||||
|
|
||||||
|
Исключение — `LEVELS`: файлы `res20xx.bin` ложатся на диск игры как есть, без
|
||||||
|
конвертации, поэтому `assets/packed/LEVELS/` тоже вне репозитория и заполняется
|
||||||
|
из `orig` командой `make resources-levels`.
|
||||||
|
|
||||||
|
## Что и откуда
|
||||||
|
|
||||||
|
| Каталог | Что это | Где взять |
|
||||||
|
|---|---|---|
|
||||||
|
| `SDLPoP/data/` | Распакованные ресурсы DOS-версии: PNG-спрайты (`VDUNGEON`, `VPALACE`, `KID`, `GUARD`, `SKEL`, `VIZIER`, `SHADOW`, `TITLE`, `PV`, `PRINCE`), палитры, `LEVELS/res20xx.bin`, оцифровка `DIGISND*.DAT` | `make fetch-sdlpop` — <https://github.com/NagyD/SDLPoP> (GPLv3) |
|
||||||
|
| `SDLPoP/src/` | Исходники SDLPoP. Нужны не только упаковщикам (`menu.c` и `seg009.c` содержат встроенные шрифты, из `seg005/seg006` берутся таблицы кадров и `seqtbl`), но и как **эталон механики**: расхождение в поведении всегда сверяется с ними | тем же `make fetch-sdlpop` |
|
||||||
|
| `PoP1_DOS_music/` | Записи музыки DOS-версии в четырёх наборах: `flac/` (по умолчанию), `mp3/`, `ogg/`, `ogg_MT-32/` (исполнение Roland MT-32). Имена треков во всех наборах одинаковые; какой брать — задаёт `MUSIC_FMT` | `make fetch-music`. Адрес архива скрипт читает из самого SDLPoP (`ReadMe.amigaos4`, секция «AUDIO IS SLOW/AWFULL»), запасной — popot.org |
|
||||||
|
| `MSDOS/` | **Необязательно.** Оригинальная поставка PoP 1.0 для MS-DOS. Используется только `digisnd*.dat` со звуком, и только если попросить: `make SND_SRC=msdos` | Не качается — дистрибутив игры, положить руками |
|
||||||
|
|
||||||
|
## MSDOS: зачем он и почему без него живётся
|
||||||
|
|
||||||
|
По умолчанию **всё** берётся из SDLPoP — и уровни, и оцифровка звука. Так
|
||||||
|
сборка работает на чистой машине сразу после `make fetch`.
|
||||||
|
|
||||||
|
`MSDOS/` даёт ровно одно: более полный набор оцифрованных эффектов. Из 31
|
||||||
|
звука 28 совпадают побайтно, а три — нет, и все три лучше в дистрибутиве:
|
||||||
|
`spiked` (48, насаживание на пики) в наборе SDLPoP длиной 7 сэмплов, то есть
|
||||||
|
его нет; `sword_vs_sword` (10) короче; `sword_moving` (11) — другая запись.
|
||||||
|
Разбор — `docs/sound_plan.md`. Включается это явно:
|
||||||
|
|
||||||
|
```
|
||||||
|
make SND_SRC=msdos resources-sound # перепаковать звук из дистрибутива
|
||||||
|
make SND_SRC=msdos # и собрать с ним
|
||||||
|
```
|
||||||
|
|
||||||
|
Источник входит в имя stamp'а, поэтому переключение `SND_SRC` само делает
|
||||||
|
звук устаревшим — `-B` не нужен.
|
||||||
|
|
||||||
|
## Кто что читает
|
||||||
|
|
||||||
|
| Упаковщик | Читает |
|
||||||
|
|---|---|
|
||||||
|
| `pop_pack_bg.py`, `render_room.py` | `SDLPoP/data/{VDUNGEON,VPALACE,LEVELS}` |
|
||||||
|
| `pop_pack_kid.py` | `SDLPoP/data/KID` |
|
||||||
|
| `pop_pack_guard.py` | `SDLPoP/data/{GUARD,SKEL,VIZIER,PRINCE}` |
|
||||||
|
| `pop_pack_shadow.py` | `SDLPoP/data/{KID,SHADOW}` |
|
||||||
|
| `pop_pack_title.py`, `pop_pack_intro.py` | `SDLPoP/data/{TITLE,PV,PRINCE,KID}` |
|
||||||
|
| `pop_extract_font.py` | `SDLPoP/src/{menu.c,seg009.c}` |
|
||||||
|
| `pop_extract_kid_data.py` | `SDLPoP/src/` (таблицы кадров и `seqtbl`) |
|
||||||
|
| `pop_pack_sound.py` | `SDLPoP/data/DIGISND*.DAT` (`--source sdlpop`, умолчание) либо `MSDOS/digisnd*.dat` (`--source msdos`) |
|
||||||
|
| `pop_pack_music.py` | `PoP1_DOS_music/<набор>/` (`--format flac\|mp3\|ogg\|mt32`) |
|
||||||
|
|
||||||
|
Точная раскладка путей — в `tools/paths.py`; менять её нужно там, а не в
|
||||||
|
отдельных упаковщиках. Адреса внешних источников — в `tools/fetch_orig.py`.
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
| [T-1](#t-1) | пики перерисовываются безусловно | оптимизация | открыт |
|
| [T-1](#t-1) | пики перерисовываются безусловно | оптимизация | открыт |
|
||||||
| [BUG-SPIKE-1](#bug-spike-1) | пики залипают выдвинутыми рядом с Кидом | **низкий** | маловоспроизводим: ни сценарием, ни попиксельной подгонкой X не поднимается |
|
| [BUG-SPIKE-1](#bug-spike-1) | пики залипают выдвинутыми рядом с Кидом | **низкий** | маловоспроизводим: ни сценарием, ни попиксельной подгонкой X не поднимается |
|
||||||
| [BUG-CHOMP-JUMP-1](#bug-chomp-jump-1) | прыжок с места вплотную к чомперу: кадр с отступом назад | **низкий** | маловоспроизводим: на повторе не поднялся; на пререлиз |
|
| [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** |
|
| [GUARD-ENTRY-DEATH](#guard-entry-death) | вход в комнату вплотную к стражу = мгновенная смерть (не портирован `bump_into_opponent`) | порт | **фикс есть, ждёт проверки в MAME** |
|
||||||
| [BUG-SHADOW-SET](#bug-shadow-set) | Тень на 12-м уровне дерётся спрайтами СТРАЖА: SHADOW.DAT у нас нет вовсе | порт | открыт, разбор готов |
|
| [BUG-SHADOW-SET](#bug-shadow-set) | Тень на 12-м уровне дерётся спрайтами СТРАЖА: SHADOW.DAT у нас нет вовсе | порт | открыт, разбор готов |
|
||||||
| [PAL-L1-AFTER-INTRO](#pal-l1-after-intro) | вход в игру на уровень 1 после интро — игровая палитра не установлена (чёрный экран) | палитра/fade | открыт: воспроизведение нестабильно, корень не установлен |
|
| [PAL-L1-AFTER-INTRO](#pal-l1-after-intro) | вход в игру на уровень 1 после интро — игровая палитра не установлена (чёрный экран) | палитра/fade | открыт: воспроизведение нестабильно, корень не установлен |
|
||||||
@@ -32,12 +32,14 @@
|
|||||||
| [MOB-CLIP-RIGHT](#mob-clip-right) | окна и узор кладки рисуются ПОВЕРХ падающих плит | окклюзия | открыт: корень найден 2026-08-13 |
|
| [MOB-CLIP-RIGHT](#mob-clip-right) | окна и узор кладки рисуются ПОВЕРХ падающих плит | окклюзия | открыт: корень найден 2026-08-13 |
|
||||||
| [MID-OVERLAY-LAYER](#mid-overlay-layer) | оверлей кромки всегда поверх плит и персонажей (у оригинала — midtable с сортировкой) | слои | открыт: нужен разбор архитектуры |
|
| [MID-OVERLAY-LAYER](#mid-overlay-layer) | оверлей кромки всегда поверх плит и персонажей (у оригинала — midtable с сортировкой) | слои | открыт: нужен разбор архитектуры |
|
||||||
| [GUARD-FALLOUT-VICTORY](#guard-fallout-victory) | страж, выпавший из комнаты, не засчитывается убитым | порт | открыт: две строки |
|
| [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) — щебень есть, а решётки закрыты | кнопки/ворота | открыт: **не воспроизводится**, проверено многое (см. разбор) |
|
| [L10-BUTTON-DEBRIS](#l10-button-debris) | ур.10 к.1: плита упала на кнопку (1,8) — щебень есть, а решётки закрыты | кнопки/ворота | открыт: **не воспроизводится**, проверено многое (см. разбор) |
|
||||||
| [LOOSE-SEAM-ANIM](#loose-seam-anim) | плита в шве (col −1), уроненная в прошлой комнате, исчезает без анимации падения | **мелкий** | открыт по решению пользователя: «пока пусть будет так» |
|
| [LOOSE-SEAM-ANIM](#loose-seam-anim) | плита в шве (col −1), уроненная в прошлой комнате, исчезает без анимации падения | **мелкий** | открыт по решению пользователя: «пока пусть будет так» |
|
||||||
| [SND-PACE-DEAD](#snd-pace-dead) | пейсинг сцен по насосу CBL не включается: признак «часы идут» вычисляется двумя чтениями подряд | **тайминг/звук** | открыт: диагноз полный, симптом обойдён подгонкой (2026-08-27) |
|
| [SND-PACE-DEAD](#snd-pace-dead) | пейсинг сцен по насосу CBL не включается: признак «часы идут» вычисляется двумя чтениями подряд | **тайминг/звук** | **снят 2026-08-28**: ветка удалена, сцена на единых часах по лучу |
|
||||||
| [PV-RENDER-BOUND](#pv-render-bound) | сцена с принцессой рисуется дороже бюджета: ~49 тиков/с вместо 60, музыка уезжает от картинки | производительность | открыт: замер есть, лечение — удешевить кадр |
|
| [PV-RENDER-BOUND](#pv-render-bound) | сцена с принцессой рисуется дороже бюджета: ~49 тиков/с вместо 60, музыка уезжает от картинки | производительность | **исправлено 2026-08-28**: кадр разложен на блоки по интервалу, удешевлять не понадобилось |
|
||||||
| [MUS-LEFT-TEAR](#mus-left-tear) | `pop_mus_left` (16 бит, пишет прерывание) читается из главного цикла неатомарно — возможен ложный «трек кончился» | **потенциальный** | открыт: хазард показан рассуждением, в прогоне не проявился |
|
| [MUS-LEFT-TEAR](#mus-left-tear) | `pop_mus_left` (16 бит, пишет прерывание) читается из главного цикла неатомарно — возможен ложный «трек кончился» | **потенциальный** | открыт: хазард показан рассуждением, в прогоне не проявился |
|
||||||
|
| [CLIMB-VS-GUARD](#climb-vs-guard) | Кид подтягивается к стражу этажом выше: у нас удар засчитывается и убивает, в оригинале Кид просто срывается без урона; страж при этом способен провалиться сквозь пол вслед за Кидом | бой/физика | открыт: цепочка удара сверена — совпадает, расходятся входные данные (2026-08-31) |
|
||||||
|
| [HP-BAR-RESTART](#hp-bar-restart) | после гибели и Ctrl+A на ОДНОЙ из двух страниц остаётся полоса HP по результатам боя | дабл-буфер/UI | КОРЕНЬ НАЙДЕН, фикс есть, ждёт проверки (2026-08-31) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -98,8 +100,67 @@ POS=18`, MAME): Кид стоит на кнопке -> решётка `(2,7)` о
|
|||||||
<a id="kbd-stuck-wait"></a>
|
<a id="kbd-stuck-wait"></a>
|
||||||
## KBD-STUCK-WAIT. Потерянный break-код вешает игру насмерть в межуровневой заставке
|
## 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 (чит «следующий
|
**Наблюдение (пользователь, 2026-08-24).** Нажат Shift+L (чит «следующий
|
||||||
уровень») — игра встала намертво: картинка прежнего уровня, реакции нет.
|
уровень») — игра встала намертво: картинка прежнего уровня, реакции нет.
|
||||||
@@ -169,6 +230,13 @@ Rx-overrun не срабатывала. Гашение бита отладчи
|
|||||||
> **Решение пользователя (2026-08-12): отложено до готовности всех уровней.**
|
> **Решение пользователя (2026-08-12): отложено до готовности всех уровней.**
|
||||||
> Механика работает, это вопрос ощущения, а не проходимости.
|
> Механика работает, это вопрос ощущения, а не проходимости.
|
||||||
|
|
||||||
|
> **Обновление 2026-08-28.** Гипотеза «дело в клавиатурном модуле, а не в
|
||||||
|
> физике» подтвердилась ЧАСТИЧНО: найдена и устранена кража скан-кодов
|
||||||
|
> обработчиком DSS (разбор — [KBD-STUCK-WAIT](#kbd-stuck-wait)). Украденный
|
||||||
|
> make Shift'а или стрелки — это ровно «нажал, а не сработало», и на
|
||||||
|
> железе при удержании клавиши такое случалось раз в ~10 с. Перепроверить
|
||||||
|
> ощущение зацепа на новой сборке ДО того, как лезть в физику.
|
||||||
|
|
||||||
**Наблюдение (пользователь).** Вход на уровень 7: Кид влетает в комнату сверху
|
**Наблюдение (пользователь).** Вход на уровень 7: Кид влетает в комнату сверху
|
||||||
и, пролетая мимо края пола, МОЖЕТ за него зацепиться — но получается заметно
|
и, пролетая мимо края пола, МОЖЕТ за него зацепиться — но получается заметно
|
||||||
реже, чем в оригинале. «Похоже, это проблемы нашего клавиатурного модуля».
|
реже, чем в оригинале. «Похоже, это проблемы нашего клавиатурного модуля».
|
||||||
@@ -1140,7 +1208,15 @@ fade_in по построению пишет все 256 записей из сн
|
|||||||
---
|
---
|
||||||
|
|
||||||
<a id="snd-pace-dead"></a>
|
<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()` зонды показали
|
**Замер (MAME, 2026-08-27).** В сцене `intro_pv_scene()` зонды показали
|
||||||
`snd_done == 0` на всём прогоне — то есть ветка `if (snd_pace)`, ради
|
`snd_done == 0` на всём прогоне — то есть ветка `if (snd_pace)`, ради
|
||||||
@@ -1171,7 +1247,17 @@ snd_pace = (uint8_t)(pop_snd_tick != snd_prev); /* часы реально и
|
|||||||
---
|
---
|
||||||
|
|
||||||
<a id="pv-render-bound"></a>
|
<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 порции насоса,
|
**Замер (MAME, 2026-08-27).** Реплика Джафара `m53` — 1403 порции насоса,
|
||||||
то есть 16,42 с реального времени. За это время шкала сцены прошла
|
то есть 16,42 с реального времени. За это время шкала сцены прошла
|
||||||
@@ -1229,3 +1315,130 @@ uint16_t pop_music_left(void) { uint16_t a, b;
|
|||||||
раз в кадр, задержка прерывания ничтожна против 11,7 мс периода насоса),
|
раз в кадр, задержка прерывания ничтожна против 11,7 мс периода насоса),
|
||||||
но двойное чтение не трогает состояние прерываний вовсе — на фоне
|
но двойное чтение не трогает состояние прерываний вовсе — на фоне
|
||||||
[cbl_w0_bios_conflict](../../PoP/roomtest/BUGS_CLOSED.md) это плюс.
|
[cbl_w0_bios_conflict](../../PoP/roomtest/BUGS_CLOSED.md) это плюс.
|
||||||
|
|
||||||
|
|
||||||
|
## CLIMB-VS-GUARD
|
||||||
|
|
||||||
|
Кид стоит рядом 2,6, страж — этажом выше на 1,6. Кид тянется подтянуться
|
||||||
|
на 1,6.
|
||||||
|
|
||||||
|
**Оригинал (SDLPoP).** Страж машет мечом, Кид срывается обратно на 2,6
|
||||||
|
**без потери HP**.
|
||||||
|
|
||||||
|
**У нас.** Тот же замах убивает Кида на месте. Если Кид безоружен, это
|
||||||
|
мгновенная смерть (ветка «заколот без меча», HP разом в ноль) — и дальше
|
||||||
|
тело падает, что до 2026-08-31 давало отдельный симптом «мёртвый
|
||||||
|
вприсядку» (закрыт развилкой в `land()`, см. коммит того же дня).
|
||||||
|
|
||||||
|
**Наблюдение сверх того (пользователь, 2026-08-31).** В этой же связке
|
||||||
|
поведение расходится сильнее: страж провалился с ряда 1 на ряд 2 ВМЕСТЕ с
|
||||||
|
Кидом — причём Кид ушёл в провал в колонке 7 (там дыра), а страж между
|
||||||
|
колонками 5 и 6, где на ряду 1 пол ЕСТЬ. То есть страж проваливается
|
||||||
|
сквозь целый пол. Это может быть тем же корнем, что и удар: обоим нужен
|
||||||
|
корректный ряд/колонка персонажа в момент подтягивания.
|
||||||
|
|
||||||
|
**Что уже проверено статически (2026-08-31).** Вся цепочка засчитывания
|
||||||
|
удара сверена с оригиналом и совпадает ДОСЛОВНО:
|
||||||
|
|
||||||
|
- гейт по кадру Кида (нулевой кадр и кадры выхода по лестнице пропускают
|
||||||
|
разбор целиком) — `pop_check_sword_hurting`;
|
||||||
|
- приоритет стража при встречном попадании (отметка «ранен» у Кида
|
||||||
|
снимается) — `pop_check_sword_hurt`;
|
||||||
|
- требование ОДНОГО ряда у бьющего и жертвы — `check_hurting`;
|
||||||
|
- кадры удара (укол/третий удар), диапазон дистанции (8 для безоружной
|
||||||
|
жертвы, 12 для вооружённой, верхняя граница 29);
|
||||||
|
- формула расстояния `pop_char_opp_dist` — совпадает с `char_opp_dist`.
|
||||||
|
|
||||||
|
Значит расходятся не правила, а ВХОДНЫЕ данные: ряд, колонка или X
|
||||||
|
персонажа во время виса/подтягивания. Наиболее вероятный кандидат —
|
||||||
|
дистанция: висящий вплотную под кромкой в оригинале не дотягивает до
|
||||||
|
нижнего порога 8, а у нас пара пикселей переводит его через порог.
|
||||||
|
|
||||||
|
**Как чинить.** Не гадать — замерить на живой сцене: в момент, когда
|
||||||
|
`Opp.action` становится «ранен», снять у обоих `curr_row`, `curr_col`, `x`,
|
||||||
|
`direction` и сравнить с теми же величинами в SDLPoP на том же кадре
|
||||||
|
(метод — lldb-трасса живого SDLPoP, как в
|
||||||
|
[[sdlpop_odd_pixel_char_x]]). Отдельно снять, почему страж теряет опору:
|
||||||
|
`get_tile_at_char` под ним в кадре провала.
|
||||||
|
|
||||||
|
**Уточнение пользователя (2026-08-31, важное).** Оригинальное поведение —
|
||||||
|
Кид сорвался без потери HP — ВОСПРОИЗВОДИТСЯ и у нас: исход сильно зависит
|
||||||
|
от того, где именно стоит страж. То есть расхождение не абсолютное, а
|
||||||
|
пороговое, и это прямо подкрепляет версию про ДИСТАНЦИЮ: пара пикселей
|
||||||
|
переводит расстояние через нижний порог засчитывания удара (8 для
|
||||||
|
безоружной жертвы), и удар из «мимо» становится смертельным. Значит
|
||||||
|
искать надо не потерянную ветку, а сдвиг координаты/порога — сравнивать
|
||||||
|
`pop_char_opp_dist` в момент замаха при ОДИНАКОВОЙ расстановке.
|
||||||
|
|
||||||
|
**ЗВУК КАК ИНДИКАТОР ПУТИ (пользователь, 2026-08-31) — лучшая зацепка.**
|
||||||
|
В той же сцене оригинал играет ВЗМАХ (звук 11, «клинок движется»), а у нас
|
||||||
|
слышен звук, похожий на упор Кида в стену (звук 8, `bumped`).
|
||||||
|
|
||||||
|
Почему это важно. Звук 11 оригинал играет в `check_hurting` СРАЗУ на
|
||||||
|
кадре укола — ДО проверки расстояния и до отметки «ранен». То есть он
|
||||||
|
звучит при ЛЮБОМ уколе, попал тот или нет. Значит его отсутствие
|
||||||
|
означает не «промахнулись», а «до укола дело вообще не дошло»: страж не в
|
||||||
|
кадре укола, либо разбор вышел раньше (меч не вынут / ряды не совпали).
|
||||||
|
А звучащий вместо него упор в стену говорит, что у нас сработало
|
||||||
|
СТОЛКНОВЕНИЕ, а не атака.
|
||||||
|
|
||||||
|
Набор звуков проверен и НЕ виноват: в `assets/packed/SND/snd.idx` слот 11
|
||||||
|
на месте и содержит собственный короткий сэмпл (1280 Б), слот 8 — другой
|
||||||
|
(1664 Б). Раскладка не сдвинута.
|
||||||
|
|
||||||
|
Отсюда рабочая версия: в этой связке оригинал ведёт стража по ветке
|
||||||
|
«атака с промахом», а мы — по ветке «столкновение с персонажем»
|
||||||
|
(`bump_into_opponent` / `check_bumped`). Это же объясняет и провалившегося
|
||||||
|
сквозь пол стража: столкновение двигает его координату, а не атака.
|
||||||
|
Проверять надо ветку выбора действия стража, а не только дистанцию.
|
||||||
|
|
||||||
|
**Решение пользователя:** отложено на будущее (2026-08-31) — поведение в
|
||||||
|
этой связке расходится широко, чинить нужно целиком, а не по одному
|
||||||
|
симптому.
|
||||||
|
|
||||||
|
|
||||||
|
## HP-BAR-RESTART
|
||||||
|
|
||||||
|
**Симптом (пользователь, 2026-08-31).** Гибель на 2-м уровне, возврат к
|
||||||
|
началу по Ctrl+A: строка HP на одном из двух экранов остаётся отрисованной
|
||||||
|
по результатам боя. Замечание там же: «в начале уровня она отрисовывается
|
||||||
|
только в один экран».
|
||||||
|
|
||||||
|
**Что уже известно.**
|
||||||
|
|
||||||
|
Ctrl+A у нас — РЕСТАРТ УРОВНЯ (`sprpop_cold.c`, тот же путь, что пункт меню
|
||||||
|
`POP_MENU_RESTART_LEVEL`), а не возврат в заставку.
|
||||||
|
|
||||||
|
Перерисовка полосы устроена счётчиком страниц, а не флагом:
|
||||||
|
`pop_hp_invalidate` ставит `hp_todo = 2`, и `pop_hp_draw` тратит по одной
|
||||||
|
странице за кадр (`src/pop_cdraw.c`). Все четыре холодных пути
|
||||||
|
(старт уровня, вход в комнату, быстрая загрузка, возврат из меню)
|
||||||
|
инвалидацию зовут — механизм на месте.
|
||||||
|
|
||||||
|
**Главный подозреваемый — зона статус-строки.** Полоса HP делит строку со
|
||||||
|
статус-текстом, и пока текст висит (`pop_status_ticks != 0`), стирание
|
||||||
|
чистит ТОЛЬКО края — левее `POP_STATUS_L` и правее `POP_STATUS_R`, —
|
||||||
|
а середину не трогает, чтобы текст не мигал. При старте уровня текст как
|
||||||
|
раз висит («LEVEL 2»), поэтому всё, что от прошлой полосы попало в
|
||||||
|
середину, там и остаётся. Полоса стража рисуется справа налево от 314 и
|
||||||
|
при большом запасе HP заходит именно в эту незачищаемую зону.
|
||||||
|
|
||||||
|
**КОРЕНЬ (2026-08-31).** Счётчик считает СТРАНИЦЫ, но тратился по КАДРАМ,
|
||||||
|
а это не одно и то же: между двумя вызовами отрисовки переворота может не
|
||||||
|
быть, и оба прохода уходили в одну страницу — вторая оставалась с
|
||||||
|
делениями прошлого боя. Фикс: проход тратится только когда
|
||||||
|
`gfx_get_draw_page()` отличается от страницы прошлого прохода (первый
|
||||||
|
проход идёт всегда). Полная чистка всей ширины при инвалидации добавлена
|
||||||
|
там же — старая полоса могла заходить под статус-текст, где щадящая чистка
|
||||||
|
её не трогала; текст сразу перезапрашивается, чтобы не пропал.
|
||||||
|
|
||||||
|
**Что проверить при взятии в работу.**
|
||||||
|
|
||||||
|
1. Значения `POP_STATUS_L`/`POP_STATUS_R` против реальной ширины полос:
|
||||||
|
при скольких делениях полоса стража (или Кида) заходит под текст.
|
||||||
|
2. Уходят ли оба прохода `hp_todo` в РАЗНЫЕ страницы на старте уровня —
|
||||||
|
если между ними нет переворота, обе перерисовки лягут в одну.
|
||||||
|
3. Возможное решение: на старте уровня чистить полосу во всю ширину
|
||||||
|
независимо от статус-текста (текст всё равно перерисовывается заново
|
||||||
|
через `pop_status_invalidate`), либо запоминать максимальную ширину
|
||||||
|
прошлой полосы и стирать по ней.
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
| [`impl_diff.md`](impl_diff.md) | **Осознанные расхождения с SDLPoP**: где сделано не дословно и почему. Новое расхождение — записью сюда, а не только комментарием в коде |
|
| [`impl_diff.md`](impl_diff.md) | **Осознанные расхождения с SDLPoP**: где сделано не дословно и почему. Новое расхождение — записью сюда, а не только комментарием в коде |
|
||||||
| [`keys.txt`](keys.txt) | **Целевая раскладка управления**, к которой подгоняем SprPoP |
|
| [`keys.txt`](keys.txt) | **Целевая раскладка управления**, к которой подгоняем SprPoP |
|
||||||
| [`keys_plan.md`](keys_plan.md) | План перевода на неё: что совпадает, что переназначить, каких функций нет вовсе |
|
| [`keys_plan.md`](keys_plan.md) | План перевода на неё: что совпадает, что переназначить, каких функций нет вовсе |
|
||||||
|
| [`../assets/orig/README.md`](../assets/orig/README.md) | **Откуда берутся исходные данные**: `make fetch` (SDLPoP + музыка), зачем нужен и почему необязателен `MSDOS/`, `SND_SRC` |
|
||||||
|
|
||||||
## Производительность
|
## Производительность
|
||||||
|
|
||||||
@@ -41,6 +42,7 @@
|
|||||||
| [`palette_plan.md`](palette_plan.md) | Карта всех 256 слотов палитры + механика fade |
|
| [`palette_plan.md`](palette_plan.md) | Карта всех 256 слотов палитры + механика fade |
|
||||||
| [`status_line_text.md`](status_line_text.md) | Строка HP как статус-строка: инвентаризация ВСЕХ текстов SDLPoP |
|
| [`status_line_text.md`](status_line_text.md) | Строка HP как статус-строка: инвентаризация ВСЕХ текстов SDLPoP |
|
||||||
| [`sound_plan.md`](sound_plan.md) | Звук через CBL: разбор и архитектура |
|
| [`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` |
|
| [`shadow_render.md`](shadow_render.md) | Вид Тени (OR+XOR) — отложено: почему XOR несовместим с прозрачностью `#FF` |
|
||||||
| [`roomnav_skip.md`](roomnav_skip.md) | Комнаты для отладочного телепорта `+`/`−`: какие пропускать и почему |
|
| [`roomnav_skip.md`](roomnav_skip.md) | Комнаты для отладочного телепорта `+`/`−`: какие пропускать и почему |
|
||||||
| [`host_tests_plan.md`](host_tests_plan.md) | Модульные тесты движка под ucsim_z80: два шва, регрессии, дифф против SDLPoP |
|
| [`host_tests_plan.md`](host_tests_plan.md) | Модульные тесты движка под ucsim_z80: два шва, регрессии, дифф против SDLPoP |
|
||||||
|
|||||||
@@ -1009,6 +1009,124 @@ tp/10 у факелов таблицей, пустой слот соперник
|
|||||||
|
|
||||||
## P1 — берётся в любой момент
|
## P1 — берётся в любой момент
|
||||||
|
|
||||||
|
### <a id="time-speed-modes"></a>TIME-SPEED. Часы бегут быстрее в FAST/FASTEST — СДЕЛАНО 2026-08-31, остался вариант 3 (RTC)
|
||||||
|
|
||||||
|
**Наблюдение (пользователь, 2026-08-31):** «при режиме FAST/FASTEST время
|
||||||
|
начинает бежать быстрее — похоже, время мы считаем в наших логических
|
||||||
|
кадрах, и если они отрисовываются чаще, то и время быстрее».
|
||||||
|
|
||||||
|
Так и есть. `pop_timer_tick` (`src/pop_timer.c`) уменьшает счётчик на
|
||||||
|
КАЖДОМ логическом кадре — ровно как оригинал. Но длина логического кадра
|
||||||
|
у нас зависит от режима скорости (`src/pop_pace.h`):
|
||||||
|
|
||||||
|
| режим | обычный кадр | бой |
|
||||||
|
|---|---|---|
|
||||||
|
| NORMAL | 4 кадра луча (81,9 мс) | 5 (102,4 мс) |
|
||||||
|
| FAST | 3 (61,4 мс) | 4 (81,9 мс) |
|
||||||
|
| FASTEST | 3 (61,4 мс) | 3 (61,4 мс) |
|
||||||
|
|
||||||
|
Разная длина кадра в игре и в бою — ПОВЕДЕНИЕ ОРИГИНАЛА (подтверждено
|
||||||
|
пользователем), и часы, идущие в бою медленнее, трогать не нужно.
|
||||||
|
Расхождение только в наших добавочных режимах: NORMAL повторяет оригинал,
|
||||||
|
а FAST/FASTEST ускоряют всё разом, включая ход часов — минута игрового
|
||||||
|
времени проходит примерно на треть быстрее реальной.
|
||||||
|
|
||||||
|
**Варианты.**
|
||||||
|
|
||||||
|
1. Оставить как есть: быстрый режим ускоряет игру целиком, это честно и
|
||||||
|
предсказуемо. Ноль работы и ноль риска.
|
||||||
|
2. Развязать часы от темпа: тикать не по логическому кадру, а по
|
||||||
|
накопленным кадрам ЛУЧА (4 кадра луча = 1 тик). Тогда минута остаётся
|
||||||
|
минутой в любом режиме, а в бою часы по-прежнему замедляются, как в
|
||||||
|
оригинале. Цена — счётчик-накопитель в `pop_timer_tick`.
|
||||||
|
|
||||||
|
3. **Часы от RTC** (идея пользователя, 2026-08-31). Брать время из
|
||||||
|
часов реального времени, а не считать кадры вовсе.
|
||||||
|
|
||||||
|
**Почему третий вариант интереснее, чем кажется.** Погрешность есть уже
|
||||||
|
СЕЙЧАС и без всяких режимов: логический кадр NORMAL — 4 кадра луча, а это
|
||||||
|
81,93 мс, то есть 12,2 кадра в секунду вместо ровных 12. Оригинальная
|
||||||
|
минута из 720 тиков проходит у нас за 58,99 с — почти на секунду быстрее.
|
||||||
|
За час игры набегает около минуты. Ни один из первых двух вариантов этого
|
||||||
|
не лечит: они выравнивают режимы между собой, но обе шкалы остаются
|
||||||
|
привязанными к лучу, а луч не кратен игровой секунде.
|
||||||
|
|
||||||
|
RTC (`ESTEX $21 SYSTIME`, memory `sprinter_systime_dow`) даёт абсолютную
|
||||||
|
шкалу и снимает накопление полностью. Подводные камни, которые надо
|
||||||
|
решить при взятии в работу:
|
||||||
|
|
||||||
|
- вызов ESTEX стоит дорого и клобберит регистры — читать раз в тик, не в
|
||||||
|
кадре, и не из горячего пути (memory `estex_bios_abi`);
|
||||||
|
- разрешение RTC — секунда, а тик игры — 1/12 секунды: нужен гибрид
|
||||||
|
«кадры внутри секунды, синхронизация по RTC на границе», иначе часы
|
||||||
|
задёргаются;
|
||||||
|
- пауза, меню и загрузка НЕ должны съедать игровое время — при часах от
|
||||||
|
RTC это перестаёт получаться само собой и требует явного вычитания;
|
||||||
|
- быстрая загрузка/сохранение обязаны сохранять смещение, а не абсолютное
|
||||||
|
время.
|
||||||
|
|
||||||
|
**Решать пользователю.** Порядок по цене: вариант 1 (ничего), вариант 2
|
||||||
|
(счётчик кадров луча, лечит только разбег режимов), вариант 3 (RTC, лечит
|
||||||
|
и накопление — но требует разобраться с паузами).
|
||||||
|
|
||||||
|
|
||||||
|
### <a id="snd-speaker-38"></a>SND-SPEAKER-38. Звук мигания надписи — СДЕЛАНО и ПРОВЕРЕНО 2026-08-31
|
||||||
|
|
||||||
|
**Постановка (пользователь, 2026-08-31):** «когда надпись Press Button
|
||||||
|
начинает мигать — в SDLPoP воспроизводится звук на каждое моргание, у нас
|
||||||
|
тишина». Решение выбрано там же: «проще синтезировать как PCM и добавить
|
||||||
|
в наш SND атлас».
|
||||||
|
|
||||||
|
**Наш код НЕ виноват и правки не требует.** `pop_dead_prompt`
|
||||||
|
(`src/pop_status.c`) уже зовёт звук 38 на каждом появлении строки — ровно
|
||||||
|
как оригинал. Пусто в НАБОРЕ: в `assets/packed/SND/snd.idx` слот 38 имеет
|
||||||
|
длину 0, играть нечего.
|
||||||
|
|
||||||
|
**Почему его нет.** У оригинала три параллельных набора звука, и номера
|
||||||
|
разложены по ним не подряд:
|
||||||
|
|
||||||
|
| набор | что | номера | у нас |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `DIGISND1..3.DAT` | оцифровка | 0–23, 44–49, 51 | берём, это и есть наш SND |
|
||||||
|
| `MIDISND1..2.DAT` | мелодии | 24–30, 32, 33, 35–37, 39–41, 43, 50, 52–56 | берём отдельно, как музыку в `MUS/` |
|
||||||
|
| `IBM_SND1..2` | ноты PC-спикера | 0–56 (весь диапазон) | НЕ берём вовсе |
|
||||||
|
|
||||||
|
Звук 38 есть ТОЛЬКО в наборе спикера — ни оцифровки, ни мелодии для него
|
||||||
|
не существует, поэтому он и провалился между двумя нашими конвейерами.
|
||||||
|
|
||||||
|
**Полная ревизия недостающего (сделана 2026-08-31).** Номера, которых нет
|
||||||
|
ни в оцифровке, ни среди мелодий: **31, 34, 38, 42**. Из них 31, 34 и 42 —
|
||||||
|
пустые заглушки в один байт, нот внутри нет. **Реально звучит ровно один
|
||||||
|
номер — 38.** То есть задача закрывает единственную дыру в наборе, а не
|
||||||
|
открывает семейство.
|
||||||
|
|
||||||
|
**Формат ресурса** (канон — `docs/PoP/POP-DAT-FormatSpecifications.pdf`,
|
||||||
|
раздел «Internal PC Speaker»): заголовок 3 байта, из них байт 1 — темп в
|
||||||
|
долях на две секунды; далее тройки «частота в герцах (2 байта) + длина в
|
||||||
|
долях (1 байт)», нулевая частота = пауза; в конце маркер `12 00`.
|
||||||
|
|
||||||
|
Звук 38 (`IBM_SND1/res10038.bin`, 17 байт) — нисходящий сигнал из четырёх
|
||||||
|
нот: 2500, 2000, 1500, 1000 Гц по одной доле, темп 72 → около 110 мс.
|
||||||
|
Для сверки разбора: звук 17 (мягкое приземление) — одна нота 49 Гц на три
|
||||||
|
доли.
|
||||||
|
|
||||||
|
**Что делать.**
|
||||||
|
|
||||||
|
1. В `tools/pop_pack_sound.py` — генератор PCM из нот: меандр на нашей
|
||||||
|
частоте вывода (`RATE`), амплитуда умеренная (сигнал короткий и резкий,
|
||||||
|
полный размах будет колоть ухо), пауза = уровень тишины `0x80`.
|
||||||
|
2. Брать из спикера ТОЛЬКО те номера, которых нет ни в оцифровке, ни в
|
||||||
|
мелодиях — сейчас это ровно 38. Правило важнее списка: если брать всё
|
||||||
|
подряд, синтез перекроет собой мелодии, которые мы играем из `MUS/`.
|
||||||
|
3. Дальше всё уже готово: звук ложится в атлас и индекс общим путём,
|
||||||
|
`snd.idx` пересобирается, EXE не меняется (раскладка читается с диска).
|
||||||
|
4. Проверка: `make resources` → слот 38 в индексе получил ненулевую длину;
|
||||||
|
в MAME дождаться мигания надписи после смерти — должен звучать сигнал.
|
||||||
|
|
||||||
|
**Цена.** Один короткий звук в наборе (~2 КБ после выравнивания на блок),
|
||||||
|
кода в игре — ноль.
|
||||||
|
|
||||||
|
|
||||||
### <a id="perf-sweep"></a>PERF-SWEEP. Поиск узких мест по ВСЕЙ игре, а не в одной сцене
|
### <a id="perf-sweep"></a>PERF-SWEEP. Поиск узких мест по ВСЕЙ игре, а не в одной сцене
|
||||||
|
|
||||||
**Постановка (пользователь, 2026-08-18):** «пока мы тестируем на регресс
|
**Постановка (пользователь, 2026-08-18):** «пока мы тестируем на регресс
|
||||||
|
|||||||
@@ -89,3 +89,95 @@ raw-клавиатура (`<kbd_raw.h>`), которую мы и так заби
|
|||||||
после выхода — заметный. Делать после того, как закроем стражей и
|
после выхода — заметный. Делать после того, как закроем стражей и
|
||||||
займёмся бюджетом всерьёз (там же, где батчинг кроссбанковых вызовов и
|
займёмся бюджетом всерьёз (там же, где батчинг кроссбанковых вызовов и
|
||||||
возможный возврат `pop_bg` в резидент `--w3`).
|
возможный возврат `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`),
|
||||||
|
и трогать его ради экономии секунды стоит только на спокойную голову.
|
||||||
|
|
||||||
|
**Выигрыш.** Одна-две секунды один раз на уровень.
|
||||||
|
|
||||||
|
## Музыка одним постоянно открытым архивом (замер 2026-08-31, НЕ сейчас)
|
||||||
|
|
||||||
|
**Идея.** Сейчас каждый трек — отдельный файл `MUS/mNN.bin`, и
|
||||||
|
`pop_music_load_begin` открывает свой на каждый запуск. Свести треки в
|
||||||
|
один файл и держать его `fd` открытым: вместо `chdir`+`open`+`chdir`
|
||||||
|
останется `lseek` к смещению трека.
|
||||||
|
|
||||||
|
**Почему это стоит внимания — разложение окна старта музыки** (полный
|
||||||
|
замер и метод — `sound_plan.md` §9):
|
||||||
|
|
||||||
|
| шаг | цена | уйдёт? |
|
||||||
|
|---|---:|---|
|
||||||
|
| ожидание `pop_music_service` в кадре | 22,6 мс | нет |
|
||||||
|
| `chdir` #1 | 46,1 мс | **да** |
|
||||||
|
| `open` файла трека | 34,5 мс | **да** |
|
||||||
|
| `chdir` #2 | 59,0 мс | **да** |
|
||||||
|
| чтение первых 8 КБ | 34,0 мс | нет |
|
||||||
|
| **итого** | **196,7 мс** | |
|
||||||
|
|
||||||
|
Уходит **139,6 мс** — но это ВЕРХНЯЯ граница: появляется `lseek` к
|
||||||
|
смещению трека, которого сейчас нет вовсе (файл читается последовательно
|
||||||
|
с нуля), и его цена НЕ ИЗМЕРЕНА. Реальный выигрыш = 139,6 минус `lseek`;
|
||||||
|
померить можно тем же способом, `lseek` уже используется в
|
||||||
|
`pop_arc.c:89`. Остаётся ≈ 57 мс, то есть старт трека ускоряется
|
||||||
|
примерно вчетверо.
|
||||||
|
|
||||||
|
**Где заметно.** Реплики PV-сцены идут встык (три трека подряд), плюс
|
||||||
|
каждый игровой джингл — смерть, зелье, подобранный меч.
|
||||||
|
|
||||||
|
**Чем осложнено.**
|
||||||
|
|
||||||
|
1. **`PBA1` под это не годится.** Размер элемента там `uint16` (≤ 64 КБ),
|
||||||
|
а трек — до 1,2 МБ (m56, 78 страниц); резать по страницам нельзя,
|
||||||
|
элементов вышло бы ~240 при `POP_ARC_MAX = 126`. Нужен свой индекс со
|
||||||
|
смещениями — то есть эта задача СМЫКАЕТСЯ с `MUS/MUSIC.IDX` из
|
||||||
|
`music_runtime_index_plan.md`: одно изменение раскладки, а не два.
|
||||||
|
2. **Постоянно занятый файловый манипулятор** — один из восьми (memory
|
||||||
|
`dss_fd_limit`, девятый `open` вешает DSS). Рядом свои открывают
|
||||||
|
`POP.CFG`, quicksave и загрузка уровня.
|
||||||
|
3. **`chdir` убрать нельзя** — он и есть половина выигрыша, но нужен
|
||||||
|
старым DSS (`POP_PATH_CALL`); экономия берётся не его удалением, а тем,
|
||||||
|
что открытие вообще перестаёт выполняться на каждый трек.
|
||||||
|
|
||||||
|
**Оговорка.** Задержка старта музыки сама по себе НЕ является дефектом:
|
||||||
|
трек начинается на границе события, на слух это не сбой. У эффектов
|
||||||
|
такой задержки нет вовсе — они целиком в EMM (см. `sound_plan.md` §9.1).
|
||||||
|
Так что это оптимизация, а не исправление.
|
||||||
|
|||||||
@@ -0,0 +1,592 @@
|
|||||||
|
# Аудио без перелинковки: runtime-индексы музыки и эффектов
|
||||||
|
|
||||||
|
> Статус: **ОБЕ ЧАСТИ СДЕЛАНЫ 2026-08-31** — эффекты (SI0..SI4) и музыка
|
||||||
|
> (MI0..MI5). Разбор и результаты: `sound_plan.md` §10 (эффекты) и §11
|
||||||
|
> (музыка). Ниже — исходный план; расхождения перечислены следом.
|
||||||
|
>
|
||||||
|
> Что в реализации разошлось с планом ниже: файл назван `SND/snd.idx`
|
||||||
|
> (единый basename с `snd.arc`, оба в нижнем регистре), а при отсутствии
|
||||||
|
> индекса поднимается ПУСТОЙ набор с блоком тишины — иначе встаёт музыка
|
||||||
|
> и игра непроходима (§10.5 sound_plan). Про `ticks60` в `PMI1` решено:
|
||||||
|
> поле в формате оставить даже если сценарии перейдут на ожидание звука —
|
||||||
|
> загрузчик волен не тащить его в память.
|
||||||
|
>
|
||||||
|
> Расхождения части I с планом ниже:
|
||||||
|
> * файл назван `MUS/mus.idx` (единый стиль с `snd.idx`);
|
||||||
|
> * пауза конца уровня переведена НЕ на runtime-тики, а на состояние
|
||||||
|
> «заявка/загрузка/звучание» (`pop_music_active`) — §11.2: это снимает и
|
||||||
|
> зависимость от набора, и враньё делителя `/4` в быстрых режимах, и
|
||||||
|
> расхождение с SDLPoP при выключенном звуке;
|
||||||
|
> * PV-сцена (§8 плана) — не структура времён через пять функций, а четыре
|
||||||
|
> якоря-статики (8 байт), от которых отсчитываются прежние выражения;
|
||||||
|
> * `pages` в PMI1 не хранится — считается из `blocks`.
|
||||||
|
|
||||||
|
## 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/snd.idx` + `SND/snd.arc`, без
|
||||||
|
генерации C-заголовка и перелинковки EXE.
|
||||||
|
|
||||||
|
Набор состоит из двух согласованных файлов:
|
||||||
|
|
||||||
|
- `SND/snd.arc` — обычный архив PBA1 с PCM-страницами;
|
||||||
|
- `SND/snd.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/snd.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/snd.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 и файловый дескриптор освобождаются, таблица не
|
||||||
|
используется, эффекты остаются выключенными. Набор без `snd.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/snd.idx` в формате `PSI1`, включая фактическое число
|
||||||
|
страниц;
|
||||||
|
5. собрать обычный `SND/snd.arc` из полученных страниц;
|
||||||
|
6. больше не генерировать `gen/pop_sound_tbl.h`.
|
||||||
|
|
||||||
|
Чтобы число страниц не было захардкожено списком `s0.bin`..`s9.bin` в
|
||||||
|
Makefile, упаковщик звука предпочтительно должен сразу формировать
|
||||||
|
`assets/packed/SND/snd.arc` либо передавать упаковщику архивов динамический
|
||||||
|
список результатов без сохранения устаревшей десятой страницы.
|
||||||
|
|
||||||
|
Стабильные тип и константы переносятся в обычный internal-заголовок. EXE не
|
||||||
|
должен зависеть от результата упаковки звука: цель выбора набора меняет
|
||||||
|
согласованную пару `snd.idx` + `snd.arc`.
|
||||||
|
|
||||||
|
## 20. Этапы реализации эффектов
|
||||||
|
|
||||||
|
### SI0 — формат и host-тест
|
||||||
|
|
||||||
|
- writer/reader отдельного `snd.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` из десяти имён;
|
||||||
|
- сделать `snd.idx` и `snd.arc` согласованными результатами выбора набора;
|
||||||
|
- проверить неизменность SHA/даты `sprpop.exe` при смене набора.
|
||||||
|
|
||||||
|
### SI4 — приёмка
|
||||||
|
|
||||||
|
- проиграть короткий, обычный и переходящий страницу эффекты;
|
||||||
|
- проверить приоритеты и перебивание звуков;
|
||||||
|
- проверить музыку поверх общего CBL после runtime-загрузки эффектов;
|
||||||
|
- отсутствующий IDX и повреждённые magic/count/pages/page/off/len должны
|
||||||
|
безопасно отключать эффекты;
|
||||||
|
- перед каждым MAME-прогоном завершать предыдущий экземпляр и никогда не
|
||||||
|
запускать две копии одновременно.
|
||||||
|
|
||||||
|
## 21. Критерии готовности эффектов
|
||||||
|
|
||||||
|
- один `sprpop.exe` работает с MSDOS- и SDLPoP-наборами;
|
||||||
|
- замена согласованной пары `SND/snd.idx` + `SND/snd.arc` не требует
|
||||||
|
компиляции C и не меняет EXE;
|
||||||
|
- `pop_snd_tbl` больше не генерируется как C-код;
|
||||||
|
- загружается фактическое число страниц в диапазоне 1..16;
|
||||||
|
- таблица не расходует дополнительную страницу EMM и не увеличивает
|
||||||
|
суммарный резидентный CODE+DATA на свои 285 байт;
|
||||||
|
- горячий путь и ISR используют прежние прямые `page/off/len`;
|
||||||
|
- повреждённый индекс не приводит к чтению за пределами EMM-блока;
|
||||||
|
- файловые операции сохраняют совместимость со старыми DSS через
|
||||||
|
восстановление каталога приложения.
|
||||||
@@ -0,0 +1,755 @@
|
|||||||
|
# Аудит расхождений с SDLPoP: Кид, стражи, seqtbl, отрисовка
|
||||||
|
|
||||||
|
> Начат 2026-08-31. КОД НЕ МЕНЯЛСЯ — это только разбор. Задача: найти
|
||||||
|
> места, где наш движок может вести себя иначе, чем оригинал, и оценить
|
||||||
|
> вероятность того, что расхождение реально.
|
||||||
|
|
||||||
|
## Как читать
|
||||||
|
|
||||||
|
Ранги вероятности того, что расхождение ЕСТЬ и проявляется в игре:
|
||||||
|
|
||||||
|
| ранг | смысл |
|
||||||
|
|---|---|
|
||||||
|
| **А** | гарантированное различие: код объективно разный, эффект понятен |
|
||||||
|
| **Б** | весьма вероятное: код разный, эффект вероятен, но не доказан |
|
||||||
|
| **В** | средневероятное: код разный, но эффект может гаситься другим местом |
|
||||||
|
| **Г** | маловероятное: различие есть в форме, эффект скорее отсутствует |
|
||||||
|
| **Д** | почти невероятное: сходство подтверждено, остаётся крайний случай |
|
||||||
|
|
||||||
|
Ссылки вида `seg005:114` — строка в `assets/orig/SDLPoP/src/`. Наши
|
||||||
|
ссылки — `файл:строка` в `src/`.
|
||||||
|
|
||||||
|
## Метод и охват
|
||||||
|
|
||||||
|
Сравниваются НАШИ реализации с оригиналом построчно по функциям. Первый
|
||||||
|
проход (2026-08-31) охватил:
|
||||||
|
|
||||||
|
* диспетчер `control()` (seg005:251) — целиком;
|
||||||
|
* `land()` (seg005:114) и `start_fall()` (seg006:1099);
|
||||||
|
* цепочку смерти: `control_kid` (seg006:1390), `play_kid` (seg006:1348),
|
||||||
|
`take_hp` (seg006:986), `control()` ветка `alive >= 0`.
|
||||||
|
|
||||||
|
НЕ охвачено первым проходом (список для следующих):
|
||||||
|
|
||||||
|
* интерпретатор `play_seq` и полный набор опкодов seqtbl;
|
||||||
|
* `frame_table` и модификаторы кадров;
|
||||||
|
* бой целиком: `control_with_sword`, парирование, `strike`, `hurt_by_sword`;
|
||||||
|
* ИИ стражей (`guard_ai`), особенности скелета, Тени, Джафара;
|
||||||
|
* `add_kid_to_objtable`/`add_guard_to_objtable`, порядок слоёв, `clip_char`;
|
||||||
|
* `check_bumped` — сверен ЧАСТИЧНО (находки 14, 15); `check_grab`,
|
||||||
|
`in_wall`, `check_bumped_look_left` — нет;
|
||||||
|
* `do_fall` целиком (проверен только вход).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Симптом, с которого начат аудит
|
||||||
|
|
||||||
|
**Наблюдение (пользователь, 2026-08-31):** идёт бой, за Кидом провал на
|
||||||
|
этаж. Страж колет на последнем HP, Кид отшатывается назад и падает.
|
||||||
|
Кид умирает — но на экране он этажом ниже В ПРИСЕДЕ, как после мягкого
|
||||||
|
приземления.
|
||||||
|
|
||||||
|
**Что говорит код SDLPoP.** Разбор цепочки:
|
||||||
|
|
||||||
|
1. `start_fall` (seg006:1099) ПЕРВЫМ ДЕЛОМ убирает меч
|
||||||
|
(`Char.sword = sword_0_sheathed`) — для любого падения, независимо от
|
||||||
|
здоровья. Значит к моменту приземления меч уже в ножнах.
|
||||||
|
2. `land` (seg005:114) при падении на один ряд выбирает
|
||||||
|
`seq_63_guard_active_after_fall`, только если `charid >= guard` ИЛИ
|
||||||
|
меч вынут; иначе — `seq_17_soft_land`, то есть ПРИСЕД. Из-за п.1 для
|
||||||
|
Кида это всегда присед.
|
||||||
|
3. `land` НЕ смотрит ни на `alive`, ни на `hitp_curr` вовсе.
|
||||||
|
4. `control` (seg005:251) при мёртвом персонаже (`alive >= 0`) не
|
||||||
|
диспетчеризует ничего; он переводит в `seq_71_dying` ТОЛЬКО из
|
||||||
|
четырёх кадров стойки (15, 166, 158, 171). Присед в этот список не
|
||||||
|
входит.
|
||||||
|
|
||||||
|
**Вывод:** по букве оригинала мёртвый Кид, застигнутый смертью в полёте,
|
||||||
|
тоже долетает, приземляется в присед и остаётся в нём — `control` его не
|
||||||
|
трогает. То есть наблюдаемое, СКОРЕЕ ВСЕГО, воспроизводится и в SDLPoP.
|
||||||
|
|
||||||
|
Гипотеза «hp стал нулевым, поэтому спрятали меч» кодом НЕ подтверждается:
|
||||||
|
единственное место, где SDLPoP связывает `hitp_curr == 0` с чем-либо, —
|
||||||
|
`control_kid` (seg006:1395), и там взводится только `Char.alive = 0`.
|
||||||
|
|
||||||
|
**Как проверить окончательно:** прогнать сцену на живом SDLPoP (он
|
||||||
|
собирается в `assets/orig/SDLPoP/`), где для этого уже есть наши врезки
|
||||||
|
`POP_TRACE`. До проверки считаю симптом НЕ доказанным расхождением —
|
||||||
|
ранг **В** (см. находку 5).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Находки первого прохода
|
||||||
|
|
||||||
|
### 1. `land()`: лишний `determine_col()` — ранг **А**
|
||||||
|
|
||||||
|
*Оригинал:* `land` (seg005:114) заканчивается тремя действиями:
|
||||||
|
`seqtbl_offset_char(seq_id)`, `play_seq()`, `Char.fall_y = 0`. Никакого
|
||||||
|
пересчёта колонки.
|
||||||
|
|
||||||
|
*У нас:* `pop_map.c:715` (и в ветке смерти `:705`) после `play_seq()`
|
||||||
|
дополнительно вызывается `determine_col()`.
|
||||||
|
|
||||||
|
*Чем грозит:* `determine_col` пересчитывает `Char.curr_col` из `Char.x`.
|
||||||
|
Оригинал делает это в другом месте и в другой момент —
|
||||||
|
`load_fram_det_col` (seg006:0144) перед разбором кадра. Лишний пересчёт
|
||||||
|
сразу после приземления может дать другую колонку, если `play_seq`
|
||||||
|
успел сдвинуть `x` первым же `dx`. Колонка — вход для проверок тайла,
|
||||||
|
пик и коллизий.
|
||||||
|
|
||||||
|
*Замечание:* приём применён у нас системно (`pop_map.c` — 8 вызовов), то
|
||||||
|
есть это, вероятно, осознанная адаптация, но в `docs/impl_diff.md` она НЕ
|
||||||
|
записана. Нужно либо обосновать и записать, либо снять.
|
||||||
|
|
||||||
|
### 2. `land()`: обнуляется ещё и `fall_x` — ранг **Б**
|
||||||
|
|
||||||
|
*Оригинал:* в `land` обнуляется ТОЛЬКО `fall_y`, и притом в самом конце,
|
||||||
|
ПОСЛЕ `play_seq()`.
|
||||||
|
|
||||||
|
*У нас:* `pop_map.c:697` и `:713` обнуляют пару `Char.fall_x = Char.fall_y = 0`,
|
||||||
|
причём ДО `pop_char_set_seq`/`play_seq`.
|
||||||
|
|
||||||
|
*Чем грозит:* два отличия сразу. Во-первых, `fall_x` в оригинале
|
||||||
|
переживает приземление — если наш сброс лишний, теряется горизонтальный
|
||||||
|
импульс, влияющий на последующие кадры. Во-вторых, момент: если
|
||||||
|
`play_seq` читает `fall_y` (а он читает при обработке своих опкодов
|
||||||
|
движения), оригинал видит ещё НЕ обнулённое значение, а мы — уже ноль.
|
||||||
|
|
||||||
|
### 3. `land()`: проверка пик до коррекции X — ранг **Б**
|
||||||
|
|
||||||
|
*Оригинал:* сначала (внутри ветки «тайл под ногами не пика») делается
|
||||||
|
коррекция `Char.x = char_dx_forward(-3)` при `distance_to_edge_weight() < 3`,
|
||||||
|
и лишь ПОТОМ проверяется падение на пики, причём условие для пики ПОЗАДИ
|
||||||
|
использует `distance_to_edge_weight() >= 12`.
|
||||||
|
|
||||||
|
*У нас:* `pop_map.c:664` — `fell_on_spikes()` вызывается ПЕРВЫМ, до
|
||||||
|
коррекции X.
|
||||||
|
|
||||||
|
*Чем грозит:* `distance_to_edge_weight()` считается от `Char.x`, а
|
||||||
|
коррекция этот `x` меняет на 3 пикселя. У края тайла порядок решает,
|
||||||
|
попадёт ли персонаж в ветку «пика позади» — то есть умрёт он или нет.
|
||||||
|
|
||||||
|
### 4. Смерть: у нас свой флаг вместо счётчика — ранг **В**
|
||||||
|
|
||||||
|
*Оригинал:* `play_kid` (seg006:1348) после смерти ведёт СЧЁТЧИК
|
||||||
|
`Char.alive`, и по его значениям запускает музыку смерти (`alive == 6`) и
|
||||||
|
надпись «Press Button to Continue» (`alive == 7`), причём переход
|
||||||
|
задерживается, пока играет звук (`check_sound_playing`).
|
||||||
|
|
||||||
|
*У нас:* `pop_ctrl.c` (`ctrl_kid_death`) взводит `pop_kid_dead` — сигнал
|
||||||
|
главному циклу на респавн; счётчика стадий нет.
|
||||||
|
|
||||||
|
*Чем грозит:* момент респавна и порядок «музыка смерти → сообщение →
|
||||||
|
рестарт» могут отличаться, особенно если смерть застала персонажа в
|
||||||
|
длинной анимации (падение). Сюда же относится симптом выше: в оригинале
|
||||||
|
поза сохраняется, пока крутится счётчик.
|
||||||
|
|
||||||
|
### 5. Мёртвый доигрывает приземление — ранг **В**
|
||||||
|
|
||||||
|
Разобрано выше. Код у нас и в оригинале В ЭТОМ МЕСТЕ совпадает, поэтому
|
||||||
|
ранг не выше среднего: расхождение может сидеть не в `land`, а в моменте
|
||||||
|
взведения смерти (находка 4) — тогда оригинал успевает поставить кадр
|
||||||
|
смерти до приземления, а мы нет. Проверяется прогоном на живом SDLPoP.
|
||||||
|
|
||||||
|
### 6. Диспетчер `control()` — ранг **Д**
|
||||||
|
|
||||||
|
Сверен ветка в ветку (seg005:251 против `pop_ctrl.c:618`): совпадают и
|
||||||
|
порядок проверок (`bumped/freefall` → меч → charid → кадры), и границы
|
||||||
|
диапазонов кадров, и обработка мёртвого. Расхождений не видно; остаются
|
||||||
|
только опциональные `#ifdef`-фиксы SDLPoP, которых у нас нет намеренно.
|
||||||
|
|
||||||
|
### 7. `JMP_IF_FEATHER`: у нас эффект только для Кида — ранг **Б**
|
||||||
|
|
||||||
|
*Оригинал:* опкод `SEQ_JMP_IF_FEATHER` (seg006, play_seq) смотрит ТОЛЬКО
|
||||||
|
на глобальный `is_feather_fall`. Кто именно проигрывает последовательность,
|
||||||
|
роли не играет.
|
||||||
|
|
||||||
|
*У нас:* `pop_kid.c:306` добавляет условие `Char.charid != CHARID_0_KID` —
|
||||||
|
для всех, кроме Кида, ветка «пера» не берётся никогда.
|
||||||
|
|
||||||
|
*Чем грозит:* под зельем медленного падения любой НЕ-Кид, попавший в
|
||||||
|
последовательности `stepfloat`/`bumpfloat`, у нас пойдёт по обычной ветке
|
||||||
|
(с уроном), а в оригинале — по парящей. Практически это Тень (charid 1) на
|
||||||
|
уровне 4-6 и скелет; страж в эти seq попадает редко, но попадает через
|
||||||
|
`bumpfloat` при отскоке.
|
||||||
|
|
||||||
|
*Замечание:* отличие ОСОЗНАННОЕ (в комментарии сказано «эффект достаётся
|
||||||
|
только Киду — как и сама физика пера»), но в `docs/impl_diff.md` не
|
||||||
|
записано, хотя правило проекта этого требует.
|
||||||
|
|
||||||
|
### 8. `start_chompers` отложен до конца `play_seq` — ранг **Б**
|
||||||
|
|
||||||
|
*Оригинал:* опкоды `SEQ_UP`/`SEQ_DOWN` меняют ряд и ТУТ ЖЕ зовут
|
||||||
|
`start_chompers()` — то есть внутри цикла интерпретатора, до разбора
|
||||||
|
следующих опкодов.
|
||||||
|
|
||||||
|
*У нас:* `pop_kid.c:318-325` только взводит `chomp_pending`, а сам вызов
|
||||||
|
происходит после выхода из цикла (`:396`). Причина архитектурная и
|
||||||
|
описана в коде: seqtbl читается через окно W0, а `start_chompers` лезет в
|
||||||
|
другое окно.
|
||||||
|
|
||||||
|
*Чем грозит:* два следствия. Во-первых, последовательность с ДВУМЯ
|
||||||
|
сменами ряда (`UP` `UP`, спуск/подъём по лестнице) в оригинале будит
|
||||||
|
чомперов в обоих рядах, у нас — только в конечном. Во-вторых, между
|
||||||
|
`SEQ_UP` и концом цикла успевают отработать `dx`/`dy`/`action`, то есть
|
||||||
|
оригинал будит чомперов с ДРУГИМИ координатами персонажа.
|
||||||
|
|
||||||
|
Симптом «челюсти не заводятся» уже ловился в этом проекте (memory
|
||||||
|
`pop_chomper_needs_trigger`), и это место — кандидат в его причины.
|
||||||
|
|
||||||
|
### 9. Набор опкодов seqtbl — ранг **Д**
|
||||||
|
|
||||||
|
Сверены все пятнадцать кодов (`0xF1`..`0xFF`): совпадают и значения, и
|
||||||
|
семантика, включая проваливание `JMP_IF_FEATHER` в `JMP` и то, что
|
||||||
|
`SEQ_DIE` — пустышка в обоих движках. Отличия только в находках 7 и 8.
|
||||||
|
|
||||||
|
### 10. Отрисовка: шаги те же, но разнесены — ранг **В**
|
||||||
|
|
||||||
|
*Оригинал:* `add_kid_to_objtable` (seg008:1667) и его двойник для стража —
|
||||||
|
это строго упорядоченная цепочка: `loadkid`/`loadshad` →
|
||||||
|
`load_fram_det_col` → `load_frame_to_obj` → `stuck_lower` →
|
||||||
|
`set_char_collision` → `set_objtile_at_char` → `redraw_at_char` →
|
||||||
|
`redraw_at_char2` → `clip_char` → `add_objtable`.
|
||||||
|
|
||||||
|
*У нас:* все звенья присутствуют, но распределены по слоям: `clip_char`,
|
||||||
|
`load_frame_to_obj`, `check_mirror`, брызги — в `pop_cdraw.c`;
|
||||||
|
`redraw_at_char`/`set_objtile_at_char`/`set_char_collision` — в `pop_bg.c`
|
||||||
|
(единый проход на всех Char, см. CLAUDE.md).
|
||||||
|
|
||||||
|
*Чем грозит:* сам по себе перенос не ошибка, но ПОРЯДОК внутри цепочки
|
||||||
|
влияет на результат: `set_char_collision` и `set_objtile_at_char` готовят
|
||||||
|
данные, которыми пользуются `redraw_at_char` и `clip_char`. Если наш
|
||||||
|
общий проход выполняет их для ОБОИХ персонажей до отрисовки, а оригинал —
|
||||||
|
для каждого непосредственно перед его выводом, то при наложении Кида и
|
||||||
|
стража состояние на момент клипа будет разным.
|
||||||
|
|
||||||
|
*Отдельно:* `stuck_lower` найден только в `pop_cdraw.h` — надо убедиться,
|
||||||
|
что он реализован, а не только объявлен. Если его нет, персонаж,
|
||||||
|
застрявший на границе тайла, будет рисоваться на пиксель выше.
|
||||||
|
|
||||||
|
### 11. Порядок вывода Кида и стража — ранг **В**
|
||||||
|
|
||||||
|
*Оригинал:* `draw_people` (seg008:1635) всегда ставит сначала Кида
|
||||||
|
(`draw_kid`), затем стража (`draw_guard`), а КТО ОКАЖЕТСЯ СВЕРХУ решает
|
||||||
|
`add_objtable` — таблица объектов упорядочена по позиции тайла.
|
||||||
|
|
||||||
|
*У нас:* по CLAUDE.md порядок задаёт обход тайлов, «кто позже — тот
|
||||||
|
поверх». Это близко по смыслу, но не тождественно сортировке objtable.
|
||||||
|
|
||||||
|
*Чем грозит:* при наложении персонажей (бой вплотную, страж перед Кидом)
|
||||||
|
верхний может оказаться другим. Проверять сравнением кадров боя вплотную
|
||||||
|
с эталонным SDLPoP (метод — memory `pop_pixel_diff_vs_sdlpop`).
|
||||||
|
|
||||||
|
### 12. `hurt_by_sword`: ветка «сбит с уступа» не портирована — ранг **А**
|
||||||
|
|
||||||
|
*Оригинал:* `hurt_by_sword` (seg002:911) при уколе ВООРУЖЁННОГО персонажа
|
||||||
|
выбирает одну из двух смертей по обстановке ПОЗАДИ:
|
||||||
|
|
||||||
|
* тайл позади не пустой ИЛИ до кромки меньше 4 → `seq_85_stabbed_to_death`
|
||||||
|
(заколот на месте);
|
||||||
|
* иначе → `seq_81_kid_pushed_off_ledge` — отдельная последовательность
|
||||||
|
«убит и сброшен с уступа», которая сама отыгрывает падение замертво.
|
||||||
|
|
||||||
|
*У нас:* `guards.c:1015` — ветки `seq_81` НЕТ вовсе, всегда `seq_85`.
|
||||||
|
Упрощение ЗАДОКУМЕНТИРОВАНО в комментарии (`guards.c:1011`): ей нужны
|
||||||
|
тайловые запросы от `Char`, а `pop_map` умеет их только от `Kid`.
|
||||||
|
|
||||||
|
*Чем грозит:* именно тем, что наблюдал пользователь. Заколотый на краю
|
||||||
|
обрыва Кид в оригинале уходит в собственную анимацию падения с уступа; у
|
||||||
|
нас он получает «смерть на месте», продолжая при этом висеть в воздухе —
|
||||||
|
дальше им распоряжается обычная физика падения, и он приземляется этажом
|
||||||
|
ниже по общим правилам (а с убранным в `start_fall` мечом — в присед,
|
||||||
|
находка 5).
|
||||||
|
|
||||||
|
*Как проверить:* поставить Кида спиной к обрыву с 1 HP и дать стражу
|
||||||
|
уколоть. В оригинале — падение замертво (кадры seq_81), у нас — смерть
|
||||||
|
на месте с последующим отдельным падением.
|
||||||
|
|
||||||
|
### 13. `hurt_by_sword`: прижатие к полу стало безусловным — ранг **А**
|
||||||
|
|
||||||
|
*Оригинал:* `Char.y = y_land[Char.curr_row + 1]` и `Char.fall_y = 0`
|
||||||
|
выполняются ТОЛЬКО в ветке выжившего удара (seg002:962, рядом с
|
||||||
|
`seq_74_hit_by_sword`). Смертельные ветки координату не трогают.
|
||||||
|
|
||||||
|
*У нас:* `guards.c` — те же две строки стоят ПОСЛЕ всего `if/else`, то
|
||||||
|
есть выполняются и при смерти тоже.
|
||||||
|
|
||||||
|
*Чем грозит:* персонажа, убитого в воздухе, мы принудительно ставим на
|
||||||
|
пол текущего ряда и обнуляем накопленную скорость падения. Дальше физика
|
||||||
|
обнаруживает, что пола под ним нет, и запускает падение ЗАНОВО — уже без
|
||||||
|
`fall_y`, то есть с другой высотой и другим исходом приземления. Это
|
||||||
|
вторая половина механизма из находки 12 и вероятная причина того, что
|
||||||
|
мёртвый Кид доезжает до нижнего этажа «своим ходом».
|
||||||
|
|
||||||
|
*Как проверить:* тот же сценарий; в отладчике смотреть `Char.y` и
|
||||||
|
`fall_y` сразу после попадания — оригинал их не меняет.
|
||||||
|
|
||||||
|
### 14. Отскок с мечом: нет `seq_64` — ранг **Б**
|
||||||
|
|
||||||
|
*Оригинал:* при отскоке (`bumped`, seg004:328) живой персонаж с вынутым
|
||||||
|
мечом получает ОДНУ ИЗ ДВУХ последовательностей по направлению толчка:
|
||||||
|
толкнули вперёд — `seq_65_bump_forward_with_sword`, отбросило назад —
|
||||||
|
`seq_64_pushed_back_with_sword`.
|
||||||
|
|
||||||
|
*У нас:* `pop_map.c` знает только `SEQ_65_BUMP_FWD_SWORD` (объявлен на
|
||||||
|
`:103`, используется на `:2231`); константы и ветки `seq_64` нет вовсе.
|
||||||
|
|
||||||
|
*Чем грозит:* персонаж, отброшенный назад с мечом (страж у стены, Кид в
|
||||||
|
тесной комнате), проигрывает не ту анимацию — либо ветку без меча, либо
|
||||||
|
`seq_65`. Кадры разные, а вместе с ними расходятся и смещения `dx` в
|
||||||
|
последовательности, то есть итоговая позиция после отскока.
|
||||||
|
|
||||||
|
*Как проверить:* бой вплотную к стене, толчок в сторону стены и от неё;
|
||||||
|
сверять кадры с эталонным прогоном SDLPoP.
|
||||||
|
|
||||||
|
### 15. `check_bumped_look_right`: гейт по направлению — ранг **В**
|
||||||
|
|
||||||
|
В нашей реализации (`pop_map.c:2148`) стоит ранний выход по
|
||||||
|
`Char.direction` с пометкой «(меча в руке у нас нет)». Пометка означает,
|
||||||
|
что ветка писалась до появления боя, а оригинал в этом месте учитывает и
|
||||||
|
меч, и `push_direction` (находка 14). Область `check_bumped_look_left`
|
||||||
|
не сверялась вовсе — её надо пройти отдельно.
|
||||||
|
|
||||||
|
### 16. `control_with_sword` — ранг **Д**
|
||||||
|
|
||||||
|
Сверен целиком (seg005:964 против `pop_ctrl.c:587`): гейт по `action`,
|
||||||
|
условие «пол под ногами loose ИЛИ страж видит Кида», пороги дистанции
|
||||||
|
(90 и −4), `seq_60_turn_with_sword`, ветка «соперник умер» с
|
||||||
|
`seq_92_put_sword_away`, разделение по `charid`. Совпадает.
|
||||||
|
|
||||||
|
Отдельно отмечу: в оригинале сравнение дистанции сделано ЗНАКОВО-НЕЯВНО
|
||||||
|
(приведением к `word`), из-за чего ветка «соперник за спиной» вообще
|
||||||
|
достижима. У нас то же самое выражено явными знаковыми сравнениями — и
|
||||||
|
диапазоны совпадают, включая «вплотную за спиной» (−4..−1), где обе
|
||||||
|
реализации ведут бой, а не разворачиваются.
|
||||||
|
|
||||||
|
### 17. `parry` — ранг **Д**
|
||||||
|
|
||||||
|
Сверен целиком (seg005:1064 против `pop_ctrl.c:513`): список кадров
|
||||||
|
стойки, порог 32 для не-Кида, обработка кадров соперника (151/152/162,
|
||||||
|
особый случай 153 с отложенным `play_seq`), ветка стража по кадру 152,
|
||||||
|
ветка `frame_167_blocked` с `seq_61`, сброс автоповтора `control_up`.
|
||||||
|
Совпадает вплоть до порядка условий.
|
||||||
|
|
||||||
|
### 18. `check_hurting`: звук «меч в движении» в других условиях — ранг **Б**
|
||||||
|
|
||||||
|
*Оригинал:* звук 11 играется в САМОМ КОНЦЕ `check_hurting` (seg002) и
|
||||||
|
только при трёх условиях сразу: направление персонажа не `none`, его кадр
|
||||||
|
— укол (154), а соперник при этом НЕ парирует и НЕ ранен. Первое условие
|
||||||
|
добавлено в SDLPoP специально против зацикливания звука.
|
||||||
|
|
||||||
|
*У нас:* `guards.c:1068` — звук играется в начале ветки укола,
|
||||||
|
безусловно, ещё до того, как определено попадание.
|
||||||
|
|
||||||
|
*Чем грозит:* лишние срабатывания в двух ситуациях, где оригинал молчит —
|
||||||
|
когда удар парирован и когда он попал. То есть в самой частой части боя
|
||||||
|
звук звучит чаще, чем должен. Плюс отсутствует защита от зацикливания
|
||||||
|
при `direction == none`.
|
||||||
|
|
||||||
|
*Как проверить:* бой с парирующим стражем; считать срабатывания звука 11
|
||||||
|
на серии ударов и сравнить с эталонным прогоном SDLPoP.
|
||||||
|
|
||||||
|
### 19. Остальной бой сверен — ранг **Д**
|
||||||
|
|
||||||
|
Прочитаны целиком и совпадают:
|
||||||
|
|
||||||
|
* `swordfight` (seg005:998) — включая ветку кадра 161, `sword_strike`,
|
||||||
|
побочные эффекты уборки меча (`offguard`, `guard_refrac`,
|
||||||
|
`holding_sword`), разделение `seq_93`/`seq_92`/`seq_87` по `charid` и
|
||||||
|
хвост (`parry` / `forward_with_sword` / `back_with_sword`);
|
||||||
|
* `sword_strike` (seg005:1037) — список кадров, выбор `seq_75`/`seq_58`,
|
||||||
|
`seq_66` после парирования, сброс автоповтора;
|
||||||
|
* `check_sword_hurt` (seg002:971) — включая ПРИОРИТЕТ СТРАЖА при
|
||||||
|
одновременном ранении и сброс `Kid.action` в бег, а также
|
||||||
|
`refractimer` по навыку;
|
||||||
|
* `check_hurting` в основной части — гейты по мечу, ряду и кадрам, пороги
|
||||||
|
дистанции (29), `min_hurt_range` 8/12 по мечу соперника, ветка
|
||||||
|
парирования с `justblocked` и `seq_69`. Единственное расхождение —
|
||||||
|
звук, находка 18.
|
||||||
|
|
||||||
|
### 20. Стражи: «стена впереди» сужена до одного тайла — ранг **Б**
|
||||||
|
|
||||||
|
*Оригинал:* `guard_follows_kid_down` (seg002:811) и соседние ветки ИИ
|
||||||
|
спрашивают `wall_type(tile) != 0`. Эта функция (seg006:1626) считает
|
||||||
|
преградой ПЯТЬ видов тайлов: ворота, верх двери с полом, верх двери,
|
||||||
|
зеркало, чомпер и собственно стену — с разной стороной блокировки.
|
||||||
|
|
||||||
|
*У нас:* `guards.c:683` и `:685` сравнивают тайл напрямую с `TILE_WALL`
|
||||||
|
(тип 20). Ворота, верх двери, зеркало и чомпер преградой не считаются.
|
||||||
|
|
||||||
|
*Причина:* `wall_type` реализована у нас (`pop_map.c:504`, таблица на
|
||||||
|
`:498`), но НЕ экспортирована — в `pop_map.h` её нет, поэтому `guards.c`
|
||||||
|
до неё не дотягивается. То есть это не пробел в портировании логики, а
|
||||||
|
следствие границы модулей.
|
||||||
|
|
||||||
|
*Чем грозит:* страж, преследующий упавшего Кида, у нас шагнёт вперёд там,
|
||||||
|
где оригинал отступает — перед закрытыми воротами, верхом двери,
|
||||||
|
зеркалом и чомпером. Отсюда возможны и проход стража сквозь препятствие,
|
||||||
|
и падение туда, куда оригинал его не пускает.
|
||||||
|
|
||||||
|
*Как проверить:* уровень с воротами (например, 3-й) — заманить стража к
|
||||||
|
закрытым воротам после падения Кида и сравнить, отступает ли он.
|
||||||
|
|
||||||
|
*Замечание:* в `guards.c` таких мест ЧЕТЫРЕ (`:148`, `:683`, `:685`);
|
||||||
|
одно из них (`:148`) уже перечисляет три тайла вручную, то есть
|
||||||
|
расхождение частично компенсировано, но не везде одинаково.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Область: столкновение со стенами и падение внутри стены
|
||||||
|
|
||||||
|
Заведена по наблюдению пользователя (2026-08-31): разбег, прыжок сделан
|
||||||
|
рано, Кид не долетел, врезался в стену и начал падать — но по X он
|
||||||
|
оказался ВНУТРИ стены и падал частично в ней.
|
||||||
|
|
||||||
|
### 21. Фикс «скольжения сквозь стену» — ПОРТИРОВАН 2026-08-31
|
||||||
|
|
||||||
|
> Решением пользователя исправление взято в ТЕКУЩИЙ билд: играбельность
|
||||||
|
> важнее буквальности. Реализация — `glide_through_wall_guard()` в
|
||||||
|
> `pop_map.c`, вызывается из `do_fall` в ветке «ещё летим». Цена: +57
|
||||||
|
> байт в банке 3, резидент и куча не изменились.
|
||||||
|
>
|
||||||
|
> Подтверждено host-тестом: в наборе `t_wall` число заходов в кладку
|
||||||
|
> упало с двух до нуля, остальные 15 наборов остались зелёными.
|
||||||
|
>
|
||||||
|
> Ниже — исходный разбор, по которому принималось решение.
|
||||||
|
|
||||||
|
### 21a. Исходный разбор: было соответствие ванили — ранг **Г**
|
||||||
|
|
||||||
|
*Оригинал:* в `do_fall` (seg005:37) есть блок `FIX_GLIDE_THROUGH_WALL` с
|
||||||
|
собственным комментарием SDLPoP: «Кид падает сквозь стены после разворота
|
||||||
|
в беге, особенно в невесомости». Блок опциональный — то есть в ВАНИЛЬНОЙ
|
||||||
|
игре этот баг ЕСТЬ, а SDLPoP его чинит по желанию. Рядом такие же
|
||||||
|
опциональные `FIX_JUMP_THROUGH_WALL_ABOVE_GATE` и `FIX_DROP_THROUGH_TAPESTRY`.
|
||||||
|
|
||||||
|
*У нас:* ни один из трёх не портирован — мы намеренно повторяем ваниль.
|
||||||
|
|
||||||
|
*Вывод по симптому:* «падение частично в стене» — с большой вероятностью
|
||||||
|
ОРИГИНАЛЬНОЕ поведение PoP, а не наша ошибка. Ранг Г означает: различия
|
||||||
|
с ванилью, скорее всего, нет. Но проверить стоит другое — не ХУЖЕ ли у
|
||||||
|
нас, чем в ванили (см. находки 22 и 15).
|
||||||
|
|
||||||
|
*Как проверить:* повторить сцену на живом SDLPoP с выключенными фиксами
|
||||||
|
(они выключаемы в его настройках) и сравнить глубину захода в стену.
|
||||||
|
|
||||||
|
### 22. `do_fall`: наш гард `curr_row <= 2` — ранг **В**
|
||||||
|
|
||||||
|
*Оригинал:* в `do_fall` ветка «достигли нового ряда» выполняется БЕЗ
|
||||||
|
условия на номер ряда: проверка тайла стены с вызовом выталкивания, затем
|
||||||
|
`land()` либо переход на ряд ниже.
|
||||||
|
|
||||||
|
*У нас:* `pop_map.c:909` — вся ветка обёрнута в `if (Char.curr_row <= 2)`.
|
||||||
|
Причина задокументирована (`:768`): наш `get_tile` за нижней кромкой
|
||||||
|
комнаты отдаёт СТЕНУ как сентинель, тогда как в оригинале там комната
|
||||||
|
снизу, и без гарда выталкивание срабатывало ложно, смещая падение на тайл.
|
||||||
|
|
||||||
|
*Чем грозит:* гард гасит не только ложные срабатывания. Если персонаж
|
||||||
|
достиг `curr_row == 3` легитимно (падение между комнатами по вертикали),
|
||||||
|
у нас не выполнится ни выталкивание из стены, ни `land()`, ни переход
|
||||||
|
ряда — всё это ляжет на следующий кадр и другую ветку. Именно такая
|
||||||
|
комбинация (падение у границы комнаты рядом со стеной) даёт кандидата в
|
||||||
|
причины наблюдения пользователя.
|
||||||
|
|
||||||
|
*Как проверить:* падение вдоль стены точно на стыке комнат по вертикали;
|
||||||
|
в отладчике смотреть `curr_row`, `Char.x` и факт вызова выталкивания.
|
||||||
|
|
||||||
|
### 23. `bumped_fall` — ранг **Д**
|
||||||
|
|
||||||
|
Сверен (seg004 против `pop_map.c`): откат X на 4 пикселя назад, обнуление
|
||||||
|
горизонтальной скорости в свободном падении, иначе `seq_45_bumpfall` с
|
||||||
|
проигрыванием, звук удара. Совпадает; у нас добавлен только флаг «стражи
|
||||||
|
услышали», который в оригинале ставится внутри звуковой функции.
|
||||||
|
|
||||||
|
**Замечание по области:** глубина отката при столкновении — ровно 4
|
||||||
|
пикселя в обоих движках. Если Кид вошёл в стену глубже (а при
|
||||||
|
недолёте с разбега скорость по X велика), одного отката не хватит ни там,
|
||||||
|
ни у нас — и дальше всё зависит от того, сработает ли выталкивание из
|
||||||
|
стены на следующем кадре. У нас его может съесть гард из находки 22.
|
||||||
|
Это главная зацепка по симптому.
|
||||||
|
|
||||||
|
### 24. `in_wall`: не перезагружается кадр — ранг **Б**, ОТЛОЖЕНА
|
||||||
|
|
||||||
|
> **Правка сделана и откачена 2026-08-31.** По букве оригинала находка
|
||||||
|
> верна, но практического эффекта показать не удалось: все 15 наборов
|
||||||
|
> host-тестов дали одинаковый результат до и после, включая специально
|
||||||
|
> написанный тест на заход в кладку (`t_wall`). При этом правка не
|
||||||
|
> бесплатна — добавляет маппинг окна и перезагрузку кадра на каждое
|
||||||
|
> выталкивание из стены. Платить за недоказанное не стали.
|
||||||
|
>
|
||||||
|
> Задача переехала в `docs/vanilla_vs_bugfixed.md`: вернуться к ней при
|
||||||
|
> работе над двумя режимами поведения, где появится сценарий, в котором
|
||||||
|
> кадр меняется перед выталкиванием.
|
||||||
|
|
||||||
|
|
||||||
|
*Оригинал:* `in_wall` (seg006) после выталкивания персонажа из стены
|
||||||
|
делает `load_fram_det_col()` — ЗАГРУЖАЕТ КАДР и следом определяет колонку,
|
||||||
|
затем перечитывает тайл.
|
||||||
|
|
||||||
|
*У нас:* `pop_map.c` (`in_wall`) вызывает только `determine_col()`.
|
||||||
|
Пороги (`>= 8`), формулы смещения (`6 - d` и `d + 4`), условие по тайлу
|
||||||
|
впереди и финальное чтение тайла совпадают — расходится только этот шаг.
|
||||||
|
|
||||||
|
*Чем грозит:* после выталкивания данные кадра (картинка, смещения, флаги
|
||||||
|
— включая «нужен пол» и «чётный пиксель») остаются от позиции ДО
|
||||||
|
коррекции, а ими пользуются проверки того же кадра: падение, клип,
|
||||||
|
коллизия. Это ровно область, где наблюдалось падение внутри стены
|
||||||
|
(находки 21, 22).
|
||||||
|
|
||||||
|
*Как проверить:* недолёт с разбега в стену; в отладчике сравнить `Char.x`,
|
||||||
|
колонку и поля текущего кадра сразу после выталкивания.
|
||||||
|
|
||||||
|
### 25. Таблицы кадров и seqtbl — ранг **Д**
|
||||||
|
|
||||||
|
`frame_table_kid`, `original_seqtbl` и таблица смещений извлекаются
|
||||||
|
АВТОМАТИЧЕСКИ из исходников SDLPoP (`tools/pop_extract_kid_data.py` →
|
||||||
|
`gen/kid_data.h`), поэтому расхождение в данных маловероятно по
|
||||||
|
построению. Применение тоже сверено: используются все четыре флага кадра
|
||||||
|
(«нужен пол» 0x40, вес по X 0x1F, «тонкий» 0x20, чётный пиксель 0x80), а
|
||||||
|
байт клинка маскируется как в оригинале (`& 0x3F`, `pop_kdraw.c:31`).
|
||||||
|
|
||||||
|
Не сверено: старшие два бита байта клинка (номер набора спрайтов) — у
|
||||||
|
Кида он нулевой, у прочих персонажей стоит проверить отдельно.
|
||||||
|
|
||||||
|
### 26. Полнота автоуправления — ранг **Д**
|
||||||
|
|
||||||
|
Из двенадцати функций `autocontrol_*` оригинала у нас есть одиннадцать.
|
||||||
|
Отсутствующая — тривиальная обёртка над общей логикой стража; у нас она
|
||||||
|
встроена в вызывающего. Расхождения нет.
|
||||||
|
|
||||||
|
Не сверены ПОСТРОЧНО тела: `autocontrol_guard_kid_armed`,
|
||||||
|
`autocontrol_guard_kid_far`, `autocontrol_shadow*`, `autocontrol_skeleton`,
|
||||||
|
`check_grab`, `check_bumped_look_left`, `back_with_sword`,
|
||||||
|
`forward_with_sword`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ИТОГ АУДИТА
|
||||||
|
|
||||||
|
Проверено 26 позиций за пять проходов.
|
||||||
|
|
||||||
|
| ранг | находки | суть |
|
||||||
|
|---|---|---|
|
||||||
|
| **А** | 1, 12, 13 | лишний пересчёт колонки в `land`; нет ветки «убит и сброшен с уступа»; безусловное прижатие к полу при смерти |
|
||||||
|
| **Б** | 2, 3, 7, 8, 14, 18, 20, 24 | `fall_x` и момент сброса; порядок «пики / коррекция X»; перо только для Кида; отложенные чомперы; нет `seq_64`; звук удара; «стена» сужена до одного тайла; нет перезагрузки кадра в `in_wall` |
|
||||||
|
| **В** | 4, 5, 10, 11, 15, 22 | флаг смерти вместо счётчика стадий; мёртвый доигрывает приземление; разнесённая цепочка отрисовки; порядок Кид/страж; гейт в `check_bumped_look_right`; гард `curr_row <= 2` в `do_fall` |
|
||||||
|
| **Г** | 21 | опциональные фиксы SDLPoP не портированы — соответствие ванили |
|
||||||
|
| **Д** | 6, 9, 16, 17, 19, 23, 25, 26 | сверено и совпадает |
|
||||||
|
|
||||||
|
### Три узла, вокруг которых группируются расхождения
|
||||||
|
|
||||||
|
1. **Смерть при активной физике** (1, 12, 13, 4, 5). Здесь все находки
|
||||||
|
ранга А. Общая причина: у нас смерть — это флаг, а физика продолжает
|
||||||
|
работать с персонажем как с живым.
|
||||||
|
2. **Границы модулей** (12, 20, 24). `pop_map` не отдаёт наружу то, что
|
||||||
|
нужно `guards.c` и работе с произвольным `Char`: тайловые запросы от
|
||||||
|
`Char`, `wall_type`, загрузку кадра. Ветки упрощались не по логике, а
|
||||||
|
по доступности функций.
|
||||||
|
3. **Момент побочных действий** (2, 8, 18, 24). Делаем то же самое, но
|
||||||
|
раньше или позже оригинала: сброс скорости, побудка чомперов, звук,
|
||||||
|
перезагрузка кадра. По отдельности мелочь, вместе — сдвиг состояния
|
||||||
|
на кадр.
|
||||||
|
|
||||||
|
### Что делать дальше
|
||||||
|
|
||||||
|
1. Проверить находки А и Б в MAME по сценариям из их описаний — начиная с
|
||||||
|
12/13 (смерть на краю) и 24 (выталкивание из стены).
|
||||||
|
2. Те же сцены прогнать на живом SDLPoP: часть наблюдений может оказаться
|
||||||
|
ванильным поведением (как находка 21).
|
||||||
|
3. Подтверждённые осознанные отличия записать в `docs/impl_diff.md` —
|
||||||
|
сейчас там нет ни одного из найденных, хотя правило проекта требует.
|
||||||
|
|
||||||
|
|
||||||
|
Порядок по ожидаемой отдаче:
|
||||||
|
|
||||||
|
1. **Бой** — ПРОЙДЕН. Находки: 12, 13 (ранг А), 18 (Б); совпадают
|
||||||
|
`control_with_sword`, `parry`, `swordfight`, `sword_strike`,
|
||||||
|
`check_sword_hurt`, `check_hurting` (кроме звука). Не сверены мелочи:
|
||||||
|
`back_with_sword`, `forward_with_sword`, `check_skel`.
|
||||||
|
2. **`play_seq` и опкоды** — самая опасная область: ошибка в одном опкоде
|
||||||
|
меняет все последовательности разом.
|
||||||
|
3. **Отрисовка** — `add_kid_to_objtable`/`add_guard_to_objtable`, порядок
|
||||||
|
слоёв, `clip_char`.
|
||||||
|
4. **`do_fall`/`check_bumped`/`check_grab`** — остаток физики.
|
||||||
|
5. **ИИ стражей** и особенности скелета/Тени/Джафара.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Глубокое ревью находок А и Б: можно ли починить и чем платим
|
||||||
|
|
||||||
|
> 2026-08-31. КОД ПО-ПРЕЖНЕМУ НЕ МЕНЯЛСЯ. Здесь только оценка.
|
||||||
|
>
|
||||||
|
> **Главное ограничение (требование пользователя): фикс не должен заметно
|
||||||
|
> замедлять игру.** Поэтому у каждой находки первым делом указана ЧАСТОТА
|
||||||
|
> вызова места, а уже потом сама правка.
|
||||||
|
>
|
||||||
|
> **Оговорка к ограничению:** для КРИТИЧНЫХ фиксов скорость — не вето.
|
||||||
|
> Если такой фикс всерьёз бьёт по производительности, он выносится в
|
||||||
|
> отдельный разбор, где ищется способ получить правильное поведение
|
||||||
|
> дёшево (иной момент вызова, кэш, предвычисление, перенос в холодный
|
||||||
|
> путь). То есть порядок такой: сначала решаем, критично ли поведение, и
|
||||||
|
> только потом — какой ценой его добиться.
|
||||||
|
|
||||||
|
## Снятое препятствие
|
||||||
|
|
||||||
|
Обоснование сразу двух упрощений (`guards.c:1011` — «нужны тайловые
|
||||||
|
запросы ОТ Char, а pop_map умеет только от Kid») **устарело**. Проверено:
|
||||||
|
`get_tile_at_char`, `get_tile_infrontof_char`, `get_tile_behind_char` и
|
||||||
|
`distance_to_edge_weight` в `pop_map.c` УЖЕ работают от `Char`
|
||||||
|
(строки 460, 465, 477, 561). Они лишь не выведены в `pop_map.h`.
|
||||||
|
|
||||||
|
Так же обстоит с данными: `pop_char_set_seq()` ставит любую из 115
|
||||||
|
последовательностей по индексу, то есть `seq_81` и `seq_64` доступны без
|
||||||
|
единого нового байта данных — таблица генерируется из оригинала целиком.
|
||||||
|
|
||||||
|
То есть три находки (12, 14, 20) упираются не в архитектуру, а в четыре
|
||||||
|
строки объявлений.
|
||||||
|
|
||||||
|
## Классификация мест по частоте вызова
|
||||||
|
|
||||||
|
| место | частота | вывод |
|
||||||
|
|---|---|---|
|
||||||
|
| `play_seq` (находки 7, 8) | КАЖДЫЙ кадр каждого персонажа | правка обязана быть бесплатной |
|
||||||
|
| `check_hurting` (18) | каждый кадр боя, дважды | почти горячий |
|
||||||
|
| ИИ стража (20) | каждый кадр, пока страж активен | почти горячий |
|
||||||
|
| `land`, `in_wall`, `bumped` (1, 2, 3, 14, 24) | событие раз в несколько секунд | холодный, цена не важна |
|
||||||
|
| `hurt_by_sword` (12, 13) | момент попадания | холодный |
|
||||||
|
|
||||||
|
## Разбор по находкам
|
||||||
|
|
||||||
|
### 12 + 13 (ранг А) — смерть на краю. ТОЛЬКО ВМЕСТЕ, НЕ ПООТДЕЛЬНОСТИ
|
||||||
|
|
||||||
|
> **Проверено на живой машине 2026-08-31 и провалилось.** Правка 13 была
|
||||||
|
> сделана в изоляции (прижатие к полу перенесено в ветки пережитого
|
||||||
|
> удара) — и сломала смерть: страж бьёт Кида, тот погибает, а вместо
|
||||||
|
> нормальной смерти идут вспышка, стопкадр и немедленный выход в
|
||||||
|
> заставку.
|
||||||
|
>
|
||||||
|
> Причина: у нас прижатие к полу работало КОМПЕНСАЦИЕЙ отсутствующей
|
||||||
|
> ветки «убит и сброшен с уступа» (находка 12). Убрав компенсацию и не
|
||||||
|
> добавив то, что она компенсировала, мы оставляем мёртвого персонажа с
|
||||||
|
> ненулевой `fall_y` и незакреплённой `Char.y` — физика продолжает вести
|
||||||
|
> его вниз, он проваливается за пределы уровня, и срабатывает аварийный
|
||||||
|
> путь.
|
||||||
|
>
|
||||||
|
> Вывод: обе находки — ОДНА правка. Оценка «чистое перемещение строк,
|
||||||
|
> риск низкий» была неверной; риск ВЫСОКИЙ, пока ветка 12 отсутствует.
|
||||||
|
> Порядок внутри правки: сперва добавить ветку `seq_81` (с экспортом
|
||||||
|
> тайловых запросов), убедиться, что смерть на краю отыгрывается ею, и
|
||||||
|
> только затем убирать безусловное прижатие.
|
||||||
|
|
||||||
|
|
||||||
|
*Место:* `guards.c`, `hurt_by_sword` — холодный путь.
|
||||||
|
|
||||||
|
*Правка:* (а) перенести две строки прижатия к полу внутрь ветки
|
||||||
|
выжившего удара — это чистое перемещение, минус ноль байт; (б) добавить
|
||||||
|
ветку выбора `seq_81` по тайлу позади и расстоянию до кромки.
|
||||||
|
|
||||||
|
*Что нужно:* экспорт `get_tile_behind_char()` и `distance_to_edge_weight()`
|
||||||
|
из `pop_map.c` в `pop_map.h` как `__banked`.
|
||||||
|
|
||||||
|
*Цена скорости:* два межбанковых вызова (`guards.c` — банк 1, `pop_map.c`
|
||||||
|
— банк 3) в момент попадания мечом, то есть несколько раз за бой.
|
||||||
|
Незаметно.
|
||||||
|
|
||||||
|
*Цена памяти:* банк 1 занят на 19,8 % (13 142 Б свободно) — места вдоволь;
|
||||||
|
банк 3 занят на 81,1 % (3 100 Б), но там прибавятся только две обёртки.
|
||||||
|
|
||||||
|
*Риск:* низкий. Ветка симметрична существующей, данные есть.
|
||||||
|
|
||||||
|
### 1 (ранг А) — лишний `determine_col()` в `land`
|
||||||
|
|
||||||
|
*Место:* холодный путь. *Правка:* убрать вызов и проверить, не
|
||||||
|
понадобился ли он нам вместо оригинального `load_fram_det_col`, который
|
||||||
|
оригинал делает в другом месте цепочки. *Цена:* отрицательная (кода
|
||||||
|
меньше). *Риск:* СРЕДНИЙ — вызов мог компенсировать наш иной порядок
|
||||||
|
загрузки кадра; убирать только с прогоном сцен падения и приземления.
|
||||||
|
|
||||||
|
### 2, 3 (ранг Б) — `land`: `fall_x` и порядок проверки пик
|
||||||
|
|
||||||
|
*Место:* холодный. *Правка 2:* сбрасывать только `fall_y` и после
|
||||||
|
`play_seq`, как оригинал. *Правка 3:* перенести проверку пик после
|
||||||
|
коррекции X. *Цена:* нулевая, это перестановка строк. *Риск:* низкий,
|
||||||
|
но обе меняют поведение на краю тайла — нужны прогоны с пиками.
|
||||||
|
|
||||||
|
### 24 (ранг Б) — `in_wall` не перезагружает кадр. Одна строка
|
||||||
|
|
||||||
|
*Место:* холодный. *Правка:* заменить `determine_col()` на
|
||||||
|
`pop_load_fram_det_col()` — он УЖЕ экспортирован (`pop_kid.h:109`) и, что
|
||||||
|
важно, НЕ банковый, то есть вызов прямой. *Цена скорости:* одна
|
||||||
|
перезагрузка кадра при выталкивании из стены — доли процента кадра.
|
||||||
|
*Риск:* низкий; это возврат к оригиналу.
|
||||||
|
|
||||||
|
### 14 (ранг Б) — нет `seq_64`
|
||||||
|
|
||||||
|
*Место:* `bumped`, холодный. *Правка:* добавить выбор между 64 и 65 по
|
||||||
|
направлению толчка. *Цена:* нулевая. *Риск:* низкий.
|
||||||
|
|
||||||
|
### 18 (ранг Б) — звук удара
|
||||||
|
|
||||||
|
*Место:* `check_hurting` — дважды за кадр боя. *Правка:* перенести звук
|
||||||
|
в конец функции и обвесить тремя условиями оригинала. *Цена:*
|
||||||
|
ОТРИЦАТЕЛЬНАЯ — звук перестанет играть в двух случаях из трёх, то есть
|
||||||
|
уменьшится и число обращений к звуковой очереди. *Риск:* низкий.
|
||||||
|
|
||||||
|
### 20 (ранг Б) — «стена» у стражей. Требует осторожности со скоростью
|
||||||
|
|
||||||
|
*Место:* ИИ стража — вызывается каждый кадр, пока страж активен.
|
||||||
|
|
||||||
|
*Плохой вариант:* экспортировать `wall_type` из `pop_map.c` и звать из
|
||||||
|
`guards.c`. Это МЕЖБАНКОВЫЙ вызов (банк 1 → банк 3) в почти горячем
|
||||||
|
пути — трамплин с переключением W3 на каждый шаг ИИ. Против требования
|
||||||
|
по скорости.
|
||||||
|
|
||||||
|
*Хороший вариант:* завести копию таблицы `wall_type_tbl` (32 байта) в
|
||||||
|
rodata банка 1 и обращаться к ней напрямую — стоимость чтения байта,
|
||||||
|
ноль переключений банка. Дублирование данных здесь оправдано: таблица
|
||||||
|
константная и вшита в формат уровней.
|
||||||
|
|
||||||
|
*Риск:* низкий, но нужно следить, чтобы копия не разошлась с оригиналом —
|
||||||
|
лучше генерировать обе из одного места или снабдить перекрёстным
|
||||||
|
комментарием.
|
||||||
|
|
||||||
|
### 7 (ранг Б) — перо только для Кида. Правка ускоряет
|
||||||
|
|
||||||
|
*Место:* `play_seq`, самый горячий путь. *Правка:* убрать лишнее условие
|
||||||
|
по `charid`. *Цена:* ОТРИЦАТЕЛЬНАЯ — из горячего цикла уходит сравнение.
|
||||||
|
*Риск:* средний: надо убедиться, что физика пера у нас применяется к
|
||||||
|
любому персонажу так же, как в оригинале, иначе анимация разойдётся с
|
||||||
|
физикой.
|
||||||
|
|
||||||
|
### 8 (ранг Б) — отложенные чомперы. Чинить ДЕШЁВЫМ способом
|
||||||
|
|
||||||
|
*Место:* `play_seq`, горячий путь.
|
||||||
|
|
||||||
|
*Почему отложено:* `play_seq` маппит страницу байткода в окно W0 ОДИН раз
|
||||||
|
перед циклом (`pop_kid.c:284`) и снимает после (`:389`). Вызвать
|
||||||
|
`start_chompers` внутри цикла — значит снять окно, позвать, вернуть окно,
|
||||||
|
и так на каждый переход ряда. Это прямая деградация горячего пути и
|
||||||
|
против требования по скорости.
|
||||||
|
|
||||||
|
*Дешёвая замена:* сейчас копится ОДИН флаг, из-за чего теряются
|
||||||
|
промежуточные ряды. Достаточно копить не флаг, а НОМЕРА рядов — один
|
||||||
|
байт-битовую маску (рядов всего 0..3) плюс запомненную колонку. После
|
||||||
|
цикла пройти по взведённым битам и разбудить чомперов в каждом. Цена в
|
||||||
|
цикле: одна операция «выставить бит» вместо присваивания флага, то есть
|
||||||
|
ноль. Разница с оригиналом останется только в МОМЕНТЕ побудки (после
|
||||||
|
цикла, а не внутри), но ряды перестанут теряться.
|
||||||
|
|
||||||
|
*Риск:* низкий. Полное совпадение с оригиналом здесь недостижимо без
|
||||||
|
потери скорости — это осознанный компромисс, который надо записать в
|
||||||
|
`docs/impl_diff.md`.
|
||||||
|
|
||||||
|
## Сводка: что делать в каком порядке
|
||||||
|
|
||||||
|
| приоритет | находки | почему |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | 24 | одна строка, риск низкий, готовая экспортированная функция |
|
||||||
|
| 2 | 12+13 ВМЕСТЕ | порознь ломают смерть (проверено); нужен экспорт двух функций |
|
||||||
|
| 3 | 18, 8 | обе УСКОРЯЮТ или бесплатны; 8 — по дешёвому варианту |
|
||||||
|
| 4 | 2, 3, 20 | перестановки и копия таблицы; нужны прогоны |
|
||||||
|
| 5 | 1, 7 | риск средний: обе могут компенсировать наши отличия в другом месте |
|
||||||
|
|
||||||
|
**Ни один фикс не требует переделки архитектуры и ни один не ложится на
|
||||||
|
горячий путь с накладными расходами** — при условии, что находка 20
|
||||||
|
делается копией таблицы, а находка 8 — битовой маской рядов.
|
||||||
@@ -163,10 +163,16 @@ B имеет смысл только если C окажется неподъё
|
|||||||
1.3/1.4) и распакованные ассеты `SDLPoP/data/` (версия 1.0/1.1). Разница
|
1.3/1.4) и распакованные ассеты `SDLPoP/data/` (версия 1.0/1.1). Разница
|
||||||
есть, и она влияет на выбор источника.
|
есть, и она влияет на выбор источника.
|
||||||
|
|
||||||
### Оцифровка: берём MSDOS
|
### Оцифровка: по умолчанию SDLPoP, MSDOS — опция
|
||||||
|
|
||||||
Заголовок разный (`digi_new_type` против `digi_type`), но **28 звуков из
|
**С 2026-08-31 умолчание — SDLPoP** (`SND_SRC=sdlpop`): он выкачивается
|
||||||
31 совпадают побайтно**. Различаются три, и все не в пользу SDLPoP:
|
автоматически (`make fetch`), а оригинального дистрибутива DOS у сборки
|
||||||
|
может не быть вовсе — и она обязана работать без него. У кого дистрибутив
|
||||||
|
есть, включает лучший набор явно: `make SND_SRC=msdos`.
|
||||||
|
|
||||||
|
Платим за это ровно тем, что разобрано ниже. Заголовок в поставках разный
|
||||||
|
(`digi_new_type` против `digi_type`), но **28 звуков из 31 совпадают
|
||||||
|
побайтно**. Различаются три, и все не в пользу SDLPoP:
|
||||||
|
|
||||||
| id | звук | MSDOS | SDLPoP |
|
| id | звук | MSDOS | SDLPoP |
|
||||||
|---|---|---:|---:|
|
|---|---|---:|---:|
|
||||||
@@ -174,9 +180,13 @@ B имеет смысл только если C окажется неподъё
|
|||||||
| 11 | `sword_moving` | 1 172 | 1 172, но **другие байты** |
|
| 11 | `sword_moving` | 1 172 | 1 172, но **другие байты** |
|
||||||
| 48 | `spiked` | 5 069 | **7** — то есть звука нет |
|
| 48 | `spiked` | 5 069 | **7** — то есть звука нет |
|
||||||
|
|
||||||
`spiked` в наборе SDLPoP фактически пустой. Поэтому упаковщик читает
|
`spiked` в наборе SDLPoP фактически пустой: это звук насаживания на пики
|
||||||
`MSDOS/digisnd*.dat`, а не распакованные ассеты — в отличие от графики,
|
(`spiked()`, seg005.c:245, кадр 177), и с умолчанием он не звучит — от пик
|
||||||
где источник наоборот SDLPoP.
|
остаётся только шипение выдвижения (49 `spikes`). Звон клинков (10) на
|
||||||
|
секунду короче, а 11 просто другая запись.
|
||||||
|
|
||||||
|
Всё остальное — включая уровни `LEVELS/res20xx.bin` — берётся из SDLPoP
|
||||||
|
всегда: `MSDOS/` нужен ровно для этих трёх звуков и ни для чего больше.
|
||||||
|
|
||||||
### MIDI: если дойдём до музыки — брать SDLPoP
|
### MIDI: если дойдём до музыки — брать SDLPoP
|
||||||
|
|
||||||
@@ -961,3 +971,316 @@ load_intro у оригинала и так начинается с ожидан
|
|||||||
Проверено в MAME на сборке `LEVEL=14`: после встречи с принцессой звучит
|
Проверено в MAME на сборке `LEVEL=14`: после встречи с принцессой звучит
|
||||||
тема победы (`pop_mus_id` = 56, `pop_mus_ring` = 6), курсор уходит далеко
|
тема победы (`pop_mus_id` = 56, `pop_mus_ring` = 6), курсор уходит далеко
|
||||||
за размер кольца — то есть подкачка успевает.
|
за размер кольца — то есть подкачка успевает.
|
||||||
|
|
||||||
|
## 9. ЗАДЕРЖКА СТАРТА ЗВУКА: ЗАМЕР (2026-08-31)
|
||||||
|
|
||||||
|
Замерено в MAME на живой сборке: точка отсчёта — подстановка заявки
|
||||||
|
отладчиком при ОСТАНОВЛЕННОЙ машине, конец — условный watchpoint
|
||||||
|
`wpset 19FF6,1,w,{wpdata != 0}` на старший байт `pop_mus_left`, то есть
|
||||||
|
момент, когда трек реально зазвучал. Масштаб тактов откалиброван не по
|
||||||
|
памяти, а по аппаратным часам: период тика насоса CBL — ровно
|
||||||
|
`128/10937,5 = 11,7029` мс, измерено 245 748 и 245 778 тактов, отсюда
|
||||||
|
**21,00 МГц** (кадр луча — 429 973 такта, развёртка 48,8 Гц, не 50).
|
||||||
|
|
||||||
|
### 9.1 Эффекты стартуют без задержки, музыка — через ~200 мс
|
||||||
|
|
||||||
|
Разница принципиальная и вытекает из устройства:
|
||||||
|
|
||||||
|
* **Оцифрованные эффекты диска не касаются вовсе.** `pop_sfx_init()`
|
||||||
|
вызывается ОДИН раз за запуск (`sprpop_cold.c`), вычитывает весь
|
||||||
|
`SND/snd.arc` в EMM и закрывает файл. Дальше `pop_sfx_play` только
|
||||||
|
номинирует, а `pop_sfx_tick` берёт `page/off/len` из таблицы в памяти.
|
||||||
|
Задержка = остаток текущего кадра + ближайший запрос CBL (≤ 11,7 мс).
|
||||||
|
Держать архив открытым НЕЧЕГО: после старта он не нужен.
|
||||||
|
* **Музыка читается с диска на каждый трек** — `mNN.bin` лежат
|
||||||
|
отдельными файлами, и `pop_music_load_begin` открывает свой.
|
||||||
|
|
||||||
|
### 9.2 Разложение окна музыки
|
||||||
|
|
||||||
|
От заявки (`pop_mus_req`) до звучания (`pop_mus_left != 0`), одна
|
||||||
|
загрузка по шагам:
|
||||||
|
|
||||||
|
| шаг | цена |
|
||||||
|
|---|---:|
|
||||||
|
| ожидание `pop_music_service` в кадре | 22,6 мс |
|
||||||
|
| `chdir` #1 (вход в `POP_PATH_CALL`) | **46,1 мс** |
|
||||||
|
| `open` файла трека | 34,5 мс |
|
||||||
|
| `chdir` #2 (восстановление каталога) | **59,0 мс** |
|
||||||
|
| чтение первых 8 КБ (`bank_read_page`) | 34,0 мс |
|
||||||
|
| **итого** | **196,7 мс** |
|
||||||
|
|
||||||
|
**Главное здесь: два `chdir` стоят 105 мс — больше половины окна, и
|
||||||
|
каждый дороже самого `open`.** Убрать их нельзя: старые DSS оставляют
|
||||||
|
рабочий каталог внутри последнего подкаталога, и без восстановления
|
||||||
|
ломаются относительные пути (ради этого `POP_PATH_CALL` и заведён).
|
||||||
|
Теоретический запас — снять ОДИН из двух (≈ 50 мс), но это риск для
|
||||||
|
других мест, которые полагаются на восстановленный каталог.
|
||||||
|
|
||||||
|
Для музыки задержка непринципиальна: трек и так начинается на границе
|
||||||
|
события. Критична она была бы для эффектов — там её нет по построению.
|
||||||
|
|
||||||
|
### 9.3 Разброс и режимы скорости
|
||||||
|
|
||||||
|
Семь прогонов: **189,3 … 232,8 мс**, среднее 200,1. Величина
|
||||||
|
ФИЗИЧЕСКАЯ и от режима скорости не зависит, меняется только её пересчёт
|
||||||
|
в логические кадры:
|
||||||
|
|
||||||
|
| режим | логический кадр (замерено) | окно в кадрах |
|
||||||
|
|---|---:|---:|
|
||||||
|
| NORMAL вне боя | 81,93 мс = 4,00 кадра луча | 2,31 … 2,84 |
|
||||||
|
| FAST / FASTEST вне боя | 61,4 мс = 3,00 кадра луча | 3,09 … 3,42 |
|
||||||
|
|
||||||
|
Худший случай (232,8 мс в коротком кадре) — 3,79 логического кадра.
|
||||||
|
Пейсер при этом работает ровно как задумано: `pop_pace_n` заказывает 4
|
||||||
|
кадра луча вне боя при NORMAL и 3 при FAST/FASTEST, замер это
|
||||||
|
подтвердил. Подкачка трека кадр НЕ просаживает.
|
||||||
|
|
||||||
|
### 9.4 Следствие для паузы конца уровня
|
||||||
|
|
||||||
|
Ждать окончания мелодии по одному `pop_music_busy()` НЕЛЬЗЯ: в этом окне
|
||||||
|
он ещё ложен, и уровень сменился бы до первой ноты. Правильный признак
|
||||||
|
— состояние «аудио-событие в работе», которое выводится из уже
|
||||||
|
существующих переменных без новых сущностей:
|
||||||
|
|
||||||
|
```c
|
||||||
|
uint8_t pop_music_active(void) __banked /* банк 9 — там ld_busy */
|
||||||
|
{
|
||||||
|
return (uint8_t)(pop_mus_req || ld_busy || pop_mus_left);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Переход «запрошен → грузится → звучит» снаружи непрерывен: `service`
|
||||||
|
обнуляет `pop_mus_req` и тут же в `load_begin` взводит `ld_busy`, а
|
||||||
|
главный цикл между этими операторами не выполняется.
|
||||||
|
|
||||||
|
Это лучше фиксированной задержки «на раскачку»: не нужна подобранная
|
||||||
|
константа, нет зависимости от режима скорости, а при провале загрузки
|
||||||
|
(нет файла, нет памяти) все три признака ложны и уровень меняется сразу,
|
||||||
|
вместо слепого ожидания. Заодно чинится расхождение с SDLPoP при
|
||||||
|
ВЫКЛЮЧЕННОМ звуке: `pop_sfx_play` возвращается рано при `!pop_snd_ok`,
|
||||||
|
заявка не кладётся, паузы нет — как в оригинале (`seg006:651` ставит
|
||||||
|
трек только `if (is_sound_on)`, а `seg003:387` ждёт
|
||||||
|
`check_sound_playing()`). Сейчас `pop_endmus_left` взводится
|
||||||
|
безусловно, и при выключенном звуке игра держит пройденный уровень
|
||||||
|
лишние 12 с молча.
|
||||||
|
|
||||||
|
## 10. ЭФФЕКТЫ БЕЗ ПЕРЕЛИНКОВКИ: РАСКЛАДКА УЕХАЛА НА ДИСК (2026-08-31)
|
||||||
|
|
||||||
|
Сделана часть II плана `music_runtime_index_plan.md` (этапы SI0..SI4).
|
||||||
|
Раскладка звукового набора больше не компилируется в EXE: пара
|
||||||
|
`SND/snd.arc` + `SND/snd.idx` меняется целиком, бинарник остаётся прежним.
|
||||||
|
|
||||||
|
### 10.1 Что именно изменилось
|
||||||
|
|
||||||
|
`gen/pop_sound_tbl.h` УДАЛЁН — упаковщик его не печатает. Вместо него:
|
||||||
|
|
||||||
|
* `src/pop_snd_tbl.h` — рукописный: тип записи и инварианты программы
|
||||||
|
(`POP_SND_COUNT` 57, `POP_SND_BLOCK` 128, `POP_SND_MAX_PAGES` 16,
|
||||||
|
константы блока тишины). От набора не зависит НИЧЕГО из этого;
|
||||||
|
* `src/pop_snd_data.c` — резидентные данные: `pop_snd_tbl[57]`,
|
||||||
|
`pop_snd_page[16]`, `pop_snd_pages`;
|
||||||
|
* `SND/snd.idx` — дисковый индекс, формат `PSI1` (писатель и разборщик —
|
||||||
|
`tools/pop_idx.py`, покрыт `make test-tools`, 22 теста);
|
||||||
|
* `pop_sfx_cold.c` — чтение и полная валидация индекса.
|
||||||
|
|
||||||
|
`POP_SND_PAGES` как compile-time размер набора БОЛЬШЕ НЕ СУЩЕСТВУЕТ. Его
|
||||||
|
заменили две разные величины: `POP_SND_MAX_PAGES` — вместимость
|
||||||
|
загрузчика (16, запас под чужие наборы), и runtime `pop_snd_pages` —
|
||||||
|
сколько страниц у ЭТОГО набора. Насос сверяет переход длинного эффекта
|
||||||
|
через границу страницы со вторым, а не с первым.
|
||||||
|
|
||||||
|
### 10.2 Формат `PSI1` и почему запись ровно 5 байт
|
||||||
|
|
||||||
|
Один сектор DSS: 16 байт заголовка (магия, число записей, размер записи,
|
||||||
|
log2 блока, формат PCM, число страниц) и 57 записей по 5 байт —
|
||||||
|
`page` (u8), `off` (u16 LE), `len` (u16 LE), — то есть побайтовый образ
|
||||||
|
`pop_snd_tbl[]`. Записи читаются ОДНИМ `read` прямо в резидентную
|
||||||
|
таблицу, без разбора по полям.
|
||||||
|
|
||||||
|
Поэтому `sizeof(pop_snd_ent_t) == 5` в ABI SDCC z80 — часть ДИСКОВОГО
|
||||||
|
контракта, а не деталь компилятора. Ловится с двух сторон: статически
|
||||||
|
(`pop_snd_data.c`) и полем размера записи в заголовке, которое загрузчик
|
||||||
|
сверяет со своим `sizeof`.
|
||||||
|
|
||||||
|
### 10.3 Цена в памяти
|
||||||
|
|
||||||
|
Таблица была `static const` в `_CODE`, стала изменяемой в `_DATA`.
|
||||||
|
Замер до/после:
|
||||||
|
|
||||||
|
| | было | стало | Δ |
|
||||||
|
|---|---:|---:|---:|
|
||||||
|
| `_CODE` | 23865 | 23582 | −283 |
|
||||||
|
| данные | 7128 | 7421 | +293 |
|
||||||
|
| куча резидента | 239 Б | 229 Б | −10 |
|
||||||
|
| банк 8 | 14824 | 15425 | +601 |
|
||||||
|
|
||||||
|
Суммарный резидентный расход почти не изменился — в huge-модели `_CODE`
|
||||||
|
и данные лежат подряд в одном диапазоне `0x4000..0xBFFF`. Реальные
|
||||||
|
10 байт — это расширение `pop_snd_page` с размера набора до
|
||||||
|
`POP_SND_MAX_PAGES` плюс байт счётчика.
|
||||||
|
|
||||||
|
### 10.4 Валидация: чему именно нельзя доверять
|
||||||
|
|
||||||
|
Индекс — файл на диске, и битый индекс увёл бы насос за пределы
|
||||||
|
выделенного блока EMM. Проверяются заголовок целиком и КАЖДАЯ запись:
|
||||||
|
`page < pop_snd_pages`, `off` внутри страницы, кратность `off`/`len`
|
||||||
|
блоку 128, непересечение с блоком тишины и то, что длинный эффект не
|
||||||
|
выходит за последнюю страницу набора.
|
||||||
|
|
||||||
|
Последняя проверка считается В БЛОКАХ, а не в байтах: байтовый адрес
|
||||||
|
конца (`page * 16384 + off + len`) не влезает в `uint16`, а 32-битная
|
||||||
|
арифметика на Z80 дорога. В блоках весь набор — не больше
|
||||||
|
`16 * 128 = 2048`, и всё считается шестнадцатью битами.
|
||||||
|
|
||||||
|
Число страниц из индекса дополнительно сверяется с числом элементов в
|
||||||
|
`snd.arc`: расходящаяся пара означает, что файлы из разных наборов.
|
||||||
|
|
||||||
|
### 10.5 Нет индекса — эффектов нет, но МУЗЫКА ОСТАЁТСЯ
|
||||||
|
|
||||||
|
Первая версия просто возвращала ошибку, и это оказалось непроходимой
|
||||||
|
игрой: тишину в паузах насос льёт из первого блока набора, поэтому без
|
||||||
|
набора CBL не открывался вовсе — а вместе с ним вставала музыка, чьи
|
||||||
|
блоки отсчитывает тот же насос. Заставка ждала конца трека, который не
|
||||||
|
начинал играть, и `pop_music_busy()` не гас никогда.
|
||||||
|
|
||||||
|
Теперь при отказе индекса поднимается ПУСТОЙ набор: одна страница EMM с
|
||||||
|
блоком тишины. Все `len` нулевые, поэтому `pop_sfx_play` уходит по ветке
|
||||||
|
«нет оцифровки» — эффекты молчат, музыка играет, игра проходима.
|
||||||
|
|
||||||
|
Две тонкости этой заливки, обе выяснены на живой машине:
|
||||||
|
|
||||||
|
1. **Заливается только первый блок (128 байт), а не вся страница.**
|
||||||
|
`gfx_w0_page_prepare` ставит в страницу IRQ/NMI-стабы, чтобы
|
||||||
|
прерывание при замапленном W0 нашло валидный код. Заливка всей
|
||||||
|
страницы затирает их, и первое же прерывание уводит процессор в
|
||||||
|
мусор — чёрный экран на старте. Насосу больше и не нужно: он берёт
|
||||||
|
ровно блок тишины, да и то через W3.
|
||||||
|
2. **Запись под `IRQ_DISABLE`** — 128 байт накрывают стаб RST38, и до
|
||||||
|
`gfx_w0_unmap` прерываний быть не должно.
|
||||||
|
|
||||||
|
### 10.6 Грабли сборки: секундная гранулярность mtime
|
||||||
|
|
||||||
|
Смена `SND_SRC` тихо давала НЕВЕРНЫЙ результат: `sdlpop -> msdos ->
|
||||||
|
sdlpop` перепаковывал `build/atl`, а в `assets/packed` оставался прежний
|
||||||
|
набор. Причина не в логике, а во времени: упаковка укладывается в одну
|
||||||
|
секунду с записью stamp'а, а `make` считает цель, сделанную в ту же
|
||||||
|
секунду, что и зависимость, актуальной. С паузой в секунду не
|
||||||
|
воспроизводится вовсе.
|
||||||
|
|
||||||
|
Лечится двумя приёмами, и оба убирают время из решения:
|
||||||
|
|
||||||
|
* смена варианта СНОСИТ stamp'ы своего семейства (`rm -f
|
||||||
|
$(STAMP)/sound-*`) — нет файла, значит правило обязано выполниться;
|
||||||
|
* упаковка, сборка архива и копия индекса делаются ОДНИМ рецептом:
|
||||||
|
внутри рецепта сравнивать нечего.
|
||||||
|
|
||||||
|
То же лечение получила и музыка (`MUSIC_FMT`) — механизм общий.
|
||||||
|
|
||||||
|
### 10.7 Что проверено на живой машине
|
||||||
|
|
||||||
|
* таблица в памяти совпадает с файлом из образа ПОБАЙТОВО (285 байт);
|
||||||
|
* один и тот же EXE (sha256 не меняется) поднимает SDLPoP (9 страниц) и
|
||||||
|
MSDOS (10 страниц);
|
||||||
|
* СТРОГАЯ проверка: у двух реальных наборов совпадают 23 записи из 31,
|
||||||
|
поэтому сделан отладочный ключ `pop_pack_sound.py --order reverse` —
|
||||||
|
набор с обратным порядком укладки, где от штатного отличаются 30 из 31
|
||||||
|
записей. Игра на нём звучит правильно, значит раскладка берётся с
|
||||||
|
диска, а не из памяти;
|
||||||
|
* битый индекс (испорченная магия) — эффекты выключаются, игра жива;
|
||||||
|
* отсутствующий индекс — музыка играет, эффектов нет, PV-сцена проходит;
|
||||||
|
* Ctrl+S в обоих режимах работает штатно.
|
||||||
|
|
||||||
|
Маркер для ручной проверки: звук 51 `princess_door_opening` (дверь в
|
||||||
|
сцене с принцессой) лежит у наборов по РАЗНЫМ страницам — 8 у SDLPoP,
|
||||||
|
9 у MSDOS. А вот 4 `gate_closing` из той же сцены совпадает побайтно и
|
||||||
|
для проверки подмены не годится.
|
||||||
|
|
||||||
|
## 11. МУЗЫКА БЕЗ ПЕРЕЛИНКОВКИ: ДЛИНЫ И ДЛИТЕЛЬНОСТИ УЕХАЛИ НА ДИСК (2026-08-31)
|
||||||
|
|
||||||
|
Сделана часть I плана `music_runtime_index_plan.md` (этапы MI0..MI5, у нас
|
||||||
|
B0..B7). `gen/pop_music_tbl.h` и `gen/pop_music_ticks.h` УДАЛЕНЫ: длины
|
||||||
|
треков и длительности реплик читаются из `MUS/mus.idx` (формат `PMI1`).
|
||||||
|
Один и тот же `sprpop.exe` работает с любым из четырёх наборов записей.
|
||||||
|
|
||||||
|
### 11.1 Где живёт индекс и почему именно там
|
||||||
|
|
||||||
|
228 байт таблицы в W2 положить нельзя — там свободной кучи порядка двух
|
||||||
|
сотен. Поэтому индекс лежит в ОДНОЙ странице EMM, а в резиденте от него
|
||||||
|
остаются два байта (номер блока и страницы). Наружу указатель на EMM не
|
||||||
|
выдаётся: `pop_music_info()` подключает страницу в W0, копирует четыре
|
||||||
|
байта в буфер вызывающего и сразу снимает окно.
|
||||||
|
|
||||||
|
**Данные в странице лежат со смещения `POP_MUS_IDX_BASE` (0x100), а не с
|
||||||
|
нуля.** Причина в `gfx_w0_page_prepare`: он делает страницу безопасной
|
||||||
|
для W0, записывая В НЕЁ переход на IRQ-восстановитель (0x38) и RETN для
|
||||||
|
NMI (0x66). С нуля эти байты легли бы прямо в записи (id 10 и 21) и
|
||||||
|
испортили индекс; со смещением стабы и данные не пересекаются, и работает
|
||||||
|
штатная защита от прерываний, а не запрет их. Тот же приём и по той же
|
||||||
|
причине — в `pop_config.c` (`CFG_BASE`).
|
||||||
|
|
||||||
|
Числа страниц в индексе НЕТ: это производная от `blocks`, и дублирование
|
||||||
|
однажды разъехалось бы с ним. Считает `pop_music_pages()`.
|
||||||
|
|
||||||
|
### 11.2 Пауза конца уровня: состояние вместо счётчика
|
||||||
|
|
||||||
|
`pop_endmus_left` и `POP_MUS_TICKS_32/41` УДАЛЕНЫ. Главный цикл ждёт
|
||||||
|
`pop_music_active()` — «заявка лежит, идёт загрузка или трек звучит».
|
||||||
|
|
||||||
|
Одного `pop_music_busy()` там мало: между заявкой и первой нотой проходит
|
||||||
|
190–230 мс (§9), и в этой щели он ложен. Раньше щель закрывал счётчик по
|
||||||
|
длине записи, но он тянул за собой две беды: зависимость EXE от набора и
|
||||||
|
жёсткий делитель `/4`, который врал в быстрых режимах (там логический кадр
|
||||||
|
3 кадра луча, а не 4). Три признака подряд закрывают щель без единой
|
||||||
|
подобранной константы, потому что `pop_music_service` обнуляет `req` и
|
||||||
|
тут же взводит `ld_busy`, а тот держится до записи `pop_mus_left`.
|
||||||
|
|
||||||
|
Побочно исправилось расхождение с оригиналом: при ВЫКЛЮЧЕННОМ звуке
|
||||||
|
`pop_sfx_play` не кладёт заявку, все три признака ложны, и уровень
|
||||||
|
меняется сразу — как в SDLPoP, где трек ставится под `if (is_sound_on)`
|
||||||
|
(seg006:651), а ожидание это `check_sound_playing()` (seg003:387).
|
||||||
|
Прежний счётчик взводился безусловно и держал пройденный уровень лишние
|
||||||
|
12 секунд в тишине.
|
||||||
|
|
||||||
|
### 11.3 PV-сцена: четыре якоря вместо compile-time шкалы
|
||||||
|
|
||||||
|
Шкала сцены осталась абсолютной (переход на ожидание звука — отдельный
|
||||||
|
разговор, см. план), но точки, зависящие от длины реплик, считаются при
|
||||||
|
входе в сцену: `pv_m50_end`, `pv_dialog1`, `pv_exit`, `pv_anim_end`.
|
||||||
|
Остальные выражения не изменились — они отсчитываются от якорей.
|
||||||
|
|
||||||
|
Восемь байт статики вместо протаскивания структуры времён через пять
|
||||||
|
функций (`pv_princess_step`, `pv_jaffar_step`, `pv_seq_period`,
|
||||||
|
`intro_pv_draw_frame`, `intro_pv_animated`), как предлагал план: сцена
|
||||||
|
одна на запуск, и переделка всех сигнатур того не стоит.
|
||||||
|
|
||||||
|
Нет индекса — длительности нулевые, и сцена просто идёт без пауз на
|
||||||
|
реплики: ждать нечего, звука-то нет.
|
||||||
|
|
||||||
|
### 11.4 Цена в памяти
|
||||||
|
|
||||||
|
За обе фазы (эффекты и музыка) резидент не вырос, а освободился:
|
||||||
|
|
||||||
|
| | до фазы A | после фазы B |
|
||||||
|
|---|---:|---:|
|
||||||
|
| `_CODE` | 23865 | 23544 |
|
||||||
|
| куча резидента | 239 Б | 256 Б |
|
||||||
|
|
||||||
|
Банк 9 похудел на 118 Б (ушла `pop_mus_tbl` из его rodata), банк 11 — на
|
||||||
|
длительности реплик. Прибавка: 2 байта на страницу индекса музыки и 8 на
|
||||||
|
якоря PV.
|
||||||
|
|
||||||
|
### 11.5 Что проверено на живой машине
|
||||||
|
|
||||||
|
* `sprpop.exe` ПОБАЙТОВО одинаков для наборов flac и mt32;
|
||||||
|
* все 22 трека в индексах различаются, и контрольные значения совпали с
|
||||||
|
тем, что план предсказывал по замерам: m32 438→449, m41 732→685,
|
||||||
|
m50 831→867, m53 985→1044, m56 9865→10462 блоков (78→82 страницы);
|
||||||
|
* на mt32-наборе PV-сцена проходит целиком: m50 → m53 → m52 звучат по
|
||||||
|
длительностям mt32, хотя в EXE их больше нет;
|
||||||
|
* без `mus.idx` музыки нет, эффекты работают, игра проходима — сцены
|
||||||
|
проскакивают без пауз на реплики.
|
||||||
|
|
||||||
|
**НЕ ПРОВЕРЕНО:** потоковый трек m56 на 82 страницах (у flac 78).
|
||||||
|
Кольцевой проигрыватель получает число страниц из индекса, и код к этому
|
||||||
|
готов, но живого прогона финала на mt32 не было — до него надо дойти в
|
||||||
|
игре. Это оставшийся пункт приёмки.
|
||||||
|
|||||||
@@ -0,0 +1,275 @@
|
|||||||
|
# Два поведения: VANILLA и BUGFIXED
|
||||||
|
|
||||||
|
> Заведено 2026-08-31. Задача поставлена, работа НЕ начата.
|
||||||
|
|
||||||
|
## Зачем
|
||||||
|
|
||||||
|
У оригинального PoP есть баги, которые игроки знают наизусть и на
|
||||||
|
которых построены известные трюки. SDLPoP чинит их не молча, а
|
||||||
|
ОПЦИОНАЛЬНО: каждое исправление отдельным переключателем, по умолчанию
|
||||||
|
часть включена, часть нет. Мы до сих пор повторяли ваниль — сознательно,
|
||||||
|
чтобы порт вёл себя как оригинал.
|
||||||
|
|
||||||
|
Задача: дать ДВА поведения на выбор, не размазывая условия по всему коду:
|
||||||
|
|
||||||
|
* **VANILLA** — как в оригинале 1989 года, со всеми его багами;
|
||||||
|
* **BUGFIXED** — с портированными исправлениями SDLPoP.
|
||||||
|
|
||||||
|
## Что уже известно (из аудита)
|
||||||
|
|
||||||
|
Разбор расхождений — `docs/sdlpop_audit.md`. Оттуда прямо в эту задачу
|
||||||
|
переезжает следующее.
|
||||||
|
|
||||||
|
### Опциональные фиксы SDLPoP, которых у нас НЕТ
|
||||||
|
|
||||||
|
Все три относятся к прохождению сквозь препятствия и живут в `do_fall`
|
||||||
|
(seg005) и рядом:
|
||||||
|
|
||||||
|
| фикс SDLPoP | что чинит |
|
||||||
|
|---|---|
|
||||||
|
| «скольжение сквозь стену» | Кид падает сквозь стены после разворота в беге, особенно под зельем медленного падения |
|
||||||
|
| «прыжок сквозь стену над воротами» | пролёт в тайл над воротами |
|
||||||
|
| «проваливание сквозь гобелен» | падение сквозь тайл гобелена |
|
||||||
|
|
||||||
|
Ни один не портирован — это и есть наше нынешнее VANILLA.
|
||||||
|
|
||||||
|
### Находка 24 — отложена сюда
|
||||||
|
|
||||||
|
`in_wall` у нас пересчитывает только колонку, а оригинал перезагружает
|
||||||
|
ещё и данные кадра (`load_fram_det_col`, seg006). Правка сделана и
|
||||||
|
ОТКАЧЕНА 2026-08-31 по такой причине:
|
||||||
|
|
||||||
|
* по букве оригинала находка верна;
|
||||||
|
* но практического эффекта показать НЕ УДАЛОСЬ — все 15 наборов
|
||||||
|
host-тестов дают одинаковый результат до и после, включая специально
|
||||||
|
написанный тест на заход в кладку;
|
||||||
|
* при этом правка не бесплатна: добавляет маппинг окна и перезагрузку
|
||||||
|
кадра на каждое выталкивание из стены.
|
||||||
|
|
||||||
|
Платить за недоказанное не стали. Вернуться к ней имеет смысл именно
|
||||||
|
здесь: при работе над BUGFIXED появится сценарий, где кадр меняется перед
|
||||||
|
выталкиванием, и тогда эффект станет наблюдаемым.
|
||||||
|
|
||||||
|
### Готовый детектор поведения
|
||||||
|
|
||||||
|
`tests/host/t_wall.c` расширен с одной проверки до трёх. Ключевая —
|
||||||
|
`wall_stops_jump_from_left_side`: она НЕ требует нуля заходов в кладку, а
|
||||||
|
сторожит их ЧИСЛО (сейчас ровно два случая из четырнадцати стартовых
|
||||||
|
позиций).
|
||||||
|
|
||||||
|
Это и есть переключатель ожиданий между режимами:
|
||||||
|
|
||||||
|
* больше двух — правка сделала нас хуже ванили, регресс;
|
||||||
|
* ровно два — ведём себя как оригинал (режим VANILLA);
|
||||||
|
* меньше двух — кто-то портировал фикс; в режиме BUGFIXED тест должен
|
||||||
|
ждать нуля.
|
||||||
|
|
||||||
|
То есть когда появится BUGFIXED, этому тесту понадобится ожидание,
|
||||||
|
зависящее от режима, — и он готов стать первым таким.
|
||||||
|
|
||||||
|
## Переключатель уже есть — второго не нужно
|
||||||
|
|
||||||
|
Уточнено 2026-08-31: в настройках игры ПЕРЕКЛЮЧАТЕЛЬ VANILLA/ENHANCED
|
||||||
|
СУЩЕСТВУЕТ (`docs/menu_settings_plan.md`), просто сейчас он жёстко
|
||||||
|
зафиксирован в положении VANILLA. Отдельную сущность заводить не надо —
|
||||||
|
эта задача про то, чтобы наполнить смыслом уже имеющееся положение
|
||||||
|
ENHANCED.
|
||||||
|
|
||||||
|
**Наш «ванильный» билд уже не чистая ваниль.** Часть ванильных багов у
|
||||||
|
нас пофикшена по ходу портирования. Значит:
|
||||||
|
|
||||||
|
* нельзя считать текущее поведение эталоном ванили — оно смешанное;
|
||||||
|
* при разделении режимов придётся пройтись по уже сделанным фиксам и
|
||||||
|
решить по каждому, остаётся он в VANILLA или уезжает в ENHANCED;
|
||||||
|
* и наоборот: отдельные исправления (например, падение сквозь стену)
|
||||||
|
вполне могут быть сделаны прямо в нынешнем «ванильном» билде, если
|
||||||
|
сочтём, что играбельность важнее буквальности.
|
||||||
|
|
||||||
|
## Что предстоит решить
|
||||||
|
|
||||||
|
1. **Что считать ванилью на практике.** Составить список уже сделанных
|
||||||
|
отступлений от оригинала и распределить их между режимами.
|
||||||
|
2. **Цена рантайм-проверки.** Условия попадают в физику и `play_seq`, то
|
||||||
|
есть в горячий путь. Если переключать в рантайме, проверка должна
|
||||||
|
быть дешевле самого фикса: флаг в резиденте, а не вызов через банк.
|
||||||
|
3. **Что считать умолчанием.** Оригинальное поведение честнее для порта,
|
||||||
|
но часть фиксов SDLPoP включает по умолчанию.
|
||||||
|
4. **Как тестировать оба режима.** Host-тесты гоняются одним прогоном;
|
||||||
|
для двух режимов нужен либо параметр сборки тестов, либо ожидания,
|
||||||
|
зависящие от флага.
|
||||||
|
|
||||||
|
## Инвентаризация: что уже решено по каждому фиксу SDLPoP
|
||||||
|
|
||||||
|
Составлено 2026-08-31 обходом кода. В движке эти решения УЖЕ приняты и
|
||||||
|
задокументированы прямо в комментариях — таблица лишь сводит их в одно
|
||||||
|
место, чтобы при разделении режимов не перечитывать исходники.
|
||||||
|
|
||||||
|
| фикс SDLPoP | где у нас | что взято |
|
||||||
|
|---|---|---|
|
||||||
|
| `fix_feather_fall_affects_guards` | `pop_map.c:941` | **ФИКС** — перо действует только на Кида |
|
||||||
|
| `fix_exit_door` | `pop_map.c:1212` | **ФИКС** — ветка фикса вместо ванильного глобала |
|
||||||
|
| `FIX_GATE_SOUNDS` | `pop_trob.c:579` | **ФИКС** — условия через ИЛИ |
|
||||||
|
| `fix_sound_priorities` | `pop_sfx.c:196` | **ФИКС** — в SDLPoP включён безусловно, сравниваемся с исправленным |
|
||||||
|
| `FIX_STAND_ON_THIN_AIR` | `pop_map.c:1499` | **ВАНИЛЬ** — взяты 2 части из 3, третья ждёт патча `seqtbl` |
|
||||||
|
| `fix_painless_fall_on_guard` | `pop_map.c:1611` | **ВАНИЛЬ** — намеренно |
|
||||||
|
| `fix_jumping_over_guard` | `pop_map.c:1612` | **ВАНИЛЬ** — намеренно |
|
||||||
|
| `FIX_RETREAT_WITHOUT_LEAVING_ROOM` | `pop_map.c:3036` | **ВАНИЛЬ** — в SDLPoP выключен по умолчанию; трюк 35 сохраняем |
|
||||||
|
| `fix_skeleton_chomper_blood` | `pop_map.c:3177` | **ВАНИЛЬ** — кровь скелета в ванили есть |
|
||||||
|
| потеря HP скелетом при падении с двух этажей | `pop_map.c:676` | **ВАНИЛЬ** — баг оригинала, сохраняем сознательно |
|
||||||
|
|
||||||
|
### Фиксы, которым нужна правка байткода
|
||||||
|
|
||||||
|
`FIX_STAND_ON_THIN_AIR` не взят НЕ потому, что мы выбрали ваниль, а
|
||||||
|
потому что его нельзя применить наполовину: он состоит из трёх частей, и
|
||||||
|
третья — правка самого байткода `seqtbl` (смещения в последовательности
|
||||||
|
вставания). Взяв только первые две, мы получим вставание, уносящее
|
||||||
|
весовую точку в стену, то есть ХУЖЕ ванили.
|
||||||
|
|
||||||
|
**Это выполнимо, и раньше здесь стояла неверная оценка** (уточнено
|
||||||
|
2026-08-31): байткод можно менять и у нас. Способов три:
|
||||||
|
|
||||||
|
1. **Патч в рантайме** — после загрузки `kid.ani` в EMM-страницу
|
||||||
|
пропатчить нужные байты прямо там. Речь о единицах байт, страница уже
|
||||||
|
наша, и патч обратим — то есть режим переключается без пересборки
|
||||||
|
ресурсов. Это и делает фикс пригодным для VANILLA/ENHANCED.
|
||||||
|
2. **Патч в упаковщике** — готовить два варианта `kid.ani`. Привязывает
|
||||||
|
режим к файлам на диске, поэтому хуже: переключатель в меню перестаёт
|
||||||
|
быть чисто кодовым.
|
||||||
|
3. **Две копии в одной странице** — и это, пожалуй, лучший вариант
|
||||||
|
(решено 2026-08-31). `kid.ani` целиком около 4 КБ, а страница EMM —
|
||||||
|
16 КБ, то есть обе версии байткода спокойно помещаются рядом в уже
|
||||||
|
выделенной странице. Переключение режима сводится к смене базового
|
||||||
|
смещения, патчить ничего не нужно, откат мгновенный.
|
||||||
|
|
||||||
|
Для сравнения: у SDLPoP рабочая таблица и неизменная копия оригинала
|
||||||
|
существуют раздельно (`seqtbl` и `original_seqtbl`), причём вторая нужна
|
||||||
|
для сверки — то есть сама идея «оригинальный байткод отдельно, рабочий
|
||||||
|
отдельно» там уже заложена.
|
||||||
|
|
||||||
|
Общее правило для BUGFIXED: фиксы, требующие правки `seqtbl`, доступны
|
||||||
|
через рантайм-патч страницы; закладывать это стоит сразу, чтобы не
|
||||||
|
упереться при первом же таком фиксе.
|
||||||
|
|
||||||
|
### Вывод для разделения режимов
|
||||||
|
|
||||||
|
Четыре фикса уже взяты, шесть позиций оставлены ванильными. Значит
|
||||||
|
нынешний билд — это не VANILLA, а «ваниль плюс четыре исправления». При
|
||||||
|
разделении:
|
||||||
|
|
||||||
|
* взятые четыре надо либо оставить в обоих режимах (если считаем их
|
||||||
|
безусловными улучшениями), либо увести в ENHANCED и вернуть ванильное
|
||||||
|
поведение в VANILLA — второе честнее, но потребует обратной работы;
|
||||||
|
* ванильные шесть — кандидаты в ENHANCED; `FIX_STAND_ON_THIN_AIR` тоже,
|
||||||
|
но ему дополнительно нужен рантайм-патч байткода.
|
||||||
|
|
||||||
|
## ВСЕ исправления SDLPoP и их статус у нас
|
||||||
|
|
||||||
|
Полный перечень опциональных исправлений оригинала, какие есть в SDLPoP
|
||||||
|
(43 позиции), со статусом в нашем порте. Названия — идентификаторы
|
||||||
|
опций SDLPoP, описание — своими словами.
|
||||||
|
|
||||||
|
Статусы: **ВЗЯТ** — портирован; **ВАНИЛЬ** — сознательно не берём, держим
|
||||||
|
поведение оригинала; **НЕТ** — не реализован, кандидат в ENHANCED;
|
||||||
|
**НЕДОСТУПЕН** — требует правки байткода `seqtbl` (см. ограничение выше);
|
||||||
|
**В РАБОТЕ** — решено делать сейчас.
|
||||||
|
|
||||||
|
### Стены и препятствия
|
||||||
|
|
||||||
|
| опция | что чинит | статус |
|
||||||
|
|---|---|---|
|
||||||
|
| `fix_glide_through_wall` | проход сквозь стену при падении после разворота в беге | **ВЗЯТ** 2026-08-31 — `glide_through_wall_guard()` в `pop_map.c`, точка отвязки для VANILLA |
|
||||||
|
| `fix_jump_through_wall_above_gate` | прыжок в тайл над воротами | НЕТ |
|
||||||
|
| `fix_drop_through_tapestry` | проваливание сквозь гобелен | НЕТ |
|
||||||
|
| `fix_running_jump_through_tapestry` | прыжок с разбега сквозь гобелен | НЕТ |
|
||||||
|
| `fix_turn_running_near_wall` | разворот в беге вплотную к стене | НЕТ |
|
||||||
|
| `fix_wall_bump_triggers_tile_below` | удар о стену срабатывает на тайл ниже | НЕТ |
|
||||||
|
| `fix_bigpillar_climb` | подъём на большую колонну | НЕТ |
|
||||||
|
| `fix_land_against_gate_or_tapestry` | приземление вплотную к воротам или гобелену | НЕТ |
|
||||||
|
| `fix_caped_prince_sliding_through_gate` | проскальзывание сквозь ворота | НЕТ |
|
||||||
|
|
||||||
|
### Падение, прыжки, зацепы
|
||||||
|
|
||||||
|
| опция | что чинит | статус |
|
||||||
|
|---|---|---|
|
||||||
|
| `fix_stand_on_thin_air` | стояние на воздухе после отмены падения | НЕТ — нужен рантайм-патч `seqtbl` (см. выше) |
|
||||||
|
| `fix_jump_distance_at_edge` | дальность прыжка у самой кромки | НЕТ |
|
||||||
|
| `fix_edge_distance_check_when_climbing` | проверка расстояния до кромки при подъёме | НЕТ |
|
||||||
|
| `fix_grab_falling_speed` | зацеп на слишком большой скорости падения | НЕТ |
|
||||||
|
| `fix_drop_2_rooms_climbing_loose_tile` | провал через две комнаты при подъёме на шаткой плите | НЕТ |
|
||||||
|
| `fix_infinite_down_bug` | бесконечное падение вниз | НЕТ |
|
||||||
|
| `fix_falling_through_floor_during_sword_strike` | провал сквозь пол во время удара мечом | НЕТ |
|
||||||
|
| `fix_safe_landing_on_spikes` | безопасное приземление на невыдвинутые пики | НЕТ |
|
||||||
|
| `fix_dead_floating_in_air` | мёртвый зависает в воздухе | НЕТ |
|
||||||
|
|
||||||
|
### Бой и стражи
|
||||||
|
|
||||||
|
| опция | что чинит | статус |
|
||||||
|
|---|---|---|
|
||||||
|
| `fix_painless_fall_on_guard` | падение на стража с высоты без урона | **ВАНИЛЬ** |
|
||||||
|
| `fix_jumping_over_guard` | перепрыгивание через стража | **ВАНИЛЬ** |
|
||||||
|
| `fix_skeleton_chomper_blood` | кровь скелета в челюстях | **ВАНИЛЬ** |
|
||||||
|
| `fix_push_guard_into_wall` | вталкивание стража в стену | НЕТ |
|
||||||
|
| `fix_guard_following_through_closed_gates` | страж идёт сквозь закрытые ворота | НЕТ |
|
||||||
|
| `fix_doortop_disabling_guard` | верх двери отключает стража | НЕТ |
|
||||||
|
| `fix_offscreen_guards_disappearing` | стражи пропадают за краем экрана | НЕТ |
|
||||||
|
| `fix_unintended_sword_strike` | непреднамеренный удар мечом | НЕТ |
|
||||||
|
| `fix_two_coll_bug` | двойная проверка столкновения | НЕТ |
|
||||||
|
| `fix_move_after_sheathe` | движение сразу после убирания меча | НЕТ |
|
||||||
|
|
||||||
|
### Ворота, двери, плиты
|
||||||
|
|
||||||
|
| опция | что чинит | статус |
|
||||||
|
|---|---|---|
|
||||||
|
| `fix_exit_door` | дверь выхода с уровня | **ВЗЯТ** |
|
||||||
|
| `fix_gate_sounds` | звуки ворот | **ВЗЯТ** |
|
||||||
|
| `fix_gate_drawing_bug` | отрисовка ворот | НЕТ |
|
||||||
|
| `fix_press_through_closed_gates` | нажатие плиты сквозь закрытые ворота | НЕТ |
|
||||||
|
| `fix_chompers_not_starting` | челюсти не заводятся | НЕТ |
|
||||||
|
| `fix_loose_left_of_potion` | шаткая плита слева от зелья | НЕТ |
|
||||||
|
| `fix_hidden_floors_during_flashing` | скрытые полы во время вспышки | НЕТ |
|
||||||
|
| `fix_retreat_without_leaving_room` | отступление без смены комнаты (трюк 35) | **ВАНИЛЬ** |
|
||||||
|
|
||||||
|
### Зелья, перо, спецэффекты
|
||||||
|
|
||||||
|
| опция | что чинит | статус |
|
||||||
|
|---|---|---|
|
||||||
|
| `fix_feather_fall_affects_guards` | перо действует и на стражей | **ВЗЯТ** |
|
||||||
|
| `fix_feather_interrupted_by_leveldoor` | перо прерывается дверью уровня | НЕТ |
|
||||||
|
| `fix_move_after_drink` | движение сразу после питья | НЕТ |
|
||||||
|
| `fix_quicksave_during_feather` | быстрое сохранение под пером | НЕТ |
|
||||||
|
| `fix_hang_on_teleport` | зависание при телепорте | НЕТ (телепортов у нас нет) |
|
||||||
|
|
||||||
|
### Интерфейс и ввод
|
||||||
|
|
||||||
|
| опция | что чинит | статус |
|
||||||
|
|---|---|---|
|
||||||
|
| `fix_one_hp_stops_blinking` | индикатор перестаёт мигать на одном HP | НЕТ |
|
||||||
|
| `fix_register_quick_input` | учёт быстрого ввода | НЕТ |
|
||||||
|
|
||||||
|
### Отдельно: приоритеты звуков
|
||||||
|
|
||||||
|
`fix_sound_priorities` в SDLPoP включён БЕЗУСЛОВНО (не опция), и мы
|
||||||
|
сравниваемся с исправленным вариантом — `pop_sfx.c:196`. Статус:
|
||||||
|
**ВЗЯТ**, вернуть ванильное поведение отдельным режимом было бы
|
||||||
|
дополнительной работой.
|
||||||
|
|
||||||
|
### Сводка
|
||||||
|
|
||||||
|
| статус | сколько |
|
||||||
|
|---|---:|
|
||||||
|
| ВЗЯТ | 5 |
|
||||||
|
| ВАНИЛЬ (сознательно) | 4 |
|
||||||
|
| требует патча `seqtbl` (выполнимо) | 1 |
|
||||||
|
| НЕТ (кандидаты в ENHANCED) | 32 |
|
||||||
|
|
||||||
|
## Список кандидатов на BUGFIXED
|
||||||
|
|
||||||
|
Пополняется по мере аудита. Пока:
|
||||||
|
|
||||||
|
* три опциональных фикса SDLPoP выше;
|
||||||
|
* находка 24 (перезагрузка кадра в `in_wall`);
|
||||||
|
* находка 21 из аудита — общая рамка «мы намеренно повторяем ваниль».
|
||||||
|
|
||||||
|
Не относятся сюда находки, где мы расходимся с оригиналом НЕ в его
|
||||||
|
пользу (ранги А и Б аудита): их надо чинить в обоих режимах, потому что
|
||||||
|
это не баги оригинала, а наши.
|
||||||
@@ -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.
|
/* pop_bg_atlas.h — раскладка атласов статического фона PoP.
|
||||||
* Сгенерировано toolchain/pop_pack_bg.py — НЕ править вручную.
|
* Сгенерировано tools/pop_pack_bg.py — НЕ править вручную.
|
||||||
*
|
*
|
||||||
* Прямая адресация (ноль remap-таблиц в W2):
|
* Прямая адресация (ноль remap-таблиц в W2):
|
||||||
* ENV фон id N -> atlas env_bg[N>>4], idx N&15
|
* ENV фон id N -> atlas env_bg[N>>4], idx N&15
|
||||||
@@ -23,25 +23,15 @@
|
|||||||
#define POP_PAL_ENV 0x50
|
#define POP_PAL_ENV 0x50
|
||||||
#define POP_PAL_WALL 0x60
|
#define POP_PAL_WALL 0x60
|
||||||
|
|
||||||
/* Имена файлов атласов по тайлсету (грузятся atlas_load). Таблицы
|
/* Палитры пока остаются отдельными файлами рядом с bg.arc. Имена
|
||||||
* видны только тому, кто попросил POP_BG_ATLAS_NAMES: иначе копия
|
* видит только модуль загрузки, чтобы не размножать строки. */
|
||||||
* строк уедет в каждый включивший заголовок модуль. */
|
|
||||||
#ifdef POP_BG_ATLAS_NAMES
|
#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" };
|
static const char *const pop_tile_pal[POP_TILESETS] = { "pop_tile.pal", "pal_tile.pal" };
|
||||||
#endif /* POP_BG_ATLAS_NAMES */
|
#endif /* POP_BG_ATLAS_NAMES */
|
||||||
#define POP_POT_ATL "pop_pot.atl" /* chtab_1: зелья, от набора не зависит */
|
|
||||||
#define POP_PAL_POT 0x40
|
|
||||||
/* Пузырёк зелья: красный набор = id 16..22 (кадры оригинала),
|
/* Пузырёк зелья: красный набор = id 16..22 (кадры оригинала),
|
||||||
зелёный (перо/переворот) и синий (вред/открыть) = те же кадры
|
зелёный (перо/переворот) и синий (вред/открыть) = те же кадры
|
||||||
под id 30..36 и 40..46 (draw_tile_anim, seg008:652). */
|
под id 30..36 и 40..46 (draw_tile_anim, seg008:652). */
|
||||||
#define POP_POT_BUBB_GREEN 30
|
#define POP_POT_BUBB_GREEN 30
|
||||||
#define POP_POT_BUBB_BLUE 40
|
#define POP_POT_BUBB_BLUE 40
|
||||||
#define POP_BG_PAL "pop_bg.pal"
|
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -1,104 +0,0 @@
|
|||||||
/*
|
|
||||||
* pop_music_tbl.h — каталог музыкальных треков, СГЕНЕРИРОВАН.
|
|
||||||
* Источник: assets/orig/PoP1_DOS_music/flac (записи DOS-версии).
|
|
||||||
* Генератор: tools/pop_pack_music.py. Руками не править.
|
|
||||||
*
|
|
||||||
* Нумерация id — оригинала (SDLPoP data/music/names.txt). pages —
|
|
||||||
* сколько кусков m<id>_<nn>.bin лежит на диске, blocks — длина трека
|
|
||||||
* в порциях насоса по 128 байт (в байтах не влезло бы в uint16).
|
|
||||||
*
|
|
||||||
* Длительности реплик в тиках вынесены в pop_music_ticks.h: таблица
|
|
||||||
* ниже — 171 байт ДАННЫХ, и модулю, которому нужна одна константа,
|
|
||||||
* платить за неё копией таблицы незачем (поймано на pop_intro.c:
|
|
||||||
* +172 байта в банк 11).
|
|
||||||
*
|
|
||||||
* POP_MUS_TICKS_<id> — та же длина в ТИКАХ СЦЕНЫ (60 Гц оригинала).
|
|
||||||
* Нужна там, где сценарий ждёт окончания реплики: у оригинала это
|
|
||||||
* `do { frame(1) } while (check_sound_playing())`, у нас — пауза
|
|
||||||
* длиной в нашу запись. Считается ЗДЕСЬ, а не в C, по двум
|
|
||||||
* причинам: blocks*40 переполняет uint16 на won (9855 порций), и
|
|
||||||
* число обязано соответствовать НАБОРУ, которым собрали (flac и
|
|
||||||
* mt32 — разные исполнения разной длины).
|
|
||||||
*
|
|
||||||
* Отношение 40/57 — обратное к тому, которым шкала сцены переводит
|
|
||||||
* тики в порции насоса (57/40, см. pop_intro.c). Берём именно его,
|
|
||||||
* а не точное 10937,5/128/60, чтобы длина реплики и ход шкалы
|
|
||||||
* считались одной и той же арифметикой. Округление — к ближайшему
|
|
||||||
* (+28 перед делением): отбрасывание вниз теряло по тику на реплику.
|
|
||||||
*/
|
|
||||||
#ifndef POP_MUSIC_TBL_H
|
|
||||||
#define POP_MUSIC_TBL_H
|
|
||||||
|
|
||||||
#include <stdint.h>
|
|
||||||
|
|
||||||
#define POP_MUS_COUNT 57
|
|
||||||
#define POP_MUS_BLOCK 128
|
|
||||||
#define POP_MUS_MAX_PAGES 78
|
|
||||||
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
uint8_t pages; /* 0 — трека нет */
|
|
||||||
uint16_t blocks; /* длина в порциях по 128 байт */
|
|
||||||
} pop_mus_ent_t;
|
|
||||||
|
|
||||||
static const pop_mus_ent_t pop_mus_tbl[POP_MUS_COUNT] = {
|
|
||||||
{ 0, 0 }, /* 0 */
|
|
||||||
{ 0, 0 }, /* 1 */
|
|
||||||
{ 0, 0 }, /* 2 */
|
|
||||||
{ 0, 0 }, /* 3 */
|
|
||||||
{ 0, 0 }, /* 4 */
|
|
||||||
{ 0, 0 }, /* 5 */
|
|
||||||
{ 0, 0 }, /* 6 */
|
|
||||||
{ 0, 0 }, /* 7 */
|
|
||||||
{ 0, 0 }, /* 8 */
|
|
||||||
{ 0, 0 }, /* 9 */
|
|
||||||
{ 0, 0 }, /* 10 */
|
|
||||||
{ 0, 0 }, /* 11 */
|
|
||||||
{ 0, 0 }, /* 12 */
|
|
||||||
{ 0, 0 }, /* 13 */
|
|
||||||
{ 0, 0 }, /* 14 */
|
|
||||||
{ 0, 0 }, /* 15 */
|
|
||||||
{ 0, 0 }, /* 16 */
|
|
||||||
{ 0, 0 }, /* 17 */
|
|
||||||
{ 0, 0 }, /* 18 */
|
|
||||||
{ 0, 0 }, /* 19 */
|
|
||||||
{ 0, 0 }, /* 20 */
|
|
||||||
{ 0, 0 }, /* 21 */
|
|
||||||
{ 0, 0 }, /* 22 */
|
|
||||||
{ 0, 0 }, /* 23 */
|
|
||||||
{ 3, 305 }, /* 24 regular_death */
|
|
||||||
{ 2, 196 }, /* 25 find_mirror_and_find_shadow */
|
|
||||||
{ 9, 1132 }, /* 26 embrace */
|
|
||||||
{ 8, 915 }, /* 27 cutscene_before_2_4_6_12 */
|
|
||||||
{ 6, 656 }, /* 28 fight_death */
|
|
||||||
{ 4, 428 }, /* 29 meet_Jaffar */
|
|
||||||
{ 3, 299 }, /* 30 life_potion */
|
|
||||||
{ 0, 0 }, /* 31 */
|
|
||||||
{ 5, 624 }, /* 32 end_level_4_and_killed_by_shadow */
|
|
||||||
{ 2, 205 }, /* 33 potion */
|
|
||||||
{ 0, 0 }, /* 34 */
|
|
||||||
{ 7, 888 }, /* 35 cutscene_before_8_9 */
|
|
||||||
{ 10, 1256 }, /* 36 out_of_time */
|
|
||||||
{ 3, 280 }, /* 37 guard_death_and_obtaining_the_sword */
|
|
||||||
{ 0, 0 }, /* 38 */
|
|
||||||
{ 13, 1591 }, /* 39 floating */
|
|
||||||
{ 5, 596 }, /* 40 cutscene_not_much_time_left */
|
|
||||||
{ 9, 1043 }, /* 41 story_5_level_end_and_merge_with_shadow */
|
|
||||||
{ 0, 0 }, /* 42 */
|
|
||||||
{ 3, 285 }, /* 43 killed_Jaffar */
|
|
||||||
{ 0, 0 }, /* 44 */
|
|
||||||
{ 0, 0 }, /* 45 */
|
|
||||||
{ 0, 0 }, /* 46 */
|
|
||||||
{ 0, 0 }, /* 47 */
|
|
||||||
{ 0, 0 }, /* 48 */
|
|
||||||
{ 0, 0 }, /* 49 */
|
|
||||||
{ 10, 1184 }, /* 50 story_2_princess */
|
|
||||||
{ 0, 0 }, /* 51 */
|
|
||||||
{ 20, 2443 }, /* 52 story_4_Jaffar_leaves */
|
|
||||||
{ 11, 1403 }, /* 53 story_3_Jaffar_enters */
|
|
||||||
{ 17, 2064 }, /* 54 intro_theme */
|
|
||||||
{ 11, 1350 }, /* 55 story_1_absence */
|
|
||||||
{ 78, 9865 }, /* 56 won */
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif /* POP_MUSIC_TBL_H */
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
/*
|
|
||||||
* pop_music_ticks.h — длительности треков в ТИКАХ СЦЕНЫ, СГЕНЕРИРОВАН.
|
|
||||||
* Набор: flac. Генератор: tools/pop_pack_music.py.
|
|
||||||
* Руками не править.
|
|
||||||
*
|
|
||||||
* Нужны там, где сценарий ждёт окончания реплики: у оригинала это
|
|
||||||
* `do { frame(1) } while (check_sound_playing())`, у нас — пауза длиной
|
|
||||||
* в НАШУ запись. Число обязано соответствовать набору, которым собрана
|
|
||||||
* сборка: одна и та же реплика на flac и mt32 разной длины (m53 — 985
|
|
||||||
* тиков против 1044), и захардкоженная константа молча ломает сцену.
|
|
||||||
*
|
|
||||||
* Считается здесь, а не в C: blocks*40 переполняет uint16 на won
|
|
||||||
* (9855 порций). Отношение 40/57 — обратное к тому, которым шкала
|
|
||||||
* сцены переводит тики в порции насоса (57/40, pop_intro.c); берём
|
|
||||||
* именно его, чтобы длина реплики и ход шкалы считались одной
|
|
||||||
* арифметикой. Округление к ближайшему: вниз теряло по тику.
|
|
||||||
*/
|
|
||||||
#ifndef POP_MUSIC_TICKS_H
|
|
||||||
#define POP_MUSIC_TICKS_H
|
|
||||||
|
|
||||||
#define POP_MUS_TICKS_24 214 /* regular_death */
|
|
||||||
#define POP_MUS_TICKS_25 138 /* find_mirror_and_find_shadow */
|
|
||||||
#define POP_MUS_TICKS_26 794 /* embrace */
|
|
||||||
#define POP_MUS_TICKS_27 642 /* cutscene_before_2_4_6_12 */
|
|
||||||
#define POP_MUS_TICKS_28 460 /* fight_death */
|
|
||||||
#define POP_MUS_TICKS_29 300 /* meet_Jaffar */
|
|
||||||
#define POP_MUS_TICKS_30 210 /* life_potion */
|
|
||||||
#define POP_MUS_TICKS_32 438 /* end_level_4_and_killed_by_shadow */
|
|
||||||
#define POP_MUS_TICKS_33 144 /* potion */
|
|
||||||
#define POP_MUS_TICKS_35 623 /* cutscene_before_8_9 */
|
|
||||||
#define POP_MUS_TICKS_36 881 /* out_of_time */
|
|
||||||
#define POP_MUS_TICKS_37 196 /* guard_death_and_obtaining_the_sword */
|
|
||||||
#define POP_MUS_TICKS_39 1116 /* floating */
|
|
||||||
#define POP_MUS_TICKS_40 418 /* cutscene_not_much_time_left */
|
|
||||||
#define POP_MUS_TICKS_41 732 /* story_5_level_end_and_merge_with_shadow */
|
|
||||||
#define POP_MUS_TICKS_43 200 /* killed_Jaffar */
|
|
||||||
#define POP_MUS_TICKS_50 831 /* story_2_princess */
|
|
||||||
#define POP_MUS_TICKS_52 1714 /* story_4_Jaffar_leaves */
|
|
||||||
#define POP_MUS_TICKS_53 985 /* story_3_Jaffar_enters */
|
|
||||||
#define POP_MUS_TICKS_54 1448 /* intro_theme */
|
|
||||||
#define POP_MUS_TICKS_55 947 /* story_1_absence */
|
|
||||||
#define POP_MUS_TICKS_56 6923 /* won */
|
|
||||||
|
|
||||||
#endif /* POP_MUSIC_TICKS_H */
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/* pop_shadow_atlas.h — раскладка атласа ТЕНИ.
|
/* pop_shadow_atlas.h — раскладка атласа ТЕНИ.
|
||||||
* СГЕНЕРЕНО toolchain/pop_pack_shadow.py — НЕ править вручную.
|
* СГЕНЕРЕНО tools/pop_pack_shadow.py — НЕ править вручную.
|
||||||
*
|
*
|
||||||
* Две половины, потому что и у оригинала их две (см. шапку
|
* Две половины, потому что и у оригинала их две (см. шапку
|
||||||
* упаковщика): кадры вне боя адресуют chtab_2 (спрайты Кида),
|
* упаковщика): кадры вне боя адресуют chtab_2 (спрайты Кида),
|
||||||
@@ -25,44 +25,6 @@
|
|||||||
/* Половина «в бою» — SHADOW.DAT (chtab_5, кадры 150..189). */
|
/* Половина «в бою» — SHADOW.DAT (chtab_5, кадры 150..189). */
|
||||||
#define POP_SHADOW_FGT_PAGES 4
|
#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. */
|
/* 16 цветов, запись 4 байта (B, G, R, 0) — формат gfx_pal_load. */
|
||||||
static const uint8_t pop_shadow_pal[64] = {
|
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,
|
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,113 +0,0 @@
|
|||||||
/* pop_sound_tbl.h — раскладка оцифрованных эффектов PoP.
|
|
||||||
* СГЕНЕРЕНО toolchain/pop_pack_sound.py — НЕ править вручную.
|
|
||||||
*
|
|
||||||
* Данные: 8 бит беззнаковые, МОНО, 10937.5 Гц (CBL_FREQ_10K9) — формат
|
|
||||||
* CBL один в один, конвертировать в рантайме нечего.
|
|
||||||
*
|
|
||||||
* Начало каждого звука кратно 128 — размеру блока запроса CBL, а
|
|
||||||
* страница (16384) кратна ему же. Поэтому НИ ОДИН блок не пересекает
|
|
||||||
* границу страницы. Больше того: с 2026-08-25 каждый звук лежит
|
|
||||||
* ЦЕЛИКОМ в одной странице — не влезает в остаток, значит начинаем
|
|
||||||
* новую (страницы EMM у нас с запасом). Единственное исключение —
|
|
||||||
* эффект, который сам ДЛИННЕЕ страницы: для него насос переключает
|
|
||||||
* страницу на ходу, и эта ветка в pop_sfx_fill остаётся ради него.
|
|
||||||
*
|
|
||||||
* Смещение хранится РАЗОБРАННЫМ на страницу и адрес в окне: на Z80
|
|
||||||
* 32-битная арифметика дорога, а так всё влезает в 8/16 бит.
|
|
||||||
* Длина округлена вверх до блока — неполных блоков не бывает.
|
|
||||||
*/
|
|
||||||
#ifndef POP_SOUND_TBL_H
|
|
||||||
#define POP_SOUND_TBL_H
|
|
||||||
|
|
||||||
#include <stdint.h>
|
|
||||||
|
|
||||||
#define POP_SND_PAGES 10
|
|
||||||
#define POP_SND_COUNT 57
|
|
||||||
#define POP_SND_BLOCK 128
|
|
||||||
/* Первый блок страницы 0 — тишина для насоса (см. упаковщик). */
|
|
||||||
#define POP_SND_SILENCE_PAGE 0
|
|
||||||
#define POP_SND_SILENCE_OFF 0x0000
|
|
||||||
|
|
||||||
typedef struct { uint8_t page; uint16_t off; uint16_t len; } pop_snd_ent_t;
|
|
||||||
|
|
||||||
/* len == 0 — у звука нет оцифровки (это музыка, см. sound_plan.md). */
|
|
||||||
static const pop_snd_ent_t pop_snd_tbl[POP_SND_COUNT] = {
|
|
||||||
{ 0, 0x0080, 1664 }, /* 0 */
|
|
||||||
{ 0, 0x0700, 12032 }, /* 1 */
|
|
||||||
{ 1, 0x0000, 6272 }, /* 2 */
|
|
||||||
{ 1, 0x1880, 1280 }, /* 3 */
|
|
||||||
{ 1, 0x1D80, 1152 }, /* 4 */
|
|
||||||
{ 1, 0x2200, 1408 }, /* 5 */
|
|
||||||
{ 1, 0x2780, 5504 }, /* 6 */
|
|
||||||
{ 2, 0x0000, 896 }, /* 7 */
|
|
||||||
{ 2, 0x0380, 1664 }, /* 8 */
|
|
||||||
{ 2, 0x0A00, 2816 }, /* 9 */
|
|
||||||
{ 2, 0x1500, 4992 }, /* 10 */
|
|
||||||
{ 2, 0x2880, 1280 }, /* 11 */
|
|
||||||
{ 2, 0x2D80, 1280 }, /* 12 */
|
|
||||||
{ 3, 0x0000, 3840 }, /* 13 */
|
|
||||||
{ 3, 0x0F00, 7552 }, /* 14 */
|
|
||||||
{ 4, 0x0000, 17664 }, /* 15 */
|
|
||||||
{ 5, 0x0500, 4736 }, /* 16 */
|
|
||||||
{ 5, 0x1780, 4224 }, /* 17 */
|
|
||||||
{ 6, 0x0000, 6912 }, /* 18 */
|
|
||||||
{ 6, 0x1B00, 2816 }, /* 19 */
|
|
||||||
{ 6, 0x2600, 1408 }, /* 20 */
|
|
||||||
{ 6, 0x2B80, 1152 }, /* 21 */
|
|
||||||
{ 6, 0x3000, 1280 }, /* 22 */
|
|
||||||
{ 6, 0x3500, 1408 }, /* 23 */
|
|
||||||
{ 0, 0x0000, 0 }, /* 24 */
|
|
||||||
{ 0, 0x0000, 0 }, /* 25 */
|
|
||||||
{ 0, 0x0000, 0 }, /* 26 */
|
|
||||||
{ 0, 0x0000, 0 }, /* 27 */
|
|
||||||
{ 0, 0x0000, 0 }, /* 28 */
|
|
||||||
{ 0, 0x0000, 0 }, /* 29 */
|
|
||||||
{ 0, 0x0000, 0 }, /* 30 */
|
|
||||||
{ 0, 0x0000, 0 }, /* 31 */
|
|
||||||
{ 0, 0x0000, 0 }, /* 32 */
|
|
||||||
{ 0, 0x0000, 0 }, /* 33 */
|
|
||||||
{ 0, 0x0000, 0 }, /* 34 */
|
|
||||||
{ 0, 0x0000, 0 }, /* 35 */
|
|
||||||
{ 0, 0x0000, 0 }, /* 36 */
|
|
||||||
{ 0, 0x0000, 0 }, /* 37 */
|
|
||||||
{ 0, 0x0000, 0 }, /* 38 */
|
|
||||||
{ 0, 0x0000, 0 }, /* 39 */
|
|
||||||
{ 0, 0x0000, 0 }, /* 40 */
|
|
||||||
{ 0, 0x0000, 0 }, /* 41 */
|
|
||||||
{ 0, 0x0000, 0 }, /* 42 */
|
|
||||||
{ 0, 0x0000, 0 }, /* 43 */
|
|
||||||
{ 7, 0x0000, 2688 }, /* 44 */
|
|
||||||
{ 7, 0x0A80, 4224 }, /* 45 */
|
|
||||||
{ 7, 0x1B00, 4608 }, /* 46 */
|
|
||||||
{ 8, 0x0000, 5120 }, /* 47 */
|
|
||||||
{ 8, 0x1400, 5120 }, /* 48 */
|
|
||||||
{ 8, 0x2800, 4480 }, /* 49 */
|
|
||||||
{ 0, 0x0000, 0 }, /* 50 */
|
|
||||||
{ 9, 0x0000, 4864 }, /* 51 */
|
|
||||||
{ 0, 0x0000, 0 }, /* 52 */
|
|
||||||
{ 0, 0x0000, 0 }, /* 53 */
|
|
||||||
{ 0, 0x0000, 0 }, /* 54 */
|
|
||||||
{ 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
|
|
||||||
@@ -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,17 +1,22 @@
|
|||||||
/*
|
/*
|
||||||
* _pop_sfx.h — внутренний контракт между половинами звука (НЕ публичный,
|
* _pop_sfx.h — внутренний контракт между половинами звука (НЕ публичный,
|
||||||
* публичный — 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
|
#ifndef _POP_SFX_INTERNAL_H
|
||||||
#define _POP_SFX_INTERNAL_H
|
#define _POP_SFX_INTERNAL_H
|
||||||
|
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include "pop_sound_tbl.h" /* pop_snd_tbl, POP_SND_* — генерит упаковщик */
|
#include "pop_snd_tbl.h" /* тип записи и инварианты (НЕ генерируется) */
|
||||||
|
|
||||||
/* Физические страницы набора, по индексу из pop_snd_tbl[].page. */
|
/* РАСКЛАДКА НАБОРА и его страницы — общие данные обеих половин, живут в
|
||||||
extern uint8_t pop_snd_page[POP_SND_PAGES];
|
* pop_snd_data.c. Значения приезжают с диска (SND/snd.idx): холодная
|
||||||
|
* половина их читает и проверяет, горячая только пользуется. */
|
||||||
|
extern pop_snd_ent_t pop_snd_tbl[POP_SND_COUNT];
|
||||||
|
extern uint8_t pop_snd_page[POP_SND_MAX_PAGES];
|
||||||
|
/* Сколько страниц у загруженного набора; 0 — набор не поднят. */
|
||||||
|
extern uint8_t pop_snd_pages;
|
||||||
/* 1 — набор поднят и CBL открыт; 0 — играем молча. */
|
/* 1 — набор поднят и CBL открыт; 0 — играем молча. */
|
||||||
extern uint8_t pop_snd_ok;
|
extern uint8_t pop_snd_ok;
|
||||||
|
|
||||||
|
|||||||
@@ -999,6 +999,7 @@ uint8_t pop_demo_kid_ai(void) __banked
|
|||||||
#define FRAME_154_POKING 154
|
#define FRAME_154_POKING 154
|
||||||
#define SEQ_69_ATTACK_WAS_PARRIED 69
|
#define SEQ_69_ATTACK_WAS_PARRIED 69
|
||||||
#define SEQ_74_HIT_BY_SWORD 74
|
#define SEQ_74_HIT_BY_SWORD 74
|
||||||
|
#define SEQ_81_PUSHED_OFF_LEDGE 81 /* заколот у обрыва — падает замертво */
|
||||||
#define SEQ_85_STABBED_TO_DEATH 85
|
#define SEQ_85_STABBED_TO_DEATH 85
|
||||||
|
|
||||||
/* refractimer (seg002:36) — «отдышка» стража после того, как его ранили. */
|
/* refractimer (seg002:36) — «отдышка» стража после того, как его ранили. */
|
||||||
@@ -1008,10 +1009,40 @@ static const uint8_t REFRACTIMER[NUM_GUARD_SKILLS] =
|
|||||||
/* hurt_by_sword (seg002): применить попадание к АКТИВНОМУ персонажу.
|
/* hurt_by_sword (seg002): применить попадание к АКТИВНОМУ персонажу.
|
||||||
* Без меча в руке любое попадание смертельно; с мечом — минус 1 HP и кадр
|
* Без меча в руке любое попадание смертельно; с мечом — минус 1 HP и кадр
|
||||||
* «получил удар».
|
* «получил удар».
|
||||||
* УПРОЩЕНИЕ: ветку «сбит с уступа» (seq_81, когда сзади пусто и до кромки
|
*
|
||||||
* меньше 4) не портируем — ей нужны тайловые запросы ОТ Char, а pop_map
|
* СМЕРТЬ БЕЗОРУЖНОГО БЫВАЕТ ДВУХ ВИДОВ, и выбор между ними делается по
|
||||||
* пока умеет только от Kid. На ровном полу (тайл сзади не пустой) оригинал
|
* обстановке ПОЗАДИ (seg002): если сзади есть опора или до кромки меньше
|
||||||
* идёт ровно нашей веткой. */
|
* четырёх — «заколот на месте» (seq_85); если сзади пусто и от кромки
|
||||||
|
* далеко — «сброшен с уступа» (seq_81), отдельная последовательность,
|
||||||
|
* которая сама отыгрывает падение замертво.
|
||||||
|
*
|
||||||
|
* Вторая ветка появилась 2026-08-31 (docs/sdlpop_audit.md, находка 12):
|
||||||
|
* раньше её не было, потому что считалось, будто тайловые запросы от Char
|
||||||
|
* недоступны — на деле они давно работают, не хватало объявлений.
|
||||||
|
*
|
||||||
|
* Замечание к сцене: падение первым делом убирает меч (start_fall), так
|
||||||
|
* что персонаж, сбитый в пропасть во время боя, к следующему удару уже
|
||||||
|
* безоружен — и попадает сюда же. */
|
||||||
|
/* ПРИЖАТЬ К ПОЛУ СВОЕГО РЯДА — только для ПЕРЕЖИВШЕГО удар.
|
||||||
|
*
|
||||||
|
* В оригинале (seg002, ветка seq_74_hit_by_sword) эти две строки стоят
|
||||||
|
* ВНУТРИ ветки выжившего; смертельные ветки координату не трогают.
|
||||||
|
*
|
||||||
|
* У нас они долго выполнялись безусловно и работали СТРАХОВКОЙ за
|
||||||
|
* отсутствующую ветку «сброшен с уступа»: убитого в воздухе прижимали к
|
||||||
|
* полу, иначе он продолжал падать и выпадал за нижнюю границу, а игра
|
||||||
|
* уходила на рестарт, не показав тела (проверено на живой машине
|
||||||
|
* 2026-08-31 — правка в одиночку ломала смерть).
|
||||||
|
*
|
||||||
|
* Снято ПОСЛЕ появления ветки seq_81: теперь смерть у обрыва отыгрывает
|
||||||
|
* своя последовательность, и страховка больше не нужна. Порядок именно
|
||||||
|
* такой и важен — сперва ветка, потом снятие. */
|
||||||
|
static void hurt_stand_on_floor(void)
|
||||||
|
{
|
||||||
|
Char.y = (uint8_t)pop_y_land[Char.curr_row + 1];
|
||||||
|
Char.fall_y = 0;
|
||||||
|
}
|
||||||
|
|
||||||
static void hurt_by_sword(void)
|
static void hurt_by_sword(void)
|
||||||
{
|
{
|
||||||
if (Char.alive >= 0) return;
|
if (Char.alive >= 0) return;
|
||||||
@@ -1032,19 +1063,22 @@ static void hurt_by_sword(void)
|
|||||||
* Костыля «снять бессмертие на время вызова» здесь БОЛЬШЕ НЕТ:
|
* Костыля «снять бессмертие на время вызова» здесь БОЛЬШЕ НЕТ:
|
||||||
* pop_take_hp гасит только урон меньше 100, а тут ровно 100. */
|
* pop_take_hp гасит только урон меньше 100, а тут ровно 100. */
|
||||||
pop_take_hp(100);
|
pop_take_hp(100);
|
||||||
pop_char_set_seq(SEQ_85_STABBED_TO_DEATH);
|
if (pop_tile_behind_char() != 0 || pop_dist_to_edge_weight() < 4)
|
||||||
|
pop_char_set_seq(SEQ_85_STABBED_TO_DEATH); /* есть опора сзади */
|
||||||
|
else
|
||||||
|
pop_char_set_seq(SEQ_81_PUSHED_OFF_LEDGE); /* сзади обрыв */
|
||||||
} else if (Char.charid == CHARID_0_KID && pop_immortal) {
|
} else if (Char.charid == CHARID_0_KID && pop_immortal) {
|
||||||
/* ЧИТ, уровень 1 и выше: в боевой стойке удары не отнимают HP.
|
/* ЧИТ, уровень 1 и выше: в боевой стойке удары не отнимают HP.
|
||||||
* Кадр «получил удар» оставляем — иначе бой перестаёт читаться,
|
* Кадр «получил удар» оставляем — иначе бой перестаёт читаться,
|
||||||
* да и оригинал на выживший удар ставит ровно его. */
|
* да и оригинал на выживший удар ставит ровно его. */
|
||||||
pop_char_set_seq(SEQ_74_HIT_BY_SWORD);
|
pop_char_set_seq(SEQ_74_HIT_BY_SWORD);
|
||||||
|
hurt_stand_on_floor();
|
||||||
} else if (Char.charid != CHARID_4_SKELETON && pop_take_hp(1)) {
|
} else if (Char.charid != CHARID_4_SKELETON && pop_take_hp(1)) {
|
||||||
pop_char_set_seq(SEQ_85_STABBED_TO_DEATH); /* HP кончились */
|
pop_char_set_seq(SEQ_85_STABBED_TO_DEATH); /* HP кончились */
|
||||||
} else {
|
} else {
|
||||||
pop_char_set_seq(SEQ_74_HIT_BY_SWORD);
|
pop_char_set_seq(SEQ_74_HIT_BY_SWORD);
|
||||||
|
hurt_stand_on_floor();
|
||||||
}
|
}
|
||||||
Char.y = (uint8_t)pop_y_land[Char.curr_row + 1];
|
|
||||||
Char.fall_y = 0;
|
|
||||||
/* seg002:0C1F: у Кида свой звук боли, у соперника свой. */
|
/* seg002:0C1F: у Кида свой звук боли, у соперника свой. */
|
||||||
pop_sfx_play((uint8_t)(Char.charid == CHARID_0_KID ? 13 : 12));
|
pop_sfx_play((uint8_t)(Char.charid == CHARID_0_KID ? 13 : 12));
|
||||||
play_seq();
|
play_seq();
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <sprinter_mem.h>
|
#include <sprinter_mem.h>
|
||||||
#include "pop_arc.h"
|
#include "pop_arc.h"
|
||||||
|
#include "_pop_file.h"
|
||||||
|
|
||||||
/* Пути 8.3, как требует DSS. Держим их ЗДЕСЬ: строка обязана лежать в том
|
/* Пути 8.3, как требует DSS. Держим их ЗДЕСЬ: строка обязана лежать в том
|
||||||
* же банке, что и код, который её читает (см. pop_arc.h). */
|
* же банке, что и код, который её читает (см. pop_arc.h). */
|
||||||
@@ -42,7 +43,7 @@ int8_t pop_arc_open(pop_arc_t *a, const char *path,
|
|||||||
|
|
||||||
a->fd = -1;
|
a->fd = -1;
|
||||||
a->count = 0;
|
a->count = 0;
|
||||||
fd = open(path, O_RDONLY);
|
POP_PATH_CALL(fd, open(path, O_RDONLY));
|
||||||
if (fd < 0) return -1;
|
if (fd < 0) return -1;
|
||||||
if (read(fd, head, sizeof(head)) != (int)sizeof(head) ||
|
if (read(fd, head, sizeof(head)) != (int)sizeof(head) ||
|
||||||
memcmp(head, "PBA1", 4) != 0 ||
|
memcmp(head, "PBA1", 4) != 0 ||
|
||||||
|
|||||||
@@ -989,11 +989,24 @@ void pop_mirror_draw(int clip_top) __banked
|
|||||||
* собирает неверно — SUB затирает A, и в s уезжает разность (memory
|
* собирает неверно — SUB затирает A, и в s уезжает разность (memory
|
||||||
* sdcc_z80_cmp_store_a_bug). */
|
* sdcc_z80_cmp_store_a_bug). */
|
||||||
static uint8_t hp_todo; /* сколько страниц ещё обновить */
|
static uint8_t hp_todo; /* сколько страниц ещё обновить */
|
||||||
|
/* Сколько страниц ещё стереть ЦЕЛИКОМ. Обычное обновление щадит зону
|
||||||
|
* статус-текста (иначе он мигал бы на каждом изменении жизней), но после
|
||||||
|
* рестарта уровня в этой зоне остаются деления ПРОШЛОГО боя: полоса стража
|
||||||
|
* при большом запасе HP заходит под текст, и щадящая чистка их не трогает.
|
||||||
|
* Симптом — «после гибели и Ctrl+A на одной из страниц осталась полоса по
|
||||||
|
* результатам боя» (BUGS_OPEN, HP-BAR-RESTART). */
|
||||||
|
static uint8_t hp_wipe; /* сколько страниц ещё стереть */
|
||||||
static uint8_t hp_kid_prev, hp_kidmax_prev, hp_gd_prev, hp_gdmax_prev;
|
static uint8_t hp_kid_prev, hp_kidmax_prev, hp_gd_prev, hp_gdmax_prev;
|
||||||
|
/* Страница, в которую лёг прошлый проход. Счётчики выше считают СТРАНИЦЫ,
|
||||||
|
* но кадр и страница — не одно и то же: между двумя вызовами переворота
|
||||||
|
* может не быть, и тогда оба прохода уходили в ОДНУ страницу, а вторая
|
||||||
|
* оставалась с делениями прошлого боя. Ровно этим полоса и переживала
|
||||||
|
* Ctrl+A (BUGS_OPEN, HP-BAR-RESTART). */
|
||||||
|
static uint8_t hp_page_prev = 0xFF;
|
||||||
|
|
||||||
/* Заставить перерисовать полосу на обеих страницах: звать при входе в
|
/* Заставить перерисовать полосу на обеих страницах: звать при входе в
|
||||||
* комнату (фон перерисован целиком и стёр её) и при старте. */
|
* комнату (фон перерисован целиком и стёр её) и при старте. */
|
||||||
void pop_hp_invalidate(void) __banked { hp_todo = 2; }
|
void pop_hp_invalidate(void) __banked { hp_todo = 2; hp_wipe = 2; }
|
||||||
|
|
||||||
void pop_hp_draw(void) __banked
|
void pop_hp_draw(void) __banked
|
||||||
{
|
{
|
||||||
@@ -1010,6 +1023,13 @@ void pop_hp_draw(void) __banked
|
|||||||
if (changed) hp_todo = 2;
|
if (changed) hp_todo = 2;
|
||||||
}
|
}
|
||||||
if (hp_todo == 0) return;
|
if (hp_todo == 0) return;
|
||||||
|
{ /* Пока страница та же, что у прошлого прохода, — ждём переворота:
|
||||||
|
* иначе потратим оба прохода на одну страницу. Первый проход
|
||||||
|
* (hp_todo == 2) идёт всегда, ему сравнивать не с чем. */
|
||||||
|
uint8_t pg = gfx_get_draw_page();
|
||||||
|
if (hp_todo < 2 && pg == hp_page_prev) return;
|
||||||
|
hp_page_prev = pg;
|
||||||
|
}
|
||||||
hp_todo--;
|
hp_todo--;
|
||||||
|
|
||||||
/* Стереть прошлую полосу. Фон под ней — НЕ цвет 0, а POP_COL_OUTSIDE
|
/* Стереть прошлую полосу. Фон под ней — НЕ цвет 0, а POP_COL_OUTSIDE
|
||||||
@@ -1021,7 +1041,14 @@ void pop_hp_draw(void) __banked
|
|||||||
* текст, его зону не трогаем — иначе он мигал бы при каждом изменении
|
* текст, его зону не трогаем — иначе он мигал бы при каждом изменении
|
||||||
* жизней. Деления Кида левее POP_STATUS_L, стража — правее POP_STATUS_R,
|
* жизней. Деления Кида левее POP_STATUS_L, стража — правее POP_STATUS_R,
|
||||||
* так что чистить края по отдельности достаточно. */
|
* так что чистить края по отдельности достаточно. */
|
||||||
if (pop_status_ticks) {
|
if (hp_wipe) {
|
||||||
|
/* Полная чистка обеих страниц: старая полоса могла заходить под
|
||||||
|
* текст. Текст при этом стирается тоже, поэтому сразу просим
|
||||||
|
* перерисовать и его — иначе строка уровня пропала бы. */
|
||||||
|
hp_wipe--;
|
||||||
|
bar(0, HP_Y + POP_YOFF, 319, HP_Y + POP_YOFF + 6);
|
||||||
|
if (pop_status_ticks) pop_status_invalidate();
|
||||||
|
} else if (pop_status_ticks) {
|
||||||
bar(0, HP_Y + POP_YOFF, POP_STATUS_L - 1, HP_Y + POP_YOFF + 6);
|
bar(0, HP_Y + POP_YOFF, POP_STATUS_L - 1, HP_Y + POP_YOFF + 6);
|
||||||
bar(POP_STATUS_R + 1, HP_Y + POP_YOFF, 319, HP_Y + POP_YOFF + 6);
|
bar(POP_STATUS_R + 1, HP_Y + POP_YOFF, 319, HP_Y + POP_YOFF + 6);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
#include "pop_cheat.h"
|
#include "pop_cheat.h"
|
||||||
#include "pop_sfx.h"
|
#include "pop_sfx.h"
|
||||||
#include "_pop_config.h"
|
#include "_pop_config.h"
|
||||||
|
#include "_pop_file.h"
|
||||||
|
|
||||||
#define CFG_BASE 0x100
|
#define CFG_BASE 0x100
|
||||||
|
|
||||||
@@ -151,7 +152,7 @@ static uint8_t cfg_read_file(uint8_t *buf, uint16_t *size)
|
|||||||
int fd, got;
|
int fd, got;
|
||||||
long n;
|
long n;
|
||||||
|
|
||||||
fd = open("POP.CFG", O_RDONLY);
|
POP_PATH_CALL(fd, open("POP.CFG", O_RDONLY));
|
||||||
if (fd < 0) return 0;
|
if (fd < 0) return 0;
|
||||||
n = lseek(fd, 0, SEEK_END);
|
n = lseek(fd, 0, SEEK_END);
|
||||||
if (n < POP_CFG_MIN_SIZE || n > POP_CFG_MAX_SIZE ||
|
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 (!cfg_read_file(buf, &size) || !pop_cfg_decode(buf, size)) return 0;
|
||||||
if (buf[9] != POP_PROFILE_VANILLA)
|
if (buf[9] != POP_PROFILE_VANILLA)
|
||||||
printf("POP.CFG: ENHANCED profile -> VANILLA\n");
|
puts("POP.CFG: ENHANCED profile -> VANILLA\n");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,7 +193,7 @@ int8_t pop_cfg_save(void) __banked
|
|||||||
gfx_w0_unmap();
|
gfx_w0_unmap();
|
||||||
/* Не unlink: bank_save_file сам делает O_TRUNC и не создаёт лишнего
|
/* Не unlink: bank_save_file сам делает O_TRUNC и не создаёт лишнего
|
||||||
* окна, когда POP.CFG вообще отсутствует после ошибки unlink. */
|
* окна, когда 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);
|
mem_free_block(blk);
|
||||||
return (rc == sizeof(buf)) ? 1 : 0;
|
return (rc == sizeof(buf)) ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
#include "pop_title.h" /* титульная картинка после ввода имени */
|
#include "pop_title.h" /* титульная картинка после ввода имени */
|
||||||
#include "pop_music.h" /* подкачка потоковой темы победы */
|
#include "pop_music.h" /* подкачка потоковой темы победы */
|
||||||
#include "pop_hof.h"
|
#include "pop_hof.h"
|
||||||
|
#include "_pop_file.h"
|
||||||
|
|
||||||
/* MAX_HOF_COUNT оригинала — шесть строк. */
|
/* MAX_HOF_COUNT оригинала — шесть строк. */
|
||||||
#define HOF_COUNT 6
|
#define HOF_COUNT 6
|
||||||
@@ -162,7 +163,7 @@ static void hof_load(void)
|
|||||||
if (!blk) return;
|
if (!blk) return;
|
||||||
page = mem_get_page(blk, 0);
|
page = mem_get_page(blk, 0);
|
||||||
gfx_w0_page_prepare(page);
|
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) {
|
if (got == HOF_SIZE) {
|
||||||
gfx_w0_map(page);
|
gfx_w0_map(page);
|
||||||
for (i = 0; i < HOF_SIZE; i++)
|
for (i = 0; i < HOF_SIZE; i++)
|
||||||
@@ -187,7 +188,11 @@ static void hof_save(void)
|
|||||||
gfx_w0_unmap();
|
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);
|
mem_free_block(blk);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
#include "pop_shadow.h"
|
#include "pop_shadow.h"
|
||||||
#include "pop_sfx.h"
|
#include "pop_sfx.h"
|
||||||
#include "pop_music.h"
|
#include "pop_music.h"
|
||||||
#include "pop_music_ticks.h" /* POP_MUS_TICKS_<id> — длина реплик набора */
|
|
||||||
#include "pop_arc.h"
|
#include "pop_arc.h"
|
||||||
#include "pv_arc.h"
|
#include "pv_arc.h"
|
||||||
#include "pop_cutscene.h"
|
#include "pop_cutscene.h"
|
||||||
@@ -60,9 +59,10 @@
|
|||||||
* своего куска (например, всё поведение Джафара — от PV_DIALOG1_START).
|
* своего куска (например, всё поведение Джафара — от PV_DIALOG1_START).
|
||||||
* 2) «ЖДЁМ КОНЦА РЕПЛИКИ» — PV_M50_END и PV_EXIT_START. Это целиком
|
* 2) «ЖДЁМ КОНЦА РЕПЛИКИ» — PV_M50_END и PV_EXIT_START. Это целиком
|
||||||
* длина НАШЕЙ записи, и она у каждого набора своя: m50 — 831 тик на
|
* длина НАШЕЙ записи, и она у каждого набора своя: m50 — 831 тик на
|
||||||
* flac против 867 на mt32, m53 — 985 против 1044. Поэтому берём их
|
* flac против 867 на mt32, m53 — 985 против 1044. Раньше их
|
||||||
* из POP_MUS_TICKS_<id>, которые печатает упаковщик того набора,
|
* печатал упаковщик в gen/pop_music_ticks.h, из-за чего EXE был
|
||||||
* которым собрана сборка (gen/pop_music_tbl.h).
|
* привязан к набору; теперь они приезжают из индекса набора
|
||||||
|
* (MUS/mus.idx) при входе в сцену — см. ЯКОРЯ ниже.
|
||||||
*
|
*
|
||||||
* Всё, что стоит ПОСЛЕ реплики, сдвигается вместе с её концом — отсюда
|
* Всё, что стоит ПОСЛЕ реплики, сдвигается вместе с её концом — отсюда
|
||||||
* сложения от PV_M50_END и PV_EXIT_START вместо литералов. */
|
* сложения от PV_M50_END и PV_EXIT_START вместо литералов. */
|
||||||
@@ -72,48 +72,76 @@ enum {
|
|||||||
/* Конец m50: кадр становится 8. Прежний литерал 846 отвечал длине 834
|
/* Конец m50: кадр становится 8. Прежний литерал 846 отвечал длине 834
|
||||||
* тика — на три больше нашей записи (831); расхождение осталось с
|
* тика — на три больше нашей записи (831); расхождение осталось с
|
||||||
* времён, когда длину подставляли руками. */
|
* времён, когда длину подставляли руками. */
|
||||||
PV_M50_END = PV_MUS_2_START + POP_MUS_TICKS_50,
|
PV_MAGIC_LEAD = 0
|
||||||
PV_WAIT_END = PV_M50_END + 40, /* +frame(5) -> створка ворот */
|
|
||||||
PV_GATE_END = PV_WAIT_END + 48,
|
|
||||||
PV_DOOR_END = PV_GATE_END + 24,
|
|
||||||
PV_TURN_START = PV_DOOR_END,
|
|
||||||
PV_WALK1_START = PV_DOOR_END + 40,
|
|
||||||
PV_DIALOG1_START = PV_WALK1_START + 48, /* Джафар входит — реплика m53 */
|
|
||||||
/* Ниже — биты сценария внутри реплики Джафара, все от её начала. */
|
|
||||||
PV_MUS_4_LOAD = PV_DIALOG1_START + 54, /* подкачка m52 */
|
|
||||||
PV_WALK2_START = PV_DIALOG1_START + 176,
|
|
||||||
PV_DIALOG2_START = PV_DIALOG1_START + 416,
|
|
||||||
/* ЗАКЛИНАНИЕ СДВИНУТО РАНЬШЕ СЦЕНАРИЯ — ПОДГОНКА ПОД МУЗЫКУ.
|
|
||||||
*
|
|
||||||
* Сцена render-bound: кадр рисуется дороже своего бюджета, кадровая
|
|
||||||
* ветка пейсинга догона не имеет, и анимация идёт ~49 тиков в секунду
|
|
||||||
* вместо 60. Реплика Джафара при этом играет по РЕАЛЬНОМУ времени и
|
|
||||||
* успевает кончиться, когда шкала стоит на 1854 вместо 2028 (замер в
|
|
||||||
* MAME 2026-08-27 зондом на конец трека: длина насосу отдана полная,
|
|
||||||
* 1403 порции, курсор дошёл до последней страницы — трек доигрывает
|
|
||||||
* целиком, отстаёт именно картинка).
|
|
||||||
*
|
|
||||||
* Из-за этого кода реплики приходила примерно на секунду раньше
|
|
||||||
* молнии. Честное лечение — удешевить кадр сцены; до тех пор двигаем
|
|
||||||
* жест целиком (замах, шаг назад, вспышка), чтобы он не сломался
|
|
||||||
* внутри себя. Величина подобрана НА СЛУХ и уточняется прогоном:
|
|
||||||
* расчётные полсотни тиков (секунда отставания при ~49 тиках/с) на
|
|
||||||
* слух почти не изменили картины, поэтому взято вдвое больше.
|
|
||||||
*
|
|
||||||
* Эффектов дверей это не касается: у них и звук, и картинка идут по
|
|
||||||
* одной и той же шкале и разъехаться не могут. */
|
|
||||||
PV_MAGIC_LEAD = 100,
|
|
||||||
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,
|
|
||||||
/* Конец m53 — снова длина записи, а не бит сценария. */
|
|
||||||
PV_EXIT_START = PV_DIALOG1_START + POP_MUS_TICKS_53,
|
|
||||||
PV_MUS_4_START = PV_EXIT_START + 42, /* +frame(6) -> «Джафар уходит» */
|
|
||||||
PV_GLASS_DONE = PV_EXIT_START + 210,
|
|
||||||
PV_SLUMP_START = PV_EXIT_START + 273,
|
|
||||||
PV_ANIM_TICKS = PV_EXIT_START + 469
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* ПОДГОНКА ЗАКЛИНАНИЯ ПОД МУЗЫКУ СНЯТА (2026-08-28): теперь 0.
|
||||||
|
*
|
||||||
|
* История: сцена была render-bound — кадр рисовался дороже своего
|
||||||
|
* бюджета, кадровая ветка пейсинга догона не имела, и анимация шла
|
||||||
|
* ~49 тиков в секунду вместо 60 (замер 2026-08-27). Реплика Джафара
|
||||||
|
* при этом играет по РЕАЛЬНОМУ времени, поэтому кода приходила
|
||||||
|
* примерно на секунду раньше молнии, и жест целиком (замах, шаг
|
||||||
|
* назад, вспышка) сдвигали раньше сценария на подобранные на слух
|
||||||
|
* 100 тиков.
|
||||||
|
*
|
||||||
|
* Причину убрали: отрисовка разложена по кадровым интервалам
|
||||||
|
* (pv_restore_bg), сцена идёт по единственным часам — кадрам луча, —
|
||||||
|
* и подкачка трека оплачивается интервалом (pop_music_load_step).
|
||||||
|
* После этого подгонка стала вредной: молния била больше чем на
|
||||||
|
* секунду раньше коды (проверка пользователем 2026-08-28). Константу
|
||||||
|
* оставляем на месте — если запись другого набора (mt32/ogg) снова
|
||||||
|
* разъедется, крутить надо её, а не тайминги сценария. */
|
||||||
|
|
||||||
|
/* ЯКОРЯ ШКАЛЫ. Четыре точки, от которых отсчитывается всё остальное;
|
||||||
|
* считаются ОДИН РАЗ при входе в сцену из длительностей набора
|
||||||
|
* (pop_music_info). Держать их статикой, а не тащить структуру времён
|
||||||
|
* через пять функций: восемь байт против переделки всех сигнатур, а
|
||||||
|
* сцена всё равно одна на запуск.
|
||||||
|
*
|
||||||
|
* Нет индекса или трека — длительность нулевая, и сцена просто проходит
|
||||||
|
* без пауз на реплики: ждать нечего, звука-то нет. */
|
||||||
|
static uint16_t pv_m50_end; /* конец «принцесса ждёт» (m50) */
|
||||||
|
static uint16_t pv_dialog1; /* Джафар входит — начало реплики m53 */
|
||||||
|
static uint16_t pv_exit; /* конец m53: Джафар уходит */
|
||||||
|
static uint16_t pv_anim_end; /* конец всей сцены */
|
||||||
|
|
||||||
|
/* Ниже — те же выражения, что были в enum: биты сценария отсчитываются от
|
||||||
|
* своего якоря и от записи НЕ зависят. */
|
||||||
|
#define PV_M50_END pv_m50_end
|
||||||
|
#define PV_WAIT_END ((uint16_t)(pv_m50_end + 40))
|
||||||
|
#define PV_GATE_END ((uint16_t)(pv_m50_end + 88))
|
||||||
|
#define PV_DOOR_END ((uint16_t)(pv_m50_end + 112))
|
||||||
|
#define PV_TURN_START PV_DOOR_END
|
||||||
|
#define PV_WALK1_START ((uint16_t)(pv_m50_end + 152))
|
||||||
|
#define PV_DIALOG1_START pv_dialog1
|
||||||
|
#define PV_MUS_4_LOAD ((uint16_t)(pv_dialog1 + 54))
|
||||||
|
#define PV_WALK2_START ((uint16_t)(pv_dialog1 + 176))
|
||||||
|
#define PV_DIALOG2_START ((uint16_t)(pv_dialog1 + 416))
|
||||||
|
#define PV_RAISE_START ((uint16_t)(pv_dialog1 + 696 - PV_MAGIC_LEAD))
|
||||||
|
#define PV_STEPBACK_START ((uint16_t)(pv_dialog1 + 703 - PV_MAGIC_LEAD))
|
||||||
|
#define PV_MAGIC_START ((uint16_t)(pv_dialog1 + 822 - PV_MAGIC_LEAD))
|
||||||
|
#define PV_EXIT_START pv_exit
|
||||||
|
#define PV_MUS_4_START ((uint16_t)(pv_exit + 42))
|
||||||
|
#define PV_GLASS_DONE ((uint16_t)(pv_exit + 210))
|
||||||
|
#define PV_SLUMP_START ((uint16_t)(pv_exit + 273))
|
||||||
|
#define PV_ANIM_TICKS pv_anim_end
|
||||||
|
|
||||||
|
/* Собрать шкалу под НАШ набор записей. 200 — цепочка битов сценария от
|
||||||
|
* конца m50 до входа Джафара (40+48+24+40+48), 469 — хвост после его
|
||||||
|
* ухода; оба от записи не зависят. */
|
||||||
|
static void pv_timing_init(void)
|
||||||
|
{
|
||||||
|
pop_mus_info_t m50, m53;
|
||||||
|
|
||||||
|
(void)pop_music_info(POP_MUS_STORY_2, &m50);
|
||||||
|
(void)pop_music_info(POP_MUS_STORY_3, &m53);
|
||||||
|
pv_m50_end = (uint16_t)(PV_MUS_2_START + m50.ticks);
|
||||||
|
pv_dialog1 = (uint16_t)(pv_m50_end + 200);
|
||||||
|
pv_exit = (uint16_t)(pv_dialog1 + m53.ticks);
|
||||||
|
pv_anim_end = (uint16_t)(pv_exit + 469);
|
||||||
|
}
|
||||||
|
|
||||||
/* Возврат долга шкалы (см. цикл сцены): сколько порций насоса отдаём за один
|
/* Возврат долга шкалы (см. цикл сцены): сколько порций насоса отдаём за один
|
||||||
* кадр сцены и сколько их вообще имеет смысл копить. 1 порция — 11,7 мс;
|
* кадр сцены и сколько их вообще имеет смысл копить. 1 порция — 11,7 мс;
|
||||||
* 128 порций — полторы секунды, дальше догонять уже нечего. */
|
* 128 порций — полторы секунды, дальше догонять уже нечего. */
|
||||||
@@ -430,6 +458,44 @@ static int intro_pv_draw_base(void)
|
|||||||
return 0;
|
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)
|
static void pv_blit_floor(const atlas_t *at, uint8_t frame, int x, int floor)
|
||||||
{
|
{
|
||||||
const uint8_t *img;
|
const uint8_t *img;
|
||||||
@@ -701,12 +767,10 @@ static uint8_t intro_pv_draw_frame(const atlas_t *princess, const atlas_t *j0,
|
|||||||
uint8_t sand_frame, uint8_t flash,
|
uint8_t sand_frame, uint8_t flash,
|
||||||
uint16_t tick)
|
uint16_t tick)
|
||||||
{
|
{
|
||||||
gfx_rect_t full;
|
|
||||||
uint8_t back = (uint8_t)(gfx_get_visible_page() ^ 1);
|
uint8_t back = (uint8_t)(gfx_get_visible_page() ^ 1);
|
||||||
|
|
||||||
gfx_set_draw_page(back);
|
gfx_set_draw_page(back);
|
||||||
full.x = 0; full.y = 0; full.w = GFX_WIDTH; full.h = GFX_HEIGHT;
|
pv_restore_bg(); /* блоки 1-2: фон, два интервала */
|
||||||
gfx_copy_page(&full, GFX_COPY_DIRECT);
|
|
||||||
gfx_set_bank(GFX_BANK_SPRITE);
|
gfx_set_bank(GFX_BANK_SPRITE);
|
||||||
/* y=116 в princess_room_torch — нижняя строка, не верх спрайта. */
|
/* y=116 в princess_room_torch — нижняя строка, не верх спрайта. */
|
||||||
pv_blit_floor(torch, torch_left, 93, 116);
|
pv_blit_floor(torch, torch_left, 93, 116);
|
||||||
@@ -730,7 +794,7 @@ static uint8_t intro_pv_draw_frame(const atlas_t *princess, const atlas_t *j0,
|
|||||||
* его надо повторить ПОСЛЕ актёров, иначе Jaffar проходит перед
|
* его надо повторить ПОСЛЕ актёров, иначе Jaffar проходит перед
|
||||||
* правой передней колонной. */
|
* правой передней колонной. */
|
||||||
pv_blit_floor(hourglass, 0, 240, 167);
|
pv_blit_floor(hourglass, 0, 240, 167);
|
||||||
gfx_wait_vsync();
|
gfx_wait_vsync(); /* блок 3: актёры и декорации */
|
||||||
gfx_set_visible_page(back);
|
gfx_set_visible_page(back);
|
||||||
if (flash) {
|
if (flash) {
|
||||||
uint8_t pulse;
|
uint8_t pulse;
|
||||||
@@ -743,9 +807,9 @@ static uint8_t intro_pv_draw_frame(const atlas_t *princess, const atlas_t *j0,
|
|||||||
pop_pal_flash_white(0);
|
pop_pal_flash_white(0);
|
||||||
gfx_wait_vsync();
|
gfx_wait_vsync();
|
||||||
}
|
}
|
||||||
return 11;
|
return 13; /* три интервала кадра + десять на молнию */
|
||||||
}
|
}
|
||||||
return 1;
|
return 3; /* сколько кадровых интервалов съели */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Сцена pv_scene() SDLPoP: ожидание Princess с факелами/звёздами, поворот,
|
/* Сцена pv_scene() SDLPoP: ожидание Princess с факелами/звёздами, поворот,
|
||||||
@@ -759,12 +823,7 @@ static int intro_pv_animated(void)
|
|||||||
uint16_t tick;
|
uint16_t tick;
|
||||||
uint16_t frame_end;
|
uint16_t frame_end;
|
||||||
uint16_t render_tick;
|
uint16_t render_tick;
|
||||||
uint16_t beam = 0; /* запасной путь: кадров луча ждали */
|
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;
|
|
||||||
uint8_t period;
|
uint8_t period;
|
||||||
uint8_t anim_step = 0;
|
uint8_t anim_step = 0;
|
||||||
uint8_t torch_left = 1;
|
uint8_t torch_left = 1;
|
||||||
@@ -813,18 +872,22 @@ static int intro_pv_animated(void)
|
|||||||
* Полноэкранная accel-копия кадра насосу не мешает: leaf режет DI
|
* Полноэкранная accel-копия кадра насосу не мешает: leaf режет DI
|
||||||
* бандами по 16 строк. */
|
* бандами по 16 строк. */
|
||||||
(void)pop_sfx_start();
|
(void)pop_sfx_start();
|
||||||
/* ЧЕМ МЕРИТЬ ВРЕМЯ. Пока сцена была немой, кадры просто считали
|
/* ЧЕМ МЕРИТЬ ВРЕМЯ — ЕДИНЫЕ ЧАСЫ: КАДРЫ ЛУЧА.
|
||||||
* ожидания vsync — и этого хватало. С музыкой выяснилось, что так
|
*
|
||||||
* считаются НАШИ ожидания, а не прошедшее время: полноэкранная копия
|
* Была вторая шкала — тики насоса CBL, и выбор между ними делался
|
||||||
* страницы (~0,55 млн тактов) съедает больше кадра луча, и сцена
|
* гонкой на старте сцены (одна выборка `pop_snd_tick` через кадр:
|
||||||
* незаметно отстаёт — в MAME молния била уже ПОСЛЕ конца реплики.
|
* успел ли диск раскрутить звук). Значит от прогона к прогону сцена
|
||||||
* Часы берём у насоса CBL: он идёт от расхода буфера железом, 85,4 Гц,
|
* шла то по одним часам, то по другим, и кода реплики приходилась
|
||||||
* и ему безразлично, чем занят главный цикл. Если звук выключен,
|
* каждый раз на другое место картинки (наблюдение пользователя
|
||||||
* часы стоят — тогда работает прежний путь по кадрам луча. */
|
* 2026-08-28; баг SND-PACE-DEAD). Насос как часы был нужен потому,
|
||||||
snd_prev = pop_snd_tick;
|
* что кадр сцены рисовался ДОЛЬШЕ своего интервала и счёт наших
|
||||||
gfx_wait_vsync();
|
* ожиданий врал; теперь отрисовка разложена на блоки по кадровому
|
||||||
snd_pace = (uint8_t)(pop_snd_tick != snd_prev); /* часы реально идут? */
|
* интервалу (см. pv_restore_bg), и счёт кадров луча честен.
|
||||||
snd_prev = pop_snd_tick;
|
*
|
||||||
|
* Осталось учесть единственную работу, которая в интервал не влезала
|
||||||
|
* сама, — подкачку страницы трека: шаг читает ПОЛСТРАНИЦЫ (~16 мс,
|
||||||
|
* pop_music_load_step) и оплачивается ровно одним интервалом, как
|
||||||
|
* любой другой блок кадра. */
|
||||||
result = 0;
|
result = 0;
|
||||||
princess_actor.x = 120;
|
princess_actor.x = 120;
|
||||||
princess_actor.frame = 11; /* Pstand, facing left */
|
princess_actor.frame = 11; /* Pstand, facing left */
|
||||||
@@ -837,6 +900,7 @@ static int intro_pv_animated(void)
|
|||||||
jaffar_actor.visible = 0;
|
jaffar_actor.visible = 0;
|
||||||
jaffar_actor.step = 0;
|
jaffar_actor.step = 0;
|
||||||
tick = 0;
|
tick = 0;
|
||||||
|
pv_timing_init(); /* шкала под длительности НАШЕГО набора */
|
||||||
intro_skip_begin(&skip);
|
intro_skip_begin(&skip);
|
||||||
while (tick < PV_ANIM_TICKS) {
|
while (tick < PV_ANIM_TICKS) {
|
||||||
if (intro_skip_requested(&skip)) {
|
if (intro_skip_requested(&skip)) {
|
||||||
@@ -892,59 +956,42 @@ static int intro_pv_animated(void)
|
|||||||
torch_right, sand_frame, flash,
|
torch_right, sand_frame, flash,
|
||||||
render_tick);
|
render_tick);
|
||||||
pop_sfx_tick(); /* как в оригинале — в конце отрисовки кадра */
|
pop_sfx_tick(); /* как в оригинале — в конце отрисовки кадра */
|
||||||
|
|
||||||
/* ГАРАНТИРОВАННЫЙ ШАГ ПОДКАЧКИ, а не «если останется время».
|
/* ГАРАНТИРОВАННЫЙ ШАГ ПОДКАЧКИ, а не «если останется время».
|
||||||
*
|
*
|
||||||
* Ниже страница следующей реплики читается в ПАУЗЕ кадра, и это
|
* Раньше страница следующей реплики читалась в ПАУЗЕ кадра, и это
|
||||||
* верно ровно до тех пор, пока пауза есть. В этой сцене её почти
|
* верно ровно до тех пор, пока пауза есть. В этой сцене её почти
|
||||||
* нет: кадр дорогой, шкала работает по принципу «долг не догоняем»
|
* не было: кадр дорогой, шкала работает по принципу «долг не
|
||||||
* (snd_want подтягивается к snd_done), и цикл ожидания выходит
|
* догоняем», и цикл ожидания выходил сразу. Замер в MAME
|
||||||
* сразу. Замер в MAME 2026-08-26: ОДНА страница за семь секунд,
|
* 2026-08-26: ОДНА страница за семь секунд, то есть одиннадцать
|
||||||
* то есть одиннадцать страниц m53 не успевали за всю сцену —
|
* страниц m53 не успевали за всю сцену — pop_music_play() на своём
|
||||||
* pop_music_play() на своём тике не находил готового трека, и
|
* тике не находил готового трека, и реплики Джафара пропадали
|
||||||
* реплики Джафара пропадали совсем.
|
* совсем (memory pv_music_stall_regression).
|
||||||
*
|
*
|
||||||
* Страница стоит 33 мс против кадра сцены в 100-133 мс, поэтому
|
* Поэтому шаг безусловный — и оплачен как обычный блок кадра: он
|
||||||
* один безусловный шаг в кадр сцену не ломает: он лишь тратит
|
* читает полстраницы (~16 мс), а следующий gfx_wait_vsync ставит
|
||||||
* часть той же паузы, а если паузы не было — добавляет к кадру
|
* нас на край интервала, который мы этим и заняли. */
|
||||||
* четверть его длины, и шкала это учтёт сама. */
|
if (pop_music_loading()) {
|
||||||
if (pop_music_loading()) (void)pop_music_load_step();
|
(void)pop_music_load_step();
|
||||||
|
gfx_wait_vsync();
|
||||||
|
beam++;
|
||||||
|
}
|
||||||
/* Первый кадр собран под чёрной палитрой — теперь проявляем его
|
/* Первый кадр собран под чёрной палитрой — теперь проявляем его
|
||||||
* целиком, вместе с принцессой и факелами. */
|
* целиком, вместе с принцессой и факелами. */
|
||||||
if (!faded_in) {
|
if (!faded_in) {
|
||||||
faded_in = 1;
|
faded_in = 1;
|
||||||
if (pop_pal_fade_in_skip(INTRO_FADE)) { result = 1; break; }
|
if (pop_pal_fade_in_skip(INTRO_FADE)) { result = 1; break; }
|
||||||
}
|
}
|
||||||
/* ПЕЙСИНГ ПО ОРИГИНАЛЬНОЙ ШКАЛЕ. Кадр cutscene — 6/8/7 тиков по
|
/* ПЕЙСИНГ ПО ОРИГИНАЛЬНОЙ ШКАЛЕ. Кадр сцены — 6/8/7 тиков по
|
||||||
* 1/60 с; один тик = 85,4/60 = 1,424 тика насоса, и 57/40 = 1,425
|
* 1/60 с (pv_seq_period), в кадры луча переводит POP_T60, и цель
|
||||||
* повторяет это с точностью 0,07 % (30 мс на всю сцену) без
|
* КУМУЛЯТИВНА: нецелые 6,67 и 5,83 не копят ошибку.
|
||||||
* деления в кадре и без 32-битной арифметики. */
|
*
|
||||||
if (snd_pace) {
|
* ДОЛГ НЕ ДОГОНЯЕМ: если кадр вышел дороже своего бюджета, цикл
|
||||||
snd_acc += (uint16_t)period * 57u;
|
* ожидания просто не сработает, но beam уже учёл потраченное — и
|
||||||
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 {
|
|
||||||
uint16_t target = POP_T60(tick);
|
uint16_t target = POP_T60(tick);
|
||||||
|
|
||||||
while (beam < target) { gfx_wait_vsync(); beam++; }
|
while (beam < target) { gfx_wait_vsync(); beam++; }
|
||||||
@@ -995,7 +1042,6 @@ static uint8_t pre_hourglass_state(void)
|
|||||||
static int pre_room_animated(uint8_t picture, uint8_t state, uint8_t frames)
|
static int pre_room_animated(uint8_t picture, uint8_t state, uint8_t frames)
|
||||||
{
|
{
|
||||||
atlas_t hourglass, torch;
|
atlas_t hourglass, torch;
|
||||||
gfx_rect_t full;
|
|
||||||
uint8_t torch_left = 1, torch_right = 6, sand = 0, frame;
|
uint8_t torch_left = 1, torch_right = 6, sand = 0, frame;
|
||||||
uint8_t star_phase[6] = { 1, 1, 1, 1, 1, 1 };
|
uint8_t star_phase[6] = { 1, 1, 1, 1, 1, 1 };
|
||||||
intro_skip_t skip;
|
intro_skip_t skip;
|
||||||
@@ -1025,7 +1071,6 @@ static int pre_room_animated(uint8_t picture, uint8_t state, uint8_t frames)
|
|||||||
|
|
||||||
pop_screen_present(1);
|
pop_screen_present(1);
|
||||||
result = 0;
|
result = 0;
|
||||||
full.x = 0; full.y = 0; full.w = GFX_WIDTH; full.h = GFX_HEIGHT;
|
|
||||||
intro_skip_begin(&skip);
|
intro_skip_begin(&skip);
|
||||||
for (frame = 0; frame < frames; frame++) {
|
for (frame = 0; frame < frames; frame++) {
|
||||||
uint8_t back, hold, star;
|
uint8_t back, hold, star;
|
||||||
@@ -1033,7 +1078,7 @@ static int pre_room_animated(uint8_t picture, uint8_t state, uint8_t frames)
|
|||||||
if (intro_skip_requested(&skip)) { result = 1; break; }
|
if (intro_skip_requested(&skip)) { result = 1; break; }
|
||||||
back = (uint8_t)(gfx_get_visible_page() ^ 1);
|
back = (uint8_t)(gfx_get_visible_page() ^ 1);
|
||||||
gfx_set_draw_page(back);
|
gfx_set_draw_page(back);
|
||||||
gfx_copy_page(&full, GFX_COPY_DIRECT);
|
pv_restore_bg(); /* блоки 1-2: фон, два интервала */
|
||||||
gfx_set_bank(GFX_BANK_SPRITE);
|
gfx_set_bank(GFX_BANK_SPRITE);
|
||||||
pv_blit_floor(&torch, torch_left, 93, 116);
|
pv_blit_floor(&torch, torch_left, 93, 116);
|
||||||
pv_blit_floor(&torch, torch_right, 211, 116);
|
pv_blit_floor(&torch, torch_right, 211, 116);
|
||||||
@@ -1046,10 +1091,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, (uint8_t)(8 + sand), 160, 164);
|
||||||
}
|
}
|
||||||
pv_blit_floor(&hourglass, 0, 240, 167); /* передняя колонна */
|
pv_blit_floor(&hourglass, 0, 240, 167); /* передняя колонна */
|
||||||
gfx_wait_vsync();
|
gfx_wait_vsync(); /* блок 3: живой фон */
|
||||||
gfx_set_visible_page(back);
|
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 идут по
|
/* Проявляем ПОСЛЕ первого кадра: иначе две секунды fade идут по
|
||||||
* пустой комнате, а часы и пламя возникают уже на свету. */
|
* пустой комнате, а часы и пламя возникают уже на свету. */
|
||||||
@@ -1128,8 +1174,14 @@ static void intro_restore_game_palette(void)
|
|||||||
/* Набор PV2 (res901..930) не влезает в одну EMM: image 0..16 в a1,
|
/* Набор PV2 (res901..930) не влезает в одну EMM: image 0..16 в a1,
|
||||||
* 17..29 в a2. Номер кадра при этом остаётся исходным image-id. */
|
* 17..29 в a2. Номер кадра при этом остаётся исходным image-id. */
|
||||||
#define CUT_PV2_SPLIT 17
|
#define CUT_PV2_SPLIT 17
|
||||||
/* Один логический кадр катсцены = 5 vsync, как у остальных PV-сцен
|
/* Один логический кадр катсцены = 5 vsync, как у остальных PV-сцен: у
|
||||||
* (cutscene_frame_time SDLPoP; см. pre_room_animated). */
|
* оригинала `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
|
#define CUT_FRAME_VSYNC 5
|
||||||
|
|
||||||
static atlas_t cut_pv1, cut_pv2a, cut_pv2b, cut_hg, cut_torch;
|
static atlas_t cut_pv1, cut_pv2a, cut_pv2b, cut_hg, cut_torch;
|
||||||
@@ -1290,11 +1342,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)
|
static uint8_t cut_run(uint16_t frames, uint8_t kid_visible)
|
||||||
{
|
{
|
||||||
gfx_rect_t full;
|
|
||||||
uint16_t i;
|
uint16_t i;
|
||||||
uint8_t hold;
|
uint8_t hold;
|
||||||
|
|
||||||
full.x = 0; full.y = 0; full.w = GFX_WIDTH; full.h = GFX_HEIGHT;
|
|
||||||
for (i = 0; i < frames; i++) {
|
for (i = 0; i < frames; i++) {
|
||||||
uint8_t back;
|
uint8_t back;
|
||||||
kframe kid_fr, pr_fr;
|
kframe kid_fr, pr_fr;
|
||||||
@@ -1316,8 +1366,9 @@ static uint8_t cut_run(uint16_t frames, uint8_t kid_visible)
|
|||||||
back = (uint8_t)(gfx_get_visible_page() ^ 1);
|
back = (uint8_t)(gfx_get_visible_page() ^ 1);
|
||||||
gfx_set_draw_page(back);
|
gfx_set_draw_page(back);
|
||||||
/* Чистый фон обеих страниц лежит в shadow-копии; персонажи рисуются
|
/* Чистый фон обеих страниц лежит в shadow-копии; персонажи рисуются
|
||||||
* банком спрайтов и потому в неё не оседают. */
|
* банком спрайтов и потому в неё не оседают. Двумя блоками по
|
||||||
gfx_copy_page(&full, GFX_COPY_DIRECT);
|
* кадровому интервалу — см. pv_restore_bg. */
|
||||||
|
pv_restore_bg(); /* блоки 1-2 */
|
||||||
gfx_set_bank(GFX_BANK_SPRITE);
|
gfx_set_bank(GFX_BANK_SPRITE);
|
||||||
pv_blit_floor(&cut_torch, cut_torch_l, 93, 116);
|
pv_blit_floor(&cut_torch, cut_torch_l, 93, 116);
|
||||||
pv_blit_floor(&cut_torch, cut_torch_r, 211, 116);
|
pv_blit_floor(&cut_torch, cut_torch_r, 211, 116);
|
||||||
@@ -1339,12 +1390,17 @@ static uint8_t cut_run(uint16_t frames, uint8_t kid_visible)
|
|||||||
* сквозь неё — поймано пользователем 2026-08-25). */
|
* сквозь неё — поймано пользователем 2026-08-25). */
|
||||||
pv_blit_floor(&cut_hg, 0, 240, 167);
|
pv_blit_floor(&cut_hg, 0, 240, 167);
|
||||||
gfx_set_bank(GFX_BANK_NORMAL);
|
gfx_set_bank(GFX_BANK_NORMAL);
|
||||||
gfx_wait_vsync();
|
gfx_wait_vsync(); /* блок 3: актёры и декорации */
|
||||||
gfx_set_visible_page(back);
|
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_sfx_tick();
|
||||||
pop_music_service(); /* «встреча с принцессой» — трек, а не эффект */
|
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_l = (uint8_t)((cut_torch_l + 1) % 9);
|
||||||
cut_torch_r = (uint8_t)((cut_torch_r + 1) % 9);
|
cut_torch_r = (uint8_t)((cut_torch_r + 1) % 9);
|
||||||
cut_sand = (uint8_t)((cut_sand + 1) % 3);
|
cut_sand = (uint8_t)((cut_sand + 1) % 3);
|
||||||
@@ -1662,7 +1718,9 @@ uint8_t pop_intro_show(void) __banked
|
|||||||
/* Первая реплика PV читается ЗДЕСЬ, под чёрным экраном и в тишине:
|
/* Первая реплика PV читается ЗДЕСЬ, под чёрным экраном и в тишине:
|
||||||
* дальше внутри сцены остаётся только пуск, а следующие две реплики
|
* дальше внутри сцены остаётся только пуск, а следующие две реплики
|
||||||
* подкачиваются по странице за кадр. */
|
* подкачиваются по странице за кадр. */
|
||||||
pop_sfx_pause();
|
/* Глушение снято 2026-08-28: массовое чтение страниц насос не морит
|
||||||
|
* (замер на загрузке уровня — ни одной пропущенной порции), а трек
|
||||||
|
* титров может и должен доигрывать. */
|
||||||
(void)pop_music_load(POP_MUS_STORY_2);
|
(void)pop_music_load(POP_MUS_STORY_2);
|
||||||
pv_result = intro_pv_animated();
|
pv_result = intro_pv_animated();
|
||||||
if (pv_result > 0) {
|
if (pv_result > 0) {
|
||||||
@@ -1740,15 +1798,21 @@ uint8_t pop_intro_show(void) __banked
|
|||||||
* дальше, а не чтобы досидеть до конца трека. */
|
* дальше, а не чтобы досидеть до конца трека. */
|
||||||
static void pre_cut_finish(uint8_t aborted)
|
static void pre_cut_finish(uint8_t aborted)
|
||||||
{
|
{
|
||||||
if (!aborted) {
|
/* ТРЕК ПЕРЕЖИВАЕТ СЦЕНУ — как в оригинале. load_intro (seg001:690)
|
||||||
intro_skip_t tail;
|
* ничего не ждёт: гасит экран и возвращает управление, а звук играет
|
||||||
|
* дальше поверх загрузки уровня и первых секунд игры. Мы вместо этого
|
||||||
intro_skip_begin(&tail);
|
* ДОСИЖИВАЛИ трек на чёрном экране (27 — 10,7 с против 2,6 с картинки),
|
||||||
while (pop_music_busy() && !intro_skip_requested(&tail))
|
* то есть дарили игроку восемь секунд пустого экрана, и только потом
|
||||||
pop_wait_edge();
|
* глушили. ЗАМЕР 2026-08-28: снято вместе с глушением на загрузке
|
||||||
|
* уровня; если загрузка не укладывается в дедлайн насоса (251 000
|
||||||
|
* тактов), вернуть оба места разом.
|
||||||
|
*
|
||||||
|
* Пропуск сцены по-прежнему обрывает и музыку: игрок нажал клавишу,
|
||||||
|
* чтобы идти дальше, а не чтобы дослушать. */
|
||||||
|
if (aborted) {
|
||||||
|
pop_music_free();
|
||||||
|
pop_sfx_pause();
|
||||||
}
|
}
|
||||||
pop_music_free();
|
|
||||||
pop_sfx_pause();
|
|
||||||
intro_restore_game_palette();
|
intro_restore_game_palette();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1756,7 +1820,7 @@ void pop_pre_cutscene_show(pop_pre_cutscene_t scene) __banked
|
|||||||
{
|
{
|
||||||
const pop_cs_cmd_t *script = 0;
|
const pop_cs_cmd_t *script = 0;
|
||||||
|
|
||||||
pop_sfx_pause();
|
/* Глушение перед чтением трека снято 2026-08-28 (см. замер насоса). */
|
||||||
/* МУЗЫКА ПЕРЕЖИВАЕТ СЦЕНУ. Треки заставок между уровнями длиннее самих
|
/* МУЗЫКА ПЕРЕЖИВАЕТ СЦЕНУ. Треки заставок между уровнями длиннее самих
|
||||||
* сцен (27 — 10,7 с против 2,6 с картинки), и оригинал их не обрывает:
|
* сцен (27 — 10,7 с против 2,6 с картинки), и оригинал их не обрывает:
|
||||||
* load_intro просто гасит экран и возвращает управление, а звук
|
* load_intro просто гасит экран и возвращает управление, а звук
|
||||||
@@ -1824,7 +1888,7 @@ int pop_intro_hof_bg(uint8_t page) __banked
|
|||||||
|
|
||||||
void pop_time_expired_show(void) __banked
|
void pop_time_expired_show(void) __banked
|
||||||
{
|
{
|
||||||
pop_sfx_pause();
|
/* Глушение снято 2026-08-28 (см. замер насоса). */
|
||||||
/* time_expired (seg001:04D3) начинается с play_sound(sound_36). */
|
/* time_expired (seg001:04D3) начинается с play_sound(sound_36). */
|
||||||
if (pop_music_load(36) == 0) {
|
if (pop_music_load(36) == 0) {
|
||||||
(void)pop_sfx_start();
|
(void)pop_sfx_start();
|
||||||
@@ -1853,7 +1917,7 @@ void pop_time_expired_show(void) __banked
|
|||||||
|
|
||||||
void pop_ending_show(void) __banked
|
void pop_ending_show(void) __banked
|
||||||
{
|
{
|
||||||
pop_sfx_pause();
|
/* Глушение снято 2026-08-28 (см. замер насоса). */
|
||||||
/* Экран гасим ПЕРВЫМ действием — как load_intro оригинала, который
|
/* Экран гасим ПЕРВЫМ действием — как load_intro оригинала, который
|
||||||
* начинается с draw_rect(&screen_rect, color_0_black). Иначе загрузка
|
* начинается с draw_rect(&screen_rect, color_0_black). Иначе загрузка
|
||||||
* ресурсов сцены идёт поверх ЖИВОЙ комнаты уровня 14, и она «мелькает»
|
* ресурсов сцены идёт поверх ЖИВОЙ комнаты уровня 14, и она «мелькает»
|
||||||
|
|||||||