2349481b86
Часть II плана music_runtime_index_plan.md (SI0..SI4). gen/pop_sound_tbl.h БОЛЬШЕ НЕ ГЕНЕРИРУЕТСЯ: раскладка набора читается из SND/snd.idx (формат PSI1, писатель и разборщик — tools/pop_idx.py, 22 теста в make test-tools). Один и тот же sprpop.exe работает с набором SDLPoP (9 страниц) и MSDOS (10) — sha256 бинарника при смене набора не меняется. Заодно умолчание источника эффектов переведено на SDLPoP (SND_SRC=sdlpop): сборка обязана работать без оригинального дистрибутива DOS. У кого он есть, включает лучший набор явно — make SND_SRC=msdos (там полнее оцифровка: в SDLPoP звук 48 spiked пустой). Устройство: pop_snd_tbl/pop_snd_page/pop_snd_pages — резидентные данные (pop_snd_data.c), тип и инварианты — рукописный pop_snd_tbl.h. Записи читаются ОДНИМ read прямо в таблицу, поэтому sizeof(pop_snd_ent_t) == 5 стало частью дискового контракта: проверяется статически и полем размера записи в заголовке. POP_SND_PAGES как compile-time размер набора исчез — вместо него POP_SND_MAX_PAGES (вместимость, 16) и runtime pop_snd_pages. Цена: таблица переехала из _CODE в _DATA, суммарный резидент почти не изменился (куча 239 -> 229 Б); банк 8 +601 Б на чтение и валидацию. Валидация не доверяет файлу: заголовок целиком плюс каждая запись (страница, смещение, кратность блоку, непересечение с блоком тишины, выход за последнюю страницу). Последнее считается В БЛОКАХ — байтовый адрес конца не влезает в uint16, а 32-битная арифметика на Z80 дорога. НЕТ ИНДЕКСА — ЭФФЕКТОВ НЕТ, НО МУЗЫКА ИГРАЕТ. Первая версия просто возвращала ошибку, и игра становилась непроходимой: тишину льёт первый блок набора, без набора CBL не открывался, а с ним вставала музыка (её блоки считает тот же насос) — заставка ждала конца трека вечно. Теперь поднимается пустой набор с блоком тишины. Заливается ровно 128 байт и под DI: gfx_w0_page_prepare ставит в страницу IRQ-стабы, и заливка всей страницы затирала их — первое же прерывание давало чёрный экран. Грабли сборки: смена SND_SRC тихо давала неверный результат (sdlpop -> msdos -> sdlpop оставлял чужой набор в assets/packed). Причина не в логике, а в секундной гранулярности mtime. Лечение убирает время из решения: смена варианта сносит stamp'ы своего семейства, а упаковка, сборка архива и копия индекса делаются одним рецептом. То же получила и музыка (MUSIC_FMT). Проверено в MAME: таблица в памяти совпадает с файлом из образа побайтово; один EXE поднимает оба набора; отладочный --order reverse (30 из 31 записей отличаются от штатных) звучит правильно; битый индекс выключает эффекты, не роняя игру; без индекса PV-сцена проходит с музыкой; Ctrl+S работает в обоих режимах. Разбор — docs/sound_plan.md §10. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011MsUsEFAQfsjjQpJ7RtKVY
113 lines
5.4 KiB
Python
113 lines
5.4 KiB
Python
"""paths.py — единственное место, где инструменты SprPoP знают раскладку
|
||
каталогов приложения. Все упаковщики берут пути отсюда, поэтому папку
|
||
SprPoP можно перенести куда угодно: важно лишь, что рядом с tools/ лежат
|
||
assets/, gen/ и build/.
|
||
|
||
Раскладка:
|
||
|
||
assets/orig/ ОРИГИНАЛЬНЫЕ данные (SDLPoP, записи музыки и,
|
||
необязательно, MSDOS). В репозиторий не идут —
|
||
копирайт; выкачивает их tools/fetch_orig.py
|
||
(`make fetch`), см. assets/orig/README.md.
|
||
build/atl/<set>/ промежуточный выхлоп упаковщиков: отдельные атласы,
|
||
из которых потом склеиваются архивы. Пересоздаётся,
|
||
в репозиторий не идёт.
|
||
assets/packed/ ровно то, что ложится на диск игры, уже в раскладке
|
||
каталогов диска (BG/, KID/, ...). В репозитории —
|
||
иначе из чистого клона без assets/orig/ ничего не
|
||
собрать.
|
||
gen/ генерируемые C-заголовки. Тоже в репозитории и по
|
||
той же причине: это такой же производный артефакт,
|
||
как .arc, и без него не скомпилировать src/.
|
||
"""
|
||
|
||
from pathlib import Path
|
||
|
||
TOOLS = Path(__file__).resolve().parent
|
||
APP = TOOLS.parent
|
||
|
||
# --- вход: оригинальные данные -------------------------------------------
|
||
ORIG = APP / "assets" / "orig"
|
||
SDLPOP = ORIG / "SDLPoP"
|
||
SDLPOP_DATA = SDLPOP / "data"
|
||
SDLPOP_SRC = SDLPOP / "src"
|
||
MSDOS = ORIG / "MSDOS"
|
||
|
||
MUSIC_ROOT = ORIG / "PoP1_DOS_music"
|
||
# Записи DOS-версии лежат в четырёх вариантах. Имена треков во всех наборах
|
||
# одинаковые, различаются каталог и расширение; ogg_MT-32 — та же музыка в
|
||
# исполнении Roland MT-32, а не другой кодек. Ключ = имя формата в Makefile.
|
||
MUSIC_FORMATS = {
|
||
"flac": ("flac", ".flac"),
|
||
"mp3": ("mp3", ".mp3"),
|
||
"ogg": ("ogg", ".ogg"),
|
||
"mt32": ("ogg_MT-32", ".ogg"),
|
||
}
|
||
|
||
|
||
def music_src(fmt="flac"):
|
||
"""(каталог, расширение) исходников музыки для формата."""
|
||
if fmt not in MUSIC_FORMATS:
|
||
raise SystemExit(
|
||
"неизвестный формат музыки %r; есть: %s"
|
||
% (fmt, ", ".join(sorted(MUSIC_FORMATS))))
|
||
sub, ext = MUSIC_FORMATS[fmt]
|
||
return MUSIC_ROOT / sub, ext
|
||
|
||
# --- источники звуковых эффектов ------------------------------------------
|
||
# Контейнеры digisnd1..3 есть в ДВУХ поставках, и они не одинаковы: в SDLPoP
|
||
# лежит версия из репозитория (её и качает fetch_orig.py), в MSDOS/ — из
|
||
# оригинального дистрибутива 1.0, если пользователь его себе положил.
|
||
# ПО УМОЛЧАНИЮ БЕРЁМ SDLPoP: сборка обязана работать без MSDOS вообще.
|
||
# Ключ = значение SND_SRC в Makefile; регистр имён в поставках разный.
|
||
SND_SOURCES = {
|
||
"sdlpop": (SDLPOP_DATA, "DIGISND%d.DAT"),
|
||
"msdos": (MSDOS, "digisnd%d.dat"),
|
||
}
|
||
|
||
|
||
def snd_src(name="sdlpop"):
|
||
"""(каталог, шаблон имени) контейнеров оцифровки для источника."""
|
||
if name not in SND_SOURCES:
|
||
raise SystemExit(
|
||
"неизвестный источник звука %r; есть: %s"
|
||
% (name, ", ".join(sorted(SND_SOURCES))))
|
||
return SND_SOURCES[name]
|
||
|
||
|
||
def find_ci(directory, name):
|
||
"""Путь к файлу без оглядки на регистр (FAT-имена приезжают по-разному).
|
||
|
||
Возвращает None, если файла нет. Точное совпадение проверяется первым,
|
||
чтобы на case-insensitive ФС не читать каталог зря.
|
||
"""
|
||
p = directory / name
|
||
if p.exists():
|
||
return p
|
||
if not directory.is_dir():
|
||
return None
|
||
low = name.lower()
|
||
for e in directory.iterdir():
|
||
if e.name.lower() == low:
|
||
return e
|
||
return None
|
||
|
||
# --- выход ----------------------------------------------------------------
|
||
ATL = APP / "build" / "atl" # промежуточные атласы, по набору в папке
|
||
PACKED = APP / "assets" / "packed" # раскладка диска игры
|
||
GEN = APP / "gen" # генерируемые заголовки
|
||
|
||
|
||
def atl(name):
|
||
"""Каталог промежуточного выхлопа набора; создаётся при обращении."""
|
||
p = ATL / name
|
||
p.mkdir(parents=True, exist_ok=True)
|
||
return p
|
||
|
||
|
||
def packed(name):
|
||
"""Каталог диска игры (BG, KID, ...); создаётся при обращении."""
|
||
p = PACKED / name
|
||
p.mkdir(parents=True, exist_ok=True)
|
||
return p
|