# 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 pop_path_bank.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_snd_data.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) — # список ОБЯЗАН совпадать: недостающий файл это молча пропавшие эффекты. # Страниц звука СТАТИЧЕСКИ НЕ ПОСЧИТАТЬ: их число зависит от источника # (SDLPoP даёт 9, MSDOS — 10, см. SND_SRC). Поэтому списка s0..sN здесь нет, # а архив зависит от stamp'а упаковщика и берёт страницы из каталога уже # после того, как тот отработал (см. правило SND/snd.arc ниже). 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 # Источник оцифрованных ЭФФЕКТОВ (digisnd1..3): sdlpop (умолчание) или msdos. # Умолчание — SDLPoP, потому что он выкачивается автоматически (`make fetch`) # и сборка обязана работать вообще без оригинального дистрибутива DOS. # Наборы НЕ одинаковы: в MSDOS оцифровка полнее (в SDLPoP `spiked` (48) # фактически пустой, `sword_vs_sword` (10) короче — разбор в # docs/sound_plan.md). У кого дистрибутив есть — `make SND_SRC=msdos`. # Источник входит в имя stamp'а, поэтому смена SND_SRC сама делает звук # устаревшим, как и смена MUSIC_FMT. SND_SRC ?= sdlpop # 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, язык — суффиксом, а не # расширением: расширение .TXT должно остаться, чтобы файл открывался # просмотрщиком как текст. DISK := README_E.TXT README_R.TXT \ 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 SND/snd.idx \ $(foreach i,$(MUS_IDS),MUS/m$(i).bin) MUS/mus.idx \ 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 # pop_sound_tbl.h здесь БОЛЬШЕ НЕТ: раскладка эффектов уехала на диск # (SND/snd.idx), и EXE от звукового набора не зависит — пару # snd.idx + snd.arc можно менять без перекомпиляции. # Ни звукового, ни музыкальных заголовков здесь БОЛЬШЕ НЕТ: раскладка # эффектов и длины треков уехали на диск (SND/snd.idx, MUS/mus.idx), и EXE # не зависит ни от звукового набора, ни от набора записей. GEN_H := $(addprefix gen/,$(GEN_ARC_H) pop_bg_atlas.h kid_data.h \ pop_guard_pal.h pop_shadow_atlas.h pop_font.h) PACKED_FILES := $(addprefix $(PACKED)/,$(DISK)) STAGED := $(addprefix $(BUILD_DIR)/,$(DISK)) EXTRA_DATA := $(STAGED) # Запись вида КАТАЛОГ:файл кладёт файл в подкаталог HDD_DEST_DIR, голый путь — # непосредственно в HDD_DEST_DIR. Проверка на слэш ОБЯЗАТЕЛЬНА: без неё # `README_E.TXT` превращался # в `README_E.TXT:build/README_E.TXT`, то есть упаковщик заводил каталог с # именем файла и клал файл внутрь него (поймано пользователем на готовом # образе). HDD_PACK_ARGS := $(BUILD_DIR)/$(EXAMPLE).exe \ $(foreach f,$(DISK),$(if $(findstring /,$(f)),\ $(word 1,$(subst /, ,$(f))):$(BUILD_DIR)/$(f),\ $(BUILD_DIR)/$(f))) # Всё дерево SprPoP лежит рядом с EXE, но не меняет дефолт глобального # упаковщика: остальные приложения по-прежнему попадают в корень HDD. HDD_DEST_DIR := games/sprpop # Образ живёт в 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 # ВАРИАНТЫ РЕСУРСА (источник звука SND_SRC, набор записей MUSIC_FMT). Оба # варианта пишут в ОДИН выход — build/atl/sound, build/atl/music, — поэтому # одного stamp'а на вариант мало: вернувшись к прежнему варианту, make видит # его stamp свежим и не делает НИЧЕГО, а в выходе лежит чужое (поймано на # sdlpop -> msdos -> sdlpop: заголовок остался с 10 страницами вместо 9). # Лечится файлом с именем текущего варианта: он переписывается только при # смене (cmp), а его свежесть делает stamp устаревшим. .PHONY: variant-check variant-check: ; # СМЕНА ВАРИАНТА СНОСИТ STAMP'Ы СВОЕГО СЕМЕЙСТВА, а не полагается на то, # что make заметит свежесть variant-файла. Причина — секундная # гранулярность mtime: упаковка укладывается в одну секунду с записью # variant, и stamp того же имени считается актуальным. Возврат к прежнему # варианту (`sdlpop -> msdos -> sdlpop`) тогда ТИХО оставлял чужой набор. # Нет файла — правило обязано выполниться, и время тут ни при чём. define variant_rule $(STAMP)/$(1).variant: variant-check | $(STAMP) @echo "$(2)" | cmp -s - $$@ || { echo "$(2)" > $$@; rm -f $(STAMP)/$(1)-*; } endef $(eval $(call variant_rule,sound,$(SND_SRC))) $(eval $(call variant_rule,music,$(MUSIC_FMT))) $(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)) # ЗВУК — ОСОБЫЙ СЛУЧАЙ: упаковка, сборка архива и копия индекса делаются # ОДНИМ рецептом, а не тремя правилами через таймстемпы. # # Почему. Упаковщик и копирование укладываются в одну секунду, а make # сравнивает mtime с секундной гранулярностью: цель, сделанная в ту же # секунду, что и её зависимость, считается актуальной. При смене SND_SRC # это давало ТИХО НЕВЕРНЫЙ результат — `sdlpop -> msdos -> sdlpop` # перепаковывал build/atl, но в assets/packed оставался прежний набор # (10 страниц вместо 9; поймано 2026-08-31, с паузой в секунду не # воспроизводится). Внутри одного рецепта сравнивать нечего. $(STAMP)/sound-$(SND_SRC): $(TOOLS)/pop_pack_sound.py $(TOOLS)/pop_idx.py \ $(TOOLS)/paths.py $(TOOLS)/pop_pack_arc.py \ $(STAMP)/sound.variant | $(STAMP) check-orig cd $(TOOLS) && $(PY) pop_pack_sound.py --source $(SND_SRC) @mkdir -p $(PACKED)/SND $(ARC) $(PACKED)/SND/snd.arc $$(ls $(ATL)/sound/s*.bin | sort -V) @cp $(ATL)/sound/snd.idx $(PACKED)/SND/snd.idx @touch $@ $(eval $(call stamp_rule,music-$(MUSIC_FMT),$(TOOLS)/pop_pack_music.py $(TOOLS)/pop_idx.py $(TOOLS)/paths.py $(STAMP)/music.variant,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,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,$(foreach i,$(MUS_IDS),$(ATL)/music/m$(i).bin) $(ATL)/music/mus.idx,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, # которую печатает сам упаковщик звука. # Зависимость на stamp, а не на список страниц: сколько их будет, известно # только после запуска упаковщика (источник задаёт число). Порядок страниц # в архиве обязан быть ЧИСЛОВЫМ — pop_snd_tbl адресует их индексом, а голый # ls даёт s10 перед s2. sort -V, а не `-t s -k n`: буква s есть и в пути. # Пара snd.arc + snd.idx согласована по построению: её делает ОДИН рецепт # stamp'а выше (см. там же, почему не раздельными правилами). Здесь # только восстановление, если файл снесли руками. $(PACKED)/SND/snd.arc $(PACKED)/SND/snd.idx: $(STAMP)/sound-$(SND_SRC) @test -f $@ || { $(MAKE) GENERATE=1 -B $(STAMP)/sound-$(SND_SRC); test -f $@; } # Файлы, которые ложатся на диск как есть, — простое копирование из 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))) $(eval $(call copy_rule,MUS/mus.idx,$(ATL)/music/mus.idx)) # Уровни идут на диск ПРЯМО из оригинала: формат res20xx.bin наш загрузчик # читает как есть, упаковщика для них нет. check-orig здесь тоже order-only — # без оригиналов взять уровни неоткуда, и сказать об этом надо внятно. LVL_ORIG := assets/orig/SDLPoP/data/LEVELS SND_ORIG_MSDOS := assets/orig/MSDOS 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_E.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_R.TXT: dist/README.ru.txt @mkdir -p $(dir $@) @sed 's/$$/\r/' $< | iconv -f UTF-8 -t CP866 > $@ resources-readme: $(PACKED)/README_E.TXT $(PACKED)/README_R.TXT 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 $(PACKED)/SND/snd.idx resources-music: $(foreach i,$(MUS_IDS),$(PACKED)/MUS/m$(i).bin) $(PACKED)/MUS/mus.idx 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/SDLPoP — генерировать ресурсы не из чего." >&2; \ echo " Скачать: make fetch (подробности — assets/orig/README.md)" >&2; false; } @test "$(SND_SRC)" != msdos || test -d $(SND_ORIG_MSDOS) || { \ echo "SprPoP: SND_SRC=msdos, но нет $(SND_ORIG_MSDOS)." >&2; \ echo " Дистрибутив DOS не качается — положить руками, либо собирать" >&2; \ echo " со звуком из SDLPoP: make SND_SRC=sdlpop" >&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 # --------------------------------------------------------------------------- # Внешние исходные данные (assets/orig/) # --------------------------------------------------------------------------- # В репозитории чужих данных нет — есть tools/fetch_orig.py, который знает, # откуда их взять, и таблица источников внутри него. Качаем только то, # чего нет; `FETCH_ARGS=--force` перекачивает заново, `FETCH_ARGS=--ref v1.23` # берёт конкретный тег SDLPoP. # make fetch SDLPoP + записи музыки # make fetch-sdlpop только SDLPoP (data/ + src/-эталон) # make fetch-music только записи саундтрека (43 МБ) # make fetch-check что уже на месте # make fetch-list таблица источников # MSDOS/ не качается — это копирайтный дистрибутив; он НЕОБЯЗАТЕЛЕН, всё # нужное (уровни, оцифровка) есть в SDLPoP. См. SND_SRC выше. FETCH := $(PY) $(TOOLS)/fetch_orig.py fetch: ; $(FETCH) all $(FETCH_ARGS) fetch-sdlpop: ; $(FETCH) sdlpop $(FETCH_ARGS) fetch-music: ; $(FETCH) music $(FETCH_ARGS) fetch-check: ; @$(FETCH) --check fetch-list: ; @$(FETCH) --list FETCH_TARGETS := fetch fetch-sdlpop fetch-music fetch-check fetch-list # Тесты инструментов (хост, питон): форматы, которые читает Z80. Секунды, # ни эмулятора, ни оригиналов не нужно. test-tools: @cd $(CURDIR) && $(PY) -m unittest discover -s tools/tests # Перелинковка приложения без пересборки библиотек и ресурсов. 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) \ $(FETCH_TARGETS) test-tools relink distclean mame-link