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 и есть версируемая ветка разработки).
|
# не идёт (сама roomtest и есть версируемая ветка разработки).
|
||||||
applications/PoP/R1/
|
applications/PoP/R1/
|
||||||
|
|
||||||
# SprPoP — автономное приложение. В репозиторий идут исходники, генерируемые
|
# SprPoP — автономное приложение, и правила игнора у него СВОИ:
|
||||||
# заголовки (gen/) и упакованные ресурсы (assets/packed/), потому что без них
|
# applications/SprPoP/.gitignore. Он написан так, чтобы стать корневым
|
||||||
# из чистого клона ничего не собрать. НЕ идут:
|
# .gitignore, когда SprPoP выделят в отдельный репозиторий, — поэтому
|
||||||
# assets/orig/ оригинальные данные игры — копирайт (SDLPoP, MSDOS,
|
# здесь его содержимое НЕ дублируется (иначе разъедется). Коротко: в
|
||||||
# записи музыки DOS-версии); откуда их взять,
|
# репозиторий не идут assets/orig/ (чужие данные — их выкачивает
|
||||||
# написано в assets/orig/README.md
|
# `make fetch`) и assets/packed/LEVELS/ (уровни из оригинала как есть).
|
||||||
# assets/packed/LEVELS/ уровни res20xx.bin ложатся на диск прямо из
|
|
||||||
# оригинала, конвертации нет — то есть это тот же
|
|
||||||
# копирайтный материал, только в раскладке диска
|
|
||||||
# build/ уже покрыт общим правилом build/ выше
|
|
||||||
applications/SprPoP/assets/orig/
|
|
||||||
applications/SprPoP/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