#!/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)