From ea8efdb0fd9faad259bd5136d22d9145936d1ee7 Mon Sep 17 00:00:00 2001 From: Alexander Petrov Date: Sun, 30 Aug 2026 16:16:06 +0300 Subject: [PATCH] =?UTF-8?q?SprPoP:=20=D0=BE=D0=B1=D0=BE=D0=B1=D1=89=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20HDD-=D1=81=D0=B1=D0=BE=D1=80=D0=BA=D1=83=20?= =?UTF-8?q?=D0=B8=20=D0=BE=D1=87=D0=B8=D1=81=D1=82=D0=B8=D1=82=D1=8C=20?= =?UTF-8?q?=D0=BC=D0=B5=D1=82=D0=B0=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Добавить общий каталог назначения для HDD и удалить локальную копию упаковщика.\n\nУбрать устаревшие generated-имена ресурсов, выводить число страниц Kid из kid.arc и ограничить звуковую таблицу горячим модулем.\n\nЗафиксировать планы runtime-индексов музыки и PCM-эффектов. --- .codex/config.toml | 18 + app.mk | 6 +- applications/SprPoP/Makefile | 14 +- applications/SprPoP/docs/README.md | 1 + .../SprPoP/docs/music_runtime_index_plan.md | 573 ++++++++++++++++++ applications/SprPoP/gen/kid_atlas.h | 41 -- applications/SprPoP/gen/pop_bg_atlas.h | 16 +- applications/SprPoP/gen/pop_shadow_atlas.h | 40 +- applications/SprPoP/gen/pop_sound_tbl.h | 24 +- applications/SprPoP/src/_pop_sfx.h | 6 +- applications/SprPoP/src/pop_kboot.c | 11 +- applications/SprPoP/src/pop_kid.c | 3 +- applications/SprPoP/src/pop_kid.h | 5 + applications/SprPoP/src/pop_sfx.c | 1 + applications/SprPoP/src/pop_sfx_cold.c | 1 - applications/SprPoP/src/sprpop_cold.c | 2 +- applications/SprPoP/tools/make_hdd.sh | 92 --- applications/SprPoP/tools/pop_pack_bg.py | 22 +- applications/SprPoP/tools/pop_pack_kid.py | 18 +- applications/SprPoP/tools/pop_pack_shadow.py | 14 +- applications/SprPoP/tools/pop_pack_sound.py | 15 +- toolchain/make_hdd.sh | 79 ++- 22 files changed, 706 insertions(+), 296 deletions(-) create mode 100644 applications/SprPoP/docs/music_runtime_index_plan.md delete mode 100644 applications/SprPoP/gen/kid_atlas.h delete mode 100755 applications/SprPoP/tools/make_hdd.sh diff --git a/.codex/config.toml b/.codex/config.toml index b131593..14ff15f 100644 --- a/.codex/config.toml +++ b/.codex/config.toml @@ -9,3 +9,21 @@ args = [ "mcp<2", "/Volumes/SAM8/Projects/DIY/Z80/Sprinter/C-Compiler/mame/sources/MAME/src/mame_mcp.py", ] + +[mcp_servers.mame-z80.tools.clear_breakpoint] +approval_mode = "approve" + +[mcp_servers.mame-z80.tools.list_breakpoints] +approval_mode = "approve" + +[mcp_servers.mame-z80.tools.press_key] +approval_mode = "approve" + +[mcp_servers.mame-z80.tools.step_out] +approval_mode = "approve" + +[mcp_servers.mame-z80.tools.debugger_command] +approval_mode = "approve" + +[mcp_servers.mame-z80.tools.pause] +approval_mode = "approve" diff --git a/app.mk b/app.mk index bf0fe77..7cfbde5 100644 --- a/app.mk +++ b/app.mk @@ -13,6 +13,7 @@ # # EXTRA_SRCS := helper.c util.c # additional .c files in this dir # # EXTRA_FLAGS := --crt0=minimal # passed through to sprinter-cc # # EXTRA_DATA := test.txt # extra files to add to `make floppy` +# # HDD_DEST_DIR := games/myapp # общий каталог файлов в `make hdd` # # include $(PROJ_ROOT)/app.mk # @@ -67,6 +68,9 @@ SOURCES := $(MAIN_SRC) $(EXTRA_SRCS) # Аргументы упаковщика HDD. Обычно это exe и EXTRA_DATA; приложение со # своей раскладкой каталогов может переопределить переменную до include. HDD_PACK_ARGS ?= $(EXE) $(EXTRA_DATA) +# Общий каталог назначения внутри HDD. Пустое значение сохраняет прежнюю +# укладку в корень; вложенные КАТАЛОГ:файл считаются относительно него. +HDD_DEST_DIR ?= CC_FLAGS := --memory $(MEMORY) ifneq ($(STACK_SIZE),) @@ -125,7 +129,7 @@ run: floppy # используется MCP-мостом к MAME (run_bridge.sh). После пересборки образа # MAME ОБЯЗАН полный рестарт (chdman -f = новый inode; см. memory). hdd: $(EXE) - $(MAKE_HDD) $(HDD_IMG) $(HDD_PACK_ARGS) + $(MAKE_HDD) $(if $(strip $(HDD_DEST_DIR)),--dest "$(HDD_DEST_DIR)") $(HDD_IMG) $(HDD_PACK_ARGS) @echo @echo "HDD (D:) ready: $(HDD_IMG) (with $(EXAMPLE).exe$(if $(EXTRA_DATA), + $(EXTRA_DATA)))" @echo "ВНИМАНИЕ: перезапусти MAME (run_bridge.sh) — образ пересобран." diff --git a/applications/SprPoP/Makefile b/applications/SprPoP/Makefile index 86ccacc..3d5db69 100644 --- a/applications/SprPoP/Makefile +++ b/applications/SprPoP/Makefile @@ -34,8 +34,6 @@ EXAMPLE := sprpop SRC_DIR := src BUILD_DIR := build -MAKE_HDD := tools/make_hdd.sh - # huge: small-раскладка (CODE в W1, DATA в W2) + банки кода в W3. MEMORY ?= huge # --max-allocs-per-node: во сколько вариантов размещения регистров упирается @@ -222,7 +220,7 @@ DISK := README_E.TXT README_R.TXT \ # список — это и зависимость exe, и то, что умеет чинить себя само. GEN_ARC_H := bg_arc.h kid_arc.h guard_arc.h skel_arc.h vizier_arc.h \ shadow_arc.h title_arc.h pv_arc.h -GEN_H := $(addprefix gen/,$(GEN_ARC_H) pop_bg_atlas.h kid_atlas.h kid_data.h \ +GEN_H := $(addprefix gen/,$(GEN_ARC_H) pop_bg_atlas.h kid_data.h \ pop_guard_pal.h pop_shadow_atlas.h pop_sound_tbl.h \ pop_music_tbl.h pop_music_ticks.h pop_font.h) @@ -230,8 +228,9 @@ PACKED_FILES := $(addprefix $(PACKED)/,$(DISK)) STAGED := $(addprefix $(BUILD_DIR)/,$(DISK)) EXTRA_DATA := $(STAGED) -# Запись вида КАТАЛОГ:файл кладёт файл в подкаталог образа, голый путь — в -# корень. Проверка на слэш ОБЯЗАТЕЛЬНА: без неё `README_E.TXT` превращался +# Запись вида КАТАЛОГ:файл кладёт файл в подкаталог HDD_DEST_DIR, голый путь — +# непосредственно в HDD_DEST_DIR. Проверка на слэш ОБЯЗАТЕЛЬНА: без неё +# `README_E.TXT` превращался # в `README_E.TXT:build/README_E.TXT`, то есть упаковщик заводил каталог с # именем файла и клал файл внутрь него (поймано пользователем на готовом # образе). @@ -239,6 +238,9 @@ HDD_PACK_ARGS := $(BUILD_DIR)/$(EXAMPLE).exe \ $(foreach f,$(DISK),$(if $(findstring /,$(f)),\ $(word 1,$(subst /, ,$(f))):$(BUILD_DIR)/$(f),\ $(BUILD_DIR)/$(f))) +# Всё дерево SprPoP лежит рядом с EXE, но не меняет дефолт глобального +# упаковщика: остальные приложения по-прежнему попадают в корень HDD. +HDD_DEST_DIR := games/sprpop # Образ живёт в build/, а не в mame/v306/IMG — приложение автономно. Связь с # MAME — одна символьная ссылка, ставится однократно: make mame-link. HDD_IMG := $(CURDIR)/$(BUILD_DIR)/hdd/$(EXAMPLE).chd @@ -363,7 +365,7 @@ $(1): $(STAMP)/$(2) @test -f $$@ || { $(MAKE) GENERATE=1 -B $(STAMP)/$(2); test -f $$@; } endef $(eval $(call from_stamp,$(BG_ATL) $(ATL)/bg/pop_tile.pal $(ATL)/bg/pal_tile.pal gen/pop_bg_atlas.h,bg)) -$(eval $(call from_stamp,$(KID_ATL) $(ATL)/kid/kid.pal gen/kid_atlas.h,kid)) +$(eval $(call from_stamp,$(KID_ATL) $(ATL)/kid/kid.pal,kid)) $(eval $(call from_stamp,$(ATL)/kid/kid.ani gen/kid_data.h,kid-bin)) $(eval $(call from_stamp,$(GUARD_ATL) gen/pop_guard_pal.h,guard)) $(eval $(call from_stamp,$(SKEL_ATL),skel)) diff --git a/applications/SprPoP/docs/README.md b/applications/SprPoP/docs/README.md index de63ac7..2a39d42 100644 --- a/applications/SprPoP/docs/README.md +++ b/applications/SprPoP/docs/README.md @@ -41,6 +41,7 @@ | [`palette_plan.md`](palette_plan.md) | Карта всех 256 слотов палитры + механика fade | | [`status_line_text.md`](status_line_text.md) | Строка HP как статус-строка: инвентаризация ВСЕХ текстов SDLPoP | | [`sound_plan.md`](sound_plan.md) | Звук через CBL: разбор и архитектура | +| [`music_runtime_index_plan.md`](music_runtime_index_plan.md) | Будущее улучшение: runtime-индексы музыки и PCM-эффектов, смена аудионаборов без перелинковки EXE | | [`shadow_render.md`](shadow_render.md) | Вид Тени (OR+XOR) — отложено: почему XOR несовместим с прозрачностью `#FF` | | [`roomnav_skip.md`](roomnav_skip.md) | Комнаты для отладочного телепорта `+`/`−`: какие пропускать и почему | | [`host_tests_plan.md`](host_tests_plan.md) | Модульные тесты движка под ucsim_z80: два шва, регрессии, дифф против SDLPoP | diff --git a/applications/SprPoP/docs/music_runtime_index_plan.md b/applications/SprPoP/docs/music_runtime_index_plan.md new file mode 100644 index 0000000..0dab3ee --- /dev/null +++ b/applications/SprPoP/docs/music_runtime_index_plan.md @@ -0,0 +1,573 @@ +# Аудио без перелинковки: runtime-индексы музыки и эффектов + +> Статус: план будущего улучшения, 2026-08-30. Код пока не менялся. + +## 1. Цель + +Один и тот же `sprpop.exe` должен корректно работать с любым штатным набором +музыки (`flac`, `mp3`, `ogg`, `mt32`) без перекомпиляции и перелинковки. +Замена набора должна сводиться к замене файлов в `MUS/` вместе с описывающим +их индексом. + +Сейчас это невозможно: упаковщик печатает зависящие от набора значения в +`gen/pop_music_tbl.h` и `gen/pop_music_ticks.h`, после чего они становятся +частью EXE. Если заменить только `MUS/mNN.bin`, загрузчик продолжает верить +размерам старого набора, а сценарии — его длительностям. + +## 2. Подтверждённое текущее состояние + +После преобразования в PCM наборы `flac`, `mp3` и `ogg` имеют одинаковые +длины. Исполнение MT-32 отличается. Значимые примеры: + +| трек | FLAC/MP3/OGG | MT-32 | +|---|---:|---:| +| m32, конец уровня 4 | 438 тиков | 449 тиков | +| m41, обычный конец уровня | 732 | 685 | +| m50, принцесса ждёт | 831 | 867 | +| m53, реплика Джафара | 985 | 1044 | +| m56, финальная тема | 9865 блоков / 78 страниц | 10462 / 82 | + +Все обычные треки каждого из четырёх наборов по-прежнему помещаются в +`POP_MUS_PAGES == 20`. Только m56 требует кольцевого проигрывателя. +`uint16_t` достаточно и для самого длинного MT-32-трека: 10462 блока. + +Compile-time длительности используются не повсюду: + +- `pop_music.c` читает из `pop_music_tbl.h` наличие, число страниц и блоков; +- `pop_intro.c` использует длительности m50 и m53 для шкалы PV-сцены; +- `pop_kid.c` использует длительности m32 и m41 для паузы конца уровня, + включая режим с выключенной музыкой; +- остальные ожидания уже опираются на `pop_music_busy()` и автоматически + заработают с правильным runtime-числом блоков. + +## 3. Что должно остаться константами программы + +В индекс переносятся только свойства конкретного набора. Инварианты формата +остаются в коде, а заголовок IDX лишь подтверждает их: + +- PCM unsigned 8-bit mono; +- частота `CBL_FREQ_10K9`; +- один блок насоса — 128 байт; +- пространство оригинальных sound id — 0..56; +- `POP_MUS_PAGES == 20` для обычного загрузчика; +- размер кольца потокового проигрывателя. + +Это важно: IDX не должен обещать программе другой формат PCM, который насос +физически не умеет воспроизводить. + +## 4. Предлагаемый файл `MUS/MUSIC.IDX` + +Предпочтителен отдельный индекс набора, а не заголовок в каждом треке: + +- все сценарные длительности доступны сразу после старта; +- `mNN.bin` остаются простыми сырыми PCM-потоками; +- загрузчик трека меняется минимально; +- один индекс легко заменить вместе с набором; +- формат помещается в один 512-байтовый сектор DSS. + +Предлагаемая версия `PMI1`: + +```text +0..3 "PMI1" magic и версия +4 57 число плотных записей (id 0..56) +5 4 размер записи +6 7 log2 размера блока: 1 << 7 = 128 +7 1 формат PCM: u8 mono, CBL_FREQ_10K9 +8..15 0 резерв будущих версий + +16.. 57 записей по 4 байта: + +0..1 uint16 blocks длина PCM в блоках по 128 байт + +2..3 uint16 ticks60 длительность ожидания в тиках оригинала + +244..511 нулевой резерв до одного сектора +``` + +Нулевые `blocks` и `ticks60` означают, что трека с таким id в наборе нет. + +`pages` в IDX не хранится: это производная величина, и её дублирование может +разойтись с `blocks`: + +```c +pages = blocks / 128 + ((blocks & 127) != 0); +``` + +По умолчанию упаковщик вычисляет тики той же формулой, что сейчас: + +```text +ticks60 = round(blocks * 40 / 57) +``` + +Хранить `ticks60` отдельно всё же полезно: в будущем момент окончания +сценарной реплики можно будет уточнить независимо от технического хвоста PCM. +Генератор обязан печатать предупреждение, если явно заданные тики заметно +отличаются от длительности файла. + +## 5. Размещение индекса в памяти + +Загружать таблицу в обычный изменяемый `static` нельзя. Данные банковых +модулей сейчас попадают в общий `_DATA`/W2, а свободная куча составляет около +238 байт. Даже таблица из 228 байт практически уничтожит этот запас. + +Индекс следует держать в одной EMM-странице: + +1. `pop_music_init()` выделяет блок из одной страницы; +2. загружает туда первые 512 байт `MUS\MUSIC.IDX`; +3. проверяет magic, версию, размеры полей и формат PCM; +4. готовит страницу для безопасного временного отображения в W0; +5. сохраняет в W2 только номер блока/страницы и флаг готовности. + +Потеря 16 КБ EMM ради маленькой таблицы допустима: EMM у игры с запасом, а +W1/W2 — самый дефицитный ресурс. Не следует ради экономии страницы прятать +индекс в хвост `kid.ani` или звукового набора: это создаст ненужную связь +между независимыми ресурсами. + +Доступ к записи предоставляет банковая функция наподобие: + +```c +int8_t pop_music_info(uint8_t id, pop_music_info_t *out) __banked; +``` + +Она на короткое время отображает страницу IDX в W0, копирует четыре байта в +буфер вызывающего и сразу восстанавливает окно. Наружу указатель на EMM не +выдаётся. + +Индекс живёт до выхода из программы. `pop_music_free()`, который вызывается +между сценами, освобождать его не должен; для полного завершения нужен +отдельный shutdown либо освобождение в общем маршруте выхода. + +## 6. Изменения загрузчика музыки + +`pop_music_load_begin(id)` должен получать из runtime-индекса: + +- наличие трека; +- число блоков; +- вычисленное число EMM-страниц. + +Далее существующая архитектура почти не меняется: + +- `slot_blocks[]` уже хранит runtime-длину загруженного трека; +- `pop_mus_left` получает её при `play`/`stream`; +- насос сам останавливается на правильном блоке; +- `pop_music_busy()` автоматически отражает фактический конец; +- кольцевой проигрыватель получает правильные 82 страницы MT-32 m56 вместо + 78 страниц FLAC. + +При каждом открытии `mNN.bin` надо без дополнительного `open` проверить +фактический размер: + +1. `lseek(fd, 0, SEEK_END)`; +2. убедиться, что размер положительный и кратен 128; +3. сравнить его с `blocks * 128` из IDX; +4. вернуть позицию через `lseek(fd, 0, SEEK_SET)`. + +`lseek(SEEK_END)` уже реализован в libc поверх DSS `MOVE_FP $15` и +используется самой игрой для `POP.CFG`. + +Рекомендуемая политика несовпадения: + +- вывести диагностическое сообщение; +- считать фактический размер файла главным; +- пересчитать `blocks/pages/ticks60` в загруженной EMM-копии индекса; +- продолжить работу, если размер проходит ограничения. + +Так случайно забытый старый IDX не приведёт к чтению чужой EMM-страницы или +обрыву трека. При этом штатная поставка обязана всегда включать согласованные +IDX и PCM. + +## 7. Runtime-тики без 32-битного переполнения + +Если тики приходится восстанавливать из фактического размера, прямое +`blocks * 40` может переполнить `uint16_t`. Та же формула считается только +16-битной арифметикой: + +```c +ticks = (blocks / 57) * 40 + + ((blocks % 57) * 40 + 28) / 57; +``` + +Даже при `blocks == 65535` промежуточные значения остаются в `uint16_t`. +Это холодный путь, поэтому небольшая цена деления допустима. + +## 8. Перевод сценариев с compile-time на runtime + +### PV-сцена (`pop_intro.c`) + +Текущий большой `enum` смешивает два класса величин: + +- неизменные интервалы сценария; +- абсолютные точки, сдвигаемые длительностями m50 и m53. + +Надо сохранить относительные сценарные константы, а при входе в PV-сцену +один раз собрать локальную структуру `pv_timing_t`: + +```text +m50_end = MUS_2_START + ticks(m50) +wait_end = m50_end + 40 +... +dialog1_start = предыдущая фиксированная цепочка +exit_start = dialog1_start + ticks(m53) +anim_end = exit_start + 469 +``` + +Структуру лучше держать на стеке `intro_pv_animated()` и передавать нужным +helper-функциям указателем. File-scope изменяемая таблица снова попала бы в +W2. + +m50 уже загружен до начала PV. m53 начинает подгружаться задолго до своего +старта, поэтому метаданные обоих треков к моменту использования доступны. + +### Конец уровня (`pop_kid.c`) + +Вместо `POP_MUS_TICKS_32/41` редкое событие окончания уровня вызывает +банковый accessor IDX и вычисляет `pop_endmus_left` из runtime-тиков. + +Индекс используется даже при выключенной музыке: оригинал выдерживает эту +паузу молча. Поэтому нельзя заменять её одним `pop_music_busy()`. + +## 9. Генератор и сборка + +`tools/pop_pack_music.py` должен: + +1. как сейчас преобразовать выбранный набор в `mNN.bin`; +2. проверить кратность каждого результата 128 байтам; +3. собрать плотные записи 0..56; +4. записать `MUS/MUSIC.IDX` в формате `PMI1`; +5. больше не генерировать `pop_music_tbl.h` и `pop_music_ticks.h`. + +Makefile должен: + +- добавить `MUS/MUSIC.IDX` в `DISK` и staging; +- убрать музыкальные generated-header'ы из `GEN_H`; +- убрать их из зависимостей EXE; +- оставить `MUSIC_FMT` зависимостью только музыкальных ресурсов; +- гарантировать, что `make music-flac/mp3/ogg/mt32` заменяет и PCM, и IDX, + но не перелинковывает EXE. + +Критерий архитектуры: SHA/дата `sprpop.exe` не меняется при переключении +между четырьмя музыкальными целями. + +## 10. Ошибки и совместимость + +Игра не должна падать из-за необязательной музыки: + +- нет IDX — музыка отключена, игра продолжает работать; +- неверная magic/версия/формат — музыка отключена с диагностикой; +- записи нет — конкретный трек считается отсутствующим; +- файла нет — запрос трека завершается молча/с диагностикой, как сейчас; +- обычный трек требует больше 20 страниц — не загружать обычным путём; +- потоковый трек имеет больше 255 страниц — отвергнуть, потому что текущие + счётчики страниц восьмибитные; +- IDX и файл расходятся — применить политику §6; +- все файловые операции выполнять через `POP_PATH_CALL`, чтобы сохранить + работу на старых DSS с повреждением текущего каталога. + +Для отсутствующего/повреждённого IDX остаётся выбрать поведение немой паузы +конца уровня: нулевая пауза либо небольшой канонический fallback. Это не +мешает основной архитектуре, но решение надо принять до реализации. + +## 11. Этапы реализации + +### MI0 — формат и host-тест + +- вынести writer/reader `PMI1` в тестируемый код упаковщика; +- проверить magic, размеры, LE-поля, нулевые записи и padding; +- для всех четырёх наборов сверить `blocks/ticks` с фактическими PCM; +- зафиксировать тестом значения MT-32 m50/m53/m56 как отличающиеся от FLAC. + +### MI1 — runtime-загрузка IDX + +- `pop_music_init()`/shutdown; +- одна EMM-страница, загрузка и валидация; +- accessor одной записи; +- отказ без порчи W0/W3, EMM и файловых дескрипторов. + +### MI2 — loader/pump + +- заменить `pop_mus_tbl[]` runtime-записью; +- проверять реальный размер файла; +- обычный, немедленный и кольцевой пути должны использовать одну метаинформацию; +- удалить `pop_music_tbl.h`. + +### MI3 — runtime-шкала сцен + +- перевести m50/m53 в `pop_intro.c` на локальную runtime-шкалу; +- перевести m32/m41 в `pop_kid.c`; +- удалить `pop_music_ticks.h`. + +### MI4 — сборка и образы + +- добавить IDX в каждый музыкальный набор; +- исключить музыку из зависимостей линковки; +- собрать четыре HDD-варианта с одним EXE. + +### MI5 — приёмка в MAME/на железе + +- FLAC: title -> story -> PV, обычный конец уровня, конец уровня 4, ending; +- MT-32: те же маршруты, особенно m50/m53 и полный m56; +- музыка выключена: пауза конца уровня берётся из IDX; +- замена набора без пересборки EXE; +- повреждённый IDX, отсутствующий трек, несовпадающий размер; +- контроль, что не запущено более одного MAME и предыдущий экземпляр закрыт + перед новым прогоном. + +## 12. Критерии готовности + +- один бинарник запускается со всеми четырьмя наборами; +- ни один музыкальный generated-header не входит в сборку C; +- начало следующей сцены/уровня соответствует фактической записи; +- MT-32 m56 проигрывает все 82 страницы и не обрывается как FLAC-вариант; +- обычные треки не читают за пределами выделенного EMM-блока; +- IDX не расходует сотни байт W2; +- ошибочный набор отключает музыку безопасно; +- переключение `music-*` не меняет EXE; +- старые DSS продолжают работать через восстановление каталога приложения. + +--- + +# Часть II. Звуковые эффекты без перелинковки + +## 13. Цель и отличие от музыки + +Один и тот же `sprpop.exe` должен работать с разными наборами PCM-эффектов, +если в них сохранена исходная нумерация sound id 0..56. Замена набора должна +сводиться к замене согласованной пары `SND/SOUND.IDX` + `SND/SND.ARC`, без +генерации C-заголовка и перелинковки EXE. + +Набор состоит из двух согласованных файлов: + +- `SND/SND.ARC` — обычный архив PBA1 с PCM-страницами; +- `SND/SOUND.IDX` — описание раскладки эффектов внутри этих страниц. + +Отдельный IDX предпочтительнее расширения заголовка `snd.arc`: PBA1 остаётся +универсальным и не получает специального варианта только для звука, а +формат индекса можно независимо версионировать и проверять тем же способом, +что будущий `MUS/MUSIC.IDX`. + +Существующий формат записи менять не требуется: + +```c +typedef struct { + uint8_t page; + uint16_t off; + uint16_t len; +} pop_snd_ent_t; +``` + +Это ровно необходимые проигрывателю номер логической страницы, смещение в +ней и длина PCM. В текущем ABI SDCC/z80 запись занимает 5 байт; дисковый +формат обязан описывать эти пять байт явно (`uint16` little-endian), а код +должен проверять `sizeof(pop_snd_ent_t) == 5` на этапе сборки. + +## 14. Подтверждённое текущее состояние + +Сейчас `tools/pop_pack_sound.py` печатает раскладку в +`gen/pop_sound_tbl.h`, а `pop_sfx.c` включает её как +`static const pop_snd_tbl[57]`. Поэтому конкретные `page/off/len` становятся +частью EXE. + +В текущей сборке таблица занимает 285 байт в `_CODE`, по адресам +`0x52D8..0x53F4`. Это общий резидентный диапазон игры: при huge-модели +`_CODE`, `_DATA`, heap и stack совместно используют плоские 32 КБ +`0x4000..0xBFFF`; граница W1/W2 отдельного бюджета здесь не создаёт. + +Проверены оба имеющихся исходных набора DIGISND: + +| набор | PCM-эффектов | страниц после упаковки | отличия раскладки | +|---|---:|---:|---:| +| MSDOS 1.3/1.4 | 31 из 57 | 10 | эталон текущей сборки | +| SDLPoP 1.0/1.1 | 31 из 57 | 9 | отличаются 8 id | + +Максимальный эффект занимает 17 664 байта, или 138 блоков по 128 байт. +У SDLPoP эффект 48 содержит только 7 исходных сэмплов; это свойство самого +набора, а не ошибка runtime-индекса. + +## 15. Файл `SND/SOUND.IDX` + +IDX занимает один 512-байтовый сектор и после короткого заголовка является +точным дисковым дампом 57 записей `pop_snd_tbl`: + +```text +0..3 "PSI1" magic и версия индекса эффектов +4 57 число плотных записей, id 0..56 +5 5 размер записи +6 7 log2 размера блока: 1 << 7 = 128 +7 1 формат: unsigned 8-bit mono, CBL_FREQ_10K9 +8 pages фактическое число PCM-страниц в snd.arc +9..15 0 резерв + +16..300 57 записей по 5 байт: + +0 uint8 page + +1..2 uint16 off, little-endian + +3..4 uint16 len, little-endian + +301..511 0 резерв до одного сектора +``` + +`pages` — фактический размер конкретного набора, например 10 для MSDOS или +9 для SDLPoP. Его не следует называть или трактовать как +`POP_SND_MAX_PAGES`: максимум 16 является compile-time-вместимостью +загрузчика, а IDX сообщает число реально нужных страниц и обязан укладываться +в этот предел. + +Загрузчик сверяет `pages` с числом элементов внешнего PBA1 и не доверяет +расходящимся файлам. `off` и `len` обязаны быть кратны 128; нулевая длина +означает отсутствие PCM для данного id. + +IDX загружается прямо в резидентный `pop_snd_tbl`, поэтому отдельной страницы +EMM для него не требуется. Цена решения — одно дополнительное открытие и +чтение 512 байт при старте звука; на фоне загрузки 9–10 страниц PCM это +приемлемый холодный расход. + +## 16. Размещение runtime-таблицы в памяти + +`pop_snd_tbl` становится обычным изменяемым внутренним объектом: + +```c +pop_snd_ent_t pop_snd_tbl[POP_SND_COUNT]; +``` + +Его следует определить в отдельном internal data-модуле и объявить через +`extern` в `_pop_sfx.h`, потому что таблицу заполняет холодный загрузчик из +банка 8, а читает резидентный `pop_sfx.c`. + +После снятия `const` SDCC перенесёт эти 285 байт из `_CODE` в `_DATA`. Это +не добавляет 285 байт к общему резидентному расходу: `_CODE` одновременно +уменьшается на тот же размер, а `_CODE` и `_DATA` последовательно лежат в +одном диапазоне `0x4000..0xBFFF`. Возможна лишь небольшая разница из-за +выравнивания, которую надо проверить итоговой map-картой и size-check. + +Банк 8 может читать индекс напрямую в этот буфер: весь диапазон +`0x4000..0xBFFF` остаётся доступен, пока банковый код исполняется в W3. +Горячие обращения `pop_snd_tbl[id].page/off/len` и ISR не требуют новых +маппингов или accessor-функций. + +## 17. Фактическое число страниц + +Инвариантами программы остаются: + +```c +#define POP_SND_MAX_PAGES 16 +#define POP_SND_COUNT 57 +#define POP_SND_BLOCK 128 +``` + +`POP_SND_PAGES == 10` больше не должен означать размер конкретного набора. +Вместо него появляется runtime-состояние: + +```c +uint8_t pop_snd_pages; +uint8_t pop_snd_page[POP_SND_MAX_PAGES]; +``` + +Загрузчик выделяет `mem_alloc_pages(pop_snd_pages)` и заполняет только +фактическое число элементов массива. MSDOS-набор займёт 10 страниц, +SDLPoP-набор — 9. Предел 16 оставляет запас будущим наборам без +перекомпиляции и увеличивает постоянный массив лишь на 6 байт относительно +текущего; ещё один байт занимает `pop_snd_pages`. + +Насос при переходе длинного эффекта через границу страницы сравнивает +`sfx_pg + 1` с `pop_snd_pages`, а не с compile-time-константой. + +## 18. Загрузка и валидация + +`pop_sfx_init()` должен выполнять следующую последовательность: + +1. открыть `SND/SOUND.IDX` через существующий путь `POP_PATH_CALL`; +2. прочитать 16-байтовый заголовок, затем 285 байт записей прямо в + `pop_snd_tbl`, и проверить полный размер IDX; +3. проверить magic, версию, число/размер записей, формат и размер блока; +4. получить из IDX фактическое число страниц и проверить диапазон 1..16; +5. открыть `SND/SND.ARC` через `POP_PATH_CALL`; +6. прочитать обычную таблицу PBA1 с ёмкостью `POP_SND_MAX_PAGES`; +7. сверить `PSI1.pages` с числом элементов PBA1; +8. проверить каждую ненулевую запись; +9. только после этого выделить фактическое число EMM-страниц и загрузить их. + +Для каждой записи проверяются: + +- `page < pop_snd_pages`; +- `off < 16384` и `off % POP_SND_BLOCK == 0`; +- `len % POP_SND_BLOCK == 0`; +- диапазон `page/off/len` не выходит за загруженный набор; +- переход длинного эффекта на следующие страницы не превышает + `pop_snd_pages`. + +При любой ошибке блок EMM и файловый дескриптор освобождаются, таблица не +используется, эффекты остаются выключенными. Набор без `SOUND.IDX` считается +неполным; хранить в EXE старую таблицу как fallback не следует, иначе +зависимость бинарника от конкретного набора останется. + +## 19. Генератор и сборка + +`tools/pop_pack_sound.py` должен: + +1. как сейчас разобрать DIGISND, привести PCM к CBL_FREQ_10K9 и выровнять + начала/длины на 128 байт; +2. вычислить фактическое число страниц и отвергнуть набор больше 16; +3. сформировать плотные 57 записей `page/off/len`; +4. записать `SND/SOUND.IDX` в формате `PSI1`, включая фактическое число + страниц; +5. собрать обычный `SND/SND.ARC` из полученных страниц; +6. больше не генерировать `gen/pop_sound_tbl.h`. + +Чтобы число страниц не было захардкожено списком `s0.bin`..`s9.bin` в +Makefile, упаковщик звука предпочтительно должен сразу формировать +`assets/packed/SND/snd.arc` либо передавать упаковщику архивов динамический +список результатов без сохранения устаревшей десятой страницы. + +Стабильные тип и константы переносятся в обычный internal-заголовок. EXE не +должен зависеть от результата упаковки звука: цель выбора набора меняет +согласованную пару `SOUND.IDX` + `SND.ARC`. + +## 20. Этапы реализации эффектов + +### SI0 — формат и host-тест + +- writer/reader отдельного `SOUND.IDX` в формате `PSI1`; +- проверка точного пятибайтового LE-формата записи; +- сборка и разбор обоих имеющихся DIGISND-наборов; +- проверки 10 страниц MSDOS, 9 страниц SDLPoP и отличающихся записей. + +### SI1 — runtime-таблица + +- изменяемый `pop_snd_tbl` в общем резидентном CODE/DATA-диапазоне; +- загрузка и полная валидация `PSI1`; +- удаление generated `pop_sound_tbl.h`; +- проверка map-карты: перенос CODE -> DATA не должен съесть heap. + +### SI2 — гибкое выделение страниц + +- `POP_SND_MAX_PAGES == 16` и runtime `pop_snd_pages`; +- фактический размер стека таблицы PBA1; +- выделение/освобождение 9, 10 и тестовых 16 страниц; +- runtime-гард перехода длинного эффекта между страницами. + +### SI3 — сборка без перелинковки + +- убрать фиксированный `SND_ATL` из десяти имён; +- сделать `SOUND.IDX` и `SND.ARC` согласованными результатами выбора набора; +- проверить неизменность SHA/даты `sprpop.exe` при смене набора. + +### SI4 — приёмка + +- проиграть короткий, обычный и переходящий страницу эффекты; +- проверить приоритеты и перебивание звуков; +- проверить музыку поверх общего CBL после runtime-загрузки эффектов; +- отсутствующий IDX и повреждённые magic/count/pages/page/off/len должны + безопасно отключать эффекты; +- перед каждым MAME-прогоном завершать предыдущий экземпляр и никогда не + запускать две копии одновременно. + +## 21. Критерии готовности эффектов + +- один `sprpop.exe` работает с MSDOS- и SDLPoP-наборами; +- замена согласованной пары `SND/SOUND.IDX` + `SND/SND.ARC` не требует + компиляции C и не меняет EXE; +- `pop_snd_tbl` больше не генерируется как C-код; +- загружается фактическое число страниц в диапазоне 1..16; +- таблица не расходует дополнительную страницу EMM и не увеличивает + суммарный резидентный CODE+DATA на свои 285 байт; +- горячий путь и ISR используют прежние прямые `page/off/len`; +- повреждённый индекс не приводит к чтению за пределами EMM-блока; +- файловые операции сохраняют совместимость со старыми DSS через + восстановление каталога приложения. diff --git a/applications/SprPoP/gen/kid_atlas.h b/applications/SprPoP/gen/kid_atlas.h deleted file mode 100644 index 709400b..0000000 --- a/applications/SprPoP/gen/kid_atlas.h +++ /dev/null @@ -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 diff --git a/applications/SprPoP/gen/pop_bg_atlas.h b/applications/SprPoP/gen/pop_bg_atlas.h index 9e861ea..edb3299 100644 --- a/applications/SprPoP/gen/pop_bg_atlas.h +++ b/applications/SprPoP/gen/pop_bg_atlas.h @@ -1,5 +1,5 @@ /* pop_bg_atlas.h — раскладка атласов статического фона PoP. - * Сгенерировано toolchain/pop_pack_bg.py — НЕ править вручную. + * Сгенерировано tools/pop_pack_bg.py — НЕ править вручную. * * Прямая адресация (ноль remap-таблиц в W2): * ENV фон id N -> atlas env_bg[N>>4], idx N&15 @@ -23,25 +23,15 @@ #define POP_PAL_ENV 0x50 #define POP_PAL_WALL 0x60 -/* Имена файлов атласов по тайлсету (грузятся atlas_load). Таблицы - * видны только тому, кто попросил POP_BG_ATLAS_NAMES: иначе копия - * строк уедет в каждый включивший заголовок модуль. */ +/* Палитры пока остаются отдельными файлами рядом с bg.arc. Имена + * видит только модуль загрузки, чтобы не размножать строки. */ #ifdef POP_BG_ATLAS_NAMES -static const char *const pop_env_atl[POP_TILESETS][POP_ENV_PAGES] = { - { "pop_env0.atl", "pop_env1.atl", "pop_env2.atl", "pop_env3.atl", "pop_env4.atl", "pop_env5.atl", "pop_env6.atl", "pop_env7.atl", "pop_env8.atl", "pop_env9.atl" }, - { "pal_env0.atl", "pal_env1.atl", "pal_env2.atl", "pal_env3.atl", "pal_env4.atl", "pal_env5.atl", "pal_env6.atl", "pal_env7.atl", "pal_env8.atl", "pal_env9.atl" }, -}; -static const char *const pop_wall_atl[POP_TILESETS] = { "pop_wall.atl", "pal_wall.atl" }; -static const char *const pop_fore_atl[POP_TILESETS] = { "pop_fore.atl", "pal_fore.atl" }; static const char *const pop_tile_pal[POP_TILESETS] = { "pop_tile.pal", "pal_tile.pal" }; #endif /* POP_BG_ATLAS_NAMES */ -#define POP_POT_ATL "pop_pot.atl" /* chtab_1: зелья, от набора не зависит */ -#define POP_PAL_POT 0x40 /* Пузырёк зелья: красный набор = id 16..22 (кадры оригинала), зелёный (перо/переворот) и синий (вред/открыть) = те же кадры под id 30..36 и 40..46 (draw_tile_anim, seg008:652). */ #define POP_POT_BUBB_GREEN 30 #define POP_POT_BUBB_BLUE 40 -#define POP_BG_PAL "pop_bg.pal" #endif diff --git a/applications/SprPoP/gen/pop_shadow_atlas.h b/applications/SprPoP/gen/pop_shadow_atlas.h index 7c46fe7..1000912 100644 --- a/applications/SprPoP/gen/pop_shadow_atlas.h +++ b/applications/SprPoP/gen/pop_shadow_atlas.h @@ -1,5 +1,5 @@ /* pop_shadow_atlas.h — раскладка атласа ТЕНИ. - * СГЕНЕРЕНО toolchain/pop_pack_shadow.py — НЕ править вручную. + * СГЕНЕРЕНО tools/pop_pack_shadow.py — НЕ править вручную. * * Две половины, потому что и у оригинала их две (см. шапку * упаковщика): кадры вне боя адресуют chtab_2 (спрайты Кида), @@ -25,44 +25,6 @@ /* Половина «в бою» — SHADOW.DAT (chtab_5, кадры 150..189). */ #define POP_SHADOW_FGT_PAGES 4 -static const char *const pop_shadow_kid_atl[POP_SHADOW_KID_PAGES] = { - "SHADOW\\sk0.atl", - "SHADOW\\sk1.atl", - "SHADOW\\sk2.atl", - "SHADOW\\sk3.atl", - "SHADOW\\sk4.atl", - "SHADOW\\sk5.atl", - "SHADOW\\sk6.atl", - "SHADOW\\sk7.atl", - "SHADOW\\sk8.atl", - "SHADOW\\sk9.atl", - "SHADOW\\sk10.atl", - "SHADOW\\sk11.atl", - "SHADOW\\sk12.atl", - "SHADOW\\sk13.atl", - "SHADOW\\sk14.atl", - "SHADOW\\sk15.atl", - "SHADOW\\sk16.atl", - "SHADOW\\sk17.atl", - "SHADOW\\sk18.atl", - "SHADOW\\sk19.atl", - "SHADOW\\sk20.atl", - "SHADOW\\sk21.atl", - "SHADOW\\sk22.atl", - "SHADOW\\sk23.atl", - "SHADOW\\sk24.atl", - "SHADOW\\sk25.atl", - "SHADOW\\sk26.atl", - "SHADOW\\sk27.atl", -}; - -static const char *const pop_shadow_fgt_atl[POP_SHADOW_FGT_PAGES] = { - "SHADOW\\sf0.atl", - "SHADOW\\sf1.atl", - "SHADOW\\sf2.atl", - "SHADOW\\sf3.atl", -}; - /* 16 цветов, запись 4 байта (B, G, R, 0) — формат gfx_pal_load. */ static const uint8_t pop_shadow_pal[64] = { 0x00,0x00,0x00,0x00,0xD8,0xFC,0xFC,0x00,0x6C,0x80,0xDC,0x00,0x78,0x44,0x24,0x00,0xA0,0xB8,0xD8,0x00,0xB5,0x7C,0x21,0x00,0x00,0x90,0xB8,0x00,0x5D,0x6D,0xAB,0x00,0x0C,0xF0,0x6C,0x00,0xCC,0x38,0x05,0x00,0x38,0xCC,0xC0,0x00,0x58,0xDB,0xA7,0x00,0xBB,0x9F,0x54,0x00,0x5D,0x18,0x60,0x00,0x37,0xD7,0x37,0x00,0x2F,0x17,0xD2,0x00, diff --git a/applications/SprPoP/gen/pop_sound_tbl.h b/applications/SprPoP/gen/pop_sound_tbl.h index c6d9fb7..7a35916 100644 --- a/applications/SprPoP/gen/pop_sound_tbl.h +++ b/applications/SprPoP/gen/pop_sound_tbl.h @@ -1,5 +1,5 @@ /* pop_sound_tbl.h — раскладка оцифрованных эффектов PoP. - * СГЕНЕРЕНО toolchain/pop_pack_sound.py — НЕ править вручную. + * СГЕНЕРЕНО tools/pop_pack_sound.py — НЕ править вручную. * * Данные: 8 бит беззнаковые, МОНО, 10937.5 Гц (CBL_FREQ_10K9) — формат * CBL один в один, конвертировать в рантайме нечего. @@ -28,6 +28,8 @@ #define POP_SND_SILENCE_PAGE 0 #define POP_SND_SILENCE_OFF 0x0000 +/* Полная таблица нужна только горячей половине звука. */ +#ifdef POP_SND_TABLE typedef struct { uint8_t page; uint16_t off; uint16_t len; } pop_snd_ent_t; /* len == 0 — у звука нет оцифровки (это музыка, см. sound_plan.md). */ @@ -90,24 +92,6 @@ static const pop_snd_ent_t pop_snd_tbl[POP_SND_COUNT] = { { 0, 0x0000, 0 }, /* 55 */ { 0, 0x0000, 0 }, /* 56 */ }; - -/* Имена файлов видит ТОЛЬКО тот, кто попросил POP_SND_FILES (холодная - * половина): иначе копия строк уезжает в каждый включивший заголовок - * модуль, а в резиденте W1/W2 каждая сотня байт на счету. Тот же - * приём, что у POP_BG_ATLAS_NAMES в pop_bg_atlas.h. */ -#ifdef POP_SND_FILES -static const char *const pop_snd_files[POP_SND_PAGES] = { - "SND\\s0.bin", - "SND\\s1.bin", - "SND\\s2.bin", - "SND\\s3.bin", - "SND\\s4.bin", - "SND\\s5.bin", - "SND\\s6.bin", - "SND\\s7.bin", - "SND\\s8.bin", - "SND\\s9.bin", -}; -#endif +#endif /* POP_SND_TABLE */ #endif diff --git a/applications/SprPoP/src/_pop_sfx.h b/applications/SprPoP/src/_pop_sfx.h index b765214..e70d7b2 100644 --- a/applications/SprPoP/src/_pop_sfx.h +++ b/applications/SprPoP/src/_pop_sfx.h @@ -1,14 +1,14 @@ /* * _pop_sfx.h — внутренний контракт между половинами звука (НЕ публичный, * публичный — pop_sfx.h). Горячая половина живёт в резиденте - * (pop_sfx.c), холодная в банке (pop_sfx_cold.c); делят они таблицу - * звуков, массив физических страниц и насос. + * (pop_sfx.c), холодная в банке (pop_sfx_cold.c); делят они массив + * физических страниц и насос. */ #ifndef _POP_SFX_INTERNAL_H #define _POP_SFX_INTERNAL_H #include -#include "pop_sound_tbl.h" /* pop_snd_tbl, POP_SND_* — генерит упаковщик */ +#include "pop_sound_tbl.h" /* POP_SND_* и, по запросу, таблица эффектов */ /* Физические страницы набора, по индексу из pop_snd_tbl[].page. */ extern uint8_t pop_snd_page[POP_SND_PAGES]; diff --git a/applications/SprPoP/src/pop_kboot.c b/applications/SprPoP/src/pop_kboot.c index b51fbaf..b4cdc3b 100644 --- a/applications/SprPoP/src/pop_kboot.c +++ b/applications/SprPoP/src/pop_kboot.c @@ -23,14 +23,9 @@ /* Серии в архиве обязаны лежать подряд — так их кладёт список в Makefile, * и только поэтому индекс считается сложением. Если порядок собьётся, * сборка встанет здесь, а не рассыплется картинкой на экране. */ -#if (ARC_KID_KID27 - ARC_KID_KID0) != 27 -#error "kid0..kid27 в архиве не подряд" +#if (ARC_KID_SWORD - ARC_KID_KID0_V) != KID_PAGES +#error "обычная и зеркальная серии Kid должны иметь одинаковую длину" #endif -#if (ARC_KID_KID27_V - ARC_KID_KID0_V) != 27 -#error "kid0_v..kid27_v в архиве не подряд" -#endif - -#define KID_MAXPAGES 28 /* Таблица архива живёт на стеке вызывающего (4 байта на запись, здесь 232) * — держать её постоянно незачем, а W2 у игры и без того тесный. */ @@ -40,7 +35,7 @@ int pop_kid_load(uint8_t npages) __banked pop_arc_t arc; uint8_t i; - if (npages > KID_MAXPAGES) npages = KID_MAXPAGES; + if (npages > KID_PAGES) npages = KID_PAGES; if (pop_arc_open_id(&arc, POP_ARC_KID, tbl, ARC_KID_COUNT) < 0) return -1; for (i = 0; i < npages; i++) if (pop_arc_atlas(&arc, tbl, (uint8_t)(ARC_KID_KID0 + i), diff --git a/applications/SprPoP/src/pop_kid.c b/applications/SprPoP/src/pop_kid.c index c5c5821..3ed8877 100644 --- a/applications/SprPoP/src/pop_kid.c +++ b/applications/SprPoP/src/pop_kid.c @@ -52,8 +52,7 @@ uint8_t pop_endmus_left; * НЕ static: набор спрайтов слота POP_CH_KID общей отрисовки персонажей * (pop_cdraw.c, банк 4). Отдан как есть, а не через аксессор: читается раз * в кадр из другого банка, и вызов стоил бы дороже самого чтения. */ -#define KID_MAXPAGES 28 -atlas_t kidp[KID_MAXPAGES]; +atlas_t kidp[KID_PAGES]; uint8_t kid_npages; /* Таблица «seq_id -> адрес в seqtbl» (original_seqtbl_offsets). Определена diff --git a/applications/SprPoP/src/pop_kid.h b/applications/SprPoP/src/pop_kid.h index 110bed3..899dfc0 100644 --- a/applications/SprPoP/src/pop_kid.h +++ b/applications/SprPoP/src/pop_kid.h @@ -13,8 +13,13 @@ #include #include "pop_char.h" #include "kid_data.h" /* kframe — тип кадра для pop_sword_draw */ +#include "kid_arc.h" /* границы серий атласов в kid.arc */ #include /* atlas_t — набор спрайтов Кида (см. kidp ниже) */ +/* Число страниц Кида берём из метаданных архива: зеркальная серия начинается + * сразу после обычной. Не дублировать литералом в загрузчике/хранилище. */ +#define KID_PAGES (ARC_KID_KID0_V - ARC_KID_KID0) + /* Состояние персонажа — общий тип для Кида, стража и Char (pop_char.h). */ extern pop_char_t Kid; diff --git a/applications/SprPoP/src/pop_sfx.c b/applications/SprPoP/src/pop_sfx.c index 0afa57c..4a4c5f3 100644 --- a/applications/SprPoP/src/pop_sfx.c +++ b/applications/SprPoP/src/pop_sfx.c @@ -21,6 +21,7 @@ #include /* IRQ_DISABLE/IRQ_ENABLE вокруг курсора */ #include "pop_sfx.h" #include "pop_music.h" +#define POP_SND_TABLE /* таблица нужна только горячей половине */ #include "_pop_sfx.h" /* Физические страницы набора; заполняет холодная половина. */ diff --git a/applications/SprPoP/src/pop_sfx_cold.c b/applications/SprPoP/src/pop_sfx_cold.c index 425a8e4..51c4b99 100644 --- a/applications/SprPoP/src/pop_sfx_cold.c +++ b/applications/SprPoP/src/pop_sfx_cold.c @@ -9,7 +9,6 @@ #include #include "pop_sfx.h" #include "pop_arc.h" -#define POP_SND_FILES /* имена файлов нужны ТОЛЬКО здесь, см. pop_sound_tbl.h */ #include "_pop_sfx.h" static uint8_t snd_blk; /* блок EMM под весь набор */ diff --git a/applications/SprPoP/src/sprpop_cold.c b/applications/SprPoP/src/sprpop_cold.c index 67f70c7..ed85971 100644 --- a/applications/SprPoP/src/sprpop_cold.c +++ b/applications/SprPoP/src/sprpop_cold.c @@ -1158,7 +1158,7 @@ int pop_boot(void) __banked pop_shadow_load(); /* запечённый набор Тени — один раз на запуск */ pop_sfx_init(); /* звук: 8 страниц оцифровки (вывод пока молчит) */ pop_settings_apply(1); /* CFG -> speed/cheats/CBL */ - if (pop_kid_load(28) != 0) { /* ВСЕ страницы: бег+стоп+разворот+присед+прыжок */ + if (pop_kid_load(KID_PAGES) != 0) { /* ВСЕ страницы: бег+стоп+разворот+присед+прыжок */ puts("kid atlas load failed"); return -1; } diff --git a/applications/SprPoP/tools/make_hdd.sh b/applications/SprPoP/tools/make_hdd.sh deleted file mode 100755 index d4957ad..0000000 --- a/applications/SprPoP/tools/make_hdd.sh +++ /dev/null @@ -1,92 +0,0 @@ -#!/bin/bash -# make_hdd.sh OUT.chd FILE1 [FILE2 ...] -# -# Собирает 32 МБ RAW-образ жёсткого диска (MBR + один раздел FAT16) с -# указанными файлами в корне и конвертит его в CHD для MAME (монтируется -# как -hard2). Быстрая альтернатива floppy (mc.img): MAME эмулирует FDD -# с реальной черепашьей скоростью, HDD — в разы быстрее. -# -# Требует: mtools (mpartition/mformat/mcopy) + chdman (из MAME). -# Sprinter DSS штатно монтирует стандартный MBR+FAT16 (подтверждено). -# -# Использование: -# tools/make_hdd.sh mame/v306/IMG/test_hdd.chd prog.exe data1 data2 ... -# tools/make_hdd.sh out.chd prog.exe KID:kid0.atl GUARD:g0.atl ... -# — аргумент вида КАТАЛОГ:файл кладёт файл в подкаталог образа -# (каталог создаётся); без префикса файл идёт в корень. -set -e - -if [ $# -lt 2 ]; then - echo "usage: $0 OUT.chd FILE1 [FILE2 ...]" >&2 - exit 1 -fi - -OUT_CHD="$1"; shift - -# Геометрия: 128*16*32*512 = 32 МБ. BPS=512. -CYLS=128; HEADS=16; SECS=32 -SECTORS=$((CYLS * HEADS * SECS)) # 65536 - -# macOS mktemp требует X'ы в КОНЦЕ шаблона. -RAW=$(mktemp /tmp/sphdd.raw.XXXXXX) -MTC=$(mktemp /tmp/sphdd.mtc.XXXXXX) -trap 'rm -f "$RAW" "$MTC"' EXIT - -dd if=/dev/zero of="$RAW" bs=512 count=$SECTORS status=none - -# mtools-диск z: = раздел 1 нашего RAW-образа. -printf 'drive z: file="%s" partition=1\n' "$RAW" > "$MTC" -export MTOOLSRC="$MTC" MTOOLS_SKIP_CHECK=1 - -# mtools собран с enable-raw-term и, когда ему нужно что-то спросить, -# открывает УПРАВЛЯЮЩИЙ ТЕРМИНАЛ напрямую (/dev/tty) — в обход stdin. -# Поэтому ни `< /dev/null`, ни перенаправление stdio не спасают: из -# терминала сборка молча вставала навсегда на `mmd z:/BG` (подтверждено -# lsof: fd 0 = /dev/null, fd 4 = /dev/tty, процесс спит). Лечится только -# отсутствием управляющего терминала — запускаем mtools в новой сессии, -# тогда открыть /dev/tty нечего и mtools идёт неинтерактивным путём. -# -# setsid(1) в macOS нет, поэтому делаем то же самое питоном. -# Каналов, откуда mtools может ждать ответ, ДВА, и закрывать надо оба: -# /dev/tty — убирается новой сессией (os.setsid); -# stdin — убирается перенаправлением из /dev/null. -# Закрыть только один недостаточно: с одним лишь setsid mmd переключался на -# stdin и вис уже на нём (lsof: fd 0 = /dev/ttys002). -mt() { - python3 -c 'import os,sys; os.setsid(); os.execvp(sys.argv[1], sys.argv[1:])' \ - "$@" < /dev/null -} - -# MBR + раздел 1 на весь диск (тип FAT16), затем формат FAT16. -echo "--- разметка и формат образа ---" -mt mpartition -I -c -t $CYLS -h $HEADS -s $SECS z: -mt mformat z: - -echo "--- копирование файлов ---" - -# Аргумент может быть либо файлом (кладём в корень), либо парой -# КАТАЛОГ:файл — тогда каталог создаётся на образе и файл кладётся туда. -# Имена на Sprinter/DSS — 8.3, каталоги тоже (KID, GUARD, BG, LEVELS...). -mt mmd "z:/games" >/dev/null 2>&1 || true -mt mmd "z:/games/sprpop" >/dev/null 2>&1 || true - -for f in "$@"; do - case "$f" in - *:*) dir="${f%%:*}"; src="${f#*:}" - # Каталог может уже быть — это не ошибка; stderr НЕ прячем, иначе - # непрошедший вопрос mtools снова окажется невидимым. - mt mmd "z:/games/sprpop/$dir" >/dev/null 2>&1 || true - echo "COPY $src to z:/games/sprpop/$dir/" - mt mcopy -o "$src" "z:/games/sprpop/$dir/" ;; - *) mt mcopy -o "$f" "z:/games/sprpop/" ;; - esac -done - -echo "--- содержимое образа ---" -mt mdir -/ z: - -# RAW -> CHD (без сжатия = быстро; та же геометрия). -echo "--- конвертация RAW -> CHD ---" -chdman createhd -i "$RAW" -o "$OUT_CHD" -c none \ - -chs $CYLS,$HEADS,$SECS -ss 512 -f -echo "готово: $OUT_CHD" diff --git a/applications/SprPoP/tools/pop_pack_bg.py b/applications/SprPoP/tools/pop_pack_bg.py index d20e9cc..d94f75c 100644 --- a/applications/SprPoP/tools/pop_pack_bg.py +++ b/applications/SprPoP/tools/pop_pack_bg.py @@ -450,7 +450,7 @@ def write_header(out_path, env_pages): файлов и палитра. Поэтому здесь два массива имён и один индекс.""" lines = [ "/* pop_bg_atlas.h — раскладка атласов статического фона PoP.", - " * Сгенерировано toolchain/pop_pack_bg.py — НЕ править вручную.", + " * Сгенерировано tools/pop_pack_bg.py — НЕ править вручную.", " *", " * Прямая адресация (ноль remap-таблиц в W2):", " * ENV фон id N -> atlas env_bg[N>>%d], idx N&%d" % (ENV_SHIFT, ENV_MASK), @@ -474,27 +474,12 @@ def write_header(out_path, env_pages): "#define POP_PAL_ENV 0x50", "#define POP_PAL_WALL 0x60", "", - "/* Имена файлов атласов по тайлсету (грузятся atlas_load). Таблицы", - " * видны только тому, кто попросил POP_BG_ATLAS_NAMES: иначе копия", - " * строк уедет в каждый включивший заголовок модуль. */", + "/* Палитры пока остаются отдельными файлами рядом с bg.arc. Имена", + " * видит только модуль загрузки, чтобы не размножать строки. */", "#ifdef POP_BG_ATLAS_NAMES", - "static const char *const pop_env_atl[POP_TILESETS][POP_ENV_PAGES] = {", - ] - for prefix, _ in SETS.values(): - row = ", ".join(('"%s%s"' % (prefix, nm.split("_", 1)[1]) if nm else "0") - for nm in env_pages) - lines.append(" { %s }," % row) - lines += [ - "};", - 'static const char *const pop_wall_atl[POP_TILESETS] = { %s };' - % ", ".join('"%swall.atl"' % p for p, _ in SETS.values()), - 'static const char *const pop_fore_atl[POP_TILESETS] = { %s };' - % ", ".join('"%sfore.atl"' % p for p, _ in SETS.values()), 'static const char *const pop_tile_pal[POP_TILESETS] = { %s };' % ", ".join('"%stile.pal"' % p for p, _ in SETS.values()), "#endif /* POP_BG_ATLAS_NAMES */", - '#define POP_POT_ATL "pop_pot.atl" /* chtab_1: зелья, от набора не зависит */', - "#define POP_PAL_POT 0x%02X" % POT_PAL_BASE, "/* Пузырёк зелья: красный набор = id 16..22 (кадры оригинала),", " зелёный (перо/переворот) и синий (вред/открыть) = те же кадры", " под id %d..%d и %d..%d (draw_tile_anim, seg008:652). */" @@ -502,7 +487,6 @@ def write_header(out_path, env_pages): POT_BUBBLE_BLUE_BASE, POT_BUBBLE_BLUE_BASE + len(POT_BUBBLE_IDS) - 1), "#define POP_POT_BUBB_GREEN %d" % POT_BUBBLE_GREEN_BASE, "#define POP_POT_BUBB_BLUE %d" % POT_BUBBLE_BLUE_BASE, - '#define POP_BG_PAL "pop_bg.pal"', "", "#endif", "", diff --git a/applications/SprPoP/tools/pop_pack_kid.py b/applications/SprPoP/tools/pop_pack_kid.py index bd0c8ce..682b1a8 100644 --- a/applications/SprPoP/tools/pop_pack_kid.py +++ b/applications/SprPoP/tools/pop_pack_kid.py @@ -11,7 +11,8 @@ Sprinter 0x70-0x7F (env 0x50 / wall 0x60 заняты фоном). Пиксел фейковые записи (как в фоне). Переиспользует pack_atlas/blob/scale6to8 из pop_pack_bg. -Выход: build/atl/kid/kid0..N.atl + kid.pal + kid_atlas.h +Выход: build/atl/kid/kid0..N.atl + зеркальные атласы + kid.pal. +Количество страниц рантайм получает из сгенерированного описания kid_arc.h. """ import os import sys @@ -200,21 +201,6 @@ def main(): build_palette(os.path.join(OUT_DIR, "kid.pal")) - # C-заголовок - hdr = str(P.GEN / "kid_atlas.h") - with open(hdr, "w") as f: - f.write("/* kid_atlas.h — раскладка атласов Kid. Сгенерировано " - "pop_pack_kid.py. */\n#ifndef KID_ATLAS_H\n#define KID_ATLAS_H\n") - f.write(f"#define KID_SHIFT {SHIFT}\n#define KID_MASK {MASK}\n") - f.write(f"#define KID_PAGES {len(names)}\n#define KID_PAL 0x{PAL_BASE:02X}\n") - f.write("static const char *const kid_atl[KID_PAGES] = {\n") - for nm in names: - f.write(" %s,\n" % ('"%s"' % nm if nm else "0")) - f.write('};\n#define KID_PAL_FILE "kid.pal"\n') - f.write('#define KID_SWORD_ATL "sword.atl" /* chtab_0: меч в руке */\n') - f.write(f"#define KID_SWORD_ID0 {SWORD_IDS[0]} /* индекс в атласе = sword_tbl.id - ID0 */\n") - f.write("#endif\n") - print(f"ИТОГО: {len(ids)} спрайтов Kid, {len(names)} EMM-страниц, " f"{total} Б диск (+1КБ палитра) -> {OUT_DIR}") diff --git a/applications/SprPoP/tools/pop_pack_shadow.py b/applications/SprPoP/tools/pop_pack_shadow.py index 6b80a3a..968cb4b 100644 --- a/applications/SprPoP/tools/pop_pack_shadow.py +++ b/applications/SprPoP/tools/pop_pack_shadow.py @@ -214,7 +214,7 @@ def write_header(path, counts, pal): vals += [b, g, r, 0] # формат gfx_pal_load: B, G, R, 0 lines = [ "/* pop_shadow_atlas.h — раскладка атласа ТЕНИ.", - " * СГЕНЕРЕНО toolchain/pop_pack_shadow.py — НЕ править вручную.", + " * СГЕНЕРЕНО tools/pop_pack_shadow.py — НЕ править вручную.", " *", " * Две половины, потому что и у оригинала их две (см. шапку", " * упаковщика): кадры вне боя адресуют chtab_2 (спрайты Кида),", @@ -240,18 +240,6 @@ def write_header(path, counts, pal): "/* Половина «в бою» — SHADOW.DAT (chtab_5, кадры 150..189). */", "#define POP_SHADOW_FGT_PAGES %d" % counts["sf"], "", - "static const char *const pop_shadow_kid_atl[POP_SHADOW_KID_PAGES] = {", - ] - lines += [' "SHADOW\\\\sk%d.atl",' % i for i in range(counts["sk"])] - lines += [ - "};", - "", - "static const char *const pop_shadow_fgt_atl[POP_SHADOW_FGT_PAGES] = {", - ] - lines += [' "SHADOW\\\\sf%d.atl",' % i for i in range(counts["sf"])] - lines += [ - "};", - "", "/* 16 цветов, запись 4 байта (B, G, R, 0) — формат gfx_pal_load. */", "static const uint8_t pop_shadow_pal[64] = {", " " + ",".join("0x%02X" % v for v in vals) + ",", diff --git a/applications/SprPoP/tools/pop_pack_sound.py b/applications/SprPoP/tools/pop_pack_sound.py index 303d5bf..d607631 100644 --- a/applications/SprPoP/tools/pop_pack_sound.py +++ b/applications/SprPoP/tools/pop_pack_sound.py @@ -155,7 +155,7 @@ def main(): lines = [ "/* pop_sound_tbl.h — раскладка оцифрованных эффектов PoP.", - " * СГЕНЕРЕНО toolchain/pop_pack_sound.py — НЕ править вручную.", + " * СГЕНЕРЕНО tools/pop_pack_sound.py — НЕ править вручную.", " *", " * Данные: 8 бит беззнаковые, МОНО, %g Гц (CBL_FREQ_10K9) — формат" % RATE, " * CBL один в один, конвертировать в рантайме нечего.", @@ -180,6 +180,8 @@ def main(): "#define POP_SND_SILENCE_PAGE 0", "#define POP_SND_SILENCE_OFF 0x0000", "", + "/* Полная таблица нужна только горячей половине звука. */", + "#ifdef POP_SND_TABLE", "typedef struct { uint8_t page; uint16_t off; uint16_t len; } pop_snd_ent_t;", "", "/* len == 0 — у звука нет оцифровки (это музыка, см. sound_plan.md). */", @@ -190,16 +192,11 @@ def main(): lines.append(" { %d, 0x%04X, %5d }, /* %2d */" % (off >> 14, off & 0x3FFF, ln, sid)) lines += [ "};", + "#endif /* POP_SND_TABLE */", + "", + "#endif", "", - "/* Имена файлов видит ТОЛЬКО тот, кто попросил 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] = {", ] - lines += [' "SND\\\\s%d.bin",' % i for i in range(npages)] - lines += ["};", "#endif", "", "#endif", ""] with open(HDR, "w") as f: f.write("\n".join(lines)) diff --git a/toolchain/make_hdd.sh b/toolchain/make_hdd.sh index 072b0fd..8d704b5 100755 --- a/toolchain/make_hdd.sh +++ b/toolchain/make_hdd.sh @@ -1,28 +1,64 @@ #!/bin/bash -# make_hdd.sh OUT.chd FILE1 [FILE2 ...] +# make_hdd.sh [--dest DIR] OUT.chd FILE1 [FILE2 ...] # # Собирает 32 МБ RAW-образ жёсткого диска (MBR + один раздел FAT16) с -# указанными файлами в корне и конвертит его в CHD для MAME (монтируется -# как -hard2). Быстрая альтернатива floppy (mc.img): MAME эмулирует FDD -# с реальной черепашьей скоростью, HDD — в разы быстрее. +# указанными файлами и конвертит его в CHD для MAME (монтируется как -hard2). +# По умолчанию файлы кладутся в корень. --dest DIR (или HDD_DEST_DIR) +# переносит всё дерево под DIR, например games/sprpop. # # Требует: mtools (mpartition/mformat/mcopy) + chdman (из MAME). # Sprinter DSS штатно монтирует стандартный MBR+FAT16 (подтверждено). # # Использование: # toolchain/make_hdd.sh mame/v306/IMG/test_hdd.chd prog.exe data1 data2 ... +# toolchain/make_hdd.sh --dest games/sprpop out.chd prog.exe data1 ... # toolchain/make_hdd.sh out.chd prog.exe KID:kid0.atl GUARD:g0.atl ... -# — аргумент вида КАТАЛОГ:файл кладёт файл в подкаталог образа -# (каталог создаётся); без префикса файл идёт в корень. +# — аргумент вида КАТАЛОГ:файл кладёт файл в подкаталог относительно +# DIR; без префикса файл идёт непосредственно в DIR. set -e +DEST_DIR=${HDD_DEST_DIR:-} +while [ $# -gt 0 ]; do + case "$1" in + --dest) + if [ $# -lt 2 ]; then + echo "make_hdd.sh: --dest требует путь" >&2 + exit 1 + fi + DEST_DIR="$2" + shift 2 + ;; + --dest=*) + DEST_DIR="${1#--dest=}" + shift + ;; + --) + shift + break + ;; + *) break ;; + esac +done + if [ $# -lt 2 ]; then - echo "usage: $0 OUT.chd FILE1 [FILE2 ...]" >&2 + echo "usage: $0 [--dest DIR] OUT.chd FILE1 [FILE2 ...]" >&2 exit 1 fi OUT_CHD="$1"; shift +# DSS использует обратный слэш, но mtools — прямой. Ведущие/замыкающие +# разделители не несут смысла: DIR всегда задаётся относительно корня HDD. +DEST_DIR="${DEST_DIR//\\//}" +while [ "${DEST_DIR#/}" != "$DEST_DIR" ]; do DEST_DIR="${DEST_DIR#/}"; done +while [ "${DEST_DIR%/}" != "$DEST_DIR" ]; do DEST_DIR="${DEST_DIR%/}"; done +case "/$DEST_DIR/" in + */./*|*/../*|*:* ) + echo "make_hdd.sh: недопустимый каталог назначения: $DEST_DIR" >&2 + exit 1 + ;; +esac + # Геометрия: 128*16*32*512 = 32 МБ. BPS=512. CYLS=128; HEADS=16; SECS=32 SECTORS=$((CYLS * HEADS * SECS)) # 65536 @@ -57,6 +93,21 @@ mt() { "$@" < /dev/null } +# mmd не создаёт родителей сам: строим путь по одному компоненту. Пустой +# путь означает корень и сохраняет прежнее поведение скрипта. +make_dir_tree() { + local path="$1" part current="" old_ifs="$IFS" + + [ -z "$path" ] && return + IFS='/' + for part in $path; do + [ -z "$part" ] && continue + current="${current:+$current/}$part" + mt mmd "z:/$current" >/dev/null 2>&1 || true + done + IFS="$old_ifs" +} + # MBR + раздел 1 на весь диск (тип FAT16), затем формат FAT16. echo "--- разметка и формат образа ---" mt mpartition -I -c -t $CYLS -h $HEADS -s $SECS z: @@ -64,17 +115,21 @@ mt mformat z: echo "--- копирование файлов ---" +make_dir_tree "$DEST_DIR" +DEST_ROOT="z:${DEST_DIR:+/$DEST_DIR}" + # Аргумент может быть либо файлом (кладём в корень), либо парой -# КАТАЛОГ:файл — тогда каталог создаётся на образе и файл кладётся туда. -# Имена на Sprinter/DSS — 8.3, каталоги тоже (KID, GUARD, BG, LEVELS...). +# КАТАЛОГ:файл — тогда каталог создаётся относительно DEST_ROOT и файл +# кладётся туда. Имена на Sprinter/DSS — 8.3. for f in "$@"; do case "$f" in *:*) dir="${f%%:*}"; src="${f#*:}" + target_dir="${DEST_DIR:+$DEST_DIR/}$dir" # Каталог может уже быть — это не ошибка; stderr НЕ прячем, иначе # непрошедший вопрос mtools снова окажется невидимым. - mt mmd "z:/$dir" >/dev/null 2>&1 || true - mt mcopy -o "$src" "z:/$dir/" ;; - *) mt mcopy -o "$f" z: ;; + make_dir_tree "$target_dir" + mt mcopy -o "$src" "z:/$target_dir/" ;; + *) mt mcopy -o "$f" "$DEST_ROOT/" ;; esac done