Complete local voxel MVP with server, client, MCP, packages and world interchange
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
#!/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)
|
||||
Reference in New Issue
Block a user