FKRPG/tools/convert_pack.py
mithral 63614c9529
Some checks failed
Build & Release Pack / build (push) Failing after 1m47s
F##KRPG 1.0 — initial release
RPG modpack for Minecraft 1.21.1 (Fabric), packwiz-managed, Modrinth-sourced.

- client_pack/ (177 mods): RPG classes & combat suite (Spell Engine, Simply Swords,
  puffish skills), epic-fantasy-realism worldgen (Tectonic tuned peaks/deep oceans,
  Terralith, Wilder Wild, CTOV towns, Serene Seasons), Xaero's Minimap/World Map +
  MapLink, Distant Horizons LODs, performance stack (ChunkSmith/C2ME/Krypton).
- server_pack/ (141 mods): dedicated server set incl. BlueMap 5.7 web map (:8100),
  docker-compose deployment.
- docs/, presets/ and tools/ for validation and profile conversion.
2026-08-24 13:37:00 -04:00

214 lines
7.3 KiB
Python

#!/usr/bin/env python3
"""Convert a raw ModrinthApp profile mods/ folder into packwiz .pw.toml metadata.
All mods are Modrinth-only. For each jar:
1. read fabric.mod.json -> modid + side (client/both/server)
2. locate the Modrinth project (search by modid, fall back to filename)
3. find the exact version whose primary file matches the installed jar
4. write client_pack and/or server_pack mods/<slug>.pw.toml with correct side
Usage:
python3 tools/convert_pack.py <profile_mods_dir>
# then run packwiz refresh in each pack
"""
import glob
import hashlib
import json
import os
import re
import sys
import time
import urllib.request
import urllib.parse
import zipfile
API = "https://api.modrinth.com/v2"
def fetch(path):
req = urllib.request.Request(API + path, headers={"User-Agent": "F##KRPG-packwiz-converter/0.1"})
with urllib.request.urlopen(req, timeout=30) as r:
return json.loads(r.read())
def read_fabric_mod_json(jar):
with zipfile.ZipFile(jar) as z:
name = "fabric.mod.json"
if name not in z.namelist():
cand = [n for n in z.namelist() if n.endswith("fabric.mod.json")]
if not cand:
return None
name = cand[0]
raw = z.read(name)
# tolerate control characters in JSON (some mods emit them)
cleaned = re.sub(r"[\x00-\x08\x0b\x0c\x0e-\x1f]", " ", raw.decode("utf-8", "replace"))
return json.loads(cleaned)
def detect_side(d):
env = d.get("environment", "*")
if isinstance(env, dict):
client = env.get("client", "*")
server = env.get("server", "*")
# client=* && server=* -> both; client=* only-> both; else client-only heuristic
if client == "*" and server == "*":
return "both"
if client == "*":
return "both"
if client == "client":
return "client"
if server == "server" and client in (None, "client"):
return "server"
return "both"
return {"client": "client", "*": "both", "server": "server"}.get(env, "both")
def find_project(modid, filename):
# Gather candidate hits from a couple of search terms, then verify by
# matching the installed jar filename against each candidate's versions.
terms = [modid]
if filename and filename != modid:
stem = os.path.splitext(filename)[0]
stem = re.sub(r"[-_](fabric|neoforge|forge|mc1?\.?\d.*|v?\d[\w.\-+]*)$", "", stem, flags=re.I)
stem = re.sub(r"[+@].*$", "", stem)
stem = re.sub(r"^\[Fabric\]", "", stem).strip()
terms.append(stem)
candidates = []
seen = set()
for term in terms:
if not term:
continue
try:
d = fetch("/search?query=%s&limit=10" % urllib.parse.quote(term))
except Exception:
continue
for h in d.get("hits", []):
pid = h["project_id"]
if pid in seen:
continue
seen.add(pid)
candidates.append((pid, h["slug"]))
if not candidates:
return None, None
# 1) exact slug == modid
for pid, slug in candidates:
if slug.lower() == modid.lower():
return pid, slug
# 2) any candidate whose versions contain the exact installed filename
if filename:
for pid, slug in candidates:
try:
vers = fetch("/project/%s/version" % pid)
except Exception:
continue
for v in vers:
for f in v.get("files", []):
if f.get("filename") == filename:
return pid, slug
# 3) fall back to first candidate
return candidates[0][0], candidates[0][1]
def find_version(project_id, filename, side):
try:
versions = fetch("/project/%s/version" % project_id)
except Exception as e:
return None, None, str(e)
# candidate versions supporting 1.21.1 + fabric, newest first (API returns newest first)
cands = []
for v in versions:
gv = v.get("game_versions", [])
ld = v.get("loaders", [])
if "1.21.1" not in gv and "1.21" not in gv:
continue
if "fabric" not in ld and "quilt" not in ld:
continue
for f in v.get("files", []):
if f.get("filename") == filename:
return v["id"], f, None # exact installed match wins
cands.append(v)
# otherwise pick the latest compatible version's primary file
for v in cands:
for f in v.get("files", []):
if f.get("primary") or f.get("filename", "").lower().endswith(".jar"):
return v["id"], f, None
return None, None, "no matching 1.21.1 fabric version found"
def make_pw_toml(modid, slug, version_id, file_info, side):
sha512 = file_info["hashes"].get("sha512", "")
if not sha512:
return None
url = file_info["url"]
filename = file_info["filename"]
lines = [
'name = "%s"' % modid.title(),
'filename = "%s"' % filename,
'side = "%s"' % side,
"",
"[download]",
'url = "%s"' % url,
'hash-format = "sha512"',
'hash = "%s"' % sha512,
"",
"[update]",
"[update.modrinth]",
'mod-id = "%s"' % slug,
'version = "%s"' % version_id,
"",
]
return "\n".join(lines)
def main():
src = sys.argv[1]
base = os.path.dirname(os.path.abspath(__file__))
repo = os.path.dirname(base)
client_mods = os.path.join(repo, "client_pack", "mods")
server_mods = os.path.join(repo, "server_pack", "mods")
os.makedirs(client_mods, exist_ok=True)
os.makedirs(server_mods, exist_ok=True)
failures = []
counts = {"client": 0, "both": 0, "server": 0}
files = sorted(glob.glob(os.path.join(src, "*.jar")))
print("Converting %d mods..." % len(files))
for jar in files:
fn = os.path.basename(jar)
try:
d = read_fabric_mod_json(jar)
except Exception as e:
failures.append((fn, "read-error: %s" % e))
continue
if not d:
failures.append((fn, "no fabric.mod.json"))
continue
modid = d.get("id", fn)
side = detect_side(d)
project_id, slug = find_project(modid, fn)
if not project_id:
failures.append((fn, "modrinth project not found (modid=%s)" % modid))
continue
version_id, file_info, err = find_version(project_id, fn, side)
if not file_info:
failures.append((fn, "version not found (slug=%s): %s" % (slug, err)))
continue
tom = make_pw_toml(modid, slug, version_id, file_info, side)
if not tom:
failures.append((fn, "no sha512 in matched file"))
continue
# write to appropriate pack(s)
targets = []
if side in ("client", "both"):
targets.append(client_mods)
if side in ("server", "both"):
targets.append(server_mods)
for t in targets:
with open(os.path.join(t, slug + ".pw.toml"), "w") as fh:
fh.write(tom)
counts[side] += 1
time.sleep(0.2)
print(" ok %-30s -> %s (side=%s)" % (fn[:30], slug, side))
print("\n=== SUMMARY ===")
print("counts:", counts)
print("failures:", len(failures))
for fn, err in failures:
print(" FAIL %-40s %s" % (fn, err))
if __name__ == "__main__":
main()