Compare commits
2 Commits
v0.9.0
..
ea8efdb0fd
| Author | SHA1 | Date | |
|---|---|---|---|
| ea8efdb0fd | |||
| 623199337e |
@@ -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"
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
# # EXTRA_SRCS := helper.c util.c # additional .c files in this dir
|
# # EXTRA_SRCS := helper.c util.c # additional .c files in this dir
|
||||||
# # EXTRA_FLAGS := --crt0=minimal # passed through to sprinter-cc
|
# # EXTRA_FLAGS := --crt0=minimal # passed through to sprinter-cc
|
||||||
# # EXTRA_DATA := test.txt # extra files to add to `make floppy`
|
# # EXTRA_DATA := test.txt # extra files to add to `make floppy`
|
||||||
|
# # HDD_DEST_DIR := games/myapp # общий каталог файлов в `make hdd`
|
||||||
#
|
#
|
||||||
# include $(PROJ_ROOT)/app.mk
|
# include $(PROJ_ROOT)/app.mk
|
||||||
#
|
#
|
||||||
@@ -40,7 +41,7 @@ FLOPPY_IMG := $(MAME_DIR)/IMG/mc.img
|
|||||||
# образ у себя и связывает его с MAME символьной ссылкой.
|
# образ у себя и связывает его с MAME символьной ссылкой.
|
||||||
HDD_IMG ?= $(MAME_DIR)/IMG/test_hdd.chd
|
HDD_IMG ?= $(MAME_DIR)/IMG/test_hdd.chd
|
||||||
MAKE_DISK := $(MAME_DIR)/make_disk.py
|
MAKE_DISK := $(MAME_DIR)/make_disk.py
|
||||||
MAKE_HDD := $(PROJ_ROOT)/toolchain/make_hdd.sh
|
MAKE_HDD ?= $(PROJ_ROOT)/toolchain/make_hdd.sh
|
||||||
RUN_MAME := $(MAME_DIR)/run_mame.sh
|
RUN_MAME := $(MAME_DIR)/run_mame.sh
|
||||||
|
|
||||||
# Optional knobs — see top of file.
|
# Optional knobs — see top of file.
|
||||||
@@ -67,6 +68,9 @@ SOURCES := $(MAIN_SRC) $(EXTRA_SRCS)
|
|||||||
# Аргументы упаковщика HDD. Обычно это exe и EXTRA_DATA; приложение со
|
# Аргументы упаковщика HDD. Обычно это exe и EXTRA_DATA; приложение со
|
||||||
# своей раскладкой каталогов может переопределить переменную до include.
|
# своей раскладкой каталогов может переопределить переменную до include.
|
||||||
HDD_PACK_ARGS ?= $(EXE) $(EXTRA_DATA)
|
HDD_PACK_ARGS ?= $(EXE) $(EXTRA_DATA)
|
||||||
|
# Общий каталог назначения внутри HDD. Пустое значение сохраняет прежнюю
|
||||||
|
# укладку в корень; вложенные КАТАЛОГ:файл считаются относительно него.
|
||||||
|
HDD_DEST_DIR ?=
|
||||||
|
|
||||||
CC_FLAGS := --memory $(MEMORY)
|
CC_FLAGS := --memory $(MEMORY)
|
||||||
ifneq ($(STACK_SIZE),)
|
ifneq ($(STACK_SIZE),)
|
||||||
@@ -125,7 +129,7 @@ run: floppy
|
|||||||
# используется MCP-мостом к MAME (run_bridge.sh). После пересборки образа
|
# используется MCP-мостом к MAME (run_bridge.sh). После пересборки образа
|
||||||
# MAME ОБЯЗАН полный рестарт (chdman -f = новый inode; см. memory).
|
# MAME ОБЯЗАН полный рестарт (chdman -f = новый inode; см. memory).
|
||||||
hdd: $(EXE)
|
hdd: $(EXE)
|
||||||
$(MAKE_HDD) $(HDD_IMG) $(HDD_PACK_ARGS)
|
$(MAKE_HDD) $(if $(strip $(HDD_DEST_DIR)),--dest "$(HDD_DEST_DIR)") $(HDD_IMG) $(HDD_PACK_ARGS)
|
||||||
@echo
|
@echo
|
||||||
@echo "HDD (D:) ready: $(HDD_IMG) (with $(EXAMPLE).exe$(if $(EXTRA_DATA), + $(EXTRA_DATA)))"
|
@echo "HDD (D:) ready: $(HDD_IMG) (with $(EXAMPLE).exe$(if $(EXTRA_DATA), + $(EXTRA_DATA)))"
|
||||||
@echo "ВНИМАНИЕ: перезапусти MAME (run_bridge.sh) — образ пересобран."
|
@echo "ВНИМАНИЕ: перезапусти MAME (run_bridge.sh) — образ пересобран."
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ BANK9_SRCS := pop_menu.c pop_status.c pop_music.c pop_app.c pop_timer.c \
|
|||||||
# стоит 51 мс, так что трамплин банк→банк на их фоне не существует.
|
# стоит 51 мс, так что трамплин банк→банк на их фоне не существует.
|
||||||
# Отдельный банк для этого не нужен: в десятом лежал один pop_pal.c на
|
# Отдельный банк для этого не нужен: в десятом лежал один pop_pal.c на
|
||||||
# 485 байт, то есть 3% страницы.
|
# 485 байт, то есть 3% страницы.
|
||||||
BANK10_SRCS := pop_pal.c pop_config.c pop_hof.c
|
BANK10_SRCS := pop_pal.c pop_config.c pop_hof.c pop_path_bank.c
|
||||||
BANK11_SRCS := pop_cutscene.c pop_intro.c
|
BANK11_SRCS := pop_cutscene.c pop_intro.c
|
||||||
BANK_NUMS := 1 2 3 4 5 6 7 8 9 10 11
|
BANK_NUMS := 1 2 3 4 5 6 7 8 9 10 11
|
||||||
BANKED_SRCS := $(foreach n,$(BANK_NUMS),$(BANK$(n)_SRCS))
|
BANKED_SRCS := $(foreach n,$(BANK_NUMS),$(BANK$(n)_SRCS))
|
||||||
@@ -220,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)
|
||||||
|
|
||||||
@@ -228,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`, то есть упаковщик заводил каталог с
|
||||||
# именем файла и клал файл внутрь него (поймано пользователем на готовом
|
# именем файла и клал файл внутрь него (поймано пользователем на готовом
|
||||||
# образе).
|
# образе).
|
||||||
@@ -237,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
|
||||||
@@ -361,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))
|
||||||
|
|||||||
@@ -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 через
|
||||||
|
восстановление каталога приложения.
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
/* kid_atlas.h — раскладка атласов Kid. Сгенерировано pop_pack_kid.py. */
|
|
||||||
#ifndef KID_ATLAS_H
|
|
||||||
#define KID_ATLAS_H
|
|
||||||
#define KID_SHIFT 3
|
|
||||||
#define KID_MASK 7
|
|
||||||
#define KID_PAGES 28
|
|
||||||
#define KID_PAL 0x70
|
|
||||||
static const char *const kid_atl[KID_PAGES] = {
|
|
||||||
"kid0.atl",
|
|
||||||
"kid1.atl",
|
|
||||||
"kid2.atl",
|
|
||||||
"kid3.atl",
|
|
||||||
"kid4.atl",
|
|
||||||
"kid5.atl",
|
|
||||||
"kid6.atl",
|
|
||||||
"kid7.atl",
|
|
||||||
"kid8.atl",
|
|
||||||
"kid9.atl",
|
|
||||||
"kid10.atl",
|
|
||||||
"kid11.atl",
|
|
||||||
"kid12.atl",
|
|
||||||
"kid13.atl",
|
|
||||||
"kid14.atl",
|
|
||||||
"kid15.atl",
|
|
||||||
"kid16.atl",
|
|
||||||
"kid17.atl",
|
|
||||||
"kid18.atl",
|
|
||||||
"kid19.atl",
|
|
||||||
"kid20.atl",
|
|
||||||
"kid21.atl",
|
|
||||||
"kid22.atl",
|
|
||||||
"kid23.atl",
|
|
||||||
"kid24.atl",
|
|
||||||
"kid25.atl",
|
|
||||||
"kid26.atl",
|
|
||||||
"kid27.atl",
|
|
||||||
};
|
|
||||||
#define KID_PAL_FILE "kid.pal"
|
|
||||||
#define KID_SWORD_ATL "sword.atl" /* chtab_0: меч в руке */
|
|
||||||
#define KID_SWORD_ID0 0 /* индекс в атласе = sword_tbl.id - ID0 */
|
|
||||||
#endif
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/* pop_bg_atlas.h — раскладка атласов статического фона PoP.
|
/* pop_bg_atlas.h — раскладка атласов статического фона PoP.
|
||||||
* Сгенерировано toolchain/pop_pack_bg.py — НЕ править вручную.
|
* Сгенерировано tools/pop_pack_bg.py — НЕ править вручную.
|
||||||
*
|
*
|
||||||
* Прямая адресация (ноль remap-таблиц в W2):
|
* Прямая адресация (ноль remap-таблиц в W2):
|
||||||
* ENV фон id N -> atlas env_bg[N>>4], idx N&15
|
* ENV фон id N -> atlas env_bg[N>>4], idx N&15
|
||||||
@@ -23,25 +23,15 @@
|
|||||||
#define POP_PAL_ENV 0x50
|
#define POP_PAL_ENV 0x50
|
||||||
#define POP_PAL_WALL 0x60
|
#define POP_PAL_WALL 0x60
|
||||||
|
|
||||||
/* Имена файлов атласов по тайлсету (грузятся atlas_load). Таблицы
|
/* Палитры пока остаются отдельными файлами рядом с bg.arc. Имена
|
||||||
* видны только тому, кто попросил POP_BG_ATLAS_NAMES: иначе копия
|
* видит только модуль загрузки, чтобы не размножать строки. */
|
||||||
* строк уедет в каждый включивший заголовок модуль. */
|
|
||||||
#ifdef POP_BG_ATLAS_NAMES
|
#ifdef POP_BG_ATLAS_NAMES
|
||||||
static const char *const pop_env_atl[POP_TILESETS][POP_ENV_PAGES] = {
|
|
||||||
{ "pop_env0.atl", "pop_env1.atl", "pop_env2.atl", "pop_env3.atl", "pop_env4.atl", "pop_env5.atl", "pop_env6.atl", "pop_env7.atl", "pop_env8.atl", "pop_env9.atl" },
|
|
||||||
{ "pal_env0.atl", "pal_env1.atl", "pal_env2.atl", "pal_env3.atl", "pal_env4.atl", "pal_env5.atl", "pal_env6.atl", "pal_env7.atl", "pal_env8.atl", "pal_env9.atl" },
|
|
||||||
};
|
|
||||||
static const char *const pop_wall_atl[POP_TILESETS] = { "pop_wall.atl", "pal_wall.atl" };
|
|
||||||
static const char *const pop_fore_atl[POP_TILESETS] = { "pop_fore.atl", "pal_fore.atl" };
|
|
||||||
static const char *const pop_tile_pal[POP_TILESETS] = { "pop_tile.pal", "pal_tile.pal" };
|
static const char *const pop_tile_pal[POP_TILESETS] = { "pop_tile.pal", "pal_tile.pal" };
|
||||||
#endif /* POP_BG_ATLAS_NAMES */
|
#endif /* POP_BG_ATLAS_NAMES */
|
||||||
#define POP_POT_ATL "pop_pot.atl" /* chtab_1: зелья, от набора не зависит */
|
|
||||||
#define POP_PAL_POT 0x40
|
|
||||||
/* Пузырёк зелья: красный набор = id 16..22 (кадры оригинала),
|
/* Пузырёк зелья: красный набор = id 16..22 (кадры оригинала),
|
||||||
зелёный (перо/переворот) и синий (вред/открыть) = те же кадры
|
зелёный (перо/переворот) и синий (вред/открыть) = те же кадры
|
||||||
под id 30..36 и 40..46 (draw_tile_anim, seg008:652). */
|
под id 30..36 и 40..46 (draw_tile_anim, seg008:652). */
|
||||||
#define POP_POT_BUBB_GREEN 30
|
#define POP_POT_BUBB_GREEN 30
|
||||||
#define POP_POT_BUBB_BLUE 40
|
#define POP_POT_BUBB_BLUE 40
|
||||||
#define POP_BG_PAL "pop_bg.pal"
|
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/* pop_shadow_atlas.h — раскладка атласа ТЕНИ.
|
/* pop_shadow_atlas.h — раскладка атласа ТЕНИ.
|
||||||
* СГЕНЕРЕНО toolchain/pop_pack_shadow.py — НЕ править вручную.
|
* СГЕНЕРЕНО tools/pop_pack_shadow.py — НЕ править вручную.
|
||||||
*
|
*
|
||||||
* Две половины, потому что и у оригинала их две (см. шапку
|
* Две половины, потому что и у оригинала их две (см. шапку
|
||||||
* упаковщика): кадры вне боя адресуют chtab_2 (спрайты Кида),
|
* упаковщика): кадры вне боя адресуют chtab_2 (спрайты Кида),
|
||||||
@@ -25,44 +25,6 @@
|
|||||||
/* Половина «в бою» — SHADOW.DAT (chtab_5, кадры 150..189). */
|
/* Половина «в бою» — SHADOW.DAT (chtab_5, кадры 150..189). */
|
||||||
#define POP_SHADOW_FGT_PAGES 4
|
#define POP_SHADOW_FGT_PAGES 4
|
||||||
|
|
||||||
static const char *const pop_shadow_kid_atl[POP_SHADOW_KID_PAGES] = {
|
|
||||||
"SHADOW\\sk0.atl",
|
|
||||||
"SHADOW\\sk1.atl",
|
|
||||||
"SHADOW\\sk2.atl",
|
|
||||||
"SHADOW\\sk3.atl",
|
|
||||||
"SHADOW\\sk4.atl",
|
|
||||||
"SHADOW\\sk5.atl",
|
|
||||||
"SHADOW\\sk6.atl",
|
|
||||||
"SHADOW\\sk7.atl",
|
|
||||||
"SHADOW\\sk8.atl",
|
|
||||||
"SHADOW\\sk9.atl",
|
|
||||||
"SHADOW\\sk10.atl",
|
|
||||||
"SHADOW\\sk11.atl",
|
|
||||||
"SHADOW\\sk12.atl",
|
|
||||||
"SHADOW\\sk13.atl",
|
|
||||||
"SHADOW\\sk14.atl",
|
|
||||||
"SHADOW\\sk15.atl",
|
|
||||||
"SHADOW\\sk16.atl",
|
|
||||||
"SHADOW\\sk17.atl",
|
|
||||||
"SHADOW\\sk18.atl",
|
|
||||||
"SHADOW\\sk19.atl",
|
|
||||||
"SHADOW\\sk20.atl",
|
|
||||||
"SHADOW\\sk21.atl",
|
|
||||||
"SHADOW\\sk22.atl",
|
|
||||||
"SHADOW\\sk23.atl",
|
|
||||||
"SHADOW\\sk24.atl",
|
|
||||||
"SHADOW\\sk25.atl",
|
|
||||||
"SHADOW\\sk26.atl",
|
|
||||||
"SHADOW\\sk27.atl",
|
|
||||||
};
|
|
||||||
|
|
||||||
static const char *const pop_shadow_fgt_atl[POP_SHADOW_FGT_PAGES] = {
|
|
||||||
"SHADOW\\sf0.atl",
|
|
||||||
"SHADOW\\sf1.atl",
|
|
||||||
"SHADOW\\sf2.atl",
|
|
||||||
"SHADOW\\sf3.atl",
|
|
||||||
};
|
|
||||||
|
|
||||||
/* 16 цветов, запись 4 байта (B, G, R, 0) — формат gfx_pal_load. */
|
/* 16 цветов, запись 4 байта (B, G, R, 0) — формат gfx_pal_load. */
|
||||||
static const uint8_t pop_shadow_pal[64] = {
|
static const uint8_t pop_shadow_pal[64] = {
|
||||||
0x00,0x00,0x00,0x00,0xD8,0xFC,0xFC,0x00,0x6C,0x80,0xDC,0x00,0x78,0x44,0x24,0x00,0xA0,0xB8,0xD8,0x00,0xB5,0x7C,0x21,0x00,0x00,0x90,0xB8,0x00,0x5D,0x6D,0xAB,0x00,0x0C,0xF0,0x6C,0x00,0xCC,0x38,0x05,0x00,0x38,0xCC,0xC0,0x00,0x58,0xDB,0xA7,0x00,0xBB,0x9F,0x54,0x00,0x5D,0x18,0x60,0x00,0x37,0xD7,0x37,0x00,0x2F,0x17,0xD2,0x00,
|
0x00,0x00,0x00,0x00,0xD8,0xFC,0xFC,0x00,0x6C,0x80,0xDC,0x00,0x78,0x44,0x24,0x00,0xA0,0xB8,0xD8,0x00,0xB5,0x7C,0x21,0x00,0x00,0x90,0xB8,0x00,0x5D,0x6D,0xAB,0x00,0x0C,0xF0,0x6C,0x00,0xCC,0x38,0x05,0x00,0x38,0xCC,0xC0,0x00,0x58,0xDB,0xA7,0x00,0xBB,0x9F,0x54,0x00,0x5D,0x18,0x60,0x00,0x37,0xD7,0x37,0x00,0x2F,0x17,0xD2,0x00,
|
||||||
|
|||||||
@@ -1,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
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* _pop_file.h — внутренний файловый слой SprPoP для старых версий DSS.
|
||||||
|
*
|
||||||
|
* DSS до исправления относительных путей оставляет рабочий каталог внутри
|
||||||
|
* последнего подкаталога. POP_PATH_CALL восстанавливает абсолютный каталог
|
||||||
|
* приложения до и после операции. Это макрос намеренно: код исполняется в
|
||||||
|
* банке вызывающего, где доступен переданный указатель, и не расходует W1.
|
||||||
|
*/
|
||||||
|
#ifndef _POP_FILE_H
|
||||||
|
#define _POP_FILE_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <errno.h>
|
||||||
|
|
||||||
|
/* BANK10: получить каталог EXE через APPINFO B=1 и сделать его текущим. */
|
||||||
|
int8_t pop_path_init(void) __banked;
|
||||||
|
int8_t pop_chdir_home(void) __banked;
|
||||||
|
|
||||||
|
/* expression вычисляется только после успешного chdir. errno файловой
|
||||||
|
* операции сохраняется через завершающее восстановление каталога. */
|
||||||
|
#define POP_PATH_CALL(result, expression) do { \
|
||||||
|
if (pop_chdir_home() != 0) { \
|
||||||
|
(result) = -1; \
|
||||||
|
} else { \
|
||||||
|
int _pop_path_errno; \
|
||||||
|
(result) = (expression); \
|
||||||
|
_pop_path_errno = errno; \
|
||||||
|
(void)pop_chdir_home(); \
|
||||||
|
errno = _pop_path_errno; \
|
||||||
|
} \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
/*
|
/*
|
||||||
* _pop_sfx.h — внутренний контракт между половинами звука (НЕ публичный,
|
* _pop_sfx.h — внутренний контракт между половинами звука (НЕ публичный,
|
||||||
* публичный — pop_sfx.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];
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <sprinter_mem.h>
|
#include <sprinter_mem.h>
|
||||||
#include "pop_arc.h"
|
#include "pop_arc.h"
|
||||||
|
#include "_pop_file.h"
|
||||||
|
|
||||||
/* Пути 8.3, как требует DSS. Держим их ЗДЕСЬ: строка обязана лежать в том
|
/* Пути 8.3, как требует DSS. Держим их ЗДЕСЬ: строка обязана лежать в том
|
||||||
* же банке, что и код, который её читает (см. pop_arc.h). */
|
* же банке, что и код, который её читает (см. pop_arc.h). */
|
||||||
@@ -42,7 +43,7 @@ int8_t pop_arc_open(pop_arc_t *a, const char *path,
|
|||||||
|
|
||||||
a->fd = -1;
|
a->fd = -1;
|
||||||
a->count = 0;
|
a->count = 0;
|
||||||
fd = open(path, O_RDONLY);
|
POP_PATH_CALL(fd, open(path, O_RDONLY));
|
||||||
if (fd < 0) return -1;
|
if (fd < 0) return -1;
|
||||||
if (read(fd, head, sizeof(head)) != (int)sizeof(head) ||
|
if (read(fd, head, sizeof(head)) != (int)sizeof(head) ||
|
||||||
memcmp(head, "PBA1", 4) != 0 ||
|
memcmp(head, "PBA1", 4) != 0 ||
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
#include "pop_cheat.h"
|
#include "pop_cheat.h"
|
||||||
#include "pop_sfx.h"
|
#include "pop_sfx.h"
|
||||||
#include "_pop_config.h"
|
#include "_pop_config.h"
|
||||||
|
#include "_pop_file.h"
|
||||||
|
|
||||||
#define CFG_BASE 0x100
|
#define CFG_BASE 0x100
|
||||||
|
|
||||||
@@ -151,7 +152,7 @@ static uint8_t cfg_read_file(uint8_t *buf, uint16_t *size)
|
|||||||
int fd, got;
|
int fd, got;
|
||||||
long n;
|
long n;
|
||||||
|
|
||||||
fd = open("POP.CFG", O_RDONLY);
|
POP_PATH_CALL(fd, open("POP.CFG", O_RDONLY));
|
||||||
if (fd < 0) return 0;
|
if (fd < 0) return 0;
|
||||||
n = lseek(fd, 0, SEEK_END);
|
n = lseek(fd, 0, SEEK_END);
|
||||||
if (n < POP_CFG_MIN_SIZE || n > POP_CFG_MAX_SIZE ||
|
if (n < POP_CFG_MIN_SIZE || n > POP_CFG_MAX_SIZE ||
|
||||||
@@ -172,7 +173,7 @@ int8_t pop_cfg_load(void) __banked
|
|||||||
|
|
||||||
if (!cfg_read_file(buf, &size) || !pop_cfg_decode(buf, size)) return 0;
|
if (!cfg_read_file(buf, &size) || !pop_cfg_decode(buf, size)) return 0;
|
||||||
if (buf[9] != POP_PROFILE_VANILLA)
|
if (buf[9] != POP_PROFILE_VANILLA)
|
||||||
printf("POP.CFG: ENHANCED profile -> VANILLA\n");
|
puts("POP.CFG: ENHANCED profile -> VANILLA\n");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,7 +193,7 @@ int8_t pop_cfg_save(void) __banked
|
|||||||
gfx_w0_unmap();
|
gfx_w0_unmap();
|
||||||
/* Не unlink: bank_save_file сам делает O_TRUNC и не создаёт лишнего
|
/* Не unlink: bank_save_file сам делает O_TRUNC и не создаёт лишнего
|
||||||
* окна, когда POP.CFG вообще отсутствует после ошибки unlink. */
|
* окна, когда POP.CFG вообще отсутствует после ошибки unlink. */
|
||||||
rc = bank_save_file(page, CFG_BASE, "POP.CFG", sizeof(buf));
|
POP_PATH_CALL(rc, bank_save_file(page, CFG_BASE, "POP.CFG", sizeof(buf)));
|
||||||
mem_free_block(blk);
|
mem_free_block(blk);
|
||||||
return (rc == sizeof(buf)) ? 1 : 0;
|
return (rc == sizeof(buf)) ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
#include "pop_title.h" /* титульная картинка после ввода имени */
|
#include "pop_title.h" /* титульная картинка после ввода имени */
|
||||||
#include "pop_music.h" /* подкачка потоковой темы победы */
|
#include "pop_music.h" /* подкачка потоковой темы победы */
|
||||||
#include "pop_hof.h"
|
#include "pop_hof.h"
|
||||||
|
#include "_pop_file.h"
|
||||||
|
|
||||||
/* MAX_HOF_COUNT оригинала — шесть строк. */
|
/* MAX_HOF_COUNT оригинала — шесть строк. */
|
||||||
#define HOF_COUNT 6
|
#define HOF_COUNT 6
|
||||||
@@ -162,7 +163,7 @@ static void hof_load(void)
|
|||||||
if (!blk) return;
|
if (!blk) return;
|
||||||
page = mem_get_page(blk, 0);
|
page = mem_get_page(blk, 0);
|
||||||
gfx_w0_page_prepare(page);
|
gfx_w0_page_prepare(page);
|
||||||
got = bank_load_file(page, HOF_BASE, "POP.HOF", HOF_SIZE);
|
POP_PATH_CALL(got, bank_load_file(page, HOF_BASE, "POP.HOF", HOF_SIZE));
|
||||||
if (got == HOF_SIZE) {
|
if (got == HOF_SIZE) {
|
||||||
gfx_w0_map(page);
|
gfx_w0_map(page);
|
||||||
for (i = 0; i < HOF_SIZE; i++)
|
for (i = 0; i < HOF_SIZE; i++)
|
||||||
@@ -187,7 +188,11 @@ static void hof_save(void)
|
|||||||
gfx_w0_unmap();
|
gfx_w0_unmap();
|
||||||
/* Неудача записи не меняет только что показанную таблицу: следующее
|
/* Неудача записи не меняет только что показанную таблицу: следующее
|
||||||
* открытие разумно начнёт с прежнего файла или с пустого списка. */
|
* открытие разумно начнёт с прежнего файла или с пустого списка. */
|
||||||
bank_save_file(page, HOF_BASE, "POP.HOF", HOF_SIZE);
|
{
|
||||||
|
int saved;
|
||||||
|
POP_PATH_CALL(saved,
|
||||||
|
bank_save_file(page, HOF_BASE, "POP.HOF", HOF_SIZE));
|
||||||
|
}
|
||||||
mem_free_block(blk);
|
mem_free_block(blk);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,18 +18,14 @@
|
|||||||
#include "pop_guard.h" /* pop_guard_vflip_load — зеркала соперника */
|
#include "pop_guard.h" /* pop_guard_vflip_load — зеркала соперника */
|
||||||
#include "pop_arc.h"
|
#include "pop_arc.h"
|
||||||
#include "kid_arc.h"
|
#include "kid_arc.h"
|
||||||
|
#include "_pop_file.h"
|
||||||
|
|
||||||
/* Серии в архиве обязаны лежать подряд — так их кладёт список в 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 у игры и без того тесный. */
|
||||||
@@ -39,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),
|
||||||
@@ -75,7 +71,8 @@ int pop_kid_data_load(const char *path) __banked
|
|||||||
blk=mem_alloc_pages(1);
|
blk=mem_alloc_pages(1);
|
||||||
if (!blk) return -1;
|
if (!blk) return -1;
|
||||||
page=mem_get_page(blk,0);
|
page=mem_get_page(blk,0);
|
||||||
n=bank_load_file(page,KD_DATA_OFF,path,16384-KD_DATA_OFF);
|
POP_PATH_CALL(n, bank_load_file(page,KD_DATA_OFF,path,
|
||||||
|
16384-KD_DATA_OFF));
|
||||||
if (n < (int)(KID_BIN_SEQTBL_OFF + KID_SEQTBL_LEN)) {
|
if (n < (int)(KID_BIN_SEQTBL_OFF + KID_SEQTBL_LEN)) {
|
||||||
mem_free_block(blk); return -1;
|
mem_free_block(blk); return -1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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). Определена
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ extern uint8_t pop_current_level;
|
|||||||
void pop_level_qsave(pop_qs_io_t *io);
|
void pop_level_qsave(pop_qs_io_t *io);
|
||||||
|
|
||||||
/* Загрузить уровень НОМЕРОМ: освободить страницу прошлого, взять
|
/* Загрузить уровень НОМЕРОМ: освободить страницу прошлого, взять
|
||||||
* `LEVELS\res20NN.bin` (fallback `a:\res20NN.bin`) и выставить
|
* `LEVELS\res20NN.bin` относительно каталога EXE и выставить
|
||||||
* pop_current_level. 0 — OK, -1 — файла нет / не влез (тогда
|
* pop_current_level. 0 — OK, -1 — файла нет / не влез (тогда
|
||||||
* pop_current_level и страница НЕ тронуты — играем дальше на старом).
|
* pop_current_level и страница НЕ тронуты — играем дальше на старом).
|
||||||
* Порт связки load_lev_spr + load_level (seg000:1098/1169). */
|
* Порт связки load_lev_spr + load_level (seg000:1098/1169). */
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
#include "pop_level.h"
|
#include "pop_level.h"
|
||||||
#include "_pop_level.h"
|
#include "_pop_level.h"
|
||||||
#include "pop_geom.h" /* pop_x_bump / FIRST_ONSCREEN_COLUMN — pos_guards */
|
#include "pop_geom.h" /* pop_x_bump / FIRST_ONSCREEN_COLUMN — pos_guards */
|
||||||
|
#include "_pop_file.h"
|
||||||
|
|
||||||
/* Низкоуровневое чтение через W3 выполняет резидентный bank_load_file();
|
/* Низкоуровневое чтение через W3 выполняет резидентный bank_load_file();
|
||||||
* остальная одноразовая подготовка уровня безопасно остаётся в банке 8. */
|
* остальная одноразовая подготовка уровня безопасно остаётся в банке 8. */
|
||||||
@@ -58,7 +59,8 @@ static int level_read_file(const char *path)
|
|||||||
blk=mem_alloc_pages(1);
|
blk=mem_alloc_pages(1);
|
||||||
if (!blk) return -1;
|
if (!blk) return -1;
|
||||||
page=mem_get_page(blk,0);
|
page=mem_get_page(blk,0);
|
||||||
n=bank_load_file(page,LVL_DATA_OFF,path,16384-LVL_DATA_OFF);
|
POP_PATH_CALL(n, bank_load_file(page,LVL_DATA_OFF,path,
|
||||||
|
16384-LVL_DATA_OFF));
|
||||||
if (n < (int)MIN_SIZE) { mem_free_block(blk); return -1; }
|
if (n < (int)MIN_SIZE) { mem_free_block(blk); return -1; }
|
||||||
gfx_w0_page_prepare(page);
|
gfx_w0_page_prepare(page);
|
||||||
gfx_w0_map(page);
|
gfx_w0_map(page);
|
||||||
@@ -80,18 +82,17 @@ int pop_level_load_num(uint8_t n) __banked
|
|||||||
/* Имена патчим на месте, а не через sprintf: printf-семейство тянет в
|
/* Имена патчим на месте, а не через sprintf: printf-семейство тянет в
|
||||||
* резидент W1 форматтер, которого здесь больше негде применить. */
|
* резидент W1 форматтер, которого здесь больше негде применить. */
|
||||||
static char pri[] = "LEVELS\\res2000.bin"; /* цифры — [12],[13] */
|
static char pri[] = "LEVELS\\res2000.bin"; /* цифры — [12],[13] */
|
||||||
static char alt[] = "a:\\res2000.bin"; /* цифры — [8],[9] */
|
|
||||||
uint8_t saved_blk = pop_lvl_blk, saved_page = pop_lvl_page, saved_ok = pop_lvl_ok;
|
uint8_t saved_blk = pop_lvl_blk, saved_page = pop_lvl_page, saved_ok = pop_lvl_ok;
|
||||||
|
|
||||||
if (n > POP_LEVEL_LAST) return -1;
|
if (n > POP_LEVEL_LAST) return -1;
|
||||||
pri[12] = alt[8] = (char)('0' + n / 10);
|
pri[12] = (char)('0' + n / 10);
|
||||||
pri[13] = alt[9] = (char)('0' + n % 10);
|
pri[13] = (char)('0' + n % 10);
|
||||||
|
|
||||||
/* Старую страницу отпускаем ТОЛЬКО после успешной загрузки новой:
|
/* Старую страницу отпускаем ТОЛЬКО после успешной загрузки новой:
|
||||||
* level_read_file сам аллоцирует страницу, и если файла нет — остаёмся
|
* level_read_file сам аллоцирует страницу, и если файла нет — остаёмся
|
||||||
* на текущем уровне вместо падения в пустой уровень. */
|
* на текущем уровне вместо падения в пустой уровень. */
|
||||||
pop_lvl_ok = 0; /* чтобы level_load_path не мешался */
|
pop_lvl_ok = 0; /* чтобы level_load_path не мешался */
|
||||||
if (level_read_file(pri) != 0 && level_read_file(alt) != 0) {
|
if (level_read_file(pri) != 0) {
|
||||||
pop_lvl_blk = saved_blk; pop_lvl_page = saved_page; pop_lvl_ok = saved_ok;
|
pop_lvl_blk = saved_blk; pop_lvl_page = saved_page; pop_lvl_ok = saved_ok;
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
#include "pop_music_tbl.h"
|
#include "pop_music_tbl.h"
|
||||||
#include "pop_sfx.h"
|
#include "pop_sfx.h"
|
||||||
#include "pop_app.h" /* POP_APP_DEMO — в демке музыки нет */
|
#include "pop_app.h" /* POP_APP_DEMO — в демке музыки нет */
|
||||||
|
#include "_pop_file.h"
|
||||||
|
|
||||||
uint8_t pop_mus_page[POP_MUS_PAGES];
|
uint8_t pop_mus_page[POP_MUS_PAGES];
|
||||||
|
|
||||||
@@ -141,7 +142,7 @@ int8_t pop_music_load_begin(uint8_t id) __banked
|
|||||||
/* Трек лежит ОДНИМ файлом, и fd остаётся открытым до последней
|
/* Трек лежит ОДНИМ файлом, и fd остаётся открытым до последней
|
||||||
* страницы: `open` стоит дороже самого чтения (51,4 мс против 32,6 мс
|
* страницы: `open` стоит дороже самого чтения (51,4 мс против 32,6 мс
|
||||||
* на 16 КБ), и платить его на каждую страницу — чистые потери. */
|
* на 16 КБ), и платить его на каждую страницу — чистые потери. */
|
||||||
ld_fd = open(name, O_RDONLY);
|
POP_PATH_CALL(ld_fd, open(name, O_RDONLY));
|
||||||
if (ld_fd < 0) return -1;
|
if (ld_fd < 0) return -1;
|
||||||
slot_blk[n] = mem_alloc_pages(pages);
|
slot_blk[n] = mem_alloc_pages(pages);
|
||||||
if (!slot_blk[n]) { close(ld_fd); return -1; }
|
if (!slot_blk[n]) { close(ld_fd); return -1; }
|
||||||
@@ -288,7 +289,7 @@ int8_t pop_music_ring_start(uint8_t id) __banked
|
|||||||
pop_music_free(); /* обычные слоты кольцу не нужны */
|
pop_music_free(); /* обычные слоты кольцу не нужны */
|
||||||
pop_music_ring_stop();
|
pop_music_ring_stop();
|
||||||
mus_name(name, id);
|
mus_name(name, id);
|
||||||
ring_fd = open(name, O_RDONLY);
|
POP_PATH_CALL(ring_fd, open(name, O_RDONLY));
|
||||||
if (ring_fd < 0) return -1;
|
if (ring_fd < 0) return -1;
|
||||||
ring_fd_open = 1;
|
ring_fd_open = 1;
|
||||||
ring_blk = mem_alloc_pages(RING_PAGES);
|
ring_blk = mem_alloc_pages(RING_PAGES);
|
||||||
|
|||||||
@@ -14,10 +14,12 @@
|
|||||||
#include "pop_guard.h"
|
#include "pop_guard.h"
|
||||||
#include "pop_pal.h"
|
#include "pop_pal.h"
|
||||||
#include "pop_ui.h" /* POP_COL_OUTSIDE */
|
#include "pop_ui.h" /* POP_COL_OUTSIDE */
|
||||||
|
#include "_pop_file.h"
|
||||||
|
|
||||||
static void pal_file_load(const char *path, const char *fallback)
|
static void pal_file_load(const char *path)
|
||||||
{
|
{
|
||||||
if (gfx_pal_fload(0, path) < 0) gfx_pal_fload(0, fallback);
|
int rc;
|
||||||
|
POP_PATH_CALL(rc, gfx_pal_fload(0, path));
|
||||||
gfx_pal_sync();
|
gfx_pal_sync();
|
||||||
pop_ui_palette_snapshot();
|
pop_ui_palette_snapshot();
|
||||||
}
|
}
|
||||||
@@ -84,7 +86,7 @@ void pop_pal_menu_dim(uint8_t fade) __banked
|
|||||||
|
|
||||||
void pop_pal_game_load(void) __banked
|
void pop_pal_game_load(void) __banked
|
||||||
{
|
{
|
||||||
pal_file_load("KID\\kid.pal", "a:\\kid.pal");
|
pal_file_load("KID\\kid.pal");
|
||||||
/* kid.pal содержит все 256 записей и намеренно затирает эти три
|
/* kid.pal содержит все 256 записей и намеренно затирает эти три
|
||||||
* динамических диапазона (тайлсет, Тень, набор соперника).
|
* динамических диапазона (тайлсет, Тень, набор соперника).
|
||||||
* Восстановить их надо ДО snapshot. */
|
* Восстановить их надо ДО snapshot. */
|
||||||
@@ -110,7 +112,7 @@ void pop_pal_level_load(uint8_t full) __banked
|
|||||||
|
|
||||||
void pop_pal_title_load(void) __banked
|
void pop_pal_title_load(void) __banked
|
||||||
{
|
{
|
||||||
pal_file_load("TITLE\\title.pal", "a:\\title.pal");
|
pal_file_load("TITLE\\title.pal");
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ЕДИНСТВЕННОЕ ОТЛИЧИЕ ФИНАЛА ОТ ИНТРО — цвет фона текстовой рамки.
|
/* ЕДИНСТВЕННОЕ ОТЛИЧИЕ ФИНАЛА ОТ ИНТРО — цвет фона текстовой рамки.
|
||||||
@@ -120,7 +122,7 @@ void pop_pal_title_load(void) __banked
|
|||||||
* файла и до нового снимка — как тайлсет/Тень в pop_pal_game_load. */
|
* файла и до нового снимка — как тайлсет/Тень в pop_pal_game_load. */
|
||||||
void pop_pal_story_load(uint8_t ending) __banked
|
void pop_pal_story_load(uint8_t ending) __banked
|
||||||
{
|
{
|
||||||
pal_file_load("PV\\story.pal", "a:\\story.pal");
|
pal_file_load("PV\\story.pal");
|
||||||
if (!ending) return;
|
if (!ending) return;
|
||||||
gfx_pal_set(0, POP_PAL_STORY_BG, 0x80, 0, 0);
|
gfx_pal_set(0, POP_PAL_STORY_BG, 0x80, 0, 0);
|
||||||
gfx_pal_set(1, POP_PAL_STORY_BG, 0x80, 0, 0);
|
gfx_pal_set(1, POP_PAL_STORY_BG, 0x80, 0, 0);
|
||||||
@@ -129,5 +131,5 @@ void pop_pal_story_load(uint8_t ending) __banked
|
|||||||
|
|
||||||
void pop_pal_pv_load(void) __banked
|
void pop_pal_pv_load(void) __banked
|
||||||
{
|
{
|
||||||
pal_file_load("PV\\pv.pal", "a:\\pv.pal");
|
pal_file_load("PV\\pv.pal");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
* pop_path_bank.c — каталог приложения и восстановление cwd, BANK10.
|
||||||
|
*
|
||||||
|
* APPINFO B=1 возвращает абсолютный каталог запущенного EXE вместе с диском.
|
||||||
|
* Получаем его один раз при старте. Буфер объявлен const намеренно: у
|
||||||
|
* Sprinter банковые страницы — RAM, а --constseg кладёт объект в BANK10,
|
||||||
|
* не расходуя дефицитную резидентную _DATA в W2. Записывает его только
|
||||||
|
* ESTEX во время pop_path_init().
|
||||||
|
*
|
||||||
|
* ABI: публичные функции __banked; их вызывает POP_PATH_CALL из банка
|
||||||
|
* файлового клиента. После возврата трамплин восстанавливает прежнюю W3,
|
||||||
|
* поэтому переданный самой файловой операции указатель снова доступен.
|
||||||
|
*/
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include "_pop_file.h"
|
||||||
|
|
||||||
|
static const char pop_app_dir[256] = { 0 };
|
||||||
|
|
||||||
|
/* ESTEX APPINFO ($47), B=1: HL=буфер, CF/A=ошибка. */
|
||||||
|
static int appinfo_dir(const char *buf) __naked
|
||||||
|
{
|
||||||
|
(void)buf;
|
||||||
|
__asm
|
||||||
|
push ix
|
||||||
|
ld b, #1
|
||||||
|
ld c, #0x47
|
||||||
|
rst #0x10
|
||||||
|
pop ix
|
||||||
|
jr c, _paid_err
|
||||||
|
ld de, #0
|
||||||
|
ret
|
||||||
|
_paid_err:
|
||||||
|
call __errno_set
|
||||||
|
ld de, #-1
|
||||||
|
ret
|
||||||
|
__endasm;
|
||||||
|
}
|
||||||
|
|
||||||
|
int8_t pop_path_init(void) __banked
|
||||||
|
{
|
||||||
|
const char *dir = pop_app_dir;
|
||||||
|
|
||||||
|
if (appinfo_dir(dir) != 0 || !dir[0]) return -1;
|
||||||
|
return (int8_t)chdir(dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
int8_t pop_chdir_home(void) __banked
|
||||||
|
{
|
||||||
|
return (int8_t)chdir((const char *)pop_app_dir);
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@
|
|||||||
#include "pop_pace.h"
|
#include "pop_pace.h"
|
||||||
#include "pop_timer.h"
|
#include "pop_timer.h"
|
||||||
#include "sprpop_cold.h"
|
#include "sprpop_cold.h"
|
||||||
|
#include "_pop_file.h"
|
||||||
|
|
||||||
#define QS_VERSION 4
|
#define QS_VERSION 4
|
||||||
#define QS_SAVE 1
|
#define QS_SAVE 1
|
||||||
@@ -154,6 +155,7 @@ static int8_t qs_save(uint8_t page)
|
|||||||
uint8_t tail[4];
|
uint8_t tail[4];
|
||||||
const uint8_t h[7]={'P','O','P','Q',QS_VERSION,pop_current_level,0};
|
const uint8_t h[7]={'P','O','P','Q',QS_VERSION,pop_current_level,0};
|
||||||
uint16_t size;
|
uint16_t size;
|
||||||
|
int rc;
|
||||||
qs_init(&io,page,QS_LIMIT,0,0);
|
qs_init(&io,page,QS_LIMIT,0,0);
|
||||||
pop_qs_raw_put(&io,h,sizeof(h));
|
pop_qs_raw_put(&io,h,sizeof(h));
|
||||||
qs_payload(&io);
|
qs_payload(&io);
|
||||||
@@ -162,14 +164,18 @@ static int8_t qs_save(uint8_t page)
|
|||||||
tail[2]=(uint8_t)io.checksum; tail[3]=(uint8_t)(io.checksum >> 8);
|
tail[2]=(uint8_t)io.checksum; tail[3]=(uint8_t)(io.checksum >> 8);
|
||||||
pop_qs_raw_put(&io,tail,4); size=(uint16_t)(io.pos-QS_BASE);
|
pop_qs_raw_put(&io,tail,4); size=(uint16_t)(io.pos-QS_BASE);
|
||||||
if (io.failed) return -1;
|
if (io.failed) return -1;
|
||||||
unlink("POP.NEW");
|
POP_PATH_CALL(rc, unlink("POP.NEW"));
|
||||||
if (bank_save_file(page,QS_BASE,"POP.NEW",size) != size) {
|
POP_PATH_CALL(rc, bank_save_file(page,QS_BASE,"POP.NEW",size));
|
||||||
unlink("POP.NEW"); return -1;
|
if (rc != size) {
|
||||||
|
POP_PATH_CALL(rc, unlink("POP.NEW"));
|
||||||
|
return -1;
|
||||||
}
|
}
|
||||||
unlink("POP.BAK");
|
POP_PATH_CALL(rc, unlink("POP.BAK"));
|
||||||
(void)rename("POP.SAV", "POP.BAK");
|
POP_PATH_CALL(rc, rename("POP.SAV", "POP.BAK"));
|
||||||
if (rename("POP.NEW", "POP.SAV") != 0) {
|
POP_PATH_CALL(rc, rename("POP.NEW", "POP.SAV"));
|
||||||
(void)rename("POP.BAK", "POP.SAV"); return -1;
|
if (rc != 0) {
|
||||||
|
POP_PATH_CALL(rc, rename("POP.BAK", "POP.SAV"));
|
||||||
|
return -1;
|
||||||
}
|
}
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
@@ -178,7 +184,8 @@ static int8_t qs_load_one(const char *name, uint8_t page)
|
|||||||
{
|
{
|
||||||
uint8_t level;
|
uint8_t level;
|
||||||
uint16_t payload_size;
|
uint16_t payload_size;
|
||||||
int size=bank_load_file(page,QS_BASE,name,QS_LIMIT);
|
int size;
|
||||||
|
POP_PATH_CALL(size, bank_load_file(page,QS_BASE,name,QS_LIMIT));
|
||||||
if (size <= 0 || !qs_validate_page(page,(uint16_t)size,&level,
|
if (size <= 0 || !qs_validate_page(page,(uint16_t)size,&level,
|
||||||
&payload_size)) return -1;
|
&payload_size)) return -1;
|
||||||
if (level != pop_current_level) {
|
if (level != pop_current_level) {
|
||||||
@@ -205,11 +212,11 @@ uint8_t pop_qsave_probe(void) __banked
|
|||||||
page = mem_get_page(blk, 0);
|
page = mem_get_page(blk, 0);
|
||||||
gfx_w0_page_prepare(page);
|
gfx_w0_page_prepare(page);
|
||||||
|
|
||||||
size = bank_load_file(page, QS_BASE, "POP.SAV", QS_LIMIT);
|
POP_PATH_CALL(size, bank_load_file(page, QS_BASE, "POP.SAV", QS_LIMIT));
|
||||||
if (size > 0 && qs_validate_page(page, (uint16_t)size, &level,
|
if (size > 0 && qs_validate_page(page, (uint16_t)size, &level,
|
||||||
&payload_size))
|
&payload_size))
|
||||||
slots |= POP_QS_SLOT_SAVE;
|
slots |= POP_QS_SLOT_SAVE;
|
||||||
size = bank_load_file(page, QS_BASE, "POP.BAK", QS_LIMIT);
|
POP_PATH_CALL(size, bank_load_file(page, QS_BASE, "POP.BAK", QS_LIMIT));
|
||||||
if (size > 0 && qs_validate_page(page, (uint16_t)size, &level,
|
if (size > 0 && qs_validate_page(page, (uint16_t)size, &level,
|
||||||
&payload_size))
|
&payload_size))
|
||||||
slots |= POP_QS_SLOT_BACKUP;
|
slots |= POP_QS_SLOT_BACKUP;
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
#include "pop_kid.h" /* Kid.curr_col/curr_row — порядок оверлеев */
|
#include "pop_kid.h" /* Kid.curr_col/curr_row — порядок оверлеев */
|
||||||
#include "pop_cdraw.h" /* pop_cd — габарит кадра Кида (y-порядок плиты) */
|
#include "pop_cdraw.h" /* pop_cd — габарит кадра Кида (y-порядок плиты) */
|
||||||
#include "pop_map.h" /* pop_upside — переворот (зелье инверсии) */
|
#include "pop_map.h" /* pop_upside — переворот (зелье инверсии) */
|
||||||
|
#include "_pop_file.h"
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
@@ -83,7 +84,8 @@ static const char *bg_path(const char *name)
|
|||||||
static int bg_load_tile_pal(const char *name)
|
static int bg_load_tile_pal(const char *name)
|
||||||
{
|
{
|
||||||
uint8_t buf[64];
|
uint8_t buf[64];
|
||||||
int fd = open(bg_path(name), O_RDONLY);
|
int fd;
|
||||||
|
POP_PATH_CALL(fd, open(bg_path(name), O_RDONLY));
|
||||||
if (fd < 0) return -1;
|
if (fd < 0) return -1;
|
||||||
if (read(fd, buf, 64) != 64) { close(fd); return -1; }
|
if (read(fd, buf, 64) != 64) { close(fd); return -1; }
|
||||||
gfx_pal_load(0, POP_PAL_ENV, 16, buf);
|
gfx_pal_load(0, POP_PAL_ENV, 16, buf);
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
/* Физические страницы набора; заполняет холодная половина. */
|
/* Физические страницы набора; заполняет холодная половина. */
|
||||||
|
|||||||
@@ -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 под весь набор */
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
#include "pop_ui.h"
|
#include "pop_ui.h"
|
||||||
#include "pop_status.h"
|
#include "pop_status.h"
|
||||||
#include "pop_music.h" /* pop_music_service — подкачка кольца в долгих фейдах */
|
#include "pop_music.h" /* pop_music_service — подкачка кольца в долгих фейдах */
|
||||||
|
#include "_pop_file.h"
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uint8_t base;
|
uint8_t base;
|
||||||
@@ -54,8 +55,10 @@ static uint8_t text_index(const pop_font_desc_t *font, uint8_t ch)
|
|||||||
|
|
||||||
int pop_ui_init(void) __banked
|
int pop_ui_init(void) __banked
|
||||||
{
|
{
|
||||||
|
int rc;
|
||||||
if (font_ready) return 0;
|
if (font_ready) return 0;
|
||||||
if (atlas_load(&font_at, "FONT\\font.atl") != 0) return -1;
|
POP_PATH_CALL(rc, atlas_load(&font_at, "FONT\\font.atl"));
|
||||||
|
if (rc != 0) return -1;
|
||||||
font_ready = 1;
|
font_ready = 1;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -242,8 +245,10 @@ void pop_ui_palette_black(void) __banked
|
|||||||
uint8_t *black = pop_pal_buf;
|
uint8_t *black = pop_pal_buf;
|
||||||
|
|
||||||
/* BIOS принимает максимум 64 записи за вызов; одним буфером покрываем
|
/* BIOS принимает максимум 64 записи за вызов; одним буфером покрываем
|
||||||
* все четыре четверти палитры, не занимая постоянную память. */
|
* все четыре четверти палитры, не занимая постоянную память.
|
||||||
for (i = 0; i < sizeof(black); i++) black[i] = 0;
|
* sizeof(black) здесь нельзя: black — 16-битный указатель, и тогда
|
||||||
|
* обнулятся лишь первые два байта, а цвет заливки останется случайным. */
|
||||||
|
for (i = 0; i < sizeof(pop_pal_buf); i++) black[i] = 0;
|
||||||
for (chunk = 0; chunk < 4; chunk++) {
|
for (chunk = 0; chunk < 4; chunk++) {
|
||||||
uint8_t start = (uint8_t)(chunk << 6);
|
uint8_t start = (uint8_t)(chunk << 6);
|
||||||
gfx_pal_load(0, start, 64, black);
|
gfx_pal_load(0, start, 64, black);
|
||||||
@@ -468,8 +473,10 @@ int pop_screen_blit_atlas_strips(const char * const *parts,
|
|||||||
gfx_set_bank(GFX_BANK_NORMAL);
|
gfx_set_bank(GFX_BANK_NORMAL);
|
||||||
for (part = 0; part < count; part++) {
|
for (part = 0; part < count; part++) {
|
||||||
atlas_t at;
|
atlas_t at;
|
||||||
|
int rc;
|
||||||
|
|
||||||
if (atlas_load(&at, parts[part]) != 0) return -1;
|
POP_PATH_CALL(rc, atlas_load(&at, parts[part]));
|
||||||
|
if (rc != 0) return -1;
|
||||||
strip_blit(&at, x, &y);
|
strip_blit(&at, x, &y);
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -40,15 +40,6 @@ int pop_vflip_take(const atlas_t *orig, const atlas_t *mirror)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
int pop_vflip_add(const atlas_t *orig, const char *path)
|
|
||||||
{
|
|
||||||
atlas_t m;
|
|
||||||
|
|
||||||
if (!orig || vf_n >= VF_MAX) return -1;
|
|
||||||
if (atlas_load(&m, path) != 0) return -1;
|
|
||||||
return pop_vflip_take(orig, &m);
|
|
||||||
}
|
|
||||||
|
|
||||||
uint8_t pop_vflip_page(uint8_t src)
|
uint8_t pop_vflip_page(uint8_t src)
|
||||||
{
|
{
|
||||||
uint8_t i;
|
uint8_t i;
|
||||||
|
|||||||
@@ -26,11 +26,7 @@
|
|||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <sprite.h>
|
#include <sprite.h>
|
||||||
|
|
||||||
/* Загрузить зеркальный файл path и связать его с атласом orig.
|
/* Зеркало УЖЕ загружено из архива: таблица
|
||||||
* 0 — OK, -1 — файла нет / не хватило EMM / таблица полна. */
|
|
||||||
int pop_vflip_add(const atlas_t *orig, const char *path);
|
|
||||||
|
|
||||||
/* То же, но зеркало УЖЕ загружено (например, прочитано из архива): таблица
|
|
||||||
* забирает его себе, и дальше он освобождается вместе с остальными. */
|
* забирает его себе, и дальше он освобождается вместе с остальными. */
|
||||||
int pop_vflip_take(const atlas_t *orig, const atlas_t *mirror);
|
int pop_vflip_take(const atlas_t *orig, const atlas_t *mirror);
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,10 @@
|
|||||||
#include "pop_pal.h" /* lifecycle палитры title/intro/level */
|
#include "pop_pal.h" /* lifecycle палитры title/intro/level */
|
||||||
#include "pop_hof.h" /* persistent Hall of Fame — bank 9 */
|
#include "pop_hof.h" /* persistent Hall of Fame — bank 9 */
|
||||||
#include "pop_ui.h" /* чёрные обе страницы перед сменой палитры */
|
#include "pop_ui.h" /* чёрные обе страницы перед сменой палитры */
|
||||||
|
#include "_pop_file.h" /* cwd приложения для старых DSS */
|
||||||
|
|
||||||
|
#include <sprinter_exit.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
__sfr __at (0xFE) io_border;
|
__sfr __at (0xFE) io_border;
|
||||||
|
|
||||||
@@ -260,6 +264,11 @@ int main(void)
|
|||||||
uint8_t xdec_prev = 0, xinc_prev = 0, upsd_prev = 0;
|
uint8_t xdec_prev = 0, xinc_prev = 0, upsd_prev = 0;
|
||||||
uint8_t failed = 0;
|
uint8_t failed = 0;
|
||||||
|
|
||||||
|
/* До первой файловой операции запоминаем абсолютный каталог EXE.
|
||||||
|
* Без этого старый DSS теряет последний компонент cwd после OPEN пути
|
||||||
|
* с подкаталогом и следующий файл ищет уровнем выше. */
|
||||||
|
if (pop_path_init() != 0) return 1;
|
||||||
|
|
||||||
/* Загрузка ресурсов, палитра, первый экран — В БАНКЕ (sprpop_cold.c):
|
/* Загрузка ресурсов, палитра, первый экран — В БАНКЕ (sprpop_cold.c):
|
||||||
* отрабатывает ровно один раз за игру, а кода там на полкилобайта. */
|
* отрабатывает ровно один раз за игру, а кода там на полкилобайта. */
|
||||||
pop_app_reset();
|
pop_app_reset();
|
||||||
|
|||||||
@@ -1151,14 +1151,14 @@ int pop_boot(void) __banked
|
|||||||
/* pop_cheats/speed применит pop_settings_apply после pop_sfx_init. */
|
/* pop_cheats/speed применит pop_settings_apply после pop_sfx_init. */
|
||||||
pop_guard_reset();
|
pop_guard_reset();
|
||||||
if (pop_kid_data_load("KID\\kid.ani") != 0) { /* кадры+seqtbl в EMM-странице */
|
if (pop_kid_data_load("KID\\kid.ani") != 0) { /* кадры+seqtbl в EMM-странице */
|
||||||
printf("kid.ani load failed\n");
|
puts("kid.ani load failed\n");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
pop_guard_load(); /* атласы стража (GUARD\\g0..g4) */
|
pop_guard_load(); /* атласы стража (GUARD\\g0..g4) */
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
"",
|
"",
|
||||||
|
|||||||
@@ -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}")
|
||||||
|
|
||||||
|
|||||||
@@ -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) + ",",
|
||||||
|
|||||||
@@ -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
@@ -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
|
if [ $# -lt 2 ]; then
|
||||||
echo "usage: $0 OUT.chd FILE1 [FILE2 ...]" >&2
|
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 [--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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user