fee3bdb354
Правки пользователя, разобранные по диффу и закоммиченные как есть. Makefile: умолчание --max-allocs поднято с 6000 до 10000 — плотнее упаковка регистров. Напоминание из шапки остаётся в силе: занятость банков сравнима только при ОДНОМ значении ALLOCS, иначе сравниваются не правки, а уровни оптимизации. pop_map.c: банковая обёртка pop_dist_to_edge_weight() переехала НИЖЕ статического distance_to_edge_weight(), который она зовёт, — раньше стояла до его определения. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DSYUpuaQpKr48kBav2iiV4
628 lines
44 KiB
Makefile
628 lines
44 KiB
Makefile
# 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 ?= 10000
|
||
# 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
|