#!/usr/bin/env python3 """Rebuild the Create Aeronautics .mrpack from a live Prism instance. Resolves every mod jar in the instance's mods/ folder to its Modrinth version by SHA-1 hash (falling back to a manual CurseForge entry for aero-shadow-fix), then packs modrinth.index.json + overrides into a Prism-importable .mrpack. """ import hashlib, json, os, subprocess, sys, tempfile, zipfile, urllib.request USER_AGENT = "opencode-pack-builder/1.0" INSTANCE = os.path.expanduser( "~/.local/share/PrismLauncher/instances/Reaper's Sky - dev/.minecraft" ) MODS_DIR = os.path.join(INSTANCE, "mods") OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ReapersSky-1.2.2.mrpack") # (name in instance, override source path) — shipped as-is into .minecraft OVERRIDES = [ ("servers.dat", os.path.join(INSTANCE, "servers.dat")), ("options.txt", os.path.join(INSTANCE, "options.txt")), ("config/flywheel-client.toml", os.path.join(INSTANCE, "config/flywheel-client.toml")), ("config/iris.properties", os.path.join(INSTANCE, "config/iris.properties")), ("config/irisflw-client.toml", os.path.join(INSTANCE, "config/irisflw-client.toml")), ("resourcepacks/FreshAnimations_v1.10.4.zip", os.path.join(INSTANCE, "resourcepacks/FreshAnimations_v1.10.4.zip")), ("shaderpacks/ComplementaryReimagined_r5.8.1 + EuphoriaPatches_1.9.3", os.path.join(INSTANCE, "shaderpacks/ComplementaryReimagined_r5.8.1 + EuphoriaPatches_1.9.3")), ] # CurseForge-only mods: project 1641969 (aero-shadow-fix), file 8589529, and # project 1653477 (Sophisticated Backpacks: Ars Compat), file 8653384. CF_FILES = { "aero-shadow-fix.jar": { "url": "https://www.curseforge.com/api/v1/mods/1641969/files/8589529/download", }, "arssophisticatedcompat-0.3.0.jar": { "url": "https://www.curseforge.com/api/v1/mods/1653477/files/8653384/download", }, } def sha1(path): h = hashlib.sha1() with open(path, "rb") as f: for block in iter(lambda: f.read(65536), b""): h.update(block) return h.hexdigest() def sha512(path): h = hashlib.sha512() with open(path, "rb") as f: for block in iter(lambda: f.read(65536), b""): h.update(block) return h.hexdigest() def resolve(sha): url = f"https://api.modrinth.com/v2/version_file/{sha}?algorithm=sha1" req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT}) with urllib.request.urlopen(req, timeout=20) as r: d = json.load(r) return d.get("id", ""), d.get("files", [{}])[0].get("url", "") def main(): files = [] for fname in sorted(os.listdir(MODS_DIR)): if not fname.endswith(".jar"): continue path = os.path.join(MODS_DIR, fname) fsize = os.path.getsize(path) if fname in CF_FILES: url = CF_FILES[fname]["url"] else: vid, url = resolve(sha1(path)) if not url: print(f"!! could not resolve {fname} — add to CF_FILES and rerun") sys.exit(1) files.append({ "path": "mods/" + fname, "hashes": {"sha1": sha1(path), "sha512": sha512(path)}, "env": {"client": "required", "server": "optional"}, "downloads": [url], "fileSize": fsize, }) index = { "formatVersion": 1, "game": "minecraft", "versionId": "1.2.2", "name": "Reaper's Sky", "summary": "Airships, exploration & tech. 111 mods for Minecraft 1.21.1 / NeoForge.", "files": files, "dependencies": {"minecraft": "1.21.1", "neoforge": "21.1.248"}, } with tempfile.TemporaryDirectory() as tmp: with open(os.path.join(tmp, "modrinth.index.json"), "w") as f: json.dump(index, f, indent=2) ov = os.path.join(tmp, "overrides") for name, src in OVERRIDES: dst = os.path.join(ov, name) if os.path.isdir(src): os.makedirs(dst, exist_ok=True) for root, _, fnames in os.walk(src): for fn in fnames: sp = os.path.join(root, fn) dp = os.path.join(dst, os.path.relpath(sp, src)) os.makedirs(os.path.dirname(dp), exist_ok=True) with open(sp, "rb") as si, open(dp, "wb") as di: di.write(si.read()) else: os.makedirs(os.path.dirname(dst), exist_ok=True) with open(src, "rb") as si, open(dst, "wb") as di: di.write(si.read()) with zipfile.ZipFile(OUT, "w", zipfile.ZIP_DEFLATED) as z: z.write(os.path.join(tmp, "modrinth.index.json"), "modrinth.index.json") for root, _, fnames in os.walk(ov): for fn in fnames: sp = os.path.join(root, fn) z.write(sp, os.path.relpath(sp, tmp)) print(f"Wrote {OUT} ({len(files)} mods, " f"{os.path.getsize(OUT)//1024//1024} MB)") if __name__ == "__main__": main()