Build Sanctuary / build (push) Canceled after 0s
Add the 47 supplied Wild World insects with calendar-aware habitats, animated sprites, a 16x16 net, silent captures and natural positional audio. Ground hoppers now travel in arcs and flee through rapid consecutive hops. Include the isolated noon/clear review map and full texture export tool. Consolidate the previously validated beta.242-244 zipline/grappling and player-head/chunk-synchronized map changes retained in this checkout. Validation: check build assemblePack assembleTestPack succeeded; all 16 required insect GameTests and the native Vulkan client passed. Both beta.250 MRpacks have verified versions and embedded JARs. Runtime worlds, generated JARs and build artifacts remain ignored.
213 lines
11 KiB
Python
213 lines
11 KiB
Python
#!/usr/bin/env python3
|
||
"""Export editable static textures from the exact Sanctuary client launcher."""
|
||
import argparse
|
||
import csv
|
||
import hashlib
|
||
import json
|
||
import os
|
||
from pathlib import Path, PurePosixPath
|
||
import re
|
||
import shutil
|
||
import struct
|
||
import tempfile
|
||
import zipfile
|
||
|
||
ROOT = Path(__file__).resolve().parents[1]
|
||
IMAGE_SUFFIXES = ('.png', '.jpg', '.jpeg', '.tga', '.webp')
|
||
|
||
|
||
def image(path):
|
||
return path.lower().endswith(IMAGE_SUFFIXES)
|
||
|
||
|
||
def texture(path):
|
||
return image(path) or (path.endswith('.mcmeta') and image(path[:-7]))
|
||
|
||
|
||
def safe(path):
|
||
parts = PurePosixPath(path).parts
|
||
if not parts or PurePosixPath(path).is_absolute() or '..' in parts:
|
||
raise ValueError('Invalid asset path: ' + path)
|
||
return path
|
||
|
||
|
||
def sha(data):
|
||
return hashlib.sha256(data).hexdigest()
|
||
|
||
|
||
def export(launcher, output):
|
||
values = [json.loads(line) for line in launcher.read_text().splitlines()]
|
||
classpath = list(dict.fromkeys(values[values.index('-classpath') + 1].split(os.pathsep)))
|
||
version = re.search(r'^mod_version=(.+)$', (ROOT / 'gradle.properties').read_text(), re.M)[1]
|
||
output.parent.mkdir(parents=True, exist_ok=True)
|
||
if output.exists():
|
||
raise SystemExit('Preserving existing texture folder; choose a new output path: ' + str(output))
|
||
entries, effective, sources = [], {}, []
|
||
game = None
|
||
with tempfile.TemporaryDirectory(prefix='sanctuary-textures-', dir=output.parent) as temporary:
|
||
staging = Path(temporary)
|
||
work = staging / 'a-modifier'
|
||
|
||
def collect(source, origin, files, priority):
|
||
nonlocal entries
|
||
identifier = re.sub(r'[^a-zA-Z0-9_.-]+', '-', source)
|
||
count = 0
|
||
for path, raw in files:
|
||
if not path.startswith('assets/') or not texture(path):
|
||
continue
|
||
safe(path)
|
||
copied = staging / 'sources' / identifier / path
|
||
copied.parent.mkdir(parents=True, exist_ok=True)
|
||
copied.write_bytes(raw)
|
||
width = height = None
|
||
if raw.startswith(b'\x89PNG\r\n\x1a\n'):
|
||
width, height = struct.unpack('>II', raw[16:24])
|
||
row = dict(path=path, source=identifier, origin=origin,
|
||
width=width, height=height, bytes=len(raw), sha256=sha(raw),
|
||
source_copy=str(copied.relative_to(staging)))
|
||
entries.append(row)
|
||
if path not in effective or priority >= effective[path][0]:
|
||
effective[path] = (priority, row, raw)
|
||
count += image(path)
|
||
if count:
|
||
sources.append(dict(id=identifier, origin=origin, images=count))
|
||
|
||
for name in classpath:
|
||
path = Path(name)
|
||
if path.is_dir():
|
||
assets = path / 'assets'
|
||
if assets.is_dir():
|
||
collect('classpath-' + '-'.join(path.parts[-5:]), str(path),
|
||
((str(p.relative_to(path)), p.read_bytes()) for p in sorted(assets.rglob('*')) if p.is_file() and texture(p.name)), 20)
|
||
continue
|
||
if path.suffix != '.jar':
|
||
continue
|
||
with zipfile.ZipFile(path) as archive:
|
||
names = archive.namelist()
|
||
native = 'version.json' in names and 'assets/minecraft/textures/block/stone.png' in names
|
||
if native:
|
||
game = json.loads(archive.read('version.json'))
|
||
source = 'minecraft-' + game['id']
|
||
priority = 0
|
||
elif 'fabric.mod.json' in names:
|
||
mod = json.loads(archive.read('fabric.mod.json'))
|
||
source = 'mod-' + mod['id'] + '-' + mod['version']
|
||
priority = 20
|
||
else:
|
||
source = path.stem
|
||
priority = 20
|
||
collect(source, str(path), ((n, archive.read(n)) for n in sorted(names) if n.startswith('assets/') and texture(n)), priority)
|
||
builtin = 'resourcepacks/textures/'
|
||
if any(n.startswith(builtin + 'assets/') for n in names):
|
||
collect('pack-sanctuary-textures', str(path) + '!/' + builtin,
|
||
((n[len(builtin):], archive.read(n)) for n in sorted(names) if n.startswith(builtin + 'assets/') and texture(n)), 30)
|
||
if any(s['id'] == re.sub(r'[^a-zA-Z0-9_.-]+', '-', source) for s in sources):
|
||
for n in names:
|
||
if '/' not in n and n.upper().startswith(('LICENSE', 'NOTICE', 'THIRD_PARTY', 'PROVENANCE')):
|
||
target = staging / 'credits' / re.sub(r'[^a-zA-Z0-9_.-]+', '-', source) / n
|
||
target.parent.mkdir(parents=True, exist_ok=True)
|
||
target.write_bytes(archive.read(n))
|
||
if game is None:
|
||
raise SystemExit('The launcher must include its exact Minecraft client JAR.')
|
||
config_arg = next(v for v in values if v.startswith('-Dfabric.dli.config='))
|
||
config = Path(config_arg.split('=', 1)[1]).read_text().splitlines()
|
||
asset_directory = Path(config[config.index('\t--assetsDir') + 1].strip())
|
||
asset_index = config[config.index('\t--assetIndex') + 1].strip()
|
||
index = json.loads((asset_directory / 'indexes' / (asset_index + '.json')).read_text())['objects']
|
||
assets = []
|
||
icons = 0
|
||
for name, info in sorted(index.items()):
|
||
if not texture(name):
|
||
continue
|
||
original = asset_directory / 'objects' / info['hash'][:2] / info['hash']
|
||
raw = original.read_bytes()
|
||
if hashlib.sha1(raw).hexdigest() != info['hash']:
|
||
raise ValueError('Asset-index hash mismatch: ' + name)
|
||
if name.startswith('icons/'):
|
||
target = staging / 'icones-du-client' / safe(name)
|
||
target.parent.mkdir(parents=True, exist_ok=True)
|
||
target.write_bytes(raw)
|
||
icons += 1
|
||
else:
|
||
assets.append(('assets/' + name, raw))
|
||
collect('minecraft-asset-index-' + asset_index, str(asset_directory / 'indexes' / (asset_index + '.json')), assets, 10)
|
||
selected = []
|
||
for path, (_, row, raw) in sorted(effective.items()):
|
||
target = work / path
|
||
target.parent.mkdir(parents=True, exist_ok=True)
|
||
target.write_bytes(raw)
|
||
row['selected'] = True
|
||
if image(path):
|
||
selected.append(row)
|
||
for row in entries:
|
||
row.setdefault('selected', False)
|
||
pack_format = [game['pack_version']['resource_major'], game['pack_version']['resource_minor']]
|
||
(work / 'pack.mcmeta').write_text(json.dumps({'pack': {'description': 'Sanctuary — textures à remplacer / editable textures — ' + version, 'min_format': pack_format, 'max_format': pack_format}}, ensure_ascii=False, indent=2) + '\n')
|
||
report = dict(version=version, minecraft=game['id'], resource_format=pack_format,
|
||
effective_images=len(selected), source_images=sum(s['images'] for s in sources),
|
||
metadata_files=sum(path.endswith('.mcmeta') for path in effective),
|
||
client_icons=icons, sources=sources, entries=entries)
|
||
(staging / 'inventaire.json').write_text(json.dumps(report, ensure_ascii=False, indent=2) + '\n')
|
||
with (staging / 'inventaire.csv').open('w', encoding='utf-8-sig', newline='') as stream:
|
||
columns = ['path', 'source', 'width', 'height', 'bytes', 'sha256', 'source_copy', 'origin']
|
||
writer = csv.DictWriter(stream, fieldnames=columns, extrasaction='ignore')
|
||
writer.writeheader()
|
||
writer.writerows(selected)
|
||
(staging / 'README.md').write_text(f'''# Textures Sanctuary {version}
|
||
|
||
{len(selected)} images statiques du jeu, {report['metadata_files']} fichiers de métadonnées
|
||
et {icons} icônes du client. Minecraft {game['id']}, format de ressources
|
||
{pack_format[0]}.{pack_format[1]}. Export local depuis le client exact et le pack
|
||
« Textures Sanctuary » actif. Aucun fichier du jeu installé n’a été modifié.
|
||
|
||
- `a-modifier/assets/` : textures à dessiner, classées par namespace puis par type.
|
||
Ce sont les versions retenues après les remplacements du pack Sanctuary.
|
||
- `sources/` : chaque origine séparément, y compris les versions remplacées.
|
||
- `inventaire.csv` : chemins, dimensions, origine et empreintes des images visibles.
|
||
- `inventaire.json` : inventaire complet, variantes et métadonnées comprises.
|
||
- `credits/` : notices fournies dans les distributions des mods.
|
||
- `icones-du-client/` : icônes de fenêtre et du lanceur, hors resource pack.
|
||
|
||
Les 47 insectes sont dans `a-modifier/assets/sanctuary/textures/insect/`.
|
||
Le filet est dans `a-modifier/assets/sanctuary/textures/item/insect_net.png`.
|
||
Les blocs, objets, entités et interfaces Sanctuary sont dans les autres
|
||
sous-dossiers de `assets/sanctuary/`. Les textures natives Minecraft sont
|
||
sous `assets/minecraft/`, celles de JEI sous `assets/jei/`.
|
||
|
||
Garder les noms, chemins, transparence et dimensions d’origine. Conserver les
|
||
fichiers `.mcmeta` associés aux sprites animés et aux interfaces découpées.
|
||
Pour tester les dessins, copier `a-modifier` dans les packs de ressources
|
||
*d’une instance choisie*, l’activer au-dessus de Textures Sanctuary et recharger
|
||
avec F3 + T. Le dossier est prêt à être chargé en tant que resource pack.
|
||
Les modèles et règles de jeu restent fournis par les mods.
|
||
|
||
Les sources sont mixtes : textures Minecraft/Mojang, textures d’insectes
|
||
Animal Crossing/Nintendo et ressources Sanctuary d’origines diverses.
|
||
Les notices et chemins de l’inventaire servent de référence pour le remplacement ;
|
||
ils ne transfèrent pas la propriété des images. Les skins de joueurs,
|
||
cartes et textures générées à l’exécution ne sont pas des fichiers statiques
|
||
et ne sont pas inclus. Les archives historiques non chargées ne sont pas utilisées.
|
||
|
||
English: edit images in `a-modifier/assets/` without renaming their paths.
|
||
Keep dimensions, transparency and matching `.mcmeta` files. `sources/` retains
|
||
original variants, and CSV/JSON inventories record exact source paths and hashes.
|
||
The editable folder is a resource pack for Minecraft {game['id']}; enable it above
|
||
Sanctuary textures in your chosen test instance, then reload with F3 + T.
|
||
''', encoding='utf-8')
|
||
shutil.copy2(ROOT / 'THIRD_PARTY_NOTICES.md', staging / 'credits' / 'Sanctuary-THIRD_PARTY_NOTICES.md')
|
||
# Validate every copied source and selected image before exposing the folder.
|
||
for row in entries:
|
||
assert sha((staging / row['source_copy']).read_bytes()) == row['sha256']
|
||
for row in selected:
|
||
assert sha((work / row['path']).read_bytes()) == row['sha256']
|
||
staging.rename(output)
|
||
print(json.dumps(dict(output=str(output), images=len(selected), source_images=report['source_images'], metadata=report['metadata_files'], icons=icons), ensure_ascii=False))
|
||
|
||
|
||
if __name__ == '__main__':
|
||
parser = argparse.ArgumentParser(description=__doc__)
|
||
parser.add_argument('--launcher', type=Path, default=ROOT / 'build/duo/client.args')
|
||
parser.add_argument('--output', type=Path, required=True)
|
||
args = parser.parse_args()
|
||
export(args.launcher.resolve(), args.output.expanduser().resolve())
|