95 lines
5.6 KiB
Python
95 lines
5.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Independent interchange acceptance using nbtlib and official Java26.2 APIs.
|
|
|
|
Run: artifacts/compat-venv/bin/python scripts/compat_verify.py
|
|
Needs nbtlib==2.0.4 and the pinned catalog-cache JAR/JDK25 from catalog generation.
|
|
Never starts MinecraftServer or accepts an EULA. Writes only artifacts/.
|
|
"""
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import re
|
|
import sqlite3
|
|
import subprocess
|
|
import tempfile
|
|
from pathlib import Path
|
|
import nbtlib
|
|
|
|
ROOT=Path(__file__).resolve().parents[1]
|
|
parser=argparse.ArgumentParser()
|
|
parser.add_argument("--java-bin",type=Path,default=Path.home()/".local/share/PrismLauncher/java/java-runtime-epsilon/bin")
|
|
args=parser.parse_args()
|
|
output=Path(tempfile.mkdtemp(prefix="compat-verification-",dir=ROOT/"artifacts"))
|
|
def command(argv,**kwargs):
|
|
cwd=kwargs.pop("cwd",ROOT)
|
|
result=subprocess.run(list(map(str,argv)),cwd=cwd,text=True,capture_output=True,timeout=120,**kwargs)
|
|
if result.returncode:
|
|
raise RuntimeError(f"Command failed: {argv}\n{result.stdout}\n{result.stderr}")
|
|
return result.stdout
|
|
command(["cargo","build","-p","shacraft-compat","-p","shacraft-tools"])
|
|
compat=ROOT/"target/debug/shacraft-compat";tools=ROOT/"target/debug/shacraft-tools"
|
|
def convert(*argv):return json.loads(command([compat,*argv]))
|
|
def tool(store,request):
|
|
result=json.loads(command([tools,"--data",store,"session"],input=json.dumps(request)+"\n"))
|
|
assert result["ok"],result
|
|
return result["result"]
|
|
|
|
source=ROOT/"crates/shacraft-compat/fixtures/java26_2-world"
|
|
store=output/"imported"
|
|
import_report=convert("import-anvil",source,store)
|
|
exact=output/"exact"
|
|
convert("export-anvil",store,exact)
|
|
for f in source.rglob("*"):
|
|
if f.is_file():assert f.read_bytes()==(exact/f.relative_to(source)).read_bytes()
|
|
stone=tool(store,{"op":"register","state":"minecraft:stone"})
|
|
revision=tool(store,{"op":"revision","world":"main"})
|
|
tool(store,{"op":"edit","world":"main","expected_revision":revision,"operation_id":"independent-edit",
|
|
"changes":[{"pos":[-1,-1,31],"block":0},{"pos":[512,64,-513],"block":stone}]})
|
|
with sqlite3.connect(store/"server.sqlite3") as db:
|
|
meta=json.loads(db.execute("select json from metadata where id=1").fetchone()[0]); entity=next(iter(meta["entities"].values()))
|
|
entity["position"]=[513.5,65.,-513.5];entity["yaw"]=1.25;entity["properties"]["Invisible"]=True
|
|
meta["revision"]+=1;db.execute("update metadata set json=? where id=1",(json.dumps(meta),))
|
|
edited=output/"edited";edited_report=convert("export-anvil",store,edited,"--mode","best-effort")
|
|
native=output/"native"
|
|
tool(native,{"op":"create","name":"own","template":None})
|
|
id=tool(native,{"op":"register","state":"minecraft:stone"})
|
|
tool(native,{"op":"edit","world":"own","expected_revision":0,"operation_id":"own-build", "changes":[{"pos":[-1,63,0],"block":id},{"pos":[0,64,0],"block":id}]})
|
|
new=output/"new-world";new_report=convert("export-anvil",native,new,"--world","own","--mode","best-effort")
|
|
schem_store=output/"schem-store";schem_source=ROOT/"crates/shacraft-compat/fixtures/sponge-v3.schem"
|
|
convert("import-schem",schem_source,schem_store)
|
|
revision=tool(schem_store,{"op":"revision","world":"main"})
|
|
tool(schem_store,{"op":"edit","world":"main","expected_revision":revision,"operation_id":"break-chest", "changes":[{"pos":[-17,-1,31],"block":0}]})
|
|
schem=output/"schem";convert("export-schem",schem_store,schem,"--mode","best-effort")
|
|
a=nbtlib.load(schem_source)["Schematic"];b=nbtlib.load(schem/"world.schem")["Schematic"]
|
|
def typed(tag):
|
|
if isinstance(tag,nbtlib.Compound):return ("Compound",tuple((k,typed(v)) for k,v in sorted(tag.items())))
|
|
if isinstance(tag,nbtlib.List):return ("List",tag.subtype.__name__,tuple(typed(v) for v in tag))
|
|
if hasattr(tag,"dtype"):return (type(tag).__name__,tuple(int(v) for v in tag))
|
|
return (type(tag).__name__,str(tag))
|
|
for key in ["Metadata","Biomes","Entities","Offset","UnknownRoot"]:
|
|
assert typed(a[key])==typed(b[key]),key
|
|
assert not b["Blocks"]["BlockEntities"]
|
|
assert isinstance(b["Metadata"]["Long"],nbtlib.Long)
|
|
assert isinstance(b["Metadata"]["EmptyLongList"],nbtlib.List[nbtlib.Long])
|
|
|
|
cache=ROOT/"artifacts/catalog-cache"
|
|
jar=cache/"server.jar"
|
|
assert hashlib.sha1(jar.read_bytes()).hexdigest()=="823e2250d24b3ddac457a60c92a6a941943fcd6a"
|
|
cp=":".join(map(str,[cache/"versions/26.2/server-26.2.jar",*cache.glob("libraries/**/*.jar")]))
|
|
classes=output/"java-classes";classes.mkdir()
|
|
command([args.java_bin/"javac","-cp",cp,"-d",classes,ROOT/"scripts/compat_java.java"])
|
|
java_results=[]
|
|
for label,world in [("exact",exact),("edited",edited),("new",new)]:
|
|
text=command([args.java_bin/"java","-Xmx768m","-cp",str(classes)+":"+cp,"compat_java","validate",world],cwd=output)
|
|
(output/f"java-{label}.log").write_text(text)
|
|
line=next(line[line.index('{'):] for line in text.splitlines() if '"java26_2_verified"' in line)
|
|
result=json.loads(line);result["case"]=label;java_results.append(result)
|
|
assert [r["nonair_blocks"] for r in java_results]==[3,3,2]
|
|
summary={"success":True,"directory":str(output),"java":java_results,"nbtlib_version":nbtlib.__version__,
|
|
"source_jar_sha1":hashlib.sha1(jar.read_bytes()).hexdigest(),"source_jar_sha256":hashlib.sha256(jar.read_bytes()).hexdigest(),
|
|
"nbtlib_sponge_typed_roundtrip":True,"exact_source_files_identical":True,
|
|
"edited_world":edited_report,"native_world":new_report,
|
|
"limitations":"Java checks official disk/container/palette/world-metadata codecs, not running server, client renderer, light simulation or EULA acceptance."}
|
|
(output/"verification.json").write_text(json.dumps(summary,ensure_ascii=False,indent=2)+"\n")
|
|
print(json.dumps({"success":True,"report":str(output/"verification.json"),"java":java_results},indent=2))
|