# 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_app.c pop_timer.c \
              pop_ui.c pop_title.c pop_demo.c pop_flow.c
# Банк 10 = палитры + РЕДКОЕ С ФАЙЛАМИ.  pop_config (POP.CFG) и pop_hof
# (POP.HOF) переехали сюда из банка 9, когда тот дошёл до 98,5% (247 байт
# свободно).  Критерий тот же, что и у остальной раскладки, — частота, а не
# размер: обе вещи работают раз за партию и упираются в диск, где `open`
# стоит 51 мс, так что трамплин банк→банк на их фоне не существует.
# Отдельный банк для этого не нужен: в десятом лежал один pop_pal.c на
# 485 байт, то есть 3% страницы.
BANK10_SRCS := pop_pal.c pop_config.c pop_hof.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 есть и у стража, и у скелета, и у Джафара), а константы
# индексов упаковщик делает из имени файла.
# README для игрока идёт НА ДИСК рядом с программой.  Русский обязан быть в
# CP866: DSS и любой местный просмотрщик читают именно её, UTF-8 покажется
# кракозябрами.  Имена в 8.3: README.TXT и README.RUS.
DISK := README.TXT README.RUS \
        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))))

# README: перевод строки CRLF (текст читают в DOS-подобной среде) и, для
# русского, перекодировка в CP866.  iconv БЕЗ -c намеренно: потеря символа
# должна ломать сборку, а не молча портить текст (memory
# iconv_cp866_workflow; //TRANSLIT на macOS падает).
$(PACKED)/README.TXT: dist/README.txt
	@mkdir -p $(dir $@)
	@sed 's/$$/\r/' $< > $@

# ПОРЯДОК ВАЖЕН: CRLF ставится ДО перекодировки.  BSD sed в UTF-8 локали
# отказывается работать с байтами CP866 («RE error: illegal byte sequence»),
# а с валидным UTF-8 — работает.
$(PACKED)/README.RUS: dist/README.ru.txt
	@mkdir -p $(dir $@)
	@sed 's/$$/\r/' $< | iconv -f UTF-8 -t CP866 > $@

resources-readme: $(PACKED)/README.TXT $(PACKED)/README.RUS

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-readme 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
