0.2.6-dev: the 19 local 玩法说明 pages get a lossless WebP copy in tools/local-extract (25.6 → 17.4 MB; lossy waits for the owner's comparison) and the guide preloads only the next page (host bandwidth layer 0.3); golden: unchanged

This commit is contained in:
sganggs committed 2026-10-12 16:00:08 +08:00
1 parent 8edda589f3
commit 05b0dba8b5
7 files changed
+112 -16

No files matched your search

+38 -11
View File
@@ -32,8 +32,8 @@ aklz4.py registers a decoder for it. This script pulls the art the web sources l
DXT5nm in an older bundle: AG = XY, R = 1) and metallic/gloss with the smoothness in A; the board renderer needs
an RGB normal map (Z rebuilt) and a roughness map in G (1 − smoothness; metalness B = 0), written next to the
source as <name>_rgb.png / _rough.png
- WebP copies of the board textures every player downloads with the 3D board (WEBP), written next to their PNGs;
the manifest lists the copy
- WebP copies of the board textures every player downloads with the 3D board (WEBP) and of the 19 guidebook pages
(GUIDE_WEBP_*), written next to their PNGs; the manifest lists the copy
- the official battlefield scenes (scenes.py SCENES: one prebuilt scene per 卫戍协议 battlefield) → scene/<stageId>/
scene.json + scene.bin + lightmap pages and scene/tex/ (the materials' albedo textures). The scene bundles ship
only while the season runs: they are read from the client when it has them, else from --scenes <dir> (default
@@ -43,8 +43,9 @@ Usage:
python3 -m venv .venv && .venv/bin/pip install -r tools/local-extract/requirements.txt
.venv/bin/python tools/local-extract/extract.py [--game <AB root>] [--out public/assets/local] [--only <subdir prefix>]
python3 tools/local-extract/extract.py --print-jobs (the job table as JSON; needs no dependencies)
python3 tools/local-extract/extract.py --webp (only the WebP copies, from the PNGs already extracted —
e.g. the local art copied from a release bundle; needs Pillow)
python3 tools/local-extract/extract.py --webp (only the WebP copies of the board textures and the guide
pages, from the PNGs already extracted — e.g. the local art
copied from a release bundle; needs Pillow)
.venv/bin/python tools/local-extract/extract.py --only scene [--scenes <dir of in-season bundles>]
Writes <out>/**.png|.webp|.skel|.atlas and data/local-assets.json (manifest of what was extracted). With --only, just
@@ -188,6 +189,19 @@ WEBP_OPTIONS = {
'lossless': {'lossless': True, 'quality': 100, 'method': 6, 'exact': True},
}
# The 19 玩法说明 pages (output subdir guide: 1024², opaque) get a WebP copy too — every page PNG of the group, each a
# 1.2–1.5 MB download whenever a player opens the guide (public/js/ui/guide.js shows the local page first and preloads
# the next one). Lossless by default: 25.6 MB → 17.4 MB on the CN client's pages. Lossy quality 90 would make them
# 4.2 MB; the maintainer decides on that after a side-by-side comparison (docs/history/0.2.5.md, the 0.2.6-dev note),
# and then only GUIDE_WEBP_MODE changes (`extract.py --webp` rewrites the copies). Method 4: method 6 spends about 8 s
# a page for 0.4 % less.
GUIDE_WEBP_SUB = 'guide'
GUIDE_WEBP_MODE = 'lossless'
GUIDE_WEBP_OPTIONS = {
'lossless': {'lossless': True, 'quality': 100, 'method': 4, 'exact': True},
'lossy': {'quality': 90, 'method': 4, 'alpha_quality': 100},
}
# Shader bundles loaded beside every job that exports Materials, only so that the materials' shader references
# (external CABs) resolve to a name in materials.json (e.g. Torappu/Scene/StandardDirectional); nothing is exported
# from them.
@@ -454,11 +468,22 @@ def run_derived(out_root, sub, manifest, log):
return n
def webp_targets(sub, manifest):
"""(name, mode, Pillow options) of every WebP copy output subdir `sub` gets: its WEBP rows and, for the guide, each
page of the group (an image entry: its path ends in .png, or in .webp after an earlier run)."""
out = [(name, mode, WEBP_OPTIONS[mode]) for wsub, name, mode in WEBP if wsub == sub]
if sub == GUIDE_WEBP_SUB:
out += [(name, GUIDE_WEBP_MODE, GUIDE_WEBP_OPTIONS[GUIDE_WEBP_MODE])
for name, entry in sorted(manifest.get(sub, {}).items())
if isinstance(entry, dict) and str(entry.get('path', '')).endswith(('.png', '.webp'))]
return out
def run_webp(out_root, sub, manifest, log):
"""Write the WEBP copies of output subdir `sub` from its PNGs and point their manifest entries at them."""
"""Write the WebP copies of output subdir `sub` from its PNGs and point their manifest entries at them."""
n = 0
for wsub, name, mode in WEBP:
entry = manifest.get(sub, {}).get(name) if wsub == sub else None
for name, mode, options in webp_targets(sub, manifest):
entry = manifest.get(sub, {}).get(name)
png = Path(out_root) / sub / f'{name}.png'
if not entry or not png.exists():
continue
@@ -466,7 +491,7 @@ def run_webp(out_root, sub, manifest, log):
try:
from PIL import Image
with Image.open(png) as img:
img.save(dst, 'WEBP', **WEBP_OPTIONS[mode])
img.save(dst, 'WEBP', **options)
except Exception as e: # no WebP support in this Pillow, or a broken PNG: the entry keeps the PNG
log(f' warn webp {name}: {e}')
continue
@@ -486,7 +511,7 @@ def webp_only(out_root, manifest_path, log):
if not isinstance(groups, dict):
print(f'{manifest_path} has no groups. Extract the local art first.', file=sys.stderr)
return 2
n = sum(run_webp(out_root, sub, groups, log) for sub in dict.fromkeys(s for s, _, _ in WEBP))
n = sum(run_webp(out_root, sub, groups, log) for sub in dict.fromkeys([*(s for s, _, _ in WEBP), GUIDE_WEBP_SUB]))
Path(manifest_path).write_text(json.dumps(doc, ensure_ascii=False, separators=(',', ':')), encoding='utf-8')
log(f'done: {n} WebP copies, manifest {manifest_path}')
return 0 if n else 1
@@ -846,8 +871,8 @@ def main():
'searched after --game and before .cache/official-scenes/*)')
ap.add_argument('--print-jobs', action='store_true', help='print the job table as JSON and exit')
ap.add_argument('--webp', action='store_true',
help='only write the WebP copies of the board textures from the PNGs already under --out and list '
'them in the manifest (needs Pillow, not the client)')
help='only write the WebP copies of the board textures and the guide pages from the PNGs already '
'under --out and list them in the manifest (needs Pillow, not the client)')
args = ap.parse_args()
if args.print_jobs:
@@ -857,6 +882,8 @@ def main():
webp = [{'sub': sub, 'name': name, 'mode': mode} for sub, name, mode in WEBP]
print(json.dumps({'emoteThemes': [{'themeId': t, 'dir': d} for t, d in EMOTE_THEMES], 'jobs': jobs,
'derived': derived, 'webp': webp,
'guideWebp': {'sub': GUIDE_WEBP_SUB, 'mode': GUIDE_WEBP_MODE,
'options': GUIDE_WEBP_OPTIONS[GUIDE_WEBP_MODE]},
'enemySpines': {'bundles': ENEMY_ART, 'sub': ENEMY_SPINE_SUB, 'ids': ENEMY_SPINES},
'tokenSpines': {'bundles': TOKEN_PACKS, 'sub': TOKEN_SPINE_SUB, 'ids': TOKEN_SPINES},
'scenes': {'sub': scenes.SCENE_SUB, 'tex': scenes.TEX_SUB,