SprPoP: автономное приложение, выделенное из roomtest

Порт PoP переехал в applications/SprPoP — приложение, которое собирается
само: код, оригинальные данные, конверторы ресурсов и сборка внутри одной
папки.  Наружу знает единственный путь — корень тулчейна (SPRINTER_ROOT,
по умолчанию ../..).  applications/PoP/roomtest ЗАМОРОЖЕНА и остаётся
архивом закрытых задач, багов и исполненных планов.

Скопировано из applications/PoP/roomtest@4b74478.  Перенос проверен
побайтово: собранный sprpop.exe совпал с roomtest.exe того же коммита,
все 39 дисковых ресурсов и все 16 генерируемых заголовков — тоже, host-
тесты зелёные (15/15).

Раскладка:
  src/           рукописный C (roomtest.c -> sprpop.c)
  gen/           генерируемые заголовки, в репозитории
  assets/orig/   оригинальные данные игры, вне репозитория (копирайт)
  assets/packed/ то, что ложится на диск, в раскладке диска
  tools/         конверторы; все пути — в одном tools/paths.py
  build/         выход: exe, каталоги ресурсов, hdd/, промежуточные atl/

Сборка ресурсов: assets/packed и gen — версионируемые ВХОДЫ, а не то, что
пересчитывается каждым make.  Автоматика построена на ОТСУТСТВИИ файла, а
не на таймстемпах: git не хранит времена, и в свежем клоне сравнение по
времени превращалось бы в лотерею.  Недостающий ресурс или заголовок
чинится сам, рекурсивным вызовом в ветку генерации.

Музыка собирается из любого из четырёх наборов записей (make music-mp3,
music-mt32, ...); набор входит в имя stamp'а, поэтому смена набора сама
делает музыку устаревшей.  Длины реплик больше не захардкожены: упаковщик
печатает их в gen/pop_music_ticks.h, и шкала сцены выражена через них —
иначе mt32 (реплики на 6% длиннее) молча ломал катсцену.

Тулчейн: в app.mk два обратносовместимых крючка (SRC_DIR/BUILD_DIR),
HDD_IMG стал ?=; команда сборки roomtest не изменилась.  Корневой
make host-tests переключён на SprPoP.

Подгонка тайминга катсцены с принцессой (PV_MAGIC_LEAD): сцена
render-bound и идёт ~49 тиков/с вместо 60, из-за чего кода реплики
приходила раньше молнии.  Это обход, а не лечение; разбор с замерами —
docs/BUGS_OPEN.md, записи SND-PACE-DEAD, PV-RENDER-BOUND, MUS-LEFT-TEAR.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-27 12:12:28 +03:00
parent 4b74478d19
commit 31b82661eb
235 changed files with 51293 additions and 10 deletions
+491
View File
@@ -0,0 +1,491 @@
# SprPoP — порт Prince of Persia на Sprinter Sp2000.
#
# Приложение АВТОНОМНО: весь код, все исходные ресурсы, все конверторы лежат
# здесь. Наружу оно знает ровно один путь — корень тулчейна sprinter-cc:
#
# export SPRINTER_ROOT=/путь/к/C-Compiler
# make
#
# Без переменной берётся ../.. (раскладка applications/SprPoP внутри дерева
# компилятора), так что в родном дереве ничего настраивать не надо.
#
# Раскладка:
# src/ рукописный C
# gen/ генерируемые заголовки (в репозитории: без них src/ не
# собрать, а генератору нужны оригиналы из assets/orig/)
# assets/orig/ оригинальные данные — SDLPoP, MSDOS, записи музыки.
# НЕ в репозитории (копирайт), см. assets/orig/README.md
# assets/packed/ то, что ложится на диск игры, в раскладке диска
# tools/ конверторы ресурсов
# build/ ВЫХОД: sprpop.exe + BG/ KID/ ... + hdd/sprpop.chd
#
# Основные цели:
# make собрать sprpop.exe и разложить build/
# make hdd + образ build/hdd/sprpop.chd
# make mame-link однократно связать образ с MAME (см. ниже)
# make resources перегенерировать ресурсы из assets/orig/
# make music-mp3 музыка из другого набора записей (flac|mp3|ogg|mt32)
# make clean снести build/ (ассеты не трогает)
# make distclean clean + снести упакованные ассеты assets/packed/
SPRINTER_ROOT ?= $(abspath $(CURDIR)/../..)
PROJ_ROOT := $(SPRINTER_ROOT)
EXAMPLE := sprpop
SRC_DIR := src
BUILD_DIR := build
# huge: small-раскладка (CODE в W1, DATA в W2) + банки кода в W3.
MEMORY ?= huge
# --max-allocs-per-node: во сколько вариантов размещения регистров упирается
# SDCC на узел. Дефолт sprinter-cc (100000) на модулях такого размера — это
# МИНУТЫ на банк. 6000 — компромисс: сборка дольше секунд на двадцать, зато
# резидент W1 худеет примерно на 150 байт против дефолтных 3000 SDCC, а это
# ровно те байты, которых не хватает куче.
#
# make — обычная сборка (ALLOCS=6000)
# make ALLOCS=100000 — минимальный код, для замеров размера и релиза
#
# ВАЖНО: сравнивать занятость банков можно только при ОДНОМ ALLOCS — иначе
# сравниваются не правки, а уровни оптимизации.
ALLOCS ?= 6000
# Build info не хранится в C: ID и дата извлекаются из текущего commit.
BUILD_ID ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo dev)
BUILD_DATE ?= $(shell git log -1 --format=%cs 2>/dev/null || echo unknown)
BUILD_FLAGS := -DPOP_BUILD_ID=\"$(BUILD_ID)\" -DPOP_BUILD_DATE=\"$(BUILD_DATE)\"
# ---------------------------------------------------------------------------
# Банковая раскладка
# ---------------------------------------------------------------------------
# Единственный список банковых исходников: из него строятся И ключи
# sprinter-cc, И зависимости sprpop.exe. Держать два списка нельзя — так уже
# ловили stale-бинарник (модуль компилировался, но make не замечал правок).
#
# Банк 2 = отрисовка (pop_bg + pop_cdraw в соседних банках): вызовы банк→банк
# работают, трамплин сохраняет страницу окна на СТЕКЕ. Резидента --w3 нет.
# Банк 5 = pop_ctrl.c: кандидат выбран не по размеру, а по частоте вызова —
# диспетчер управления дёргается раз в кадр на персонажа и горячих банк→банк
# переходов не создаёт (в отличие от pop_level, чей pop_level_tile зовётся из
# банка 2 на КАЖДЫЙ тайл).
# Банк 9 = оболочка (menu/settings/config/font, автомат, title, attract-demo,
# таблица рекордов) — мелкие редкие точки входа, но не рендеры заставок.
# Банк 10 = единственный владелец экранных палитр/fade; отделён от UI, потому
# что его зовут и игровой lifecycle, и все ветки катсцен.
# Банк 11 = тяжёлая кинематика заставок, чтобы анимации не вытесняли
# menu/config из банка оболочки.
BANK1_SRCS := guards.c
BANK2_SRCS := pop_bg.c
BANK3_SRCS := pop_map.c
BANK4_SRCS := pop_cdraw.c pop_kdraw.c
BANK5_SRCS := pop_ctrl.c pop_tile_cold.c
BANK6_SRCS := pop_trob.c
BANK7_SRCS := pop_room.c pop_redraw.c
BANK8_SRCS := sprpop_cold.c pop_arc.c pop_level_cold.c pop_kboot.c pop_guard_cold.c \
pop_shadow.c pop_sfx_cold.c pop_qsave.c pop_qsave_coldio.c \
pop_level_qsave.c
BANK9_SRCS := pop_menu.c pop_status.c pop_music.c pop_config.c pop_app.c pop_timer.c \
pop_ui.c pop_title.c pop_demo.c pop_flow.c pop_hof.c
BANK10_SRCS := pop_pal.c
BANK11_SRCS := pop_cutscene.c pop_intro.c
BANK_NUMS := 1 2 3 4 5 6 7 8 9 10 11
BANKED_SRCS := $(foreach n,$(BANK_NUMS),$(BANK$(n)_SRCS))
BANK_FLAGS := $(foreach n,$(BANK_NUMS),\
$(foreach f,$(BANK$(n)_SRCS),--bank $(n)=$(SRC_DIR)/$(f)))
# Модули резидента (кроме sprpop.c — его подставляет app.mk).
EXTRA_SRCS := $(addprefix $(SRC_DIR)/,pop_vflip.c pop_pace.c pop_sfx.c pop_state.c \
pop_draw.c pop_tile.c pop_kid.c pop_level.c pop_geom.c pop_guard.c \
pop_qsave_io.c)
# ---------------------------------------------------------------------------
# Отладочные ключи
# ---------------------------------------------------------------------------
# Профилирование полосами бордюра (PROF() в sprpop.c): каждая фаза кадра красит
# бордюр в свой цвет, высота полосы на скриншоте MAME = время фазы в строках.
PROF ?= 0
PROF_FLAGS := -DPROF_BORDER=$(PROF)
# Стартовый уровень: make LEVEL=9 — начать сразу с девятого.
LEVEL ?= 1
PROF_FLAGS += -DFIRST_LEVEL=$(LEVEL)
# Стартовая КОМНАТА и позиция Кида: make ROOM=15 POS=2 — начать прямо в
# целевой комнате, минуя проход уровня. POS — тайл 0..29, то есть row*10+col.
# make ROOM= (пусто) возвращает штатный старт из данных уровня.
ROOM ?=
POS ?= 2
ifneq ($(strip $(ROOM)),)
PROF_FLAGS += -DDBG_START_ROOM=$(ROOM) -DDBG_START_POS=$(POS)
endif
EXTRA_FLAGS ?= --gfx 256 -I $(CURDIR)/gen -I $(CURDIR)/$(SRC_DIR) \
--max-allocs $(ALLOCS) $(BANK_FLAGS) $(PROF_FLAGS) $(BUILD_FLAGS)
# ---------------------------------------------------------------------------
# Ресурсы: что уходит на диск
# ---------------------------------------------------------------------------
# assets/packed/<КАТАЛОГ>/ повторяет раскладку диска (8.3, как в DSS), поэтому
# отдельного списка «файл → каталог образа» больше нет: каталог виден из пути.
PACKED := assets/packed
ATL := $(BUILD_DIR)/atl
# Промежуточные атласы (в build/atl, в репозиторий не идут) — из них
# упаковщик архивов склеивает то, что реально ложится на диск. Порядок в
# списках = порядок индексов внутри архива, а их упаковщик печатает в gen/*.h;
# менять порядок нельзя, не перегенерировав заголовок.
BG_SETS := pop pal
BG_PAGES := 0 1 2 3 4 5 6 7 8 9
BG_ATL := $(foreach s,$(BG_SETS),\
$(foreach n,$(BG_PAGES),$(ATL)/bg/$(s)_env$(n).atl) \
$(ATL)/bg/$(s)_wall.atl $(ATL)/bg/$(s)_fore.atl) \
$(ATL)/bg/pop_pot.atl
# ВСЕ 28 страниц Kid — иначе кадры торможения/разворота/приседа (image>23) не
# рисуются. Плюс зеркальный ПО ВЕРТИКАЛИ набор (зелье инверсии, уровень 9):
# разворот готовит упаковщик, а не рантайм — страница разворачивается 0,59 с
# против 0,045 с на её чтение с диска.
KID_N := 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
KID_ATL := $(foreach n,$(KID_N),$(ATL)/kid/kid$(n).atl) \
$(foreach n,$(KID_N),$(ATL)/kid/kid$(n)_v.atl) \
$(ATL)/kid/sword.atl $(ATL)/kid/sword_v.atl
GUARD_ATL := $(foreach n,0 1 2 3 4,$(ATL)/guard/g$(n).atl) \
$(foreach n,0 1 2 3 4,$(ATL)/guard/g$(n)_v.atl)
# Скелет — ОТДЕЛЬНЫЙ набор (SKEL.DAT), его грузит уровень 3; кадров меньше.
# Зеркального набора у него нет: зелье инверсии — уровень 9, там обычный страж.
SKEL_ATL := $(foreach n,0 1 2 3,$(ATL)/skel/g$(n).atl)
# Джафар (VIZIER.DAT) — уровень 13; кадров столько же, сколько у стража, но
# палитра своя, из файла набора.
VIZIER_ATL := $(foreach n,0 1 2 3 4,$(ATL)/vizier/g$(n).atl)
# ТЕНЬ — набор, запечённый упаковщиком (наложение спрайта на себя со сдвигом;
# наш блит так не умеет). sk* — кадры вне боя, sf* — кадры 150..189.
SHADOW_ATL := $(foreach n,$(KID_N),$(ATL)/shadow/sk$(n).atl) \
$(foreach n,0 1 2 3,$(ATL)/shadow/sf$(n).atl)
# Звук: оригинальная оцифровка на частоте CBL (10 937,5 Гц, 8 бит моно).
# Число файлов задаёт упаковщик (POP_SND_PAGES в gen/pop_sound_tbl.h) —
# список ОБЯЗАН совпадать: недостающий файл это молча пропавшие эффекты.
SND_ATL := $(foreach n,0 1 2 3 4 5 6 7 8 9,$(ATL)/sound/s$(n).bin)
TITLE_ATL := $(foreach s,0 1 2 3,$(foreach p,0 1 2 3 4,$(ATL)/title/t$(s)_$(p).atl))
# PV/INTRO: story и стадии режутся на полосы; у настоящего PV intro есть ещё
# чистый фон b0_* и шесть <=16K actor-атласов — они остаются в EMM на время
# сцены, а не перечитывают полный 320x200 кадр с диска.
PV_ATL := $(foreach s,0 1 2 3 4 5,$(foreach p,0 1 2 3 4,$(ATL)/pv/s$(s)_$(p).atl)) \
$(foreach s,0 1 2 3 4 5 6 7,$(foreach p,0 1 2 3 4,$(ATL)/pv/p$(s)_$(p).atl)) \
$(foreach p,0 1 2 3 4,$(ATL)/pv/b0_$(p).atl) \
$(ATL)/pv/a0.atl $(ATL)/pv/a1.atl $(ATL)/pv/a2.atl \
$(ATL)/pv/j0.atl $(ATL)/pv/j1.atl $(ATL)/pv/j2.atl \
$(ATL)/pv/h0.atl $(ATL)/pv/t0.atl
# МУЗЫКА: записи DOS-версии в формате CBL. Номера — оригинала, чтобы игровой
# код просил трек тем же числом, каким SDLPoP зовёт play_sound.
# заставка 54 55 50 53 52 | катсцены 26 27 35 36 40
# игра 24 25 28 29 30 32 33 37 39 41 43 | 56 = won (стриминг с диска)
MUS_IDS := 24 25 26 27 28 29 30 32 33 35 36 37 39 40 41 43 50 52 53 54 55 56
# Набор записей, из которого делается музыка: flac (по умолчанию — лучший
# исходник), mp3, ogg, mt32 (то же в исполнении Roland MT-32). Все четыре
# лежат в assets/orig/PoP1_DOS_music/, имена треков совпадают.
# Формат входит в имя stamp'а, поэтому смена MUSIC_FMT сама по себе делает
# музыку устаревшей — принудительная пересборка не нужна.
MUSIC_FMT ?= flac
# res2000 — attract-demo; res2001..res2014 — штатная игра. res2015 (copy
# protection) намеренно не пакуется и не существует в маршруте загрузчика.
LVL_NUMS := 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14
# ЕДИНСТВЕННЫЙ список того, что ложится на диск. Из него выводятся и
# staging в build/, и аргументы упаковщика образа.
# Архивы вместо россыпи файлов не ради места: замер 2026-08-25 — `open` 51,4 мс
# против 32,6 мс на чтение 16 КБ, то есть на одних открытиях набор Кида терял
# три секунды. Четыре архива актёров, а не один: имена внутри наборов
# совпадают (g0.atl есть и у стража, и у скелета, и у Джафара), а константы
# индексов упаковщик делает из имени файла.
DISK := BG/bg.arc BG/pop_tile.pal BG/pal_tile.pal \
KID/kid.arc KID/kid.pal KID/kid.ani \
GUARD/guard.arc SKEL/skel.arc VIZIER/vizier.arc SHADOW/shadow.arc \
SND/snd.arc \
$(foreach i,$(MUS_IDS),MUS/m$(i).bin) \
FONT/font.atl \
TITLE/title.arc TITLE/title.pal \
PV/pv.arc PV/story.pal PV/pv.pal \
$(foreach n,$(LVL_NUMS),LEVELS/res20$(n).bin)
# Все шестнадцать генерируемых заголовков перечислены явно, а не wildcard'ом:
# список — это и зависимость exe, и то, что умеет чинить себя само.
GEN_ARC_H := bg_arc.h kid_arc.h guard_arc.h skel_arc.h vizier_arc.h \
shadow_arc.h title_arc.h pv_arc.h
GEN_H := $(addprefix gen/,$(GEN_ARC_H) pop_bg_atlas.h kid_atlas.h kid_data.h \
pop_guard_pal.h pop_shadow_atlas.h pop_sound_tbl.h \
pop_music_tbl.h pop_music_ticks.h pop_font.h)
PACKED_FILES := $(addprefix $(PACKED)/,$(DISK))
STAGED := $(addprefix $(BUILD_DIR)/,$(DISK))
EXTRA_DATA := $(STAGED)
HDD_PACK_ARGS := $(BUILD_DIR)/$(EXAMPLE).exe \
$(foreach f,$(DISK),$(word 1,$(subst /, ,$(f))):$(BUILD_DIR)/$(f))
# Образ живёт в build/, а не в mame/v306/IMG — приложение автономно. Связь с
# MAME — одна символьная ссылка, ставится однократно: make mame-link.
HDD_IMG := $(CURDIR)/$(BUILD_DIR)/hdd/$(EXAMPLE).chd
include $(PROJ_ROOT)/app.mk
# ---------------------------------------------------------------------------
# Сборка приложения
# ---------------------------------------------------------------------------
# sprinter-cc собирает все модули одним вызовом и depfile не выдаёт, поэтому
# зависимости перечисляем сами. wildcard по заголовкам намеренно широкий:
# цена лишней полной сборки ниже риска получить stale-бинарник.
HEADERS := $(wildcard $(SRC_DIR)/*.h)
# ОТПЕЧАТОК КЛЮЧЕЙ СБОРКИ. LEVEL/ROOM/POS/PROF/ALLOCS — это -D на входе
# компилятора, а не файлы, и make их «не видит»: после `make LEVEL=14`
# обычный `make` считал бы отладочный бинарь свежим и молча оставил его.
# Поэтому ключи пишутся в файл, и exe зависит от него; меняется строка —
# перелинковка. Файл переписывается ТОЛЬКО при изменении, иначе каждый
# make дёргал бы полную сборку заново.
BUILD_SIG := MEMORY=$(MEMORY) ALLOCS=$(ALLOCS) $(PROF_FLAGS) $(BANK_FLAGS)
BUILD_STAMP := $(BUILD_DIR)/.buildflags
# Отпечаток обновляется НА РАЗБОРЕ Makefile, а не рецептом. Так время файла
# уже верное к моменту, когда make сверяет его с exe — иначе не работает ни
# один из простых вариантов:
# * рецепт с cmp — под `make -n` не выполняется, и make врёт про работу;
# * подпись в имени файла — при ВОЗВРАТЕ к прежним ключам файл уже лежит
# и оказывается старше exe, то есть перелинковки не будет.
# Запись идёт только при отличии, поэтому обычный make ничего не трогает.
# На целях очистки пропускаем, чтобы `make clean` не создавал build/ заново.
ifeq ($(filter clean distclean,$(MAKECMDGOALS)),)
$(shell mkdir -p $(BUILD_DIR); printf '%s\n' '$(BUILD_SIG)' | cmp -s - $(BUILD_STAMP) \
|| printf '%s\n' '$(BUILD_SIG)' > $(BUILD_STAMP))
endif
# Цепочка: exe -> gen/*.h -> ресурсы -> упаковщик -> assets/orig (см. ниже).
$(EXE): Makefile $(PROJ_ROOT)/app.mk $(addprefix $(SRC_DIR)/,$(BANKED_SRCS)) \
$(HEADERS) $(GEN_H) $(BUILD_STAMP)
# Полная сборка = exe + разложенный build/.
all: stage
# ---------------------------------------------------------------------------
# staging: assets/packed/ -> build/
# ---------------------------------------------------------------------------
# build/ обязан быть самодостаточным (его можно снести целиком), поэтому
# ресурсы именно КОПИРУЮТСЯ, а не берутся с диска из assets/.
stage: $(STAGED)
$(BUILD_DIR)/%: $(PACKED)/%
@mkdir -p $(dir $@)
@cp $< $@
# ---------------------------------------------------------------------------
# Ресурсы: цепочка зависимостей
# ---------------------------------------------------------------------------
# Направление одно и сквозное:
#
# build/sprpop.exe -> gen/*.h -> архивы и атласы -> упаковщик -> assets/orig/
#
# Код зависит от генерируемых заголовков, заголовки — от ресурсов, которые они
# описывают (индексы внутри .arc, число страниц звука, таблицы кадров), а те —
# от упаковщика и оригиналов. Поэтому собрать код против устаревших констант
# нельзя: gen/*.h делаются ТЕМ ЖЕ запуском упаковщика, что и сам ресурс, и
# объявлены его целями наравне с ним.
#
# Правила генерации живут в ветке GENERATE=1 и включаются в двух случаях:
# * явная цель `make resources*` — перегенерация с учётом таймстемпов
# (поправил упаковщик — ресурс пересоберётся);
# * какого-то ресурса или заголовка НЕТ на месте (после `make distclean`,
# либо файл снесли руками) — тогда обычный `make` сам уходит в
# рекурсивный вызов и доделывает недостающее вместе с его gen/*.h.
#
# Почему не одни таймстемпы на все случаи: assets/packed/ и gen/ лежат в
# репозитории, а git не сохраняет времена файлов — в свежем клоне упаковщик
# и его выхлоп получают одинаковую метку, и любое сравнение по времени
# превращается в лотерею «перепаковать 6 МБ ресурсов на ровном месте, причём
# на машине, где assets/orig/ может вообще не быть». Отсутствие файла —
# признак однозначный, и именно на нём построена автоматика.
TOOLS := $(CURDIR)/tools
PY := python3
ifeq ($(GENERATE),1)
# ===========================================================================
# Ветка генерации
# ===========================================================================
STAMP := $(BUILD_DIR)/.stamps
$(STAMP):
@mkdir -p $@
# Упаковщики создают сразу ГРУППУ файлов. Обычное multi-output правило
# заставляет make при `-B` запускать один и тот же упаковщик отдельно для
# каждого выходного файла. Stamp представляет ОДИН запуск генератора; все
# созданные им файлы зависят от stamp без собственных рецептов.
#
# check-orig — order-only: без оригиналов запускать упаковщик бессмысленно, и
# понятное сообщение лучше трейсбека Python. На «устарел ли stamp» он не
# влияет, только выполняется раньше.
define stamp_rule
$(STAMP)/$(1): $(2) | $(STAMP) check-orig
cd $(TOOLS) && $(PY) $(3)
@touch $$@
endef
$(eval $(call stamp_rule,bg,$(TOOLS)/pop_pack_bg.py $(TOOLS)/render_room.py $(TOOLS)/paths.py,pop_pack_bg.py dungeon && $(PY) pop_pack_bg.py palace))
$(eval $(call stamp_rule,kid,$(TOOLS)/pop_pack_kid.py $(TOOLS)/pop_pack_bg.py $(TOOLS)/paths.py,pop_pack_kid.py))
$(eval $(call stamp_rule,kid-bin,$(TOOLS)/pop_extract_kid_data.py $(TOOLS)/paths.py,pop_extract_kid_data.py))
$(eval $(call stamp_rule,guard,$(TOOLS)/pop_pack_guard.py $(TOOLS)/paths.py,pop_pack_guard.py))
$(eval $(call stamp_rule,skel,$(TOOLS)/pop_pack_guard.py $(TOOLS)/paths.py,pop_pack_guard.py SKEL))
$(eval $(call stamp_rule,vizier,$(TOOLS)/pop_pack_guard.py $(TOOLS)/paths.py,pop_pack_guard.py VIZIER))
$(eval $(call stamp_rule,shadow,$(TOOLS)/pop_pack_shadow.py $(TOOLS)/pop_pack_bg.py $(TOOLS)/pop_pack_kid.py $(TOOLS)/paths.py,pop_pack_shadow.py))
$(eval $(call stamp_rule,sound,$(TOOLS)/pop_pack_sound.py $(TOOLS)/paths.py,pop_pack_sound.py))
$(eval $(call stamp_rule,music-$(MUSIC_FMT),$(TOOLS)/pop_pack_music.py $(TOOLS)/paths.py,pop_pack_music.py --format $(MUSIC_FMT) $(MUS_IDS)))
$(eval $(call stamp_rule,font,$(TOOLS)/pop_extract_font.py $(TOOLS)/paths.py,pop_extract_font.py))
$(eval $(call stamp_rule,title,$(TOOLS)/pop_pack_title.py $(TOOLS)/paths.py,pop_pack_title.py))
$(eval $(call stamp_rule,pv,$(TOOLS)/pop_pack_intro.py $(TOOLS)/paths.py,pop_pack_intro.py))
# Файл, сделанный упаковщиком, зависит только от stamp. Если stamp свежий, а
# файла нет (снесли руками, либо build/ вычистили) — перезапускаем генератор
# принудительно. Заголовки gen/, которые упаковщик печатает попутно, стоят в
# ТЕХ ЖЕ списках: это и есть «gen зависит от ресурсов, для которых сделан».
define from_stamp
$(1): $(STAMP)/$(2)
@test -f $$@ || { $(MAKE) GENERATE=1 -B $(STAMP)/$(2); test -f $$@; }
endef
$(eval $(call from_stamp,$(BG_ATL) $(ATL)/bg/pop_tile.pal $(ATL)/bg/pal_tile.pal gen/pop_bg_atlas.h,bg))
$(eval $(call from_stamp,$(KID_ATL) $(ATL)/kid/kid.pal gen/kid_atlas.h,kid))
$(eval $(call from_stamp,$(ATL)/kid/kid.ani gen/kid_data.h,kid-bin))
$(eval $(call from_stamp,$(GUARD_ATL) gen/pop_guard_pal.h,guard))
$(eval $(call from_stamp,$(SKEL_ATL),skel))
$(eval $(call from_stamp,$(VIZIER_ATL),vizier))
$(eval $(call from_stamp,$(SHADOW_ATL) gen/pop_shadow_atlas.h,shadow))
$(eval $(call from_stamp,$(SND_ATL) gen/pop_sound_tbl.h,sound))
$(eval $(call from_stamp,$(foreach i,$(MUS_IDS),$(ATL)/music/m$(i).bin) gen/pop_music_tbl.h gen/pop_music_ticks.h,music-$(MUSIC_FMT)))
$(eval $(call from_stamp,$(ATL)/font/font.atl gen/pop_font.h,font))
$(eval $(call from_stamp,$(TITLE_ATL) $(ATL)/title/title.pal,title))
$(eval $(call from_stamp,$(PV_ATL) $(ATL)/pv/story.pal $(ATL)/pv/pv.pal,pv))
# Архивы: собираются ПРЯМО в assets/packed — это уже дисковый артефакт.
# Заголовок с индексами объявлен целью НАРАВНЕ с архивом: индексы описывают
# именно это содержимое и разъехаться с ним не должны.
ARC := $(PY) $(TOOLS)/pop_pack_arc.py
define arc_rule
$(PACKED)/$(1)/$(2).arc gen/$(3)_arc.h: $(4) $(TOOLS)/pop_pack_arc.py
@mkdir -p $(PACKED)/$(1)
$(ARC) --header gen/$(3)_arc.h --prefix $(5) $(PACKED)/$(1)/$(2).arc $(4)
endef
$(eval $(call arc_rule,BG,bg,bg,$(BG_ATL),BG))
$(eval $(call arc_rule,KID,kid,kid,$(KID_ATL),KID))
$(eval $(call arc_rule,GUARD,guard,guard,$(GUARD_ATL),GUARD))
$(eval $(call arc_rule,SKEL,skel,skel,$(SKEL_ATL),SKEL))
$(eval $(call arc_rule,VIZIER,vizier,vizier,$(VIZIER_ATL),VIZIER))
$(eval $(call arc_rule,SHADOW,shadow,shadow,$(SHADOW_ATL),SHADOW))
$(eval $(call arc_rule,TITLE,title,title,$(TITLE_ATL),TITLE))
$(eval $(call arc_rule,PV,pv,pv,$(PV_ATL),PV))
# У звука своего *_arc.h нет: содержимое адресуется таблицей pop_sound_tbl.h,
# которую печатает сам упаковщик звука.
$(PACKED)/SND/snd.arc: $(SND_ATL) $(TOOLS)/pop_pack_arc.py
@mkdir -p $(dir $@)
$(ARC) $@ $(SND_ATL)
# Файлы, которые ложатся на диск как есть, — простое копирование из staging.
define copy_rule
$(PACKED)/$(1): $(2)
@mkdir -p $$(dir $$@)
@cp $$< $$@
endef
$(eval $(call copy_rule,BG/pop_tile.pal,$(ATL)/bg/pop_tile.pal))
$(eval $(call copy_rule,BG/pal_tile.pal,$(ATL)/bg/pal_tile.pal))
$(eval $(call copy_rule,KID/kid.pal,$(ATL)/kid/kid.pal))
$(eval $(call copy_rule,KID/kid.ani,$(ATL)/kid/kid.ani))
$(eval $(call copy_rule,FONT/font.atl,$(ATL)/font/font.atl))
$(eval $(call copy_rule,TITLE/title.pal,$(ATL)/title/title.pal))
$(eval $(call copy_rule,PV/story.pal,$(ATL)/pv/story.pal))
$(eval $(call copy_rule,PV/pv.pal,$(ATL)/pv/pv.pal))
$(foreach i,$(MUS_IDS),$(eval $(call copy_rule,MUS/m$(i).bin,$(ATL)/music/m$(i).bin)))
# Уровни идут на диск ПРЯМО из оригинала: формат res20xx.bin наш загрузчик
# читает как есть, упаковщика для них нет. check-orig здесь тоже order-only —
# без оригиналов взять уровни неоткуда, и сказать об этом надо внятно.
LVL_ORIG := assets/orig/SDLPoP/data/LEVELS
define lvl_rule
$(PACKED)/LEVELS/res20$(1).bin: $(LVL_ORIG)/res20$(1).bin | check-orig
@mkdir -p $$(dir $$@)
@cp $$< $$@
endef
$(foreach n,$(LVL_NUMS),$(eval $(call lvl_rule,$(n))))
resources-bg: $(PACKED)/BG/bg.arc $(PACKED)/BG/pop_tile.pal $(PACKED)/BG/pal_tile.pal
resources-kid: $(PACKED)/KID/kid.arc $(PACKED)/KID/kid.pal $(PACKED)/KID/kid.ani
resources-actors: $(PACKED)/GUARD/guard.arc $(PACKED)/SKEL/skel.arc \
$(PACKED)/VIZIER/vizier.arc $(PACKED)/SHADOW/shadow.arc
resources-sound: $(PACKED)/SND/snd.arc
resources-music: $(foreach i,$(MUS_IDS),$(PACKED)/MUS/m$(i).bin)
resources-font: $(PACKED)/FONT/font.atl
resources-title: $(PACKED)/TITLE/title.arc $(PACKED)/TITLE/title.pal
resources-pv: $(PACKED)/PV/pv.arc $(PACKED)/PV/story.pal $(PACKED)/PV/pv.pal
resources-levels: $(foreach n,$(LVL_NUMS),$(PACKED)/LEVELS/res20$(n).bin)
resources: resources-bg resources-kid resources-actors resources-sound \
resources-music resources-font resources-title resources-pv \
resources-levels $(GEN_H)
check-orig:
@test -d $(LVL_ORIG) || { \
echo "SprPoP: нет assets/orig/ — генерировать ресурсы не из чего." >&2; \
echo " Что и откуда положить, написано в assets/orig/README.md" >&2; false; }
else
# ===========================================================================
# Обычная сборка: assets/packed/ и gen/ — готовые входы
# ===========================================================================
# Ни одного правила с таймстемпами: пока файлы на месте, make их даже не
# рассматривает. Пропал файл — доделываем ровно его, рекурсивным вызовом в
# ветку генерации (она подтянет и заголовок, и оригиналы).
$(PACKED_FILES) $(GEN_H):
@echo "SprPoP: нет $@ — генерирую ресурсы"
@$(MAKE) GENERATE=1 $@
RES_TARGETS := resources resources-bg resources-kid resources-actors \
resources-sound resources-music resources-font resources-title \
resources-pv resources-levels
$(RES_TARGETS):
@$(MAKE) GENERATE=1 $@
# Перегенерация ВСЕГО, не глядя на таймстемпы.
resources-rebuild:
@$(MAKE) GENERATE=1 -B resources
.PHONY: $(RES_TARGETS) resources-rebuild
endif
# ---------------------------------------------------------------------------
# Прочее
# ---------------------------------------------------------------------------
# Образ HDD собирается после staging — иначе на диск уедет старое содержимое.
# Рецепта здесь нет намеренно: он в app.mk, а это только зависимости.
hdd: stage | $(BUILD_DIR)/hdd
$(BUILD_DIR)/hdd:
@mkdir -p $@
# Однократная связка с MAME: run_bridge.sh жёстко открывает IMG/test_hdd.chd,
# поэтому подсовываем ему символьную ссылку на наш образ. chdman переписывает
# сам файл, ссылка остаётся живой — но MAME после пересборки образа ОБЯЗАН
# полный рестарт (новый inode).
mame-link: hdd
ln -sf $(HDD_IMG) $(PROJ_ROOT)/mame/v306/IMG/test_hdd.chd
@echo "MAME: IMG/test_hdd.chd -> $(HDD_IMG)"
# Музыка из конкретного набора записей. Каждая цель пересобирает музыку и
# сразу раскладывает её в build/, чтобы `make hdd` взял уже новую.
# make music-mp3 собрать музыку из mp3
# make music-mt32 вариант Roland MT-32
# Разовая сборка без смены умолчания: make MUSIC_FMT=ogg resources-music
MUSIC_TARGETS := music-flac music-mp3 music-ogg music-mt32
$(MUSIC_TARGETS):
@$(MAKE) MUSIC_FMT=$(patsubst music-%,%,$@) resources-music
@$(MAKE) MUSIC_FMT=$(patsubst music-%,%,$@) stage
# Перелинковка приложения без пересборки библиотек и ресурсов.
relink:
$(MAKE) -W Makefile $(EXE)
# clean (из app.mk) сносит только build/ — ассеты остаются на месте.
# distclean дополнительно убирает УПАКОВАННЫЕ ассеты; вернуть их можно
# командой `make resources`, но для неё нужны оригиналы в assets/orig/.
# Сам assets/orig/ не трогается НИКОГДА: он не производный, а исходный, и
# восстановить его командой нельзя — только положить руками.
# gen/ тоже остаётся: без него не скомпилировать src/, а вес его — байты.
distclean: clean
rm -rf $(PACKED)
@echo "assets/packed снесён; вернуть — make resources (нужен assets/orig)"
# Цели генерации ресурсов объявлены .PHONY в своей ветке (см. выше): в
# GENERATE=1 они настоящие агрегаты, в обычной сборке — переходники.
.PHONY: stage check-orig resources resources-rebuild resources-bg resources-kid \
resources-actors resources-sound resources-music resources-font \
resources-title resources-pv resources-levels $(MUSIC_TARGETS) \
relink distclean mame-link