Files
cobblemon-survival-pack/tools/check_deps.py
T
claudeandClaude Opus 5.5 398301f0be Cobblemon Survival Pack: phases 0-2 and Phase 3 zones and hazards
Free NeoForge 1.21.1 modpack on Cobblemon 1.8.4 built with packwiz, plus
the Kotlin companion mod (cobblesurvival) and its built-in data pack.

- Phase 0: packwiz pack with Modrinth/CurseForge cross-links, build and
  export tooling, Prism instance sync, test server with smoke checks.
- Phase 1: Poke Ball shaped island (WorldGen Editor preset, seed isle-26),
  Adrian's biome list, no natural ore, structures without ore or vanilla
  mobs, no Nether portals, world template builder and world report.
- Phase 2: permadeath (CobbleHardcoreMon plus the companion mod's gap
  fixes), Revival Charm, expensive Healing Machine, Lootr refreshing
  loot, in-game self-tests on Minecraft's GameTest framework.
- Phase 3 so far: 14 zones with their own spawn pools and level bands
  (tools/zone_layout.py, tools/zone_spawns.py, "zone" spawn condition),
  Cold Sweat heat and cold as gates with boss-drop insulation recipes,
  spawn survey and hazard self-tests in tools/quick_check.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 19:01:21 +01:00

120 lines
4.9 KiB
Python

#!/usr/bin/env python3
"""Static dependency check for a folder of NeoForge mod jars.
Reads META-INF/neoforge.mods.toml from every jar (and from jar-in-jar files
under META-INF/jarjar/), then checks that each required dependency is present
and inside its declared version range. Needs no server and no EULA, so it runs
anywhere; the server smoke test is still the real proof.
Version ranges use Maven syntax ("[1.8.4,1.9)", "[21.1,)", "1.0" = at least).
Build metadata after "+" is ignored, so Cobblemon "1.8.4+1.21.1" compares as
1.8.4; qualifiers follow Maven (21-beta < 21 < 21.1.265). A mismatch on the `minecraft` dependency is only a warning: every JEI
build for 1.21.1 declares "[1.21, 1.21.1)" and NeoForge 21.1 still loads it
(checked on the test server, 2026-10-28), so this check is stricter than the
loader there.
Usage: tools/check_deps.py <mods_dir> [--neoforge VER] [--minecraft VER]
"""
import argparse
import io
import re
import sys
import tomllib
import zipfile
from pathlib import Path
PLATFORM = {"neoforge", "minecraft", "javafml", "fml", "kotlinforforge_loader"}
QUALIFIERS = {"alpha": 1, "a": 1, "beta": 2, "b": 2, "milestone": 3, "m": 3, "rc": 4, "cr": 4, "snapshot": 5}
RELEASE = (0, 10, "") # pads the shorter version: a release sorts above its own alpha/beta/rc
def key(version):
"""Maven-like ordering: numbers sort above qualifiers, so 21.1.265 > 21-beta > 21-alpha."""
parts = re.split(r"[.\-]", version.split("+", 1)[0])
return [(1, int(p), "") if p.isdigit() else (0, QUALIFIERS.get(p.lower(), 6), p.lower()) for p in parts if p]
def compare(a, b):
ka, kb = key(a), key(b)
n = max(len(ka), len(kb))
ka, kb = ka + [RELEASE] * (n - len(ka)), kb + [RELEASE] * (n - len(kb))
return (ka > kb) - (ka < kb)
def in_range(version, spec):
spec = spec.strip()
if spec in ("", "*"):
return True
if spec[0] not in "[(":
return compare(version, spec) >= 0
for part in re.findall(r"[\[(][^\])]*[\])]", spec):
lo_inc, hi_inc = part[0] == "[", part[-1] == "]"
bounds = part[1:-1].split(",")
if len(bounds) == 1:
if compare(version, bounds[0]) == 0:
return True
continue
lo, hi = (b.strip() for b in bounds)
if lo and (compare(version, lo) < 0 or (compare(version, lo) == 0 and not lo_inc)):
continue
if hi and (compare(version, hi) > 0 or (compare(version, hi) == 0 and not hi_inc)):
continue
return True
return False
def read_mods(data, name, found, deps):
z = zipfile.ZipFile(io.BytesIO(data))
names = set(z.namelist())
if "META-INF/neoforge.mods.toml" in names:
toml = tomllib.loads(z.read("META-INF/neoforge.mods.toml").decode("utf-8", "replace"))
manifest = z.read("META-INF/MANIFEST.MF").decode("utf-8", "replace") if "META-INF/MANIFEST.MF" in names else ""
jar_version = (re.search(r"Implementation-Version: *(\S+)", manifest) or [None, "0"])[1]
for mod in toml.get("mods", []):
version = str(mod.get("version", "0")).replace("${file.jarVersion}", jar_version)
found[mod["modId"]] = (version, name)
for mod_id, entries in toml.get("dependencies", {}).items():
for dep in entries:
required = dep.get("type", "required" if dep.get("mandatory", True) else "optional") == "required"
if required:
deps.append((mod_id, dep["modId"], dep.get("versionRange", "*"), name))
for inner in (n for n in names if n.startswith("META-INF/jarjar/") and n.endswith(".jar")):
read_mods(z.read(inner), f"{name}!{inner.rsplit('/', 1)[-1]}", found, deps)
def main():
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("mods_dir", type=Path)
parser.add_argument("--neoforge")
parser.add_argument("--minecraft", default="1.21.1")
args = parser.parse_args()
found, deps = {}, []
for jar in sorted(args.mods_dir.glob("*.jar")):
read_mods(jar.read_bytes(), jar.name, found, deps)
if args.neoforge:
found["neoforge"] = (args.neoforge, "loader")
found["minecraft"] = (args.minecraft, "game")
problems, warnings = [], []
for owner, dep, spec, jar in deps:
if dep not in found:
if dep not in PLATFORM:
problems.append(f"{owner} ({jar}) needs {dep} {spec}: not installed")
elif not in_range(found[dep][0], spec):
(warnings if dep == "minecraft" else problems).append(
f"{owner} ({jar}) needs {dep} {spec}: found {found[dep][0]}")
print(f"{len(found)} mods, {len(deps)} required dependencies checked")
for w in warnings:
print(f" warning: {w}")
if problems:
print("\n".join(f" - {p}" for p in problems))
sys.exit(1)
print("all required dependencies satisfied")
if __name__ == "__main__":
main()