102 lines
4.3 KiB
Python
102 lines
4.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Download the 卫戍协议 battlefield scene bundles of an in-season version from the official asset CDN (optional).
|
|
|
|
The scene bundles (scenes.py) ship only while the season runs: an off-season client has none, and the current
|
|
hot-update list does not name them. The CDN still serves the versions that had them; this fetches the bundles of
|
|
scenes.SCENES (scene, lighting data, lighting settings) into .cache/official-scenes/<version>/<bundle path>, where
|
|
extract.py --only scene finds them. Every file is checked against the md5 of that version's hot-update list.
|
|
|
|
Usage:
|
|
python3 tools/local-extract/fetch_scenes.py [--version 26-04-13-12-31-42_e9b560] [--platform Android] [--out <dir>]
|
|
python3 tools/local-extract/fetch_scenes.py --list (what would be downloaded; reads the hot-update list only)
|
|
|
|
VERSION is the last version of 盟约·下半 that carried all eleven scenes (the `Data:` version of the game-data commits
|
|
of that season). Needs no dependencies. Everything is (c) Hypergryph; for private, non-commercial fan use only.
|
|
"""
|
|
import argparse
|
|
import hashlib
|
|
import io
|
|
import json
|
|
import sys
|
|
import urllib.request
|
|
import zipfile
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
import scenes # noqa: E402
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
VERSION = '26-04-13-12-31-42_e9b560'
|
|
PLATFORM = 'Android'
|
|
CDN = 'https://ak.hycdn.cn/assetbundle/official/{platform}/assets/{version}/'
|
|
|
|
|
|
def dat_name(bundle):
|
|
"""File name of a bundle on the CDN: the path without its extension, '/' → '_', '#' → '__', as .dat (a zip)."""
|
|
return bundle.rsplit('.', 1)[0].replace('/', '_').replace('#', '__') + '.dat'
|
|
|
|
|
|
def wanted(names, stage_ids):
|
|
"""The bundles of `names` (a hot-update list) that belong to the scenes of `stage_ids`, in list order."""
|
|
dirs = tuple(scenes.scene_bundle(sid).rsplit('/', 1)[0] + '/' for sid in stage_ids)
|
|
return [n for n in names if n.startswith(dirs)]
|
|
|
|
|
|
def get(url):
|
|
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
|
|
with urllib.request.urlopen(req, timeout=180) as r:
|
|
return r.read()
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
ap.add_argument('--version', default=VERSION)
|
|
ap.add_argument('--platform', default=PLATFORM)
|
|
ap.add_argument('--out', help='output root (default .cache/official-scenes/<version>)')
|
|
ap.add_argument('--list', action='store_true', help='print the bundles and sizes, download nothing')
|
|
args = ap.parse_args()
|
|
base = CDN.format(platform=args.platform, version=args.version)
|
|
out = Path(args.out) if args.out else ROOT / '.cache' / 'official-scenes' / args.version
|
|
try:
|
|
infos = json.loads(get(base + 'hot_update_list.json'))['abInfos']
|
|
except Exception as e:
|
|
print(f'cannot read the hot-update list of {args.version} ({args.platform}): {e}', file=sys.stderr)
|
|
return 2
|
|
by_name = {x['name']: x for x in infos}
|
|
names = wanted(list(by_name), [sid for sid, _ in scenes.SCENES])
|
|
if not names:
|
|
print(f'version {args.version} lists no 卫戍协议 scene bundle (an off-season version?)', file=sys.stderr)
|
|
return 1
|
|
if args.list:
|
|
for n in names:
|
|
print(f'{by_name[n].get("totalSize", 0):>9} {n}')
|
|
print(f'{sum(by_name[n].get("totalSize", 0) for n in names):>9} total ({len(names)} bundles)')
|
|
return 0
|
|
bad = 0
|
|
for n in names:
|
|
dst = out / n
|
|
md5 = by_name[n].get('md5')
|
|
if dst.is_file() and hashlib.md5(dst.read_bytes()).hexdigest() == md5:
|
|
print(f'have {n}')
|
|
continue
|
|
try:
|
|
with zipfile.ZipFile(io.BytesIO(get(base + dat_name(n)))) as z:
|
|
blob = z.read(z.namelist()[0])
|
|
except Exception as e:
|
|
print(f'FAIL {n}: {e}', file=sys.stderr)
|
|
bad += 1
|
|
continue
|
|
if md5 and hashlib.md5(blob).hexdigest() != md5:
|
|
print(f'FAIL {n}: md5 mismatch', file=sys.stderr)
|
|
bad += 1
|
|
continue
|
|
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
dst.write_bytes(blob)
|
|
print(f'got {n} ({len(blob)} bytes)')
|
|
print(f'done: {len(names) - bad}/{len(names)} bundles under {out}')
|
|
return 1 if bad else 0
|
|
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main())
|