diff --git a/.gitignore b/.gitignore index 7450615..1be549f 100644 --- a/.gitignore +++ b/.gitignore @@ -132,15 +132,9 @@ applications/PoP/PoP1_DOS_music/ # не идёт (сама roomtest и есть версируемая ветка разработки). applications/PoP/R1/ -# SprPoP — автономное приложение. В репозиторий идут исходники, генерируемые -# заголовки (gen/) и упакованные ресурсы (assets/packed/), потому что без них -# из чистого клона ничего не собрать. НЕ идут: -# assets/orig/ оригинальные данные игры — копирайт (SDLPoP, MSDOS, -# записи музыки DOS-версии); откуда их взять, -# написано в assets/orig/README.md -# assets/packed/LEVELS/ уровни res20xx.bin ложатся на диск прямо из -# оригинала, конвертации нет — то есть это тот же -# копирайтный материал, только в раскладке диска -# build/ уже покрыт общим правилом build/ выше -applications/SprPoP/assets/orig/ -applications/SprPoP/assets/packed/LEVELS/ +# SprPoP — автономное приложение, и правила игнора у него СВОИ: +# applications/SprPoP/.gitignore. Он написан так, чтобы стать корневым +# .gitignore, когда SprPoP выделят в отдельный репозиторий, — поэтому +# здесь его содержимое НЕ дублируется (иначе разъедется). Коротко: в +# репозиторий не идут assets/orig/ (чужие данные — их выкачивает +# `make fetch`) и assets/packed/LEVELS/ (уровни из оригинала как есть). diff --git a/applications/SprPoP/.gitignore b/applications/SprPoP/.gitignore new file mode 100644 index 0000000..08b9da9 --- /dev/null +++ b/applications/SprPoP/.gitignore @@ -0,0 +1,75 @@ +# =========================================================================== +# SprPoP — .gitignore приложения +# =========================================================================== +# Пока SprPoP лежит внутри репозитория C-Compiler, git читает этот файл как +# правила ДЛЯ ЭТОГО ПОДДЕРЕВА, и он дополняет корневой .gitignore тулчейна. +# Когда SprPoP выделят в самостоятельный проект, файл станет КОРНЕВЫМ и +# продолжит работать как есть: все пути написаны от корня SprPoP и ничего +# не знают ни про applications/, ни про раскладку тулчейна. + +# --------------------------------------------------------------------------- +# ЧУЖИЕ ДАННЫЕ — в репозиторий не идут никогда +# --------------------------------------------------------------------------- +# assets/orig/ — оригинальные данные игры (копирайт). В репозитории вместо +# них лежит ЗНАНИЕ, откуда их взять: tools/fetch_orig.py (`make fetch`) +# качает SDLPoP и записи саундтрека, MSDOS/ пользователь кладёт сам (он +# необязателен). Скачанные архивы кэшируются в assets/orig/.cache/. +# +# Шаблон со ЗВЁЗДОЧКОЙ, а не `assets/orig/`: git не заходит внутрь +# игнорируемого каталога, и исключение ниже иначе не сработало бы, а +# README.md — единственное, что отсюда ОБЯЗАНО быть в репозитории: без +# него в чистом клоне не написано, откуда брать данные. +assets/orig/* +!assets/orig/README.md + +# Уровни res20xx.bin ложатся на диск ПРЯМО из оригинала, конвертации нет — +# то есть это тот же копирайтный материал, только в раскладке диска. +# Остальное содержимое assets/packed/ версионируется намеренно: без него из +# чистого клона не собрать игру, а исходников (assets/orig/) может не быть. +assets/packed/LEVELS/ + +# --------------------------------------------------------------------------- +# ВЫХОД СБОРКИ +# --------------------------------------------------------------------------- +# build/ — .exe, раскладка диска, hdd/, промежуточные атласы, stamp'ы. +# Правило без слэша спереди намеренно: под ним и tests/host/build/. +build/ + +# Промежуточный каталог sprinter-cc. +.sprinter-cc-*/ + +# Артефакты компиляции, которые SDCC кладёт рядом с целью — в корень +# приложения. Только корень: единственный версионируемый ассемблерный +# файл (tests/host/bank_stub.s) лежит глубже и под правило не попадает. +/*.exe +/*.asm +/*.lst +/*.lk +/*.ihx +/*.noi +/*.sym +/*.map +/*.rel +/*.cdb +/*.mem +/*.rst + +# Хостовые тесты и утилиты собираются нативным cc. +*.o +*.obj +*.dSYM/ + +# --------------------------------------------------------------------------- +# ОС / редакторы / локальные настройки +# --------------------------------------------------------------------------- +.DS_Store +._* +*~ +*.swp +*.bak +*.orig +*.rej +.vscode/ +.idea/ +.kilo/ +.claude/ diff --git a/applications/SprPoP/assets/orig/README.md b/applications/SprPoP/assets/orig/README.md new file mode 100644 index 0000000..360080d --- /dev/null +++ b/applications/SprPoP/assets/orig/README.md @@ -0,0 +1,82 @@ +# assets/orig — оригинальные данные + +Здесь лежат **исходные** данные Prince of Persia, из которых `make resources` +делает ресурсы Sprinter-версии. В репозиторий этот каталог не идёт — +материал копирайтный (см. `.gitignore`). Вместо данных в репозитории лежит +**знание, откуда они берутся**: `tools/fetch_orig.py` и таблица источников +внутри него. + +## Как заполнить + +``` +make fetch # SDLPoP + записи саундтрека (всё, что качается) +make fetch-check # что уже на месте +make fetch-list # таблица источников с адресами +``` + +Тонкости: + +``` +make fetch-sdlpop только SDLPoP +make fetch-music только музыка (43 МБ) +make FETCH_ARGS=--force fetch перекачать заново +make FETCH_ARGS="--ref v1.23" fetch-sdlpop конкретный тег SDLPoP +``` + +Скачанные архивы кэшируются в `assets/orig/.cache/` — повторный `make fetch` +сеть не трогает. Версия SDLPoP записывается в `assets/orig/SDLPoP/.fetch.json` +(ветка/тег, sha, дата), и следующий `make fetch` по ней понимает, последняя +у нас версия или нет. + +Собрать игру без `assets/orig/` **можно**: упакованные ресурсы лежат в +`../packed/`, а генерируемые заголовки — в `../../gen/`. `assets/orig` нужен +только чтобы ресурсы **перегенерировать**. + +Исключение — `LEVELS`: файлы `res20xx.bin` ложатся на диск игры как есть, без +конвертации, поэтому `assets/packed/LEVELS/` тоже вне репозитория и заполняется +из `orig` командой `make resources-levels`. + +## Что и откуда + +| Каталог | Что это | Где взять | +|---|---|---| +| `SDLPoP/data/` | Распакованные ресурсы DOS-версии: PNG-спрайты (`VDUNGEON`, `VPALACE`, `KID`, `GUARD`, `SKEL`, `VIZIER`, `SHADOW`, `TITLE`, `PV`, `PRINCE`), палитры, `LEVELS/res20xx.bin`, оцифровка `DIGISND*.DAT` | `make fetch-sdlpop` — (GPLv3) | +| `SDLPoP/src/` | Исходники SDLPoP. Нужны не только упаковщикам (`menu.c` и `seg009.c` содержат встроенные шрифты, из `seg005/seg006` берутся таблицы кадров и `seqtbl`), но и как **эталон механики**: расхождение в поведении всегда сверяется с ними | тем же `make fetch-sdlpop` | +| `PoP1_DOS_music/` | Записи музыки DOS-версии в четырёх наборах: `flac/` (по умолчанию), `mp3/`, `ogg/`, `ogg_MT-32/` (исполнение Roland MT-32). Имена треков во всех наборах одинаковые; какой брать — задаёт `MUSIC_FMT` | `make fetch-music`. Адрес архива скрипт читает из самого SDLPoP (`ReadMe.amigaos4`, секция «AUDIO IS SLOW/AWFULL»), запасной — popot.org | +| `MSDOS/` | **Необязательно.** Оригинальная поставка PoP 1.0 для MS-DOS. Используется только `digisnd*.dat` со звуком, и только если попросить: `make SND_SRC=msdos` | Не качается — дистрибутив игры, положить руками | + +## MSDOS: зачем он и почему без него живётся + +По умолчанию **всё** берётся из SDLPoP — и уровни, и оцифровка звука. Так +сборка работает на чистой машине сразу после `make fetch`. + +`MSDOS/` даёт ровно одно: более полный набор оцифрованных эффектов. Из 31 +звука 28 совпадают побайтно, а три — нет, и все три лучше в дистрибутиве: +`spiked` (48, насаживание на пики) в наборе SDLPoP длиной 7 сэмплов, то есть +его нет; `sword_vs_sword` (10) короче; `sword_moving` (11) — другая запись. +Разбор — `docs/sound_plan.md`. Включается это явно: + +``` +make SND_SRC=msdos resources-sound # перепаковать звук из дистрибутива +make SND_SRC=msdos # и собрать с ним +``` + +Источник входит в имя stamp'а, поэтому переключение `SND_SRC` само делает +звук устаревшим — `-B` не нужен. + +## Кто что читает + +| Упаковщик | Читает | +|---|---| +| `pop_pack_bg.py`, `render_room.py` | `SDLPoP/data/{VDUNGEON,VPALACE,LEVELS}` | +| `pop_pack_kid.py` | `SDLPoP/data/KID` | +| `pop_pack_guard.py` | `SDLPoP/data/{GUARD,SKEL,VIZIER,PRINCE}` | +| `pop_pack_shadow.py` | `SDLPoP/data/{KID,SHADOW}` | +| `pop_pack_title.py`, `pop_pack_intro.py` | `SDLPoP/data/{TITLE,PV,PRINCE,KID}` | +| `pop_extract_font.py` | `SDLPoP/src/{menu.c,seg009.c}` | +| `pop_extract_kid_data.py` | `SDLPoP/src/` (таблицы кадров и `seqtbl`) | +| `pop_pack_sound.py` | `SDLPoP/data/DIGISND*.DAT` (`--source sdlpop`, умолчание) либо `MSDOS/digisnd*.dat` (`--source msdos`) | +| `pop_pack_music.py` | `PoP1_DOS_music/<набор>/` (`--format flac\|mp3\|ogg\|mt32`) | + +Точная раскладка путей — в `tools/paths.py`; менять её нужно там, а не в +отдельных упаковщиках. Адреса внешних источников — в `tools/fetch_orig.py`. diff --git a/applications/SprPoP/tools/fetch_orig.py b/applications/SprPoP/tools/fetch_orig.py new file mode 100755 index 0000000..2f7617f --- /dev/null +++ b/applications/SprPoP/tools/fetch_orig.py @@ -0,0 +1,411 @@ +#!/usr/bin/env python3 +"""fetch_orig.py — забирает ВНЕШНИЕ исходные данные в assets/orig/. + +Репозиторий SprPoP не хранит ни байта чужих данных: `assets/orig/` целиком +в .gitignore. Хранится вместо них ЗНАНИЕ, откуда эти данные берутся, — +таблица SOURCES ниже, и этот скрипт, который по ней всё выкачивает. + + python3 tools/fetch_orig.py # SDLPoP + музыка (чего нет) + python3 tools/fetch_orig.py --check # что уже на месте, ничего не качать + python3 tools/fetch_orig.py --list # печать таблицы источников + python3 tools/fetch_orig.py sdlpop --force --ref v1.23 + +Что откуда: + + SDLPoP (GPLv3, github.com/NagyD/SDLPoP) — распакованные ресурсы DOS-версии + (`data/`: спрайты, палитры, LEVELS/res20xx.bin, DIGISND*.DAT) и исходники + (`src/`), которые нам одновременно ЭТАЛОН МЕХАНИКИ. Тянем архивом ветки + или тега, а не git-клоном: в assets/orig/ не нужен вложенный .git, а + зафиксировать версию всё равно надо — пишем её в SDLPoP/.fetch.json. + + PoP1_DOS_music (popot.org, 43 МБ) — записи саундтрека DOS-версии в четырёх + наборах (flac/mp3/ogg/ogg_MT-32), из них `pop_pack_music.py` делает треки + для CBL. Ссылку берём ИЗ САМОГО SDLPoP (ReadMe.amigaos4, секция «AUDIO IS + SLOW/AWFULL»), чтобы она не протухла молча вместе с нашей копией; если + секцию не нашли — падаем на константу MUSIC_URL_FALLBACK. + + MSDOS/ — оригинальная поставка PoP 1.0 для MS-DOS. НЕ качается: это + копирайтный дистрибутив, качать его неоткуда и незачем. Он НЕОБЯЗАТЕЛЕН — + всё, что нужно сборке (уровни, звуки), лежит в SDLPoP. Если папка у вас + есть, её звуковые .dat можно включить явно: `make SND_SRC=msdos ...` + (см. assets/orig/README.md). +""" +import argparse +import io +import json +import os +import re +import shutil +import sys +import tarfile +import time +import urllib.error +import urllib.request +import zipfile +from pathlib import Path + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import paths as P # раскладка каталогов SprPoP (tools/paths.py) + +# --- источники ------------------------------------------------------------- +SDLPOP_REPO = "https://github.com/NagyD/SDLPoP" +SDLPOP_REF = "master" # ветка по умолчанию = «последняя версия» +SDLPOP_API = "https://api.github.com/repos/NagyD/SDLPoP" + +# Секция «AUDIO IS SLOW/AWFULL» в этом файле SDLPoP держит ссылку на записи. +MUSIC_README = "ReadMe.amigaos4" +MUSIC_URL_FALLBACK = \ + "https://www.popot.org/get_the_games/various/PoP1_DOS_music.zip" + +UA = {"User-Agent": "SprPoP-fetch-orig/1"} +CACHE = P.ORIG / ".cache" # скачанные архивы; переживает make clean + +SOURCES = [ + ("sdlpop", P.SDLPOP, SDLPOP_REPO, + "ресурсы DOS-версии (data/) + исходники-эталон (src/)"), + ("music", P.MUSIC_ROOT, MUSIC_URL_FALLBACK, + "записи саундтрека DOS-версии, 4 набора (flac/mp3/ogg/ogg_MT-32)"), + ("MSDOS (необязательно, вручную)", P.MSDOS, "дистрибутив игры", + "оригинальные .dat; нужен только для SND_SRC=msdos"), +] + + +# --- мелочи ---------------------------------------------------------------- +def say(msg): + print("fetch: " + msg, flush=True) + + +def fetch_url(url, dest, expect_type=None): + """Скачать url в dest (через .part), с индикатором на одной строке. + + Если dest уже есть и его размер совпал с Content-Length — не качаем. + """ + req = urllib.request.Request(url, headers=UA) + with urllib.request.urlopen(req, timeout=60) as r: + total = int(r.headers.get("Content-Length") or 0) + ctype = r.headers.get("Content-Type", "") + if expect_type and expect_type not in ctype: + say(f"ПРЕДУПРЕЖДЕНИЕ: {url} отдал Content-Type {ctype!r}") + if dest.exists() and total and dest.stat().st_size == total: + say(f"уже скачано: {dest.name} ({total} Б)") + return dest + dest.parent.mkdir(parents=True, exist_ok=True) + tmp = dest.with_suffix(dest.suffix + ".part") + # Проценты — только в терминал: в перенаправленном выводе (лог сборки, + # CI) \r не схлопывается, и одна закачка даёт полторы сотни строк. + tty = sys.stdout.isatty() + got, t0 = 0, time.time() + with open(tmp, "wb") as f: + while True: + chunk = r.read(256 * 1024) + if not chunk: + break + f.write(chunk) + got += len(chunk) + if total and tty: + print("\r %s: %3d%% (%d/%d КБ)" + % (dest.name, got * 100 // total, + got >> 10, total >> 10), end="", flush=True) + print("%s %s: готово, %d КБ за %.1f с%s" + % ("\r" if tty else "", dest.name, got >> 10, + time.time() - t0, " " * 20 if tty else "")) + tmp.replace(dest) + return dest + + +def safe_members(names, root): + """Проверка архива на выход за пределы каталога распаковки.""" + for n in names: + p = Path(n) + if p.is_absolute() or ".." in p.parts: + sys.exit(f"архив содержит небезопасный путь {n!r} — распаковка отменена") + return root + + +def backup_dir(dest): + """Сложить текущий каталог в .cache архивом — перед заменой.""" + CACHE.mkdir(parents=True, exist_ok=True) + tar = CACHE / ("%s-backup-%s.tar.gz" % (dest.name, + time.strftime("%Y%m%d-%H%M%S"))) + with tarfile.open(tar, "w:gz") as t: + t.add(dest, arcname=dest.name) + say(f"старая копия сохранена: {tar} ({tar.stat().st_size >> 10} КБ)") + return tar + + +def swap_dir(new, dest): + """Поставить свежераспакованный каталог на место старого.""" + dest.parent.mkdir(parents=True, exist_ok=True) + old = dest.with_name(dest.name + ".old") + if old.exists(): + shutil.rmtree(old) + if dest.exists(): + dest.rename(old) + new.rename(dest) + if old.exists(): + shutil.rmtree(old) + + +# --- SDLPoP ---------------------------------------------------------------- +def sdlpop_head(ref): + """(sha, дата) верхушки ref на GitHub; (None, None) если API недоступен.""" + try: + req = urllib.request.Request(f"{SDLPOP_API}/commits/{ref}", headers=UA) + with urllib.request.urlopen(req, timeout=30) as r: + d = json.load(r) + return d["sha"], d["commit"]["committer"]["date"] + except (urllib.error.URLError, KeyError, ValueError) as e: + # Лимит запросов GitHub или отсутствие сети: не повод падать — + # просто не сможем сказать «у нас уже последняя». + say(f"GitHub API недоступен ({e}); версию не проверяю") + return None, None + + +def stamp_path(dest): + return dest / ".fetch.json" + + +def manifest(root): + """{относительный путь: sha256} по всему дереву, кроме служебного.""" + import hashlib + out = {} + for f in sorted(root.rglob("*")): + if not f.is_file() or f.name == ".fetch.json": + continue + rel = str(f.relative_to(root)) + h = hashlib.sha256() + with open(f, "rb") as fh: + for chunk in iter(lambda: fh.read(1 << 16), b""): + h.update(chunk) + out[rel] = h.hexdigest() + return out + + +def local_edits(dest): + """Файлы, изменённые ПОСЛЕ распаковки: (изменённые, добавленные). + + Считается по манифесту из .fetch.json. Нет манифеста — вернуть None: + происхождение копии неизвестно, и трогать её нельзя. + """ + base = read_stamp(dest).get("files") + if not base: + return None + now = manifest(dest) + changed = sorted(k for k, v in now.items() if k in base and base[k] != v) + added = sorted(k for k in now if k not in base) + return changed, added + + +def read_stamp(dest): + try: + return json.loads(stamp_path(dest).read_text()) + except (OSError, ValueError): + return {} + + +def fetch_sdlpop(ref=SDLPOP_REF, force=False): + dest = P.SDLPOP + sha, date = sdlpop_head(ref) + have = read_stamp(dest) + if dest.exists() and (dest / "data").is_dir(): + if sha and have.get("sha") == sha and not force: + say(f"SDLPoP уже последний: {ref} {sha[:8]} от {date[:10]}") + return + # ЗАМЕНА КАТАЛОГА — ТОЛЬКО ОСОЗНАННО. Наша копия SDLPoP не просто + # справочник: в неё внесены отладочные врезки (POP_TRACE — покадровая + # трасса Кида для сверки с портом, дампы спрайтов и палитры), и + # перекачка их сотрёт. Поэтому существующее дерево не трогаем без + # --force, а что именно потеряется — говорим заранее. + edits = local_edits(dest) + if edits is None: + note = ("происхождение копии неизвестно (нет манифеста .fetch.json) " + "— возможно, в ней есть ваши правки") + else: + changed, added = edits + if changed or added: + note = "локально изменено: %s%s" % ( + ", ".join(changed[:8]) + ("…" if len(changed) > 8 else ""), + "; добавлено %d файлов" % len(added) if added else "") + else: + note = "" + if not force: + say("SDLPoP уже на месте" + (f"; {note}" if note else "")) + say(" перекачать поверх: make FETCH_ARGS=--force fetch-sdlpop") + say(" (правки будут сохранены в бэкап, см. ниже)") + return + if note: + say("ВНИМАНИЕ: " + note) + backup_dir(dest) + # Тег и ветка лежат по разным путям codeload; пробуем оба. + urls = [f"https://codeload.github.com/NagyD/SDLPoP/tar.gz/refs/heads/{ref}", + f"https://codeload.github.com/NagyD/SDLPoP/tar.gz/refs/tags/{ref}"] + CACHE.mkdir(parents=True, exist_ok=True) + tar = CACHE / f"SDLPoP-{ref.replace('/', '_')}.tar.gz" + last = None + for u in urls: + try: + say(f"качаю SDLPoP {ref} <- {u}") + fetch_url(u, tar) + last = None + break + except urllib.error.HTTPError as e: + last = e + if last is not None: + sys.exit(f"не удалось скачать SDLPoP {ref!r}: {last}") + + tmp = P.ORIG / ".SDLPoP.new" + if tmp.exists(): + shutil.rmtree(tmp) + tmp.mkdir(parents=True) + with tarfile.open(tar, "r:gz") as t: + names = t.getnames() + safe_members(names, tmp) + # В архиве GitHub всё лежит в одном каталоге SDLPoP-/ — снимаем его. + top = os.path.commonpath([n for n in names if n]) if names else "" + t.extractall(tmp) + src = tmp / top if top else tmp + inner = P.ORIG / ".SDLPoP.unpacked" + if inner.exists(): + shutil.rmtree(inner) + src.rename(inner) + shutil.rmtree(tmp, ignore_errors=True) + + if not (inner / "data").is_dir() or not (inner / "src").is_dir(): + shutil.rmtree(inner) + sys.exit("в архиве SDLPoP нет data/ или src/ — раскладка репозитория " + "изменилась, чините fetch_orig.py") + # Манифест распакованного дерева: по нему следующий fetch отличит наши + # правки от нетронутых файлов и не сотрёт их молча. + stamp_path(inner).write_text(json.dumps( + {"repo": SDLPOP_REPO, "ref": ref, "sha": sha, "date": date, + "fetched": time.strftime("%Y-%m-%d %H:%M:%S"), + "files": manifest(inner)}, + ensure_ascii=False, indent=2) + "\n") + swap_dir(inner, dest) + say(f"SDLPoP: {ref} {(sha or '?')[:8]} -> {dest}") + + +# --- музыка ---------------------------------------------------------------- +def music_url(): + """Ссылка на архив записей: из ReadMe SDLPoP, иначе константа.""" + rm = P.SDLPOP / MUSIC_README + if rm.is_file(): + text = rm.read_text(errors="replace") + m = re.search(r"https?://\S*[Mm]usic\S*\.zip", text) + if m: + url = m.group(0).rstrip(".,)") + say(f"ссылка на музыку из SDLPoP/{MUSIC_README}: {url}") + return url + say(f"в SDLPoP/{MUSIC_README} ссылки не нашлось — беру запасную") + else: + say(f"нет SDLPoP/{MUSIC_README} (сначала `fetch_orig.py sdlpop`) — " + "беру запасную ссылку") + return MUSIC_URL_FALLBACK + + +def fetch_music(force=False): + dest = P.MUSIC_ROOT + if dest.is_dir() and not force: + have = [d for d, _ in P.MUSIC_FORMATS.values() if (dest / d).is_dir()] + if len(have) == len(P.MUSIC_FORMATS): + say(f"музыка уже на месте: {dest} ({len(have)} набора)") + return + say(f"музыка неполная (наборы: {', '.join(have) or 'нет'}) — перекачиваю") + + url = music_url() + CACHE.mkdir(parents=True, exist_ok=True) + zpath = CACHE / os.path.basename(url) + fetch_url(url, zpath, expect_type="zip") + + with zipfile.ZipFile(zpath) as z: + names = z.namelist() + safe_members(names, P.ORIG) + top = {Path(n).parts[0] for n in names if n} + if top != {dest.name}: + sys.exit(f"в архиве музыки ожидался один каталог {dest.name}/, " + f"а там {sorted(top)} — чините fetch_orig.py") + tmp = P.ORIG / ".music.new" + if tmp.exists(): + shutil.rmtree(tmp) + say(f"распаковываю {len(names)} файлов") + z.extractall(tmp) + swap_dir(tmp / dest.name, dest) + shutil.rmtree(tmp, ignore_errors=True) + got = [d for d, _ in P.MUSIC_FORMATS.values() if (dest / d).is_dir()] + say(f"музыка: наборы {', '.join(sorted(got))} -> {dest}") + + +# --- отчёт ----------------------------------------------------------------- +def do_list(): + print("Внешние источники SprPoP (assets/orig/ в репозиторий не идёт):\n") + for name, path, where, what in SOURCES: + print(f" {name}") + print(f" куда: {path}") + print(f" откуда: {where}") + print(f" что: {what}\n") + + +def do_check(): + ok = True + st = read_stamp(P.SDLPOP) + if (P.SDLPOP / "data").is_dir() and (P.SDLPOP / "src").is_dir(): + v = f"{st.get('ref', '?')} {(st.get('sha') or '?')[:8]}" + print(f" SDLPoP есть ({v}, {st.get('fetched', 'версия неизвестна')})") + else: + print(" SDLPoP НЕТ -> make fetch-sdlpop") + ok = False + have = [d for d, _ in P.MUSIC_FORMATS.values() if (P.MUSIC_ROOT / d).is_dir()] + if len(have) == len(P.MUSIC_FORMATS): + print(f" музыка есть (наборы: {', '.join(sorted(have))})") + elif have: + print(f" музыка ЧАСТЬ (только {', '.join(sorted(have))}) -> make fetch-music") + ok = False + else: + print(" музыка НЕТ -> make fetch-music") + ok = False + if P.MSDOS.is_dir(): + print(" MSDOS есть (необязательно; звуки оттуда — make SND_SRC=msdos)") + else: + print(" MSDOS нет (необязательно: звуки и уровни берутся из SDLPoP)") + return 0 if ok else 1 + + +def main(): + ap = argparse.ArgumentParser( + description="скачать внешние исходные данные SprPoP в assets/orig/") + ap.add_argument("what", nargs="*", default=["all"], + help="что качать: all (умолчание), sdlpop, music") + ap.add_argument("--force", action="store_true", + help="качать заново, даже если данные на месте") + ap.add_argument("--ref", default=SDLPOP_REF, + help=f"ветка или тег SDLPoP (по умолчанию {SDLPOP_REF})") + ap.add_argument("--check", action="store_true", + help="только показать, что уже на месте") + ap.add_argument("--list", action="store_true", + help="только показать таблицу источников") + a = ap.parse_args() + + if a.list: + do_list() + return 0 + if a.check: + return do_check() + + what = set(a.what) or {"all"} + bad = what - {"all", "sdlpop", "music"} + if bad: + ap.error("не знаю, что такое %s; есть: all, sdlpop, music" + % ", ".join(sorted(bad))) + if "all" in what: + what = {"sdlpop", "music"} + P.ORIG.mkdir(parents=True, exist_ok=True) + if "sdlpop" in what: + fetch_sdlpop(a.ref, a.force) + if "music" in what: + fetch_music(a.force) + say("готово; дальше — make resources") + return 0 + + +if __name__ == "__main__": + sys.exit(main())