Some checks failed
Build & Release Pack / build (push) Failing after 2m22s
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.
214 lines
7.3 KiB
Python
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()
|