#!/usr/bin/env python3 """Materialise a 1.12 asset tree out of a hash-addressed asset store. Minecraft 1.12.2 does not read an index plus an object store. It reads ``/minecraft/`` directly. A launcher such as AstralRinth keeps assets the modern way, as ``indexes/1.12.json`` pointing at ``objects//``, and a launcher that wants them for an old client is responsible for laying them out. Passing the store root as ``--assetsDir`` does not fail loudly. Texts and textures still appear, because those live inside the client jar, so the game looks correct while every sound is silently absent: nothing requested is ever found on disk. Usage: materialize_assets.py [--index 1.12] Hard links are used where the filesystem allows it, so the materialised tree costs almost nothing and repeated runs are cheap. """ import argparse import json import os import sys def human(count): return "{:,}".format(count).replace(",", " ") def main(argv): ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) ap.add_argument("meta_assets", help="store root holding indexes/ and objects/") ap.add_argument("out", help="assets dir to create for the client") ap.add_argument("--index", default="1.12", help="index file name (default: 1.12)") args = ap.parse_args(argv) meta = args.meta_assets index_path = os.path.join(meta, "indexes", args.index + ".json") if not os.path.isfile(index_path): sys.exit("no asset index at " + index_path) with open(index_path, "r", encoding="utf-8") as fh: objects = json.load(fh)["objects"] out = args.out linked = copied = reused = 0 missing = [] for name, info in sorted(objects.items()): digest = info["hash"] src = os.path.join(meta, "objects", digest[:2], digest) dst = os.path.join(out, *name.split("/")) if os.path.isfile(src) and os.path.getsize(src) == info["size"]: if os.path.exists(dst) and os.path.getsize(dst) == info["size"]: reused += 1 continue os.makedirs(os.path.dirname(dst), exist_ok=True) if os.path.exists(dst): os.remove(dst) try: os.link(src, dst) linked += 1 except OSError: with open(src, "rb") as r, open(dst, "wb") as w: w.write(r.read()) copied += 1 else: missing.append(name) sounds = sum(1 for n in objects if n.endswith(".ogg")) print("index : {} ({} objects, {} ogg)".format( os.path.join("indexes", args.index + ".json"), human(len(objects)), human(sounds))) print("output : {}".format(out)) print("hardlink: {} copy: {} reused: {}".format( human(linked), human(copied), human(reused))) if missing: print("MISSING : {} object(s) not in the store".format(human(len(missing)))) for name in missing[:10]: print(" " + name) return 1 print("ok : every indexed object is present") return 0 if __name__ == "__main__": raise SystemExit(main(sys.argv[1:]))