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:
commit
398301f0be
232 files changed
+72665
No files matched your search
Executable
+142
@@ -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()
|
||||
Reference in new issue
Block a user