SprPoP: автономность — внешние данные качаются, а не хранятся
В репозитории нет ни байта чужих данных, но есть знание, откуда их взять: tools/fetch_orig.py качает SDLPoP (ресурсы + исходники-эталон) и записи саундтрека, причём адрес архива музыки читает из самого SDLPoP (ReadMe.amigaos4, секция «AUDIO IS SLOW/AWFULL»); запасной адрес вшит константой. Цели: make fetch / fetch-sdlpop / fetch-music / fetch-check / fetch-list. Версия SDLPoP пишется в .fetch.json вместе с манифестом sha256 всего дерева. По нему следующий fetch отличает НАШИ отладочные врезки (POP_TRACE — покадровая трасса Кида, дампы палитры и спрайтов) от нетронутых файлов и не сносит их молча: без --force каталог не трогается вовсе, с --force старая копия уезжает в бэкап .cache/. MSDOS/ не качается и НЕ НУЖЕН: уровни и оцифровка берутся из SDLPoP. У SprPoP теперь свой .gitignore, написанный так, чтобы стать корневым при выделении в отдельный репозиторий (пути от корня приложения, ничего про applications/). Из корневого .gitignore тулчейна SprPoP-секция убрана, чтобы две копии не разъезжались. Заодно закрылась дыра: шаблон applications/*/*/*.exe не покрывал артефакты в корне SprPoP. assets/orig/README.md выведен из-под игнора — без него в чистом клоне не написано, откуда брать данные, а это и есть смысл затеи. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011MsUsEFAQfsjjQpJ7RtKVY
This commit is contained in:
+6
-12
@@ -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/ (уровни из оригинала как есть).
|
||||
|
||||
@@ -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/
|
||||
@@ -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` — <https://github.com/NagyD/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`.
|
||||
Executable
+411
@@ -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-<ref>/ — снимаем его.
|
||||
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())
|
||||
Reference in New Issue
Block a user