Complete local voxel MVP with server, client, MCP, packages and world interchange

This commit is contained in:
Emil
2026-09-14 17:35:17 +03:00
parent cf8d1aae2e
commit b6ba064339
98 changed files with 58116 additions and 194 deletions
+206
View File
@@ -0,0 +1,206 @@
// Fixed local release-server budgets. This is a reproducible Shacraft-only workload,
// not a comparison with Minecraft and not a production player-capacity claim.
import assert from "node:assert/strict";
import path from "node:path";
import {readFile} from "node:fs/promises";
import { harness, Client, delay, options, root } from "./server-harness.mjs";
const opts = options();
opts.binary ??= path.join(root, "target/release/shacraft-server");
const budgets = { ten_client_rss_bytes: 256 * 1024 * 1024, p95_tick_ms: 50 };
const duration = Number(opts["sample-ms"] || 15000);
assert.ok(
duration >= 3000 && duration <= 120000,
"--sample-ms must be 3000..120000",
);
const h = await harness(opts),
clients = [],
samples = [],
phases = [];
let reportWritten = false;
const percentile = (values, p) =>
[...values].sort((a, b) => a - b)[
Math.min(values.length - 1, Math.floor((values.length - 1) * p))
];
function summary(name, list) {
const rss = list.map((s) => s.rss_bytes).filter(Number.isFinite),
ticks = list.map((s) => s.tick_ms).filter(Number.isFinite);
assert.ok(
rss.length && ticks.length,
"Measured RSS and tick durations are required",
);
const last = list.at(-1);
return {
phase: name,
samples: list.length,
rss_bytes: {
p50: percentile(rss, 0.5),
p95: percentile(rss, 0.95),
max: Math.max(...rss),
},
tick_ms: {
p50: percentile(ticks, 0.5),
p95: percentile(ticks, 0.95),
max: Math.max(...ticks),
},
process_high_water_bytes: last.process_high_water_bytes,
players: last.players,
active_worlds: last.active_worlds,
world_count: last.world_count,
storage: last.storage,
slow_clients_disconnected: last.slow_clients_disconnected,
queued_client_bytes: last.queued_client_bytes,
};
}
async function sample(name, milliseconds = duration) {
const list = [],
end = Date.now() + milliseconds;
while (Date.now() < end) {
const m = await h.get("/api/metrics");
const processStatus = await readFile(`/proc/${h.pid}/status`, "utf8");
const highWater = Number(processStatus.match(/^VmHWM:\s+(\d+)/m)?.[1]) * 1024;
assert.ok(Number.isFinite(highWater), "Linux process high-water RSS required");
m.process_high_water_bytes = highWater;
const s = { phase: name, time: Date.now(), ...m };
samples.push(s);
list.push(s);
await delay(100);
}
const result = summary(name, list);
phases.push(result);
return result;
}
let mover;
async function moving(count, distinct) {
const manifest = await h.get("/api/manifest");
for (let i = 0; i < count; i++) {
const c = new Client(h.url);
clients.push(c);
await c.join(
manifest,
`test_bench_${String(distinct ? i : 0).padStart(3, "0")}`,
`Bench${i}`,
);
}
const started = Date.now();
mover = setInterval(() => {
const t = (Date.now() - started) / 1000;
for (let i = 0; i < clients.length; i++)
clients[i].input({
yaw: (t * 0.75 + (i * Math.PI) / 5) % (Math.PI * 2),
pitch: -0.1,
forward: 1,
strafe: 0,
jump: Math.floor(t) % 5 === 0,
});
}, 50);
await delay(2000);
}
async function disconnect() {
clearInterval(mover);
await Promise.all(clients.splice(0).map((c) => c.close()));
for (let n = 0; n < 50; n++) {
const m = await h.get("/api/metrics");
if (m.players === 0) return;
await delay(100);
}
throw Error("Players did not disconnect within 5 seconds");
}
try {
let created = 0;
for (const count of [1, 10, 100]) {
while (created < count) {
await h.control("world.create", {
world: `test_bench_${String(created).padStart(3, "0")}`,
template: "lobby",
});
created++;
}
await delay(1000);
const phase = await sample(`idle_${count}_template_forks`, 3000);
assert.equal(phase.players, 0);
assert.equal(phase.active_worlds, 0);
}
await moving(10, false);
await sample("10_moving_clients_shared_world");
await disconnect();
await moving(10, true);
await sample("10_moving_clients_separate_worlds");
await disconnect();
await delay(1500);
const idle = await sample("after_disconnect_idle", 3000);
assert.equal(idle.players, 0);
assert.equal(idle.active_worlds, 0);
const active = phases.filter((p) => p.phase.includes("moving_clients"));
const budgetResults = {
rss_passed: active.every(
(p) => p.rss_bytes.max <= budgets.ten_client_rss_bytes && p.process_high_water_bytes <= budgets.ten_client_rss_bytes,
),
tick_passed: active.every((p) => p.tick_ms.p95 <= budgets.p95_tick_ms),
};
const cacheSamples = samples.map((s) => {
const storage = s.storage;
return {
entries: storage.cache_entries ?? storage.cache?.entries,
capacity:
storage.cache_capacity_sections ??
storage.cache_capacity ??
storage.cache_sections ??
storage.cache?.capacity,
};
});
const recognizedCache = cacheSamples.filter(
(s) => Number.isFinite(s.entries) && Number.isFinite(s.capacity),
);
const cacheBounded =
recognizedCache.length === cacheSamples.length &&
recognizedCache.every((s) => s.entries <= s.capacity);
const passed =
budgetResults.rss_passed && budgetResults.tick_passed && cacheBounded;
await h.report({
passed,
profile: h.binary.includes("/release/")
? "release"
: "custom binary; verify optimization separately",
binary: h.binary,
budgets,
budget_results: budgetResults,
cache_bounded: cacheBounded,
workload: {
fork_counts: [1, 10, 100],
players: 10,
input_hz: 20,
sampling_interval_ms: 100,
active_sample_ms: duration,
template: "lobby",
scope:
"local seeded map; storage and authoritative movement; not full vanilla mechanics",
},
limitations: [
"No comparison with Minecraft or a modded server",
"Tick metrics measure server step work, not end-to-end latency",
"Process RSS excludes browser clients and operating-system disk cache",
"Cache entries remain bounded after disconnect; zero active worlds does not imply zero retained cache",
],
phases,
samples,
});
reportWritten = true;
assert.ok(
passed,
"Predetermined benchmark budget or bounded-cache check failed; inspect report",
);
} catch (error) {
if (!reportWritten)
await h.report({
passed: false,
budgets,
error: error.stack,
server_logs: h.logs(),
phases,
samples,
});
throw error;
} finally {
await disconnect().catch(() => {});
await h.stop();
}
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env python3
"""Deterministically author original PNG/WAV/WASM package resources using stdlib."""
from pathlib import Path
import hashlib,json,struct,zlib,math,wave
ROOT=Path(__file__).resolve().parents[1]
def png(path,w,h,pixel):
def chunk(kind,data):return struct.pack('>I',len(data))+kind+data+struct.pack('>I',zlib.crc32(kind+data)&0xffffffff)
raw=b''.join(b'\x00'+b''.join(bytes(pixel(x,y)) for x in range(w)) for y in range(h))
path.write_bytes(b'\x89PNG\r\n\x1a\n'+chunk(b'IHDR',struct.pack('>IIBBBBB',w,h,8,2,0,0,0))+chunk(b'IDAT',zlib.compress(raw,9))+chunk(b'IEND',b''))
def js(path,obj):path.write_text(json.dumps(obj,indent=2)+'\n')
def manifest(path,ident,deps,caps,resources):
js(path/'manifest.json',{'schema':1,'id':ident,'version':'1.0.0','license':'MIT OR Apache-2.0',
'dependencies':[{'id':d,'version':'1.0.0'} for d in deps], 'capabilities':caps,
'resources':[{'path':p,'scope':scope,'role':role,'size':(path/p).stat().st_size,'sha256':hashlib.sha256((path/p).read_bytes()).hexdigest()}for p,scope,role in resources]})
base=ROOT/'packages/base'; trampoline=ROOT/'packages/trampoline'
for p in [base/'client',base/'common',trampoline/'client',trampoline/'common',trampoline/'server']:p.mkdir(parents=True,exist_ok=True)
# Pixel noise tile, generated by integer hash; intentionally original, no external art.
def grain(x,y):
v=((x*374761393+y*668265263)^((x+y*17)*1274126177))&0xffffffff
value=211+((v^(v>>13))%29);return[value,value,value]
png(base/'client/texture.png',64,64,grain)
js(base/'client/style.json',{'schema':1,'texture':'client/texture.png','texture_mode':'repeat-tint','pixel_size':64,'license':'MIT OR Apache-2.0'})
js(base/'common/definitions.json',{'schema':1,'catalog':'builtin-java-26.2','data_version':4903,'coordinates':'+Y up; block models in [0,1], entity models relative feet','vanilla_assets':False})
# Authored spring mat: dark frame, mint mesh, small crossing weave.
def mat(x,y):
if x<3 or y<3 or x>=29 or y>=29:return[53,78,73]
if x%4==0 or y%4==0:return[123,217,166]
return[75,153,118]
png(trampoline/'client/texture.png',32,32,mat)
js(trampoline/'client/style.json',{'schema':1,'block':'shacraft:trampoline','color':[97,205,151],'texture':'client/texture.png','effect':'bounce','sound':'client/bounce.wav','shader':'client/tint.glsl'})
(trampoline/'client/tint.glsl').write_text('// Original optional Shacraft material function. GLSL ES 3.00.\nvec3 shacraftBounceTint(vec3 color, float pulse) {\n return color * (1.0 + 0.12 * clamp(pulse, 0.0, 1.0));\n}\n')
with wave.open(str(trampoline/'client/bounce.wav'),'wb') as wav:
rate=22050;n=int(rate*.18);wav.setnchannels(1);wav.setsampwidth(2);wav.setframerate(rate)
wav.writeframes(b''.join(struct.pack('<h',int(10000*(1-i/n)**2*math.sin(2*math.pi*(260*i/rate+450*(i/rate)**2))))for i in range(n)))
js(trampoline/'common/definitions.json',{'schema':1,'blocks':[{'state':'shacraft:trampoline','color':[97,205,151],'collision':[{'min':[0,0,0],'max':[1,1,1]}],'behavior':{'server_hook':'on_jump'}}], 'client_effect':{'on_jump':'bounce'},'capability_contract':{'hook':'on_jump','parameters':[],'result':'f32','units':'blocks/second','fuel':10000,'memory_pages':1,'imports':[]}})
(trampoline/'server/trampoline.wat').write_text(';; Original executable server extension. No imports, memory, filesystem or network.\n(module (func (export "on_jump") (result f32) f32.const 10))\n')
# Binary encoding of the tiny WAT above, independently loaded/tested by server wasmi.
wasm=b'\x00asm\x01\x00\x00\x00'+bytes([1,5,1,0x60,0,1,0x7d])+bytes([3,2,1,0])+bytes([7,11,1,7])+b'on_jump'+bytes([0,0])+bytes([10,9,1,7,0,0x43])+struct.pack('<f',10.)+bytes([0x0b])
(trampoline/'server/trampoline.wasm').write_bytes(wasm)
manifest(base,'shacraft.base',[],['blocks.define','entities.define','client.texture','client.style'],[('common/definitions.json','common','definitions'),('client/texture.png','client','texture'),('client/style.json','client','client-style')])
manifest(trampoline,'shacraft.trampoline',['shacraft.base'],['blocks.define','client.texture','client.style','client.shader','client.sound','server.on_jump'],[('common/definitions.json','common','definitions'),('client/texture.png','client','texture'),('client/style.json','client','client-style'),('client/bounce.wav','client','sound'),('client/tint.glsl','client','shader'),('server/trampoline.wasm','server','wasm-on-jump'),('server/trampoline.wat','server','wasm-source')])
print('Original package assets and checksummed manifests generated.')
+85
View File
@@ -0,0 +1,85 @@
// Copyright Shacraft contributors. MIT OR Apache-2.0.
// Public API calls only: extract factual registries and collision measurements.
// Run by catalog_generate.py; never starts a server or accepts an EULA.
import java.nio.file.*;
import java.util.*;
import com.google.gson.*;
import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
import net.minecraft.core.BlockPos;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.material.FluidState;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.ai.attributes.DefaultAttributes;
import net.minecraft.world.phys.shapes.CollisionContext;
import net.minecraft.world.phys.shapes.VoxelShape;
class catalog_extract {
static JsonArray boxes(VoxelShape shape) {
var result = new JsonArray();
for (var b : shape.toAabbs()) {
var box = new JsonArray();
for (double x : new double[]{b.minX,b.minY,b.minZ,b.maxX,b.maxY,b.maxZ}) box.add(x);
result.add(box);
}
return result;
}
static class SampleWorld implements BlockGetter {
final BlockState center, other;
SampleWorld(BlockState center, BlockState other) { this.center=center; this.other=other; }
public BlockEntity getBlockEntity(BlockPos pos) { return null; }
public BlockState getBlockState(BlockPos pos) { return pos.equals(BlockPos.ZERO)?center:other; }
public FluidState getFluidState(BlockPos pos) { return getBlockState(pos).getFluidState(); }
public int getHeight() { return 384; }
public int getMinY() { return -64; }
}
public static void main(String[] args) throws Exception {
SharedConstants.tryDetectVersion(); Bootstrap.bootStrap();
var root = new JsonObject(); var states = new JsonObject(); var entities = new JsonObject();
var air = Block.stateById(0);
var stone = BuiltInRegistries.BLOCK.getValue(net.minecraft.resources.Identifier.parse("minecraft:stone")).defaultBlockState();
int errors=0, contextual=0;
for (var block : BuiltInRegistries.BLOCK) for (var state : block.getStateDefinition().getPossibleStates()) {
var out = new JsonObject();
var world = new SampleWorld(state,air);
try {
var shape = boxes(state.getCollisionShape(world,BlockPos.ZERO,CollisionContext.empty()));
out.add("collision",shape);
boolean context = !shape.equals(boxes(state.getCollisionShape(new SampleWorld(state,stone),BlockPos.ZERO,CollisionContext.empty())))
|| !shape.equals(boxes(state.getCollisionShape(world,BlockPos.ZERO,CollisionContext.positionContext(10))))
|| !shape.equals(boxes(state.getCollisionShape(world,BlockPos.ZERO,CollisionContext.positionContext(-10))));
out.addProperty("sample_context_dependent",context); if(context)contextual++;
out.addProperty("air",state.isAir()); out.addProperty("light",state.getLightEmission());
out.addProperty("hardness",state.getDestroySpeed(world,BlockPos.ZERO));
} catch (Throwable e) { out.addProperty("error",e.toString()); errors++; }
states.add(Integer.toString(Block.getId(state)),out);
}
for(var entity : BuiltInRegistries.ENTITY_TYPE) {
var out = new JsonObject();
out.addProperty("width",entity.getWidth()); out.addProperty("height",entity.getHeight());
out.addProperty("eye_height",entity.getDimensions().eyeHeight());
out.addProperty("fixed_dimensions",entity.getDimensions().fixed());
out.addProperty("category",entity.getCategory().getName());
out.addProperty("summonable",entity.canSummon()); out.addProperty("serializable",entity.canSerialize());
out.addProperty("fire_immune",entity.fireImmune()); out.addProperty("tracking_range",entity.clientTrackingRange());
out.addProperty("update_interval",entity.updateInterval()); out.addProperty("peaceful_allowed",entity.isAllowedInPeaceful());
var attrs = new JsonObject();
if(DefaultAttributes.hasSupplier(entity)) {
var supplier = DefaultAttributes.getSupplier((EntityType)entity);
for(var holder : BuiltInRegistries.ATTRIBUTE.listElements().toList())
if(supplier.hasAttribute(holder)) attrs.addProperty(holder.unwrapKey().get().identifier().toString(),supplier.getBaseValue(holder));
}
out.add("attributes",attrs);
entities.add(EntityType.getKey(entity).toString(),out);
}
root.add("states",states); root.add("entities",entities);
root.addProperty("errors",errors); root.addProperty("context_dependent_samples",contextual);
Files.writeString(Path.of(args[0]),new GsonBuilder().create().toJson(root));
System.out.println("Extracted "+states.size()+" states, "+entities.size()+" entities, "+errors+" errors, "+contextual+" contextual sample differences");
if(errors!=0)System.exit(2);
}
}
+96
View File
@@ -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)
+54
View File
@@ -0,0 +1,54 @@
// Start an isolated server, then exercise its separate stdio MCP through the real SDK.
// --binary is the server; --mcp-binary selects the independent MCP executable.
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import path from "node:path";
import { harness, options, root } from "./server-harness.mjs";
const opts = options();
opts.port ??= "4002";
opts.binary ??= path.join(root, "target/release/shacraft-server");
const mcpBinary = path.resolve(
opts["mcp-binary"] ||
path.join(path.dirname(path.resolve(opts.binary)), "shacraft-mcp"),
);
const h = await harness(opts);
try {
const script = path.join(root, "crates/shacraft-mcp/tests/sdk-smoke.mjs");
const args = [
script,
"--binary",
mcpBinary,
"--server",
h.url,
"--data",
h.data,
];
if (opts.sdk) args.push("--sdk", opts.sdk);
const { stdout, stderr } = await promisify(execFile)(process.execPath, args, {
cwd: root,
env: process.env,
timeout: 120000,
maxBuffer: 8 * 1024 * 1024,
});
if (stderr.trim()) throw Error(`SDK smoke stderr: ${stderr.trim()}`);
const sdk = JSON.parse(stdout);
await h.report({
passed: true,
server_binary: h.binary,
mcp_binary: mcpBinary,
...sdk,
});
} catch (error) {
await h.report({
passed: false,
server_binary: h.binary,
mcp_binary: mcpBinary,
error: error.stack,
stdout: error.stdout,
stderr: error.stderr,
server_logs: h.logs(),
});
throw error;
} finally {
await h.stop();
}
+305
View File
@@ -0,0 +1,305 @@
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { harness, Client, delay, options } from "./server-harness.mjs";
import { raycast, direction, key } from "../client/math.js";
const opts = options(),
clients = [],
checks = [];
let h = await harness(opts);
const createClient = () => {
const c = new Client(h.url);
clients.push(c);
return c;
};
try {
const manifest = await h.get("/api/manifest");
assert.equal(manifest.protocol, 1);
for (const pack of manifest.packages)
for (const file of pack.files) {
assert.notEqual(file.scope, "server");
const r = await fetch(h.url + file.url);
assert.equal(r.status, 200);
const bytes = Buffer.from(await r.arrayBuffer());
assert.equal(bytes.length, file.size);
assert.equal(
createHash("sha256").update(bytes).digest("hex"),
file.sha256,
);
}
const denied = await fetch(h.url + "/api/control", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ method: "world.list" }),
});
assert.equal(denied.status, 401);
const secret = await fetch(
h.url + "/packages/shacraft.trampoline/1.0.0/server/trampoline.wasm",
);
assert.equal(secret.status, 404);
checks.push(
"Manifest resource SHA-256/size, admin token required, server WASM not public",
);
const bad = createClient();
await bad.open;
bad.send({
type: "join",
protocol: 1,
manifest_hash: "incorrect",
world: "lobby",
name: "BadManifest",
});
assert.match((await bad.next("error")).message, /manifest|package/i);
await bad.close();
checks.push("Incompatible manifest rejected");
const catalog = await h.control("catalog.search", {
query: "minecraft:stone",
limit: 64,
}),
stone = catalog.items.find((m) => m.state === "minecraft:stone");
assert.ok(stone);
await h.control("world.create", { world: "test_e2e_base" });
const plan = await h.control("build.plan", {
world: "test_e2e_base",
expected_revision: 0,
operations: [
{ type: "box", min: [-8, -1, -8], max: [8, 0, 12], block: stone.id },
],
});
await h.control("build.commit", {
plan_id: plan.plan_id,
operation_id: "test-floor",
});
for (const world of ["test_e2e_a", "test_e2e_b"])
await h.control("world.create", { world, template: "test_e2e_base" });
let a = createClient();
const b = createClient();
const wa = await a.join(manifest, "test_e2e_a", "TestAlice");
await b.join(manifest, "test_e2e_a", "TestBob");
assert.equal(wa.revision, 0);
assert.ok(wa.blocks.length > 500);
await a.state((p, m) => m.players.length === 2);
checks.push("Two clients receive shared snapshot and presence");
const initial = (await b.state((p) => p.position[1] < 1.15)).players.find(
(p) => p.id === b.id,
).position;
b.input({ forward: 1, position: [1000, 1000, 1000] });
const moved = (
await b.state((p) => p.position[2] < initial[2] - 0.5)
).players.find((p) => p.id === b.id).position;
b.input();
assert.ok(moved.every((v) => Math.abs(v) < 20));
assert.ok(moved[1] < 2);
checks.push(
"Authoritative movement and collision; client position spoof ignored",
);
a.send({ type: "break", pos: [1000, 0, 1000] });
assert.match((await a.next("error")).message, /reach|sight|ray|target/i);
checks.push("Forged out-of-reach block break rejected");
const p = (await a.state((p) => p.position[1] < 1.15)).players.find(
(p) => p.id === a.id,
).position;
const materials = new Map(wa.materials.map((m) => [m.id, m]));
const blocks = new Map(wa.blocks.map((b) => [key(b.pos), b.block]));
const hit = raycast(
[p[0], p[1] + 1.62, p[2]],
direction(0.2, -1.3),
blocks,
materials,
);
assert.ok(hit);
const seq = a.input({ yaw: 0.2, pitch: -1.3 });
await a.state((_p, m) => m.ack >= seq);
await delay(150);
a.send({ type: "break", pos: hit.pos });
const removed = await a.next("blocks", (m) =>
m.changes.some((c) => key(c.pos) === key(hit.pos) && c.block === 0),
);
await b.next("blocks", (m) => m.revision === removed.revision);
assert.equal(removed.revision, 1);
await delay(160);
a.send({ type: "place", pos: hit.pos, block: stone.id });
const restored = await a.next("blocks", (m) => m.revision === 2);
await b.next("blocks", (m) => m.revision === 2);
assert.equal(restored.changes[0].block, stone.id);
checks.push("Reachable break/place, server revision ordering and broadcast");
const late = createClient();
const lateSnapshot = await late.join(manifest, "test_e2e_a", "TestLate");
assert.equal(lateSnapshot.revision, 2);
assert.equal(
lateSnapshot.blocks.find((c) => key(c.pos) === key(hit.pos)).block,
stone.id,
);
await late.close();
checks.push("Late join receives current geometry and revision");
await h.control("world.edit", {
world: "test_e2e_a",
expected_revision: 2,
operation_id: "test-isolation",
changes: [{ pos: [3, 1, 3], block: stone.id }],
});
await a.next("blocks", (m) => m.revision === 3);
for (const world of ["test_e2e_base", "test_e2e_b"]) {
const r = await h.control("world.read", {
world,
min: [3, 1, 3],
max: [3, 1, 3],
});
assert.equal(r.blocks.length, 0);
}
checks.push("Pinned template and independent clone isolation");
a.send({ type: "chat", text: "local integration check" });
const chat = await b.next(
"chat",
(m) => m.text === "local integration check",
);
assert.equal(chat.name, "TestAlice");
checks.push("Scoped game chat delivered to peer");
const entity = await h.control("entity.spawn", {
world: "test_e2e_a",
kind: "minecraft:pig",
position: [3, 2, 3],
properties: { name: "TestPig" },
});
await a.next("entities", (m) =>
m.entities.some((e) => e.id === entity.entity.id),
);
await b.next("entities", (m) =>
m.entities.some((e) => e.id === entity.entity.id),
);
await h.control("entity.delete", { id: entity.entity.id });
checks.push("Persisted entity events reach both clients");
await a.close();
a = createClient();
const reconnect = await a.join(manifest, "test_e2e_a", "TestAlice");
assert.equal(reconnect.revision, 3);
assert.ok(reconnect.blocks.some((c) => key(c.pos) === "3,1,3"));
checks.push("Reconnect restores authoritative world snapshot");
await h.control("world.create", {
world: "test_e2e_arena",
template: "spleef_template",
});
await delay(1150);
for (const c of [a, b]) {
c.send({ type: "switch_world", world: "test_e2e_arena" });
await c.next("snapshot", (m) => m.world === "test_e2e_arena");
}
a.send({ type: "start_match" });
await a.state((_p, m) => m.match?.phase === "countdown");
const active = await a.state((_p, m) => m.match?.phase === "active", 7000);
const victim = active.players.find((p) => p.id === a.id),
at = victim.position.map(Math.floor);
const floor = [];
for (let x = at[0] - 1; x <= at[0] + 1; x++)
for (let z = at[2] - 1; z <= at[2] + 1; z++)
floor.push({ pos: [x, 0, z], block: 0 });
const arena = (await h.control("world.list")).find(
(w) => w.name === "test_e2e_arena",
);
await h.control("world.edit", {
world: "test_e2e_arena",
expected_revision: arena.revision,
operation_id: "test-eliminate",
changes: floor,
});
const finished = await b.state(
(_p, m) => m.match?.phase === "finished",
7000,
);
assert.equal(finished.match.winner, "TestBob");
await b.state(
(_p, m) => m.tick > finished.tick && m.match?.phase === "waiting",
8000,
);
const reset = await h.control("world.read", {
world: "test_e2e_arena",
min: [at[0], 0, at[2]],
max: [at[0], 0, at[2]],
});
assert.equal(reset.blocks.length, 1);
checks.push(
"Full two-player Spleef countdown, elimination, winner and automatic floor reset",
);
for (const c of clients) await c.close();
let metrics;
for (let n = 0; n < 30; n++) {
metrics = await h.get("/api/metrics");
if (metrics.players === 0) break;
await delay(100);
}
assert.equal(metrics.players, 0);
assert.equal(metrics.active_worlds, 0);
checks.push("Disconnect removes players and active worlds");
const durableEdit = await h.control("world.edit", {
world: "test_e2e_a",
expected_revision: 3,
operation_id: "test-durable-edit",
changes: [{ pos: [4, 2, 4], block: stone.id }],
});
const durableEntity = await h.control("entity.spawn", {
world: "test_e2e_a",
kind: "minecraft:pig",
position: [5, 2, 5],
properties: { name: "DurablePig", marker: "before-sigkill" },
});
await h.control("arena.configure", {
world: "test_e2e_a",
mode: "creative",
spawn: [1, 3, 1],
countdown_seconds: 2,
round_seconds: 120,
elimination_y: -5,
});
const durableData = h.data;
await h.kill("SIGKILL");
h = await harness({ ...opts, data: durableData });
const afterCrash = await h.control("world.read", {
world: "test_e2e_a",
min: [4, 2, 4],
max: [4, 2, 4],
});
assert.equal(afterCrash.revision, durableEdit.revision);
assert.equal(afterCrash.blocks[0]?.block, stone.id);
const persisted = await h.control("entity.list", { world: "test_e2e_a" });
const pig = persisted.items.find(
(entity) => entity.id === durableEntity.entity.id,
);
assert.equal(pig?.properties.marker, "before-sigkill");
// No fields supplied: the server returns and resaves the loaded configuration.
const config = await h.control("arena.configure", { world: "test_e2e_a" });
assert.equal(config.countdown_seconds, 2);
assert.equal(config.round_seconds, 120);
assert.equal(config.elimination_y, -5);
assert.deepEqual(config.spawn, [1, 3, 1]);
const afterRestart = createClient();
const restartedWelcome = await afterRestart.join(
await h.get("/api/manifest"),
"test_e2e_a",
"RestartProbe",
);
assert.deepEqual(restartedWelcome.spawn, [1, 3, 1]);
assert.ok(
restartedWelcome.blocks.some(
(cell) => key(cell.pos) === "4,2,4" && cell.block === stone.id,
),
);
await afterRestart.close();
checks.push(
"Real SIGKILL/restart preserves committed blocks, entity properties and arena configuration",
);
metrics = await h.get("/api/metrics");
await h.report({ passed: true, checks, metrics, binary: h.binary });
} catch (error) {
await h.report({
passed: false,
checks,
error: error.stack,
server_logs: h.logs(),
client_errors: clients.map((c) =>
c.queue.filter((m) => m.type === "error"),
),
});
throw error;
} finally {
await Promise.allSettled(clients.map((c) => c.close()));
await h.stop();
}
+51
View File
@@ -0,0 +1,51 @@
#!/usr/bin/env python3
"""Generate tiny original Sponge fixtures using independent nbtlib2.0.4.
Run with artifacts/compat-venv/bin/python after installing nbtlib==2.0.4.
The fixture contains no game assets or third-party map content.
"""
from pathlib import Path
import gzip
import io
import nbtlib as n
ROOT = Path(__file__).resolve().parents[1]
out = ROOT / "crates/shacraft-compat/fixtures"
out.mkdir(parents=True, exist_ok=True)
metadata = n.Compound({
"Name": n.String("Шакрафт тест"), "Byte": n.Byte(-128),
"Short": n.Short(-32768), "Long": n.Long(-9223372036854775808),
"Float": n.Float(1.25), "Double": n.Double(-2.5),
"ByteArray": n.ByteArray([-1, 0, 127]),
"IntArray": n.IntArray([-2147483648, 2147483647]),
"LongArray": n.LongArray([-9223372036854775808, 9223372036854775807]),
"EmptyLongList": n.List[n.Long]([]),
"Nested": n.Compound({"custom_unknown": n.String("keep me")}),
})
entity = n.Compound({"Id": n.String("minecraft:armor_stand"),
"Pos": n.List[n.Double]([1.5, 1.0, 0.5]),
"Data": n.Compound({"UUID": n.IntArray([1, 2, 3, 4]), "NoGravity": n.Byte(1)})})
blockentity = n.Compound({"Id": n.String("minecraft:chest"),
"Pos": n.IntArray([0, 0, 0]), "Data": n.Compound({
"Items": n.List[n.Compound]([n.Compound({"Slot": n.Byte(0),
"id": n.String("minecraft:diamond"), "count": n.Int(3)})]),
"unknown_inventory_tag": n.Long(12345678901234567),
})})
states = ["minecraft:air", "minecraft:chest[facing=north,type=single,waterlogged=false]",
"minecraft:oak_stairs[facing=east,half=bottom,shape=straight,waterlogged=false]",
"example:unknown[facing=west]", "minecraft:stone"]
indices = [1, 2, 3, 4] + [0] * 20
schematic = n.Compound({"Version": n.Int(3), "DataVersion": n.Int(4903),
"Width": n.Short(4), "Height": n.Short(3), "Length": n.Short(2),
"Offset": n.IntArray([-17, -1, 31]), "Metadata": metadata,
"Blocks": n.Compound({"Palette": n.Compound({s: n.Int(i) for i, s in enumerate(states)}),
"Data": n.ByteArray(indices), "BlockEntities": n.List[n.Compound]([blockentity])}),
"Biomes": n.Compound({"Palette": n.Compound({"minecraft:plains": n.Int(0), "minecraft:desert": n.Int(1)}),
"Data": n.ByteArray([i % 2 for i in range(24)])}),
"Entities": n.List[n.Compound]([entity]),
"UnknownRoot": n.Compound({"opaque": n.String("preserve this")}),
})
f = n.File({"Schematic": schematic}, root_name="")
buffer = io.BytesIO(); f.write(buffer)
(out / "sponge-v3.schem").write_bytes(gzip.compress(buffer.getvalue(), mtime=0))
print(out / "sponge-v3.schem")
+136
View File
@@ -0,0 +1,136 @@
// Copyright Shacraft contributors. MIT OR Apache-2.0.
// Generate our own tiny world metadata and validate files using public Java26.2
// APIs. Does not start a server, open sockets, or read/accept an EULA.
import java.nio.file.*;
import java.io.*;
import java.util.*;
import com.mojang.serialization.*;
import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
import net.minecraft.nbt.*;
import net.minecraft.core.*;
import net.minecraft.core.registries.*;
import net.minecraft.data.registries.VanillaRegistries;
import net.minecraft.world.level.*;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.chunk.*;
import net.minecraft.world.level.chunk.storage.*;
import net.minecraft.world.level.levelgen.*;
import net.minecraft.world.level.levelgen.flat.*;
import net.minecraft.world.level.levelgen.presets.*;
import net.minecraft.world.level.storage.*;
import net.minecraft.world.level.gamerules.*;
import net.minecraft.world.level.biome.*;
class compat_java {
static final NbtAccounter BUDGET=NbtAccounter.create(16*1024*1024);
static void generate(Path out, HolderLookup.Provider lookup) throws Exception {
Files.createDirectories(out);
var settings=new LevelSettings("Shacraft template",GameType.CREATIVE,LevelSettings.DifficultySettings.DEFAULT,true,WorldDataConfiguration.DEFAULT);
var level=new PrimaryLevelData(settings,PrimaryLevelData.SpecialWorldProperty.FLAT,Lifecycle.stable());
level.setInitialized(true);
level.setSpawn(LevelData.RespawnData.of(Level.OVERWORLD,new BlockPos(0,80,0),0,0));
var root=new CompoundTag();root.put("Data",level.createTag(null));
NbtIo.writeCompressed(root,out.resolve("level.dat"));
var flat=new FlatLevelGeneratorSettings(Optional.of(HolderSet.direct()),lookup.lookupOrThrow(Registries.BIOME).getOrThrow(Biomes.PLAINS),List.of());
flat.getLayersInfo().add(new FlatLayerInfo(1,Blocks.AIR));flat.updateLayers();
var dims=lookup.lookupOrThrow(Registries.WORLD_PRESET).getOrThrow(WorldPresets.FLAT).value().createWorldDimensions().replaceOverworldGenerator(lookup,new FlatLevelSource(flat));
var worldgen=new WorldGenSettings(new WorldOptions(0,false,false),dims);
var encoded=WorldGenSettings.CODEC.encodeStart(lookup.createSerializationContext(NbtOps.INSTANCE),worldgen).getOrThrow();
var data=new CompoundTag();data.put("data",encoded);NbtUtils.addCurrentDataVersion(data);
Path gen=WorldGenSettings.TYPE.id().withSuffix(".dat").resolveAgainst(out.resolve("data"));
Files.createDirectories(gen.getParent());NbtIo.writeCompressed(data,gen);
var rules=new GameRules(level.enabledFeatures());rules.set(GameRules.SPAWN_MOBS,false,null);rules.set(GameRules.ADVANCE_TIME,false,null);
LevelStorageSource.writeGameRules(level,out,rules);
System.out.println("Generated metadata with public Java26.2 APIs: "+out);
}
static PalettedContainerFactory factory(HolderLookup.Provider lookup) {
var biomeRegistry=new MappedRegistry<Biome>(Registries.BIOME,Lifecycle.stable());
lookup.lookupOrThrow(Registries.BIOME).listElements().forEach(h->biomeRegistry.register(h.key(),h.value(),RegistrationInfo.BUILT_IN));
biomeRegistry.freeze();
return PalettedContainerFactory.create(new RegistryAccess.ImmutableRegistryAccess(List.of(biomeRegistry)));
}
static void fixture(Path out,HolderLookup.Provider lookup) throws Exception {
generate(out,lookup);
var factory=factory(lookup);
var section=new LevelChunkSection(factory);
section.setBlockState(15,15,15,Blocks.CHEST.defaultBlockState());
section.setBlockState(14,15,15,Blocks.OAK_STAIRS.defaultBlockState());
section.setBlockState(13,15,15,Blocks.STONE.defaultBlockState());
var chest=new CompoundTag();chest.putString("id","minecraft:chest");chest.putInt("x",-1);chest.putInt("y",-1);chest.putInt("z",31);
var item=new CompoundTag();item.putByte("Slot",(byte)0);item.putString("id","minecraft:diamond");item.putInt("count",3);
var inventory=new ListTag();inventory.add(item);chest.put("Items",inventory);chest.putLong("unknown_inventory_tag",12345678901234567L);
var chunk=new SerializableChunkData(factory,new ChunkPos(-1,1),-4,0,0,net.minecraft.world.level.chunk.status.ChunkStatus.FULL,
null,null,UpgradeData.EMPTY,null,Map.of(),new ChunkAccess.PackedTicks(List.of(),List.of()),
new it.unimi.dsi.fastutil.shorts.ShortList[24],false,
List.of(new SerializableChunkData.SectionData(-1,section,null,null)),List.of(),List.of(chest),new CompoundTag());
var tag=chunk.write();var unknown=new CompoundTag();unknown.putByte("Byte",(byte)-128);unknown.putShort("Short",(short)-32768);
unknown.putLong("Long",Long.MIN_VALUE);unknown.putString("Unicode","Привет🌍\u0000");unknown.putIntArray("UUID",new int[]{1,2,3,4});tag.put("UnknownRoot",unknown);
Files.createDirectories(out.resolve("region"));
try(var region=new RegionFile(new RegionStorageInfo("compat-fixture",Level.OVERWORLD,"chunk"),out.resolve("region/r.-1.0.mca"),out.resolve("region"),false)) {
try(var output=region.getChunkDataOutputStream(new ChunkPos(-1,1))){NbtIo.write(tag,output);}
}
var entity=new CompoundTag();entity.putString("id","minecraft:armor_stand");entity.putIntArray("UUID",new int[]{1,2,3,4});
var pos=new ListTag();pos.add(DoubleTag.valueOf(-0.5));pos.add(DoubleTag.valueOf(0));pos.add(DoubleTag.valueOf(31.5));entity.put("Pos",pos);entity.putBoolean("NoGravity",true);
var entities=new ListTag();entities.add(entity);var root=new CompoundTag();root.putInt("DataVersion",4903);root.putIntArray("Position",new int[]{-1,1});root.put("Entities",entities);
Files.createDirectories(out.resolve("entities"));
try(var region=new RegionFile(new RegionStorageInfo("compat-fixture",Level.OVERWORLD,"entities"),out.resolve("entities/r.-1.0.mca"),out.resolve("entities"),false)) {
try(var output=region.getChunkDataOutputStream(new ChunkPos(-1,1))){NbtIo.write(root,output);}
}
Files.writeString(out.resolve("custom-metadata.txt"),"own fixture; retain unknown file\n");
}
static void validate(Path world,HolderLookup.Provider lookup) throws Exception {
var levelRoot=NbtIo.readCompressed(world.resolve("level.dat"),NbtAccounter.create(16*1024*1024));
var level=levelRoot.getCompoundOrEmpty("Data");
if(level.getIntOr("DataVersion",-1)!=4903)throw new AssertionError("DataVersion");
var dynamic=new Dynamic<>(NbtOps.INSTANCE,level);
var config=LevelStorageSource.readDataConfig(dynamic);
var settings=LevelSettings.parse(dynamic,config);
var parsed=PrimaryLevelData.parse(dynamic,settings,PrimaryLevelData.SpecialWorldProperty.FLAT,Lifecycle.stable());
Path gen=WorldGenSettings.TYPE.id().withSuffix(".dat").resolveAgainst(world.resolve("data"));
var encoded=NbtIo.readCompressed(gen,NbtAccounter.create(16*1024*1024)).getCompoundOrEmpty("data");
var worldgen=WorldGenSettings.CODEC.parse(lookup.createSerializationContext(NbtOps.INSTANCE),encoded).getOrThrow();
if(worldgen.dimensions().dimensions().size()!=3)throw new AssertionError("dimensions");
var factory=factory(lookup);int chunks=0,sections=0,entities=0;long nonair=0;
try(var files=Files.walk(world)) {
for(Path path:files.filter(p->p.toString().endsWith(".mca")&&Set.of("region","entities").contains(p.getParent().getFileName().toString())&&!p.toString().contains("source-sidecar")).toList()) {
boolean entityRegion=path.getParent().getFileName().toString().equals("entities");
String[] parts=path.getFileName().toString().split("\\.");int rx=Integer.parseInt(parts[1]),rz=Integer.parseInt(parts[2]);
try(var region=new RegionFile(new RegionStorageInfo("compat-check",Level.OVERWORLD,"chunk"),path,path.getParent(),false)) {
for(int i=0;i<1024;i++) {
var pos=new ChunkPos(rx*32+i%32,rz*32+i/32);
if(!region.hasChunk(pos))continue;
try(var input=region.getChunkDataInputStream(pos)) {
var tag=NbtIo.read(input,NbtAccounter.create(16*1024*1024));
if(entityRegion) {
int[] coordinates=tag.getIntArray("Position").orElseThrow();
if(coordinates.length!=2||coordinates[0]!=pos.x()||coordinates[1]!=pos.z())throw new AssertionError("entity chunk Position");
for(var value:tag.getListOrEmpty("Entities")) {
var entity=(CompoundTag)value;
if(entity.getIntArray("UUID").orElseThrow().length!=4)throw new AssertionError("entity UUID");
if(entity.getListOrEmpty("Pos").size()!=3)throw new AssertionError("entity Pos");
if(!BuiltInRegistries.ENTITY_TYPE.containsKey(net.minecraft.resources.Identifier.parse(entity.getString("id").orElseThrow())))throw new AssertionError("unknown vanilla entity type");
entities++;
}
continue;
}
var chunk=SerializableChunkData.parse(LevelHeightAccessor.create(-64,384),factory,tag);
if(chunk==null||!chunk.chunkPos().equals(pos))throw new AssertionError("missing/misplaced chunk "+pos);
for(var section:chunk.sectionData()) {
if(section.chunkSection()!=null){sections++;for(int y=0;y<16;y++)for(int z=0;z<16;z++)for(int x=0;x<16;x++)if(!section.chunkSection().getBlockState(x,y,z).isAir())nonair++;}
}
chunks++;
}
}
}
}
}
System.out.println("{\"java26_2_verified\":true,\"chunks\":"+chunks+",\"sections\":"+sections+",\"entities\":"+entities+",\"nonair_blocks\":"+nonair+",\"level_name\":\""+parsed.getLevelName()+"\",\"scope\":\"NbtIo,RegionFile,SerializableChunkData,PrimaryLevelData,WorldGenSettings codecs; no server simulation\"}");
}
public static void main(String[] args) throws Exception {
SharedConstants.tryDetectVersion();Bootstrap.bootStrap();var lookup=VanillaRegistries.createLookup();
if(args[0].equals("generate"))generate(Path.of(args[1]),lookup);
else if(args[0].equals("fixture"))fixture(Path.of(args[1]),lookup);
else validate(Path.of(args[1]),lookup);
}
}
+94
View File
@@ -0,0 +1,94 @@
#!/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))
+66
View File
@@ -0,0 +1,66 @@
#!/usr/bin/env python3
"""Package a clean, committed source tree and optional Linux executables.
No user worlds, tokens, Cargo caches, Minecraft JAR, or untracked files enter
an archive. Every extracted file is checked against the in-archive manifest.
"""
import argparse
import gzip
import hashlib
import io
import json
import subprocess
import tarfile
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
def git(*args):
return subprocess.check_output(['git', *args], cwd=ROOT)
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--binaries', action='store_true')
args = parser.parse_args()
if git('status', '--porcelain').strip():
raise SystemExit('Commit the reviewed tree before packaging.')
revision = git('rev-parse', 'HEAD').decode().strip()
epoch = int(git('show', '-s', '--format=%ct', 'HEAD'))
paths = [Path(p.decode()) for p in git('ls-files', '-z').split(b'\0') if p]
if args.binaries:
paths += [Path('target/release') / name for name in
['shacraft-server', 'shacraft-mcp', 'shacraft-compat', 'shacraft-tools']]
files = []
for relative in sorted(paths):
path = ROOT / relative
if path.is_symlink() or not path.is_file():
raise SystemExit(f'Only regular files are distributable: {relative}')
if relative.name.endswith('.token') or relative.name == '.env':
raise SystemExit(f'Secret filename cannot be distributed: {relative}')
data = path.read_bytes()
files.append((relative.as_posix(), data, 0o755 if path.stat().st_mode & 0o111 else 0o644))
manifest = {'project':'Shacraft Core','git_revision':revision,'source_epoch':epoch,
'kind':'linux-x86_64' if args.binaries else 'source',
'license':'MIT OR Apache-2.0',
'files':{name:{'sha256':hashlib.sha256(data).hexdigest(),'size':len(data)} for name,data,_ in files}}
files.append(('RELEASE.json', (json.dumps(manifest,indent=2)+'\n').encode(), 0o644))
output = ROOT / 'artifacts' / f"shacraft-core-mvp-{revision[:8]}-{manifest['kind']}.tar.gz"
output.parent.mkdir(exist_ok=True)
with output.open('wb') as raw, gzip.GzipFile(fileobj=raw,mode='wb',mtime=0) as zipped:
with tarfile.open(fileobj=zipped,mode='w') as archive:
for name,data,mode in files:
info=tarfile.TarInfo('shacraft-core/'+name)
info.size=len(data);info.mode=mode;info.mtime=epoch
archive.addfile(info,io.BytesIO(data))
digest=hashlib.sha256(output.read_bytes()).hexdigest()
output.with_name(output.name+'.sha256').write_text(f'{digest} {output.name}\n')
with tarfile.open(output,'r:gz') as archive:
for name,data,_ in files:
actual=archive.extractfile('shacraft-core/'+name).read()
if actual!=data:
raise SystemExit(f'Archive verification failed for {name}')
print(json.dumps({'archive':str(output),'sha256':digest,'bytes':output.stat().st_size,
'git_revision':revision,'verified_files':len(files)},indent=2))
if __name__=='__main__':
main()
Executable
+8
View File
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail
shacraft_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$shacraft_root"
if [[ ! -x target/release/shacraft-server ]]; then
cargo build --release --locked -p shacraft-server -p shacraft-mcp -p shacraft-compat
fi
exec target/release/shacraft-server --client "$shacraft_root/client" --packages "$shacraft_root/packages" "$@"
+261
View File
@@ -0,0 +1,261 @@
import assert from "node:assert/strict";
import { spawn } from "node:child_process";
import { mkdtemp, mkdir, readFile, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
export const root = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
"..",
);
export const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
export function options() {
const opts = {};
for (let i = 2; i < process.argv.length; i += 2)
opts[process.argv[i].replace(/^--/, "")] = process.argv[i + 1];
return opts;
}
export async function harness(opts = {}) {
const port = Number(opts.port || 4001),
url = `http://127.0.0.1:${port}`;
try {
await fetch(`${url}/api/health`, { signal: AbortSignal.timeout(500) });
throw Error(
`Port ${port} is already serving HTTP; choose an unused --port`,
);
} catch (error) {
if (error.message?.includes("already serving")) throw error;
}
const data = opts.data
? path.resolve(opts.data)
: await mkdtemp(path.join(os.tmpdir(), "shacraft-server-test-"));
await mkdir(data, { recursive: true });
let stderr = "";
const binary = path.resolve(
opts.binary || path.join(root, "target/debug/shacraft-server"),
);
const child = spawn(
binary,
[
"--listen",
`127.0.0.1:${port}`,
"--data",
data,
"--client",
path.join(root, "client"),
"--packages",
path.join(root, "packages"),
],
{ cwd: root, stdio: ["ignore", "pipe", "pipe"] },
);
child.stdout.on("data", (chunk) => (stderr += chunk));
child.stderr.on("data", (chunk) => (stderr += chunk));
let failure;
child.on("error", (e) => (failure = e));
const running = () =>
child.exitCode === null && child.signalCode === null && !failure;
async function kill(signal = "SIGKILL") {
if (!running()) return;
const exited = new Promise((resolve) => child.once("exit", resolve));
async function waitForExit() {
let timer;
await Promise.race([
exited,
new Promise((resolve) => {
timer = setTimeout(resolve, 5000);
}),
]);
clearTimeout(timer);
}
child.kill(signal);
await waitForExit();
if (running()) {
child.kill("SIGKILL");
await waitForExit();
}
if (running()) throw Error("Test server did not exit after SIGKILL");
}
async function stop() {
await kill("SIGINT");
}
try {
const deadline = Date.now() + 45000;
while (true) {
if (failure) throw failure;
if (!running()) throw Error(`Server exited: ${stderr}`);
try {
if ((await fetch(`${url}/api/health`)).ok) break;
} catch {}
if (Date.now() > deadline)
throw Error(`Server startup timeout: ${stderr}`);
await delay(100);
}
} catch (e) {
await stop();
throw e;
}
const token = (
await readFile(path.join(data, "control.token"), "utf8")
).trim();
async function get(route) {
const r = await fetch(url + route);
assert.equal(r.status, 200, `${route}: HTTP ${r.status}`);
return r.json();
}
async function control(method, params = {}) {
const response = await fetch(url + "/api/control", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ method, params }),
});
const body = await response.json();
if (!response.ok || body.error)
throw Error(`${method}: ${body.error || response.status}`);
return body.result;
}
return {
url,
data,
pid: child.pid,
binary,
get,
control,
stop,
kill,
logs: () => stderr,
async report(value) {
const output = opts.output
? path.resolve(opts.output)
: path.join(data, "report.json");
await mkdir(path.dirname(output), { recursive: true });
await writeFile(output, JSON.stringify(value, null, 2) + "\n");
console.log(
JSON.stringify(
{
...value,
samples: undefined,
sample_count: value.samples?.length,
report_file: output,
data,
},
null,
2,
),
);
},
};
}
export class Client {
constructor(url) {
this.ws = new WebSocket(url.replace(/^http/, "ws") + "/ws");
this.queue = [];
this.waiters = [];
this.id = null;
this.seq = 0;
this.latest = null;
this.revision = null;
this.error = null;
this.open = new Promise((resolve, reject) => {
this.ws.addEventListener("open", resolve, { once: true });
this.ws.addEventListener(
"error",
() => reject(Error("WebSocket connection failed")),
{ once: true },
);
});
this.ws.addEventListener("message", (event) => {
let m;
try {
m = JSON.parse(event.data);
} catch {
return;
}
if (m.type === "state") this.latest = m;
if (m.type === "welcome") {
this.id = m.id;
this.welcome = m;
}
if (["welcome", "snapshot", "blocks"].includes(m.type))
this.revision = m.revision;
for (let i = 0; i < this.waiters.length; i++) {
const w = this.waiters[i];
if (w.predicate(m)) {
this.waiters.splice(i, 1);
clearTimeout(w.timer);
w.resolve(m);
return;
}
}
this.queue.push(m);
if (this.queue.length > 500) this.queue.shift();
});
}
send(message) {
this.ws.send(JSON.stringify(message));
}
async join(manifest, world, name = "TestPlayer") {
await this.open;
this.send({
type: "join",
protocol: 1,
manifest_hash: manifest.hash,
world,
name,
});
return this.next("welcome");
}
next(type, predicate = () => true, timeout = 10000) {
return this.wait((m) => m.type === type && predicate(m), timeout);
}
wait(predicate, timeout = 10000) {
const i = this.queue.findIndex(predicate);
if (i >= 0) return Promise.resolve(this.queue.splice(i, 1)[0]);
return new Promise((resolve, reject) => {
const w = {
predicate,
resolve,
timer: setTimeout(() => {
this.waiters = this.waiters.filter((x) => x !== w);
reject(Error(`WebSocket message timeout (${timeout} ms)`));
}, timeout),
};
this.waiters.push(w);
});
}
input(fields = {}) {
const input = {
type: "input",
seq: ++this.seq,
yaw: 0,
pitch: 0,
forward: 0,
strafe: 0,
jump: false,
...fields,
};
this.send(input);
return input.seq;
}
async state(predicate = () => true, timeout = 10000) {
return this.next(
"state",
(m) =>
predicate(
m.players.find((p) => p.id === this.id),
m,
),
timeout,
);
}
async close() {
if (this.ws.readyState === WebSocket.CLOSED) return;
const closed = new Promise((resolve) =>
this.ws.addEventListener("close", resolve, { once: true }),
);
this.ws.close();
await Promise.race([closed, delay(2000)]);
}
}
+4 -2
View File
@@ -2,8 +2,10 @@
set -euo pipefail
cd "$(dirname "${BASH_SOURCE[0]}")/.."
cargo fmt --all -- --check
rustfmt --edition 2024 --check crates/shacraft-server/src/control.rs
cargo clippy --workspace --all-targets --locked -- -D warnings
cargo test --workspace --locked
cargo build -p shacraft-tools --locked
cargo build --workspace --locked
python3 scripts/check_storage.py
node --test client/tests/*.test.js
node scripts/check_server.mjs --binary target/debug/shacraft-server --port 4001 --output artifacts/server-e2e-debug.json