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>
This commit is contained in:
claudeandClaude Opus 5.5 committed 2026-10-29 19:01:21 +01:00
commit 398301f0be
232 files changed
+72665

No files matched your search

+142
View File
@@ -0,0 +1,142 @@
#!/usr/bin/env python3
"""Give every packwiz mod both a Modrinth and a CurseForge source.
packwiz only records the platform a mod was added from. Exporting to the other
platform then copies the jar into overrides/, which would bundle a third-party
jar. This script finds the same file on the other platform and verifies it by
SHA-1 before linking it:
* Modrinth -> CurseForge: CurseForge project found via cfwidget (slug from
crosslink.json or the Modrinth slug), file matched by filename, then both
files are downloaded and compared: same SHA-1, or (for jars re-built with
other timestamps) the same entry names and CRC32s.
* CurseForge -> Modrinth: exact lookup of the SHA-1 on Modrinth.
Mods with no counterpart are reported; build.py keeps them out of the export
for the platform that lacks them instead of bundling them.
Usage: tools/crosslink.py [pack_dir] (then run `packwiz refresh`)
"""
import io
import json
import sys
import tomllib
import urllib.error
import urllib.parse
import urllib.request
import zipfile
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
OVERRIDES = json.loads((ROOT / "tools" / "crosslink.json").read_text())
UA = {"User-Agent": "cobblemon-survival-pack-tools/0.1 (packwiz crosslink)"}
def get_json(url):
req = urllib.request.Request(url, headers=UA)
try:
with urllib.request.urlopen(req, timeout=60) as r:
return json.load(r)
except urllib.error.HTTPError as e:
if e.code == 404:
return None
raise
def download(url):
req = urllib.request.Request(url, headers=UA)
with urllib.request.urlopen(req, timeout=300) as r:
return r.read()
def same_file(a, b):
"""'sha1' if byte-identical, 'content' if the jars hold identical entries.
The manifest is ignored: authors who upload to each platform from a
separate build get a different Implementation-Timestamp and nothing else.
"""
if a == b:
return "sha1"
try:
entries = [sorted((i.filename, i.CRC) for i in zipfile.ZipFile(io.BytesIO(x)).infolist()
if i.filename != "META-INF/MANIFEST.MF")
for x in (a, b)]
except zipfile.BadZipFile:
return None
return "content" if entries[0] == entries[1] else None
def forgecdn_url(file_id, filename):
return "https://edge.forgecdn.net/files/{}/{}/{}".format(
file_id // 1000, file_id % 1000, urllib.parse.quote(filename))
def link_curseforge(path, meta, override):
mr = meta["update"]["modrinth"]
version = get_json(f"https://api.modrinth.com/v2/version/{mr['version']}")
project = get_json(f"https://api.modrinth.com/v2/project/{mr['mod-id']}")
file = next(f for f in version["files"] if f["filename"] == meta["filename"])
slug = override.get("curseforge_slug", project["slug"])
cf = get_json(f"https://api.cfwidget.com/minecraft/mc-mods/{slug}")
if not cf or "files" not in cf:
return f"no CurseForge project for slug '{slug}'"
candidates = [f for f in cf["files"] if f["name"] == meta["filename"]]
if not candidates:
return f"CurseForge project {cf['id']} has no file named {meta['filename']}"
mr_bytes = download(file["url"])
for cand in candidates:
match = same_file(mr_bytes, download(forgecdn_url(cand["id"], cand["name"])))
if not match and override.get("trust_same_filename") and len(candidates) == 1:
match = f"filename, trusted: {override['trust_same_filename']}"
if match:
text = path.read_text().rstrip() + (
f"\n\n[update.curseforge]\nfile-id = {cand['id']}\nproject-id = {cf['id']}\n")
path.write_text(text)
return f"linked CurseForge {cf['id']}/{cand['id']} (same {match})"
return "CurseForge file found by name but its contents differ"
def link_modrinth(path, meta):
sha1 = meta["download"]["hash"] if meta["download"]["hash-format"] == "sha1" else None
if not sha1:
return "no SHA-1 in metadata"
version = get_json(f"https://api.modrinth.com/v2/version_file/{sha1}?algorithm=sha1")
if not version:
return "not on Modrinth"
file = next(f for f in version["files"] if f["hashes"]["sha1"] == sha1)
cf = meta["update"]["curseforge"]
path.write_text(
f'name = "{meta["name"]}"\n'
f'filename = "{meta["filename"]}"\n'
f'side = "{meta.get("side", "both")}"\n\n'
f'[download]\nurl = "{file["url"]}"\nhash-format = "sha1"\nhash = "{sha1}"\n\n'
f'[update]\n[update.curseforge]\nfile-id = {cf["file-id"]}\nproject-id = {cf["project-id"]}\n\n'
f'[update.modrinth]\nmod-id = "{version["project_id"]}"\nversion = "{version["id"]}"\n')
return f"linked Modrinth {version['project_id']}/{version['id']}"
def main():
pack = Path(sys.argv[1]) if len(sys.argv) > 1 else ROOT / "pack"
missing = []
for path in sorted((pack / "mods").glob("*.pw.toml")):
meta = tomllib.loads(path.read_text())
update = meta.get("update", {})
override = OVERRIDES.get(path.name.removesuffix(".pw.toml"), {})
if "modrinth" in update and "curseforge" in update:
status = "already linked"
elif override.get("skip"):
status = f"skipped: {override['skip']}"
elif "modrinth" in update:
status = link_curseforge(path, meta, override)
else:
status = link_modrinth(path, meta)
if not status.startswith(("linked", "already")):
missing.append(path.name)
print(f"{path.name:45} {status}")
if missing:
print("\nSingle-platform mods (kept out of the other platform's export):")
print("\n".join(f" {m}" for m in missing))
if __name__ == "__main__":
main()