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:
claudeandClaude Opus 5.5 committed 2026-11-09 16:26:30 +01:00
1 parent 0741c84077
commit 4083791944
133 files changed
+8350 -3273

No files matched your search

+103
View File
@@ -0,0 +1,103 @@
"""The build's pinned tools (tools/build-lock.toml, tools/requirements.txt) and the environment it runs in.
Each tool is downloaded once into build/.cache and checked against its hash, so what a build runs depends on the
repo, not on what the machine has installed. COBBLESURVIVAL_CACHE moves the cache (tools/check_reproducible.py shares
one between its builds: everything in it is checked by hash, so it can't change their result).
"""
import hashlib
import os
import platform
import shutil
import subprocess
import sys
import tarfile
import tomllib
import urllib.request
import venv
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
LOCK = tomllib.loads((ROOT / "tools/build-lock.toml").read_text())
CACHE = Path(os.environ.get("COBBLESURVIVAL_CACHE", ROOT / "build/.cache"))
# Nothing the build writes may depend on the machine's clock zone, language or Python's per-run hash seed.
BUILD_ENV = {"TZ": "UTC", "LC_ALL": "C.UTF-8", "PYTHONHASHSEED": "0", "PYTHONUTF8": "1"}
def digest(path, algorithm="sha256"):
return hashlib.new(algorithm, Path(path).read_bytes()).hexdigest()
def fetch(url, dest, expected, algorithm="sha256"):
"""[dest], downloaded from [url] if it isn't there yet; exits if the file is not the one pinned."""
if dest.exists() and digest(dest, algorithm) == expected:
return dest
dest.parent.mkdir(parents=True, exist_ok=True)
part = dest.with_name(dest.name + ".part")
request = urllib.request.Request(url, headers={"User-Agent": "cobblemon-survival-pack build"})
with urllib.request.urlopen(request, timeout=300) as response, open(part, "wb") as out:
shutil.copyfileobj(response, out)
if digest(part, algorithm) != expected:
part.unlink()
sys.exit(f"{url} does not match the pinned {algorithm} {expected}")
part.replace(dest)
return dest
def download(name):
"""A file listed under [downloads] in tools/build-lock.toml."""
entry = LOCK["downloads"][name]
return fetch(entry["url"], CACHE / name, entry["sha256"])
def mod_jar(slug):
"""A mod's jar as pack/mods/<slug>.pw.toml pins it, for the tools that read a mod's own data."""
meta = tomllib.loads((ROOT / f"pack/mods/{slug}.pw.toml").read_text())
file = meta["download"]
return fetch(file["url"], CACHE / "mods" / meta["filename"], file["hash"], file["hash-format"])
def jdk():
"""The pinned JDK's home, the only one Gradle gets to compile with."""
pin = LOCK["jdk"]
here = f"{platform.system().lower()}-{platform.machine()}"
if here != pin["platform"]:
sys.exit(f"tools/build-lock.toml pins a JDK for {pin['platform']}, this machine is {here}")
home = CACHE / f"jdk-{pin['version']}"
if not (home / "bin/javac").exists():
archive = fetch(pin["url"], CACHE / f"jdk-{pin['version']}.tar.gz", pin["sha256"])
unpacking = home.with_name(home.name + ".part")
shutil.rmtree(unpacking, ignore_errors=True)
unpacking.mkdir(parents=True)
with tarfile.open(archive) as tar:
tar.extractall(unpacking, filter="tar")
shutil.rmtree(home, ignore_errors=True)
next(unpacking.iterdir()).rename(home)
unpacking.rmdir()
return home
def packwiz():
"""The pinned packwiz, built from its pinned commit with the pinned Go."""
pin = LOCK["packwiz"]
binary = CACHE / f"packwiz-{pin['version']}" / "packwiz"
if not binary.exists():
if not shutil.which("go"):
sys.exit("building the pinned packwiz needs Go (https://go.dev/dl/)")
env = os.environ | {"GOBIN": str(binary.parent), "GOTOOLCHAIN": pin["go"], "GOFLAGS": "-trimpath"}
subprocess.run(["go", "install", f"{pin['module']}@{pin['version']}"], env=env, check=True)
return binary
def enter_venv():
"""Re-runs the calling script in a private venv with exactly tools/requirements.txt and the build's environment."""
requirements = ROOT / "tools/requirements.txt"
home = CACHE / f"venv-{digest(requirements)[:12]}"
if Path(sys.prefix) == home and all(os.environ.get(k) == v for k, v in BUILD_ENV.items()):
return
if f"{sys.version_info[0]}.{sys.version_info[1]}" != LOCK["python"]:
print(f"warning: tools/build-lock.toml pins Python {LOCK['python']}, this is {sys.version.split()[0]}", file=sys.stderr)
python = home / "bin/python"
if not python.exists():
venv.create(home, with_pip=True)
subprocess.run([python, "-m", "pip", "install", "--quiet", "--require-hashes", "-r", requirements], check=True)
os.execve(python, [str(python), sys.argv[0], *sys.argv[1:]], os.environ | BUILD_ENV)