Files
Sprinter-SDCC/applications/SprPoP/Makefile
T
snark13 31b82661eb 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>
2026-08-27 12:12:28 +03:00

492 lines
33 KiB
Makefile
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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