Phase 9: reproducible builds, balance from the server, spark check, play feedback
Build: every tool pinned (JDK, packwiz, Gradle plugins, Python packages), exports normalised, tools/check_reproducible.py proves two clean builds are byte-identical. Server-side balance: a server can load config/cobblesurvival/datapack.zip in place of the jar's data, /reload applies it, tools/server_update.py packages it and refuses when a client change is needed. JEI drop pages are sent by the server. Code-held balance numbers moved to tuning/server.json. Performance: spark in the pack (server only), tools/perf_test.py with fake players whose chunks really are active, docs/performance.md. Adrian's feedback: default keys R, U, J, §; drop pages link to Cobblemon Info and show real chances; bosses only hurt in battles; battle XP halved; channels between the islands low level; temperature ×3 with insulation doubled; hazards start at once; no item dots on the map; a wild attack starts a battle; the starter only faints; quest book on join with a basics page and an HMs page; regional forms obtainable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0741c84077
commit
4083791944
133 files changed
+8350
-3273
No files matched your search
+63
-16
@@ -6,6 +6,11 @@ Outputs, all under build/:
|
||||
instance and the server smoke test install this
|
||||
<slug>-<ver>.mrpack Modrinth pack
|
||||
<slug>-<ver>-curseforge.zip CurseForge pack
|
||||
SHA256SUMS what the build produced
|
||||
|
||||
Reproducible: the same commit gives the same bytes, whatever the machine has
|
||||
installed and wherever the repo is. Every tool is pinned (tools/lock.py), and
|
||||
tools/check_reproducible.py proves it with two builds from clean copies.
|
||||
|
||||
Each platform export only lists mods that platform hosts. A mod hosted on just
|
||||
one platform is left out of the other export and reported; it is never copied
|
||||
@@ -15,6 +20,7 @@ companion mod is our own work, so it is the one jar allowed in overrides/.
|
||||
Usage: tools/build.py [--skip-mod]
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
@@ -23,6 +29,8 @@ import tomllib
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
import lock
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
PACK = ROOT / "pack"
|
||||
BUILD = ROOT / "build"
|
||||
@@ -36,28 +44,37 @@ def run(cmd, cwd):
|
||||
|
||||
|
||||
def packwiz(*args, cwd):
|
||||
run([shutil.which("packwiz") or "packwiz", *args, "-y"], cwd)
|
||||
run([lock.packwiz(), *args, "-y"], cwd)
|
||||
|
||||
|
||||
def java_home():
|
||||
"""Gradle 8.14 cannot run on Java 25, so prefer an explicit Java 21."""
|
||||
for candidate in (os.environ.get("JAVA21_HOME"), os.environ.get("JAVA_HOME"),
|
||||
Path.home() / ".var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/java/java-runtime-delta"):
|
||||
if candidate and (Path(candidate) / "bin" / "java").exists():
|
||||
return str(candidate)
|
||||
return None
|
||||
def gradle_properties():
|
||||
return dict(line.split("=", 1) for line in (ROOT / "mod/gradle.properties").read_text().splitlines()
|
||||
if "=" in line and not line.startswith("#"))
|
||||
|
||||
|
||||
def check_versions():
|
||||
"""mod/gradle.properties compiles against the versions the pack installs."""
|
||||
props = gradle_properties()
|
||||
if props["neoforge_version"] != tomllib.loads((PACK / "pack.toml").read_text())["versions"]["neoforge"]:
|
||||
sys.exit("mod/gradle.properties and pack/pack.toml name different NeoForge versions")
|
||||
for slug, prop in (("cobblemon", "cobblemon_version"), ("kotlin-for-forge", "kotlinforforge_version"), ("jei", "jei_version")):
|
||||
filename = tomllib.loads((PACK / f"mods/{slug}.pw.toml").read_text())["filename"]
|
||||
if props[prop] not in filename:
|
||||
sys.exit(f"mod/gradle.properties says {prop}={props[prop]}, the pack installs {filename}")
|
||||
|
||||
|
||||
def build_mod():
|
||||
env = dict(os.environ)
|
||||
if home := java_home():
|
||||
env["JAVA_HOME"] = home
|
||||
subprocess.run(["./gradlew", "build", "--console=plain"], cwd=ROOT / "mod", env=env, check=True)
|
||||
"""From clean, with only the pinned JDK to compile against (an incremental build can differ from a full one)."""
|
||||
jdk = lock.jdk()
|
||||
subprocess.run(["./gradlew", "clean", "build", "--console=plain", "--no-build-cache",
|
||||
f"-Porg.gradle.java.installations.paths={jdk}",
|
||||
"-Porg.gradle.java.installations.auto-detect=false",
|
||||
"-Porg.gradle.java.installations.auto-download=false"],
|
||||
cwd=ROOT / "mod", env=os.environ | {"JAVA_HOME": str(jdk)}, check=True)
|
||||
|
||||
|
||||
def companion_jar():
|
||||
props = dict(line.split("=", 1) for line in (ROOT / "mod/gradle.properties").read_text().splitlines()
|
||||
if "=" in line and not line.startswith("#"))
|
||||
props = gradle_properties()
|
||||
jar = ROOT / "mod/build/libs" / f"{props['mod_id']}-{props['mod_version']}.jar"
|
||||
if not jar.exists():
|
||||
sys.exit(f"companion jar not found: {jar} (run without --skip-mod)")
|
||||
@@ -105,16 +122,44 @@ def check_default_shader():
|
||||
sys.exit(f"config/defaultoptions/extra/config/iris.properties names {named}, the pack has {files}")
|
||||
|
||||
|
||||
def normalise_export(archive):
|
||||
"""packwiz writes an export's entries, the manifest's file list and the mod list in a random order: sort them."""
|
||||
with zipfile.ZipFile(archive) as z:
|
||||
files = {i.filename: z.read(i.filename) for i in z.infolist() if not i.is_dir()}
|
||||
if "manifest.json" in files:
|
||||
manifest = json.loads(files["manifest.json"])
|
||||
manifest["files"].sort(key=lambda f: (f["projectID"], f["fileID"]))
|
||||
files["manifest.json"] = (json.dumps(manifest, indent=2, ensure_ascii=False) + "\n").encode()
|
||||
if "modlist.html" in files:
|
||||
lines = files["modlist.html"].decode().splitlines()
|
||||
items = sorted(line for line in lines if line.lstrip().startswith("<li>"))
|
||||
files["modlist.html"] = "\n".join(["<ul>", *items, "</ul>", ""]).encode()
|
||||
with zipfile.ZipFile(archive, "w", zipfile.ZIP_DEFLATED) as z:
|
||||
for name in sorted(files):
|
||||
info = zipfile.ZipInfo(name, (1980, 1, 1, 0, 0, 0))
|
||||
info.compress_type = zipfile.ZIP_DEFLATED
|
||||
info.external_attr = 0o644 << 16
|
||||
z.writestr(info, files[name])
|
||||
|
||||
|
||||
def write_checksums(paths):
|
||||
(BUILD / "SHA256SUMS").write_text("".join(f"{lock.digest(p)} {p.relative_to(ROOT).as_posix()}\n" for p in sorted(paths)))
|
||||
|
||||
|
||||
def main():
|
||||
lock.enter_venv()
|
||||
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
parser.add_argument("--skip-mod", action="store_true", help="reuse the last companion mod build")
|
||||
args = parser.parse_args()
|
||||
|
||||
check_versions()
|
||||
check_default_shader()
|
||||
# Written from the pack's data, so these go first: the drop pages ship in the data pack the mod embeds.
|
||||
run([sys.executable, str(ROOT / "tools/jei_sources.py")], ROOT)
|
||||
run([sys.executable, str(ROOT / "tools/quests.py")], ROOT) # the quest book (Phase 8) from docs/quests.yaml
|
||||
if not args.skip_mod:
|
||||
build_mod()
|
||||
jar = companion_jar()
|
||||
check_default_shader()
|
||||
run([sys.executable, str(ROOT / "tools/quests.py")], ROOT) # the quest book (Phase 8) from docs/quests.yaml
|
||||
packwiz("refresh", cwd=PACK)
|
||||
pack = tomllib.loads((PACK / "pack.toml").read_text())
|
||||
base = f"{pack['name'].lower().replace(' ', '-')}-{pack['version']}"
|
||||
@@ -127,9 +172,11 @@ def main():
|
||||
out = BUILD / f"{base}{suffix}"
|
||||
out.unlink(missing_ok=True)
|
||||
packwiz(command, "export", "-o", str(out), cwd=dest)
|
||||
normalise_export(out)
|
||||
check_overrides(out, jar)
|
||||
report[out.name] = left_out
|
||||
|
||||
write_checksums([jar, BUILD / "stage/full/pack.toml", BUILD / "stage/full/index.toml", *(BUILD / name for name in report)])
|
||||
print("\nExports (no third-party jars in overrides):")
|
||||
for name, left_out in report.items():
|
||||
print(f" build/{name}")
|
||||
|
||||
Reference in new issue
Block a user