97 lines
6.9 KiB
Python
97 lines
6.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Reproduce the Java 26.2 factual catalog without starting a Minecraft server.
|
|
|
|
Uses a pinned official Mojang JAR and public runtime APIs. JAR/runtime/data reports
|
|
stay in the ignored artifacts cache; no Minecraft textures, sounds, models, or
|
|
source code are copied. Requires Python 3 and an existing Java 25+ JDK.
|
|
"""
|
|
from pathlib import Path
|
|
import argparse, datetime, gzip, hashlib, json, os, shutil, subprocess, urllib.request, zipfile
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
JAR_URL = 'https://piston-data.mojang.com/v1/objects/823e2250d24b3ddac457a60c92a6a941943fcd6a/server.jar'
|
|
JAR_SHA1 = '823e2250d24b3ddac457a60c92a6a941943fcd6a'
|
|
JAR_SIZE = 60894273
|
|
|
|
def canonical(name, props):
|
|
return name + ('[' + ','.join(f'{k}={v}' for k,v in sorted(props.items())) + ']' if props else '')
|
|
|
|
def digest(path, algorithm='sha256'):
|
|
h = hashlib.new(algorithm)
|
|
with open(path,'rb') as f:
|
|
for part in iter(lambda:f.read(1024*1024), b''): h.update(part)
|
|
return h.hexdigest()
|
|
|
|
def write_gzip(path, obj):
|
|
raw = json.dumps(obj,sort_keys=True,separators=(',',':'),ensure_ascii=True).encode()
|
|
path.write_bytes(gzip.compress(raw,mtime=0))
|
|
|
|
def generate(cache, java, reuse):
|
|
cache.mkdir(parents=True,exist_ok=True)
|
|
jar = cache/'server.jar'
|
|
if not jar.exists():
|
|
part=cache/'server.jar.part'
|
|
urllib.request.urlretrieve(JAR_URL, part)
|
|
if part.stat().st_size!=JAR_SIZE or digest(part,'sha1')!=JAR_SHA1:
|
|
raise ValueError('Downloaded artifact does not match pinned Mojang source')
|
|
part.replace(jar)
|
|
if jar.stat().st_size!=JAR_SIZE or digest(jar,'sha1')!=JAR_SHA1:
|
|
raise ValueError('Local artifact does not match pinned Mojang source')
|
|
java = Path(java).resolve()
|
|
java_version=subprocess.run([str(java),'-version'],capture_output=True,text=True,check=True).stderr.strip()
|
|
with zipfile.ZipFile(jar) as z: version=json.loads(z.read('version.json'))
|
|
if version['id']!='26.2' or version['world_version']!=4903: raise ValueError('Unexpected version')
|
|
reports=cache/'generated/reports'
|
|
if not reuse or not (reports/'blocks.json').exists():
|
|
with open(cache/'reports.log','w') as log:
|
|
subprocess.run([str(java),'-Xmx1G','-DbundlerMainClass=net.minecraft.data.Main','-jar',str(jar),'--reports','--output',str(cache/'generated')],cwd=cache,stdout=log,stderr=subprocess.STDOUT,check=True)
|
|
cp=os.pathsep.join(str(x) for x in [cache/'versions/26.2/server-26.2.jar',*sorted((cache/'libraries').rglob('*.jar'))])
|
|
if not reuse or not (cache/'measurements.json').exists():
|
|
subprocess.run([str(java.with_name('javac')),'-cp',cp,'-d',str(cache),str(ROOT/'scripts/catalog_extract.java')],check=True)
|
|
with open(cache/'measurements.log','w') as log:
|
|
subprocess.run([str(java),'-Xmx1G','-cp',str(cache)+os.pathsep+cp,'catalog_extract',str(cache/'measurements.json')],cwd=cache,stdout=log,stderr=subprocess.STDOUT,check=True)
|
|
report=json.loads((reports/'blocks.json').read_text())
|
|
registries=json.loads((reports/'registries.json').read_text())
|
|
measured=json.loads((cache/'measurements.json').read_text())
|
|
if measured['errors']: raise ValueError('Collision measurement errors; refusing incomplete catalog')
|
|
block_defs=[]; states=[]; shapes=[]; shape_ids={}; report_identities={}
|
|
for name,b in sorted(report.items()):
|
|
default=next(s for s in b['states'] if s.get('default'))
|
|
index=len(block_defs)
|
|
block_defs.append({'name':name,'default_state':canonical(name,default.get('properties',{})),
|
|
'properties':b.get('properties',{}),'family':b['definition']['type'].removeprefix('minecraft:')})
|
|
report_identities[name]={'properties':b.get('properties',{}),'states':b['states']}
|
|
for s in b['states']:
|
|
m=measured['states'][str(s['id'])]
|
|
boxes=m['collision']; shape_key=json.dumps(boxes,separators=(',',':'))
|
|
if shape_key not in shape_ids:
|
|
shape_ids[shape_key]=len(shapes);shapes.append(boxes)
|
|
states.append({'state':canonical(name,s.get('properties',{})),'minecraft_id':s['id'],'block':index,
|
|
'shape':shape_ids[shape_key],'context':m['sample_context_dependent'],'light':m['light'],'hardness':m['hardness']})
|
|
states.sort(key=lambda x:x['minecraft_id'])
|
|
entity_ids=sorted(registries['minecraft:entity_type']['entries'])
|
|
if entity_ids!=sorted(measured['entities']):raise ValueError('Entity measurements do not match official report')
|
|
if set(s['minecraft_id'] for s in states)!=set(map(int,measured['states'])):raise ValueError('State measurements do not match official report')
|
|
compact={'version':'26.2','data_version':4903,'blocks':block_defs,'states':states,'shapes':shapes,'entities':measured['entities']}
|
|
out=ROOT/'crates/shacraft-content/data';out.mkdir(parents=True,exist_ok=True)
|
|
write_gzip(out/'catalog.json.gz',compact)
|
|
write_gzip(out/'report-identities.json.gz',{'blocks':report_identities,'entities':entity_ids})
|
|
provenance={'source':{'url':JAR_URL,'size':JAR_SIZE,'sha1':JAR_SHA1,'sha256':digest(jar)},
|
|
'version':version,'java':java_version,'extracted_utc':datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
|
'command':'python3 scripts/catalog_generate.py --java /path/to/java25/bin/java',
|
|
'data_command':'java -Xmx1G -DbundlerMainClass=net.minecraft.data.Main -jar server.jar --reports --output generated',
|
|
'collision_context':'All states at BlockPos.ZERO, with air neighbors and CollisionContext.empty(); compare solid neighbors and positionContext(+10/-10). Entity-dependent/block-entity contexts remain approximate.',
|
|
'counts':{'blocks':len(block_defs),'states':len(states),'entities':len(entity_ids),'distinct_collision_shapes':len(shapes),'collision_errors':measured['errors'],'context_sample_differences':measured['context_dependent_samples']},
|
|
'inputs':{p.name:digest(p) for p in [reports/'blocks.json',reports/'registries.json',cache/'measurements.json',ROOT/'scripts/catalog_extract.java']},
|
|
'outputs':{p.name:digest(p) for p in [out/'catalog.json.gz',out/'report-identities.json.gz']},
|
|
'license_note':'Factual interoperability data derived through public APIs. No Minecraft visual/audio assets, server binaries or decompiled code are redistributed. Authored runtime and render templates: MIT OR Apache-2.0.'}
|
|
(out/'provenance.json').write_text(json.dumps(provenance,indent=2)+'\n')
|
|
print(json.dumps(provenance['counts'],indent=2))
|
|
|
|
if __name__=='__main__':
|
|
ap=argparse.ArgumentParser(description=__doc__)
|
|
ap.add_argument('--cache',type=Path,default=ROOT/'artifacts/catalog-cache')
|
|
ap.add_argument('--java',default=shutil.which('java'))
|
|
ap.add_argument('--reuse-reports',action='store_true',help='Skip Java tools only when verified local reports/measurements already exist')
|
|
args=ap.parse_args();generate(args.cache.resolve(),args.java,args.reuse_reports)
|