SprPoP: обобщить HDD-сборку и очистить метаданные

Добавить общий каталог назначения для HDD и удалить локальную копию упаковщика.\n\nУбрать устаревшие generated-имена ресурсов, выводить число страниц Kid из kid.arc и ограничить звуковую таблицу горячим модулем.\n\nЗафиксировать планы runtime-индексов музыки и PCM-эффектов.
This commit is contained in:
2026-08-30 16:16:06 +03:00
parent 623199337e
commit ea8efdb0fd
22 changed files with 706 additions and 296 deletions
+18
View File
@@ -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"
+5 -1
View File
@@ -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
# #
@@ -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) — образ пересобран."
+8 -6
View File
@@ -34,8 +34,6 @@ EXAMPLE := sprpop
SRC_DIR := src SRC_DIR := src
BUILD_DIR := build BUILD_DIR := build
MAKE_HDD := tools/make_hdd.sh
# huge: small-раскладка (CODE в W1, DATA в W2) + банки кода в W3. # huge: small-раскладка (CODE в W1, DATA в W2) + банки кода в W3.
MEMORY ?= huge MEMORY ?= huge
# --max-allocs-per-node: во сколько вариантов размещения регистров упирается # --max-allocs-per-node: во сколько вариантов размещения регистров упирается
@@ -222,7 +220,7 @@ 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 \ 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_guard_pal.h pop_shadow_atlas.h pop_sound_tbl.h \
pop_music_tbl.h pop_music_ticks.h pop_font.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)) 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`, то есть упаковщик заводил каталог с
# именем файла и клал файл внутрь него (поймано пользователем на готовом # именем файла и клал файл внутрь него (поймано пользователем на готовом
# образе). # образе).
@@ -239,6 +238,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
@@ -363,7 +365,7 @@ $(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))
+1
View File
@@ -41,6 +41,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 |
@@ -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 через
восстановление каталога приложения.
-41
View File
@@ -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
+3 -13
View File
@@ -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 -39
View File
@@ -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,
+4 -20
View File
@@ -1,5 +1,5 @@
/* pop_sound_tbl.h — раскладка оцифрованных эффектов PoP. /* pop_sound_tbl.h — раскладка оцифрованных эффектов PoP.
* СГЕНЕРЕНО toolchain/pop_pack_sound.py — НЕ править вручную. * СГЕНЕРЕНО tools/pop_pack_sound.py — НЕ править вручную.
* *
* Данные: 8 бит беззнаковые, МОНО, 10937.5 Гц (CBL_FREQ_10K9) — формат * Данные: 8 бит беззнаковые, МОНО, 10937.5 Гц (CBL_FREQ_10K9) — формат
* CBL один в один, конвертировать в рантайме нечего. * CBL один в один, конвертировать в рантайме нечего.
@@ -28,6 +28,8 @@
#define POP_SND_SILENCE_PAGE 0 #define POP_SND_SILENCE_PAGE 0
#define POP_SND_SILENCE_OFF 0x0000 #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; typedef struct { uint8_t page; uint16_t off; uint16_t len; } pop_snd_ent_t;
/* len == 0 — у звука нет оцифровки (это музыка, см. sound_plan.md). */ /* 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 }, /* 55 */
{ 0, 0x0000, 0 }, /* 56 */ { 0, 0x0000, 0 }, /* 56 */
}; };
#endif /* POP_SND_TABLE */
/* Имена файлов видит ТОЛЬКО тот, кто попросил 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 #endif
+3 -3
View File
@@ -1,14 +1,14 @@
/* /*
* _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_sound_tbl.h" /* POP_SND_* и, по запросу, таблица эффектов */
/* Физические страницы набора, по индексу из pop_snd_tbl[].page. */ /* Физические страницы набора, по индексу из pop_snd_tbl[].page. */
extern uint8_t pop_snd_page[POP_SND_PAGES]; extern uint8_t pop_snd_page[POP_SND_PAGES];
+3 -8
View File
@@ -23,14 +23,9 @@
/* Серии в архиве обязаны лежать подряд — так их кладёт список в Makefile, /* Серии в архиве обязаны лежать подряд — так их кладёт список в Makefile,
* и только поэтому индекс считается сложением. Если порядок собьётся, * и только поэтому индекс считается сложением. Если порядок собьётся,
* сборка встанет здесь, а не рассыплется картинкой на экране. */ * сборка встанет здесь, а не рассыплется картинкой на экране. */
#if (ARC_KID_KID27 - ARC_KID_KID0) != 27 #if (ARC_KID_SWORD - ARC_KID_KID0_V) != KID_PAGES
#error "kid0..kid27 в архиве не подряд" #error "обычная и зеркальная серии Kid должны иметь одинаковую длину"
#endif #endif
#if (ARC_KID_KID27_V - ARC_KID_KID0_V) != 27
#error "kid0_v..kid27_v в архиве не подряд"
#endif
#define KID_MAXPAGES 28
/* Таблица архива живёт на стеке вызывающего (4 байта на запись, здесь 232) /* Таблица архива живёт на стеке вызывающего (4 байта на запись, здесь 232)
* — держать её постоянно незачем, а W2 у игры и без того тесный. */ * — держать её постоянно незачем, а W2 у игры и без того тесный. */
@@ -40,7 +35,7 @@ int pop_kid_load(uint8_t npages) __banked
pop_arc_t arc; pop_arc_t arc;
uint8_t i; 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; if (pop_arc_open_id(&arc, POP_ARC_KID, tbl, ARC_KID_COUNT) < 0) return -1;
for (i = 0; i < npages; i++) for (i = 0; i < npages; i++)
if (pop_arc_atlas(&arc, tbl, (uint8_t)(ARC_KID_KID0 + i), if (pop_arc_atlas(&arc, tbl, (uint8_t)(ARC_KID_KID0 + i),
+1 -2
View File
@@ -52,8 +52,7 @@ uint8_t pop_endmus_left;
* НЕ static: набор спрайтов слота POP_CH_KID общей отрисовки персонажей * НЕ static: набор спрайтов слота POP_CH_KID общей отрисовки персонажей
* (pop_cdraw.c, банк 4). Отдан как есть, а не через аксессор: читается раз * (pop_cdraw.c, банк 4). Отдан как есть, а не через аксессор: читается раз
* в кадр из другого банка, и вызов стоил бы дороже самого чтения. */ * в кадр из другого банка, и вызов стоил бы дороже самого чтения. */
#define KID_MAXPAGES 28 atlas_t kidp[KID_PAGES];
atlas_t kidp[KID_MAXPAGES];
uint8_t kid_npages; uint8_t kid_npages;
/* Таблица «seq_id -> адрес в seqtbl» (original_seqtbl_offsets). Определена /* Таблица «seq_id -> адрес в seqtbl» (original_seqtbl_offsets). Определена
+5
View File
@@ -13,8 +13,13 @@
#include <stdint.h> #include <stdint.h>
#include "pop_char.h" #include "pop_char.h"
#include "kid_data.h" /* kframe — тип кадра для pop_sword_draw */ #include "kid_data.h" /* kframe — тип кадра для pop_sword_draw */
#include "kid_arc.h" /* границы серий атласов в kid.arc */
#include <sprite.h> /* atlas_t — набор спрайтов Кида (см. kidp ниже) */ #include <sprite.h> /* atlas_t — набор спрайтов Кида (см. kidp ниже) */
/* Число страниц Кида берём из метаданных архива: зеркальная серия начинается
* сразу после обычной. Не дублировать литералом в загрузчике/хранилище. */
#define KID_PAGES (ARC_KID_KID0_V - ARC_KID_KID0)
/* Состояние персонажа — общий тип для Кида, стража и Char (pop_char.h). */ /* Состояние персонажа — общий тип для Кида, стража и Char (pop_char.h). */
extern pop_char_t Kid; extern pop_char_t Kid;
+1
View File
@@ -21,6 +21,7 @@
#include <irq.h> /* IRQ_DISABLE/IRQ_ENABLE вокруг курсора */ #include <irq.h> /* IRQ_DISABLE/IRQ_ENABLE вокруг курсора */
#include "pop_sfx.h" #include "pop_sfx.h"
#include "pop_music.h" #include "pop_music.h"
#define POP_SND_TABLE /* таблица нужна только горячей половине */
#include "_pop_sfx.h" #include "_pop_sfx.h"
/* Физические страницы набора; заполняет холодная половина. */ /* Физические страницы набора; заполняет холодная половина. */
-1
View File
@@ -9,7 +9,6 @@
#include <cbl.h> #include <cbl.h>
#include "pop_sfx.h" #include "pop_sfx.h"
#include "pop_arc.h" #include "pop_arc.h"
#define POP_SND_FILES /* имена файлов нужны ТОЛЬКО здесь, см. pop_sound_tbl.h */
#include "_pop_sfx.h" #include "_pop_sfx.h"
static uint8_t snd_blk; /* блок EMM под весь набор */ static uint8_t snd_blk; /* блок EMM под весь набор */
+1 -1
View File
@@ -1158,7 +1158,7 @@ int pop_boot(void) __banked
pop_shadow_load(); /* запечённый набор Тени — один раз на запуск */ pop_shadow_load(); /* запечённый набор Тени — один раз на запуск */
pop_sfx_init(); /* звук: 8 страниц оцифровки (вывод пока молчит) */ pop_sfx_init(); /* звук: 8 страниц оцифровки (вывод пока молчит) */
pop_settings_apply(1); /* CFG -> speed/cheats/CBL */ 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"); puts("kid atlas load failed");
return -1; return -1;
} }
-92
View File
@@ -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"
+3 -19
View File
@@ -450,7 +450,7 @@ def write_header(out_path, env_pages):
файлов и палитра. Поэтому здесь два массива имён и один индекс.""" файлов и палитра. Поэтому здесь два массива имён и один индекс."""
lines = [ lines = [
"/* 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>>%d], idx N&%d" % (ENV_SHIFT, ENV_MASK), " * 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_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] = {",
]
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 };' 'static const char *const pop_tile_pal[POP_TILESETS] = { %s };'
% ", ".join('"%stile.pal"' % p for p, _ in SETS.values()), % ", ".join('"%stile.pal"' % p for p, _ in SETS.values()),
"#endif /* POP_BG_ATLAS_NAMES */", "#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 16..22 (кадры оригинала),",
" зелёный (перо/переворот) и синий (вред/открыть) = те же кадры", " зелёный (перо/переворот) и синий (вред/открыть) = те же кадры",
" под id %d..%d и %d..%d (draw_tile_anim, seg008:652). */" " под 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), 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_GREEN %d" % POT_BUBBLE_GREEN_BASE,
"#define POP_POT_BUBB_BLUE %d" % POT_BUBBLE_BLUE_BASE, "#define POP_POT_BUBB_BLUE %d" % POT_BUBBLE_BLUE_BASE,
'#define POP_BG_PAL "pop_bg.pal"',
"", "",
"#endif", "#endif",
"", "",
+2 -16
View File
@@ -11,7 +11,8 @@ Sprinter 0x70-0x7F (env 0x50 / wall 0x60 заняты фоном). Пиксел
фейковые записи (как в фоне). Переиспользует pack_atlas/blob/scale6to8 фейковые записи (как в фоне). Переиспользует pack_atlas/blob/scale6to8
из pop_pack_bg. из 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 os
import sys import sys
@@ -200,21 +201,6 @@ def main():
build_palette(os.path.join(OUT_DIR, "kid.pal")) 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-страниц, " print(f"ИТОГО: {len(ids)} спрайтов Kid, {len(names)} EMM-страниц, "
f"{total} Б диск (+1КБ палитра) -> {OUT_DIR}") f"{total} Б диск (+1КБ палитра) -> {OUT_DIR}")
+1 -13
View File
@@ -214,7 +214,7 @@ def write_header(path, counts, pal):
vals += [b, g, r, 0] # формат gfx_pal_load: B, G, R, 0 vals += [b, g, r, 0] # формат gfx_pal_load: B, G, R, 0
lines = [ lines = [
"/* pop_shadow_atlas.h — раскладка атласа ТЕНИ.", "/* pop_shadow_atlas.h — раскладка атласа ТЕНИ.",
" * СГЕНЕРЕНО toolchain/pop_pack_shadow.py — НЕ править вручную.", " * СГЕНЕРЕНО tools/pop_pack_shadow.py — НЕ править вручную.",
" *", " *",
" * Две половины, потому что и у оригинала их две (см. шапку", " * Две половины, потому что и у оригинала их две (см. шапку",
" * упаковщика): кадры вне боя адресуют chtab_2 (спрайты Кида),", " * упаковщика): кадры вне боя адресуют chtab_2 (спрайты Кида),",
@@ -240,18 +240,6 @@ def write_header(path, counts, pal):
"/* Половина «в бою» — SHADOW.DAT (chtab_5, кадры 150..189). */", "/* Половина «в бою» — SHADOW.DAT (chtab_5, кадры 150..189). */",
"#define POP_SHADOW_FGT_PAGES %d" % counts["sf"], "#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. */", "/* 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] = {",
" " + ",".join("0x%02X" % v for v in vals) + ",", " " + ",".join("0x%02X" % v for v in vals) + ",",
+6 -9
View File
@@ -155,7 +155,7 @@ def main():
lines = [ lines = [
"/* pop_sound_tbl.h — раскладка оцифрованных эффектов PoP.", "/* pop_sound_tbl.h — раскладка оцифрованных эффектов PoP.",
" * СГЕНЕРЕНО toolchain/pop_pack_sound.py — НЕ править вручную.", " * СГЕНЕРЕНО tools/pop_pack_sound.py — НЕ править вручную.",
" *", " *",
" * Данные: 8 бит беззнаковые, МОНО, %g Гц (CBL_FREQ_10K9) — формат" % RATE, " * Данные: 8 бит беззнаковые, МОНО, %g Гц (CBL_FREQ_10K9) — формат" % RATE,
" * CBL один в один, конвертировать в рантайме нечего.", " * CBL один в один, конвертировать в рантайме нечего.",
@@ -180,6 +180,8 @@ def main():
"#define POP_SND_SILENCE_PAGE 0", "#define POP_SND_SILENCE_PAGE 0",
"#define POP_SND_SILENCE_OFF 0x0000", "#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;", "typedef struct { uint8_t page; uint16_t off; uint16_t len; } pop_snd_ent_t;",
"", "",
"/* len == 0 — у звука нет оцифровки (это музыка, см. sound_plan.md). */", "/* 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.append(" { %d, 0x%04X, %5d }, /* %2d */" % (off >> 14, off & 0x3FFF, ln, sid))
lines += [ 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: with open(HDR, "w") as f:
f.write("\n".join(lines)) f.write("\n".join(lines))
+67 -12
View File
@@ -1,28 +1,64 @@
#!/bin/bash #!/bin/bash
# make_hdd.sh OUT.chd FILE1 [FILE2 ...] # make_hdd.sh [--dest DIR] OUT.chd FILE1 [FILE2 ...]
# #
# Собирает 32 МБ RAW-образ жёсткого диска (MBR + один раздел FAT16) с # Собирает 32 МБ RAW-образ жёсткого диска (MBR + один раздел FAT16) с
# указанными файлами в корне и конвертит его в CHD для MAME (монтируется # указанными файлами и конвертит его в CHD для MAME (монтируется как -hard2).
# как -hard2). Быстрая альтернатива floppy (mc.img): MAME эмулирует FDD # По умолчанию файлы кладутся в корень. --dest DIR (или HDD_DEST_DIR)
# с реальной черепашьей скоростью, HDD — в разы быстрее. # переносит всё дерево под DIR, например games/sprpop.
# #
# Требует: mtools (mpartition/mformat/mcopy) + chdman (из MAME). # Требует: mtools (mpartition/mformat/mcopy) + chdman (из MAME).
# Sprinter DSS штатно монтирует стандартный MBR+FAT16 (подтверждено). # Sprinter DSS штатно монтирует стандартный MBR+FAT16 (подтверждено).
# #
# Использование: # Использование:
# toolchain/make_hdd.sh mame/v306/IMG/test_hdd.chd prog.exe data1 data2 ... # 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 ... # toolchain/make_hdd.sh out.chd prog.exe KID:kid0.atl GUARD:g0.atl ...
# — аргумент вида КАТАЛОГ:файл кладёт файл в подкаталог образа # — аргумент вида КАТАЛОГ:файл кладёт файл в подкаталог относительно
# (каталог создаётся); без префикса файл идёт в корень. # DIR; без префикса файл идёт непосредственно в DIR.
set -e 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 if [ $# -lt 2 ]; then
echo "usage: $0 OUT.chd FILE1 [FILE2 ...]" >&2 echo "usage: $0 [--dest DIR] OUT.chd FILE1 [FILE2 ...]" >&2
exit 1 exit 1
fi fi
OUT_CHD="$1"; shift 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. # Геометрия: 128*16*32*512 = 32 МБ. BPS=512.
CYLS=128; HEADS=16; SECS=32 CYLS=128; HEADS=16; SECS=32
SECTORS=$((CYLS * HEADS * SECS)) # 65536 SECTORS=$((CYLS * HEADS * SECS)) # 65536
@@ -57,6 +93,21 @@ mt() {
"$@" < /dev/null "$@" < /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. # MBR + раздел 1 на весь диск (тип FAT16), затем формат FAT16.
echo "--- разметка и формат образа ---" echo "--- разметка и формат образа ---"
mt mpartition -I -c -t $CYLS -h $HEADS -s $SECS z: mt mpartition -I -c -t $CYLS -h $HEADS -s $SECS z:
@@ -64,17 +115,21 @@ mt mformat z:
echo "--- копирование файлов ---" echo "--- копирование файлов ---"
make_dir_tree "$DEST_DIR"
DEST_ROOT="z:${DEST_DIR:+/$DEST_DIR}"
# Аргумент может быть либо файлом (кладём в корень), либо парой # Аргумент может быть либо файлом (кладём в корень), либо парой
# КАТАЛОГ:файл — тогда каталог создаётся на образе и файл кладётся туда. # КАТАЛОГ:файл — тогда каталог создаётся относительно DEST_ROOT и файл
# Имена на Sprinter/DSS — 8.3, каталоги тоже (KID, GUARD, BG, LEVELS...). # кладётся туда. Имена на Sprinter/DSS — 8.3.
for f in "$@"; do for f in "$@"; do
case "$f" in case "$f" in
*:*) dir="${f%%:*}"; src="${f#*:}" *:*) dir="${f%%:*}"; src="${f#*:}"
target_dir="${DEST_DIR:+$DEST_DIR/}$dir"
# Каталог может уже быть — это не ошибка; stderr НЕ прячем, иначе # Каталог может уже быть — это не ошибка; stderr НЕ прячем, иначе
# непрошедший вопрос mtools снова окажется невидимым. # непрошедший вопрос mtools снова окажется невидимым.
mt mmd "z:/$dir" >/dev/null 2>&1 || true make_dir_tree "$target_dir"
mt mcopy -o "$src" "z:/$dir/" ;; mt mcopy -o "$src" "z:/$target_dir/" ;;
*) mt mcopy -o "$f" z: ;; *) mt mcopy -o "$f" "$DEST_ROOT/" ;;
esac esac
done done