#!/usr/bin/env python3 """Regenerate server/mods.json from the Prism instance's mods directory. For every .jar in the pack instance it: - computes sha1 - looks up the Modrinth download URL via the version_file API (aero-shadow-fix is CurseForge-only, pinned manually) - drops the 19 client-only mods and writes the server manifest to mods.json. Usage: python3 build_mods.py """ import hashlib, json, os, subprocess, sys, time INSTANCE = os.path.expanduser( "~/.local/share/PrismLauncher/instances/Reaper's Sky - dev/.minecraft/mods") OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "mods.json") MC_VERSION = "1.21.1" NEOFORGE_VERSION = "21.1.248" CLIENT_ONLY = { "iris-neoforge-1.8.14-beta.1+mc1.21.1.jar", "EuphoriaPatcher-1.9.3-r5.8.1-neoforge.jar", "iris-flywheel-compat-NeoForge-2.3.1.jar", "sodium-neoforge-0.8.12+mc1.21.1.jar", "ImmediatelyFast-NeoForge-1.6.12+1.21.1.jar", "entityculling-neoforge-1.10.5-mc1.21.1.jar", "dynamic-fps-3.11.4+mc1.21.0-neoforge.jar", "MouseTweaks-neoforge-mc1.21-2.26.1.jar", "appleskin-neoforge-mc1.21-3.0.9.jar", "Controlling-neoforge-1.21.1-19.0.5.jar", "Searchables-neoforge-1.21.1-1.0.2.jar", "skinlayers3d-neoforge-1.11.2-mc1.21.1.jar", "xaerominimap-neoforge-1.21.1-26.4.2.jar", "xaeroworldmap-neoforge-1.21.1-1.44.2.jar", "Jade-1.21.1-NeoForge-15.10.6.jar", "jade-sable-compat-1.3.0.jar", "aero-shadow-fix.jar", "aeronautics-propeller-blur-1.0a.jar", "jei-1.21.1-neoforge-19.44.0.403.jar", } CURSEFORGE_ONLY = { "aero-shadow-fix.jar": "https://www.curseforge.com/api/v1/mods/1641969/files/8589529/download", "arssophisticatedcompat-0.3.0.jar": "https://www.curseforge.com/api/v1/mods/1653477/files/8653384/download", } UA = "opencode-pack-builder/1.0" def sha1(path): h = hashlib.sha1() with open(path, "rb") as f: for chunk in iter(lambda: f.read(65536), b""): h.update(chunk) return h.hexdigest() def resolve(jar, digest): if jar in CURSEFORGE_ONLY: return CURSEFORGE_ONLY[jar] r = subprocess.run( ["curl", "-s", "--max-time", "20", "-H", f"User-Agent: {UA}", f"https://api.modrinth.com/v2/version_file/{digest}?algorithm=sha1"], capture_output=True, text=True) try: return json.loads(r.stdout)["files"][0]["url"] except Exception: return "" def main(): mods = [] for jar in sorted(os.listdir(INSTANCE)): if not jar.endswith(".jar"): continue if jar in CLIENT_ONLY: print(f" skip (client-only): {jar}") continue digest = sha1(os.path.join(INSTANCE, jar)) url = resolve(jar, digest) if not url: print(f" !! no URL for {jar} -- fix manually", file=sys.stderr) mods.append({"file": jar, "url": url, "sha1": digest}) time.sleep(0.1) manifest = {"minecraft": MC_VERSION, "neoforge": NEOFORGE_VERSION, "mods": mods} with open(OUT, "w") as f: json.dump(manifest, f, indent=2) print(f"Wrote {len(mods)} server mods to {OUT}") if __name__ == "__main__": main()