Expand voxel gameplay, lighting, full-height streaming and world imports
MVP checks / mvp (push) Canceled after 0s

Add shared Rust/WASM physics, worker meshing and diagnostics, 64-chunk full-height streaming, atlas texture support, and baseline world import. Document the current implementation and include the supplied in-game lobby screenshot.
This commit is contained in:
Emil
2026-09-17 02:10:53 +03:00
parent 26748912c6
commit c7e86663d8
111 changed files with 24257 additions and 598 deletions
+7
View File
@@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/.."
# Install once with: rustup target add wasm32-unknown-unknown
# One implementation supplies native authority and browser prediction.
cargo build --locked -p shacraft-physics --target wasm32-unknown-unknown --release
cp target/wasm32-unknown-unknown/release/shacraft_physics.wasm client/physics.wasm
+293
View File
@@ -0,0 +1,293 @@
import assert from "node:assert/strict";
import { harness, Client, options } from "./server-harness.mjs";
import {
getViewBounds,
mergeChunkUpdate,
mergeSnapshot,
positionInView,
} from "../client/world-view.js";
// Isolated data and port: this check never edits the running demo world.
const opts = { port: "4012", ...options() },
h = await harness(opts),
clients = [],
checks = [],
transitions = [],
world = "test_chunk_stream";
const key = (pos) => pos.join(","),
sorted = (map) => [...map].sort(([a], [b]) => a.localeCompare(b)),
view = {
world,
revision: 0,
viewCenter: null,
viewBounds: null,
blocks: new Map(),
};
let receiveError,
explicitResync = false,
snapshots = 0;
function observe(message) {
if (receiveError) return;
try {
if (message.type === "error") throw Error(message.message);
if (["welcome", "snapshot"].includes(message.type)) {
if (message.type === "snapshot") {
assert.ok(explicitResync, "Walking must not send a full snapshot");
snapshots++;
}
assert.equal(message.world, world);
assert.ok(message.features.includes("chunk_stream_v1"));
view.revision = message.revision;
view.viewCenter = message.view_center;
view.viewBounds = getViewBounds(message);
mergeSnapshot(view.blocks, message.blocks);
} else if (message.type === "blocks") {
assert.equal(message.revision, view.revision + 1);
for (const change of message.changes) {
if (!positionInView(change.pos, view.viewBounds)) continue;
if (change.block) view.blocks.set(key(change.pos), change.block);
else view.blocks.delete(key(change.pos));
}
view.revision = message.revision;
} else if (message.type === "chunks") {
assert.ok(!("spawn" in message), "Movement must not reset the camera");
assert.ok(!("blocks" in message), "Movement must send section deltas");
assert.equal(message.sections.length, 12);
assert.equal(message.unload.length, 12);
assert.equal(
Math.abs(message.view_center[0] - message.from_center[0]),
16,
);
assert.deepEqual(message.view_center.slice(1), [0, 0]);
assert.ok(message.sections.some((section) => !section.blocks.length));
const enteringIds = new Set(
message.sections.flatMap((section) =>
section.blocks.map((record) => record.block),
),
),
materialIds = new Set(message.materials.map((material) => material.id));
assert.deepEqual(materialIds, enteringIds, "Only entering materials sent");
const departing = new Set(message.unload.map(key)),
retained = [...view.blocks].filter(
([id]) =>
!departing.has(
key(id.split(",").map((value) => Math.floor(Number(value) / 16))),
),
),
result = mergeChunkUpdate(view, message);
assert.equal(result.status, "applied", "Real client accepts server delta");
for (const [id, block] of retained)
assert.equal(view.blocks.get(id), block, "Retained geometry is unchanged");
transitions.push({
from: message.from_center,
to: message.view_center,
entering_sections: message.sections.length,
departing_sections: message.unload.length,
retained_sections: 48 - message.unload.length,
entering_blocks: message.sections.reduce(
(count, section) => count + section.blocks.length,
0,
),
retained_blocks: retained.length,
bytes: Buffer.byteLength(JSON.stringify(message)),
});
}
} catch (error) {
receiveError = error;
}
}
function assertReceived() {
if (receiveError) throw receiveError;
}
async function assertCurrentRegion() {
assertReceived();
const region = await h.control("world.read", {
world,
...view.viewBounds,
});
assert.equal(view.revision, region.revision);
assert.deepEqual(
sorted(view.blocks),
sorted(new Map(region.blocks.map((record) => [key(record.pos), record.block]))),
"Merged client view must exactly match the authoritative region",
);
}
async function edit(client, changes, operation) {
const result = await h.control("world.edit", {
world,
expected_revision: view.revision,
operation_id: operation,
changes,
});
await client.next("blocks", (message) => message.revision === result.revision);
await assertCurrentRegion();
}
async function walkTo(client, x, center, expectType = "chunks") {
const initial = (await client.state()).players.find(
(player) => player.id === client.id,
),
strafe = x > initial.position[0] ? 1 : -1,
seq = client.input({ strafe }),
pulse = setInterval(() => client.input({ strafe }), 100);
try {
await client.state(
(player, message) =>
message.ack >= seq &&
(strafe > 0 ? player.position[0] >= x : player.position[0] <= x),
10000,
);
} finally {
clearInterval(pulse);
const stopped = client.input();
await client.state((_player, message) => message.ack >= stopped);
}
const update = await client.next(
expectType,
(message) => message.view_center[0] === center,
);
assertReceived();
return update;
}
try {
const manifest = await h.get("/api/manifest");
async function material(state) {
const found = await h.control("catalog.search", { query: state, limit: 64 });
const item = found.items.find((item) => item.state === state);
assert.ok(item, `Catalog contains ${state}`);
return item.id;
}
const stone = await material("minecraft:stone"),
gold = await material("minecraft:gold_block"),
diamond = await material("minecraft:diamond_block"),
iron = await material("minecraft:iron_block");
await h.control("world.create", { world });
const plan = await h.control("build.plan", {
world,
expected_revision: 0,
operations: [
{ type: "box", min: [-64, 0, -32], max: [79, 0, 31], block: stone },
],
});
await h.control("build.commit", {
plan_id: plan.plan_id,
operation_id: "chunk-stream-floor",
});
await h.control("arena.configure", {
world,
mode: "creative",
spawn: [15, 2, 8],
});
const client = new Client(h.url);
clients.push(client);
client.ws.addEventListener("message", (event) => observe(JSON.parse(event.data)));
await client.open;
client.send({
type: "join",
protocol: 1,
manifest_hash: manifest.hash,
world,
name: "ChunkStreamProbe",
features: ["chunk_stream_v1"],
});
const welcome = await client.next("welcome");
assertReceived();
assert.deepEqual(view.viewCenter, [0, 0, 0]);
assert.deepEqual(view.viewBounds, { min: [-32, -16, -32], max: [31, 31, 31] });
await client.state((player) => player.position[1] < 1.15);
await assertCurrentRegion();
checks.push("Negotiated welcome supplies the complete 48-section view");
await edit(
client,
[
{ pos: [5, 1, 10], block: gold },
{ pos: [-24, 1, 10], block: diamond },
{ pos: [40, 1, 10], block: iron },
],
"chunk-stream-before-east",
);
assert.equal(view.blocks.has("40,1,10"), false);
await walkTo(client, 18, 16);
await assertCurrentRegion();
assert.equal(view.blocks.get("5,1,10"), gold);
assert.equal(view.blocks.has("-24,1,10"), false);
assert.equal(view.blocks.get("40,1,10"), iron);
checks.push("East crossing loads 12 sections, unloads 12, retains 36 without snapshot");
await edit(
client,
[
{ pos: [5, 1, 10], block: 0 },
{ pos: [-24, 1, 10], block: gold },
{ pos: [40, 1, 10], block: 0 },
],
"chunk-stream-while-away",
);
assert.equal(view.blocks.has("-24,1,10"), false);
await walkTo(client, 14, 0);
await assertCurrentRegion();
assert.equal(view.blocks.get("-24,1,10"), gold);
assert.equal(view.blocks.has("5,1,10"), false);
checks.push("Retained deletions persist and returning sections include edits made while unloaded");
await edit(
client,
[
{ pos: [24, 1, 10], block: gold },
{ pos: [-8, 1, 10], block: diamond },
{ pos: [-40, 1, 10], block: iron },
],
"chunk-stream-before-negative",
);
await walkTo(client, -2, -16);
await assertCurrentRegion();
assert.equal(view.blocks.has("24,1,10"), false);
assert.equal(view.blocks.get("-8,1,10"), diamond);
assert.equal(view.blocks.get("-40,1,10"), iron);
assert.equal(snapshots, 0);
assert.equal(transitions.length, 3);
assert.ok(transitions.every((transition) => transition.entering_blocks < welcome.blocks.length));
checks.push("Negative-coordinate crossing matches authoritative region and sends only entering blocks");
const beforeResync = sorted(view.blocks);
explicitResync = true;
client.send({ type: "resync" });
await client.next("snapshot");
assertReceived();
assert.equal(snapshots, 1);
assert.deepEqual(sorted(view.blocks), beforeResync);
await assertCurrentRegion();
checks.push("Explicit full resync remains available and preserves the current world contents");
const legacy = new Client(h.url);
clients.push(legacy);
const legacyWelcome = await legacy.join(manifest, world, "LegacyChunkProbe");
assert.ok(!legacyWelcome.features?.includes("chunk_stream_v1"));
await walkTo(legacy, 18, 16, "snapshot");
assert.equal(legacy.queue.some((message) => message.type === "chunks"), false);
checks.push("Clients without feature negotiation retain protocol-1 snapshot fallback");
await h.report({
passed: true,
checks,
transitions,
movement_snapshots: 0,
explicit_resync_snapshots: snapshots,
initial_blocks: welcome.blocks.length,
initial_snapshot_bytes: Buffer.byteLength(JSON.stringify(welcome)),
client_merge: "client/world-view.js",
binary: h.binary,
});
} catch (error) {
await h.report({ passed: false, checks, transitions, error: error.stack, logs: h.logs().slice(-8000) });
throw error;
} finally {
await Promise.all(clients.map((client) => client.close()));
await h.stop();
}
+221
View File
@@ -0,0 +1,221 @@
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { readFile } from "node:fs/promises";
import { harness, Client, options } from "./server-harness.mjs";
import { loadPhysics, samplePhysicsWorld } from "../client/player-physics.js";
import { getViewBounds, mergeChunkUpdate, mergeSnapshot } from "../client/world-view.js";
// This check starts a separate server and temporary database. It never connects
// to the user's running game or modifies their worlds.
const opts = {
port: "4013",
binary: "target/release/shacraft-server",
output: "artifacts/physics/network-report.json",
...options(),
};
const serverHash = createHash("sha256").update(await readFile(opts.binary)).digest("hex");
const h = await harness(opts), clients = [], checks = [], samples = [];
const world = "test_player_physics", groundedWorld = "test_player_grounded";
const inputs = new Map([[0, {}]]);
const view = { world, revision: 0, viewCenter: null, viewBounds: null, blocks: new Map() };
const materials = new Map();
let client, step, previous, heldInput = {}, receiveError, permitSnapshot = false;
let snapshots = 0, chunks = 0, comparedTicks = 0, maximumError = 0, resetCount = 0;
const zero = { yaw: 0, pitch: 0, forward: 0, strafe: 0, jump: false, sprint: false, sneak: false, fly_toggle: false };
const controls = (input) => ({
...zero,
...input,
// The wire protocol stores these fields as f32 before passing them to Rust.
...Object.fromEntries(["yaw", "pitch", "forward", "strafe"].map((key) => [key, Math.fround(input[key] || 0)])),
});
function received() { if (receiveError) throw receiveError; }
function observe(message) {
if (receiveError) return;
try {
if (message.type === "error") throw Error(message.message);
for (const material of message.materials || []) materials.set(material.id, material);
if (message.type === "welcome" || message.type === "snapshot") {
if (message.type === "snapshot") {
assert.ok(permitSnapshot, "Movement must not send a full snapshot");
snapshots++;
}
assert.ok(message.features.includes("movement_prediction_v1"));
assert.ok(message.features.includes("chunk_stream_v1"));
view.world = message.world;
view.revision = message.revision;
view.viewCenter = message.view_center;
view.viewBounds = getViewBounds(message);
mergeSnapshot(view.blocks, message.blocks);
previous = message.motion;
heldInput = { ...zero };
} else if (message.type === "chunks") {
assert.ok(!("spawn" in message) && !("blocks" in message));
assert.equal(mergeChunkUpdate(view, message).status, "applied");
chunks++;
} else if (message.type === "state") {
const motion = message.motion;
assert.ok(motion, "Negotiated state carries authoritative motion");
assert.equal(message.ack, motion.ack);
assert.equal(message.tick, motion.tick);
if (previous && motion.epoch !== previous.epoch) {
resetCount++;
heldInput = { ...zero };
} else if (previous && motion.tick === previous.tick + 1) {
assert.ok(motion.ack >= previous.ack, "Acknowledgements cannot regress");
assert.ok(motion.ack - previous.ack <= 1, "At most one queued command is processed per tick");
if (motion.ack !== previous.ack) {
assert.ok(inputs.has(motion.ack), `Known consumed command ${motion.ack}`);
heldInput = controls(inputs.get(motion.ack));
} else heldInput = { ...heldInput, fly_toggle: false };
const neighborhood = samplePhysicsWorld(previous.body, view.blocks, materials, view.viewBounds);
assert.ok(neighborhood, "Collision neighborhood is loaded during the probe");
const predicted = step({ body: previous.body, input: heldInput, world: neighborhood, settings: motion.settings });
let error = 0;
for (const field of ["position", "velocity"])
for (let axis = 0; axis < 3; axis++)
error = Math.max(error, Math.abs(predicted[field][axis] - motion.body[field][axis]));
maximumError = Math.max(maximumError, error);
assert.ok(error < 1e-10, `Native/WASM drift at tick ${motion.tick}, ack ${motion.ack}: ${error}`);
for (const field of ["pose", "on_ground", "sprinting", "flying", "jump_cooldown", "in_water", "in_lava"])
assert.equal(predicted[field], motion.body[field], `${field} matches at tick ${motion.tick}`);
comparedTicks++;
}
samples.push({ tick: motion.tick, ack: motion.ack, epoch: motion.epoch,
position: motion.body.position, pose: motion.body.pose, flying: motion.body.flying });
previous = motion;
}
} catch (error) { receiveError = error; }
}
function input(fields = {}) {
const message = { type: "input", seq: ++client.seq, ...zero, ...fields };
inputs.set(message.seq, message);
client.send(message);
return message.seq;
}
async function state(predicate = () => true, timeout = 10000) {
const message = await client.next("state", (m) => predicate(m.motion, m), timeout);
received();
return message.motion;
}
async function acknowledged(fields) {
const seq = input(fields);
return state((motion) => motion.ack >= seq);
}
async function ticks(count) {
const start = previous.tick;
return state((motion) => motion.tick >= start + count);
}
try {
const manifest = await h.get("/api/manifest");
step = await loadPhysics(`${h.url}/physics.wasm`);
const binary = Buffer.from(await (await fetch(`${h.url}/physics.wasm`)).arrayBuffer());
const wasmHash = createHash("sha256").update(binary).digest("hex");
const catalog = await h.control("catalog.search", { query: "minecraft:stone", limit: 64 });
const stone = catalog.items.find((item) => item.state === "minecraft:stone");
assert.ok(stone);
await h.control("world.create", { world });
const plan = await h.control("build.plan", { world, expected_revision: 0,
operations: [{ type: "box", min: [-96, 0, -16], max: [160, 0, 16], block: stone.id }] });
await h.control("build.commit", { plan_id: plan.plan_id, operation_id: "physics-floor" });
await h.control("arena.configure", { world, mode: "creative", spawn: [15, 1, 8] });
await h.control("world.create", { world: groundedWorld, template: world });
await h.control("arena.configure", { world: groundedWorld, mode: "spleef", spawn: [0, 1, 8] });
client = new Client(h.url); clients.push(client);
client.ws.addEventListener("message", (event) => observe(JSON.parse(event.data)));
await client.open;
client.send({ type: "join", protocol: 1, manifest_hash: manifest.hash, world,
name: "PhysicsProbe", features: ["chunk_stream_v1", "movement_prediction_v1"] });
const welcome = await client.next("welcome"); received();
assert.ok(welcome.motion.settings.allow_flight);
await state((motion) => motion.body.on_ground);
checks.push("HTTP-served WebAssembly loads and both movement/chunk features negotiate");
const beforeBurst = previous;
const burstStart = samples.length;
for (let index = 0; index < 12; index++) input({ forward: 1, strafe: .25, yaw: .3,
sprint: true, jump: index === 2 });
const burstEnd = client.seq;
const burstMotion = await state((motion) => motion.ack === burstEnd);
const acks = samples.slice(burstStart).filter((m) => m.ack > beforeBurst.ack).map((m) => m.ack);
assert.deepEqual([...new Set(acks)], Array.from({ length: 12 }, (_, i) => beforeBurst.ack + i + 1));
assert.ok(burstMotion.tick - beforeBurst.tick >= 12);
client.send({ type: "input", seq: burstEnd, ...zero, forward: -1 });
client.send({ type: "input", seq: burstEnd - 1, ...zero, forward: -1 });
const duplicate = await ticks(2); assert.equal(duplicate.ack, burstEnd);
checks.push("A 12-command burst advances at most one acknowledgement per tick; duplicate/stale inputs are ignored");
await acknowledged({}); await ticks(14);
const crouch = await acknowledged({ sneak: true });
assert.equal(crouch.body.pose, "crouching");
const jump = await acknowledged({ jump: true });
assert.equal(jump.body.pose, "standing");
assert.ok(jump.body.velocity[1] > 0 && !jump.body.on_ground);
const flying = await acknowledged({ fly_toggle: true, jump: true });
assert.equal(flying.body.flying, true);
const ascended = await ticks(4);
assert.equal(ascended.body.flying, true, "Held input cannot re-toggle flight");
assert.ok(ascended.body.position[1] > flying.body.position[1]);
const stoppedFlying = await acknowledged({ fly_toggle: true });
assert.equal(stoppedFlying.body.flying, false);
await state((motion) => motion.body.on_ground, 10000);
checks.push("Crouch dimensions, normal jump, authorized flight ascent and one-shot flight toggles are authoritative");
const epochBeforeReset = previous.epoch;
for (let i = 0; i < 12; i++) input({ forward: 1 });
client.send({ type: "input_reset" });
const reset = await state((motion) => motion.epoch > epochBeforeReset);
assert.equal(reset.ack, client.seq);
await acknowledged({}); await ticks(3);
const epochBeforeRespawn = previous.epoch;
client.send({ type: "respawn" });
const respawn = await state((motion) => motion.epoch > epochBeforeRespawn);
assert.ok(Math.abs(respawn.body.position[0] - 15) < 1e-9);
assert.ok(Math.abs(respawn.body.position[2] - 8) < 1e-9);
checks.push("Input reset discards queued commands with a new epoch; respawn clears motion and resets position");
const chunksBeforeWalk = chunks, snapshotsBeforeWalk = snapshots;
let walk = await acknowledged({ forward: 1, sprint: true, yaw: Math.PI / 2 });
const startX = walk.body.position[0];
for (let i = 0; i < 145; i++) walk = await acknowledged({ forward: 1, sprint: true, yaw: Math.PI / 2 });
await acknowledged({}); await ticks(8);
assert.ok(walk.body.position[0] - startX > 35);
assert.ok(chunks - chunksBeforeWalk >= 2);
assert.equal(snapshots, snapshotsBeforeWalk);
checks.push("Sustained predicted sprint crosses multiple chunk boundaries using deltas without full snapshots");
const oldEpoch = previous.epoch;
permitSnapshot = true;
client.send({ type: "switch_world", world: groundedWorld });
const switched = await client.next("snapshot", (m) => m.world === groundedWorld); received();
permitSnapshot = false;
assert.ok(switched.motion.epoch > oldEpoch);
assert.equal(switched.motion.settings.allow_flight, false);
const deniedFlight = await acknowledged({ fly_toggle: true, jump: true });
assert.equal(deniedFlight.body.flying, false);
await acknowledged({});
checks.push("World switch resets the epoch and denies flight outside creative mode");
const legacy = new Client(h.url); clients.push(legacy);
const legacyWelcome = await legacy.join(manifest, groundedWorld, "LegacyPhysicsProbe");
assert.equal(legacyWelcome.motion, undefined);
const legacyStart = (await legacy.state()).players.find((p) => p.id === legacy.id).position;
const legacySeq = legacy.input({ strafe: 1 });
const legacyMoved = await legacy.state((player, message) => message.ack >= legacySeq && player.position[0] > legacyStart[0] + .5);
assert.equal(legacyMoved.motion, undefined);
legacy.input();
checks.push("A client without prediction support still receives movement and acknowledgements without motion payloads");
received();
assert.ok(comparedTicks > 170);
await h.report({ ok: true, checks, server: h.url, server_sha256: serverHash, wasm_sha256: wasmHash,
native_wasm_tolerance: 1e-10, native_wasm_max_error: maximumError,
native_wasm_compared_ticks: comparedTicks, chunk_updates: chunks,
expected_full_snapshots: snapshots, observed_epoch_resets: resetCount, samples });
} catch (error) {
await h.report({ ok: false, checks, error: String(error.stack || error),
receive_error: receiveError?.stack, native_wasm_compared_ticks: comparedTicks,
native_wasm_max_error: maximumError, samples, server_log: h.logs() });
throw error;
} finally {
await Promise.all(clients.map((client) => client.close()));
await h.stop();
}
+129
View File
@@ -0,0 +1,129 @@
// Copyright Shacraft contributors. MIT OR Apache-2.0.
// Extract factual lighting properties through the pinned runtime's public API.
import java.nio.file.*;
import java.util.*;
import com.google.gson.*;
import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
import net.minecraft.core.Direction;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.Identifier;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.lighting.LightEngine;
import net.minecraft.world.phys.shapes.Shapes;
import net.minecraft.world.phys.shapes.VoxelShape;
class lighting_reference {
static String canonical(BlockState state) {
String id=BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString();
var properties=new TreeMap<String,String>();
for(var property:state.getProperties())
properties.put(property.getName(),state.getValue(property).toString().toLowerCase(Locale.ROOT));
return id+(properties.isEmpty()?"":"["+String.join(",",properties.entrySet().stream().map(e->e.getKey()+"="+e.getValue()).toList())+"]");
}
static JsonArray boxes(VoxelShape shape) {
var result=new JsonArray();
for(var box:shape.toAabbs()) {
var coordinates=new JsonArray();
for(double value:new double[]{box.minX,box.minY,box.minZ,box.maxX,box.maxY,box.maxZ})coordinates.add(value);
result.add(coordinates);
}
return result;
}
static BlockState state(String name) {
return BuiltInRegistries.BLOCK.getValue(Identifier.parse("minecraft:"+name)).defaultBlockState();
}
static JsonObject properties(BlockState state) {
var result=new JsonObject();
result.addProperty("minecraft_id",Block.getId(state));
result.addProperty("state",canonical(state));
result.addProperty("emission",state.getLightEmission());
result.addProperty("dampening",state.getLightDampening());
result.addProperty("can_occlude",state.canOcclude());
result.addProperty("use_shape",state.useShapeForLightOcclusion());
result.addProperty("propagates_skylight_down",state.propagatesSkylightDown());
result.add("occlusion",boxes(state.canOcclude()&&state.useShapeForLightOcclusion()?state.getOcclusionShape():Shapes.empty()));
return result;
}
static JsonObject crossing(BlockState from,BlockState to,Direction direction) {
var result=new JsonObject();
result.addProperty("from",canonical(from)); result.addProperty("to",canonical(to));
result.addProperty("direction",direction.getName());
result.addProperty("block_dampening",LightEngine.getLightDampeningInto(from,to,direction,Math.max(1,to.getLightDampening())));
result.addProperty("sky_column_dampening",LightEngine.getLightDampeningInto(from,to,direction,to.getLightDampening()));
return result;
}
public static void main(String[] args) throws Exception {
SharedConstants.tryDetectVersion(); Bootstrap.bootStrap();
var root=new JsonObject(); root.addProperty("version","26.2");
var states=new JsonArray(); var shapeTable=new JsonArray(); var shapes=new HashMap<String,Integer>();
var defaults=new JsonObject(); var blocks=new JsonObject(); var schemas=new JsonArray();
var schemaIds=new HashMap<String,Integer>();
// Block state IDs are the same IDs already recorded by catalog generation.
var byId=new TreeMap<Integer,BlockState>();
for(var block:BuiltInRegistries.BLOCK) {
String name=BuiltInRegistries.BLOCK.getKey(block).toString();
defaults.addProperty(name,Block.getId(block.defaultBlockState()));
var possible=block.getStateDefinition().getPossibleStates();
int first=possible.stream().mapToInt(Block::getId).min().orElseThrow();
var schema=new JsonArray();
for(var property:block.getStateDefinition().getProperties()) {
var item=new JsonArray(); item.add(property.getName());
var values=new JsonArray();
for(var value:property.getPossibleValues())values.add(value.toString().toLowerCase(Locale.ROOT));
item.add(values); schema.add(item);
}
for(var state:possible) {
byId.put(Block.getId(state),state);
int offset=0;
for(var property:block.getStateDefinition().getProperties()) {
var values=new ArrayList<>(property.getPossibleValues());
offset=offset*values.size()+values.indexOf(state.getValue(property));
}
if(first+offset!=Block.getId(state))throw new IllegalStateException("Non-contiguous state schema: "+canonical(state));
}
String schemaKey=schema.toString();
if(!schemaIds.containsKey(schemaKey)) { schemaIds.put(schemaKey,schemas.size()); schemas.add(schema); }
var definition=new JsonArray(); definition.add(first); definition.add(Block.getId(block.defaultBlockState()));
definition.add(schemaIds.get(schemaKey)); blocks.add(name,definition);
}
for(var entry:byId.entrySet()) {
var state=entry.getValue();
var occlusion=boxes(state.canOcclude()&&state.useShapeForLightOcclusion()?state.getOcclusionShape():Shapes.empty());
String key=occlusion.toString();
if(!shapes.containsKey(key)) { shapes.put(key,shapeTable.size());shapeTable.add(occlusion); }
var record=new JsonArray(); record.add(entry.getKey()); record.add(state.getLightDampening());
record.add(shapes.get(key)); record.add(state.getLightEmission());
record.add(state.canOcclude()); record.add(state.useShapeForLightOcclusion());
record.add(state.propagatesSkylightDown()); states.add(record);
}
root.add("states",states); root.add("shapes",shapeTable); root.add("defaults",defaults);
root.add("blocks",blocks); root.add("schemas",schemas);
root.addProperty("state_columns","minecraft_id,dampening,occlusion_shape_id,emission,can_occlude,use_shape,propagates_skylight_down");
var examples=new JsonArray();
for(String name:List.of("air","stone","glass","tinted_glass","white_stained_glass","glass_pane","ice","packed_ice","blue_ice","water","lava","oak_leaves","azalea_leaves","oak_slab","oak_stairs","oak_trapdoor","oak_door","snow","snow_block","torch","wall_torch","redstone_torch","soul_torch","glowstone","sea_lantern","redstone_lamp","lantern","jack_o_lantern","light","sculk_sensor","copper_bulb","copper_grate"))
examples.add(properties(state(name)));
examples.add(properties(Blocks.REDSTONE_LAMP.defaultBlockState().setValue(BlockStateProperties.LIT,true)));
examples.add(properties(Blocks.OAK_SLAB.defaultBlockState().setValue(BlockStateProperties.SLAB_TYPE,net.minecraft.world.level.block.state.properties.SlabType.TOP)));
examples.add(properties(Blocks.OAK_SLAB.defaultBlockState().setValue(BlockStateProperties.SLAB_TYPE,net.minecraft.world.level.block.state.properties.SlabType.DOUBLE)));
examples.add(properties(Blocks.OAK_SLAB.defaultBlockState().setValue(BlockStateProperties.WATERLOGGED,true)));
root.add("examples",examples);
var crossings=new JsonArray(); var air=Blocks.AIR.defaultBlockState();
for(String name:List.of("stone","glass","tinted_glass","ice","water","oak_leaves","oak_slab","oak_stairs","glowstone","sea_lantern"))
for(Direction direction:Direction.values())crossings.add(crossing(air,state(name),direction));
var slab=Blocks.OAK_SLAB.defaultBlockState();
var top=slab.setValue(BlockStateProperties.SLAB_TYPE,net.minecraft.world.level.block.state.properties.SlabType.TOP);
for(Direction direction:Direction.values()) {
crossings.add(crossing(slab,top,direction));
crossings.add(crossing(top,slab,direction));
crossings.add(crossing(state("glowstone"),air,direction));
}
root.add("crossings",crossings);
root.addProperty("measurement_scope","Public original Java 26.2 block-state light properties and LightEngine.getLightDampeningInto calls. Effective shape is empty unless canOcclude and useShapeForLightOcclusion are both true. Crossings measure directional shape blocking and target attenuation, not a full running light engine or skylight source map.");
Files.writeString(Path.of(args[0]),new GsonBuilder().create().toJson(root)+"\n");
System.out.println("Measured "+states.size()+" states, "+shapeTable.size()+" effective shapes, "+examples.size()+" examples and "+crossings.size()+" directional crossings.");
}
}
+88
View File
@@ -0,0 +1,88 @@
#!/usr/bin/env python3
"""Measure Java 26.2 light properties using the verified local catalog cache."""
from pathlib import Path
import argparse
import hashlib
import json
import os
import subprocess
import zipfile
ROOT = Path(__file__).resolve().parents[1]
SOURCE_SHA1 = "823e2250d24b3ddac457a60c92a6a941943fcd6a"
def digest(path, algorithm="sha256"):
with path.open("rb") as source:
return hashlib.file_digest(source, algorithm).hexdigest()
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--java", type=Path, required=True)
parser.add_argument("--cache", type=Path, default=ROOT / "artifacts/catalog-cache")
parser.add_argument("--output", type=Path, default=ROOT / "artifacts/lighting/reference.json")
parser.add_argument("--runtime-output", type=Path, default=ROOT / "client/light-properties.json")
parser.add_argument("--fixture-output", type=Path, default=ROOT / "client/tests/fixtures/lighting-java26.2.json")
args = parser.parse_args()
cache = args.cache.resolve()
source = cache / "server.jar"
if digest(source, "sha1") != SOURCE_SHA1:
raise SystemExit("The official source JAR does not match pinned Java 26.2")
executable = cache / "versions/26.2/server-26.2.jar"
with zipfile.ZipFile(source) as bundle:
with bundle.open("META-INF/versions/26.2/server-26.2.jar") as embedded:
expected = hashlib.file_digest(embedded, "sha256").hexdigest()
if digest(executable) != expected:
raise SystemExit("The cached executable differs from the verified official bundle")
classpath = os.pathsep.join(map(str, [executable, *sorted((cache / "libraries").rglob("*.jar"))]))
java = args.java.resolve()
script = ROOT / "scripts/lighting_reference.java"
measured = cache / "lighting-measurements.json"
commands = [
[str(java.with_name("javac")), "-cp", classpath, "-d", str(cache), str(script)],
[str(java), "-Xmx1G", "-cp", str(cache) + os.pathsep + classpath, "lighting_reference", str(measured)],
]
with (cache / "lighting-measurements.log").open("w") as log:
for command in commands:
result = subprocess.run(command, cwd=cache, stdout=log, stderr=subprocess.STDOUT)
if result.returncode:
raise SystemExit(f"Reference probe failed. Read {cache / 'lighting-measurements.log'}")
reference = json.loads(measured.read_text())
reference["provenance"] = {
"source_url": f"https://piston-data.mojang.com/v1/objects/{SOURCE_SHA1}/server.jar",
"source_sha1": SOURCE_SHA1,
"source_sha256": digest(source),
"executable_sha256": digest(executable),
"probe_sha256": digest(script),
"command": "python3 scripts/measure_lighting.py --java /path/to/java25/bin/java",
}
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(json.dumps(reference, separators=(",", ":")) + "\n")
print(f"Wrote {args.output}: {len(reference['states'])} states, {len(reference['shapes'])} shapes, {args.output.stat().st_size} bytes")
codes = []
for state in reference["states"]:
state_id, dampening, shape_id = state[:3]
assert state_id == len(codes), "Minecraft state IDs must be contiguous"
assert 0 <= dampening <= 15 and 0 <= shape_id < len(reference["shapes"])
codes.append(dampening + 16 * shape_id)
runtime = {
"version": reference["version"],
"encoding": "codes[minecraft_id] = light_dampening + 16 * occlusion_shape_index; blocks[name] = [first_state_id, default_state_id, schema_index]",
"codes": codes,
"shapes": reference["shapes"],
"blocks": reference["blocks"],
"schemas": reference["schemas"],
"provenance": reference["provenance"],
}
args.runtime_output.parent.mkdir(parents=True, exist_ok=True)
args.runtime_output.write_text(json.dumps(runtime, separators=(",", ":")) + "\n")
print(f"Wrote {args.runtime_output}: {args.runtime_output.stat().st_size} bytes")
fixture = {key: reference[key] for key in ("version", "examples", "crossings", "measurement_scope", "provenance")}
args.fixture_output.parent.mkdir(parents=True, exist_ok=True)
args.fixture_output.write_text(json.dumps(fixture, indent=2) + "\n")
print(f"Wrote {args.fixture_output}: {len(fixture['crossings'])} original directional crossings")
if __name__ == "__main__":
main()
+70
View File
@@ -0,0 +1,70 @@
#!/usr/bin/env python3
"""Measure factual Java 26.2 movement through a pinned local official runtime.
Requires the cache prepared by catalog_generate.py and an existing Java 25 JDK.
Does not download assets or start a Minecraft server. See measurement_scope in
the generated fixture for the precise boundary of the original-runtime probe.
"""
from pathlib import Path
import argparse
import hashlib
import json
import os
import subprocess
import zipfile
ROOT = Path(__file__).resolve().parents[1]
SOURCE_SHA1 = "823e2250d24b3ddac457a60c92a6a941943fcd6a"
def digest(path, algorithm="sha256"):
with path.open("rb") as source:
return hashlib.file_digest(source, algorithm).hexdigest()
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--java", type=Path, required=True)
parser.add_argument("--cache", type=Path, default=ROOT / "artifacts/catalog-cache")
parser.add_argument("--output", type=Path, default=ROOT / "artifacts/catalog-cache/physics-reference.json")
args = parser.parse_args()
cache = args.cache.resolve()
source = cache / "server.jar"
if digest(source, "sha1") != SOURCE_SHA1:
raise SystemExit("The official source JAR does not match the pinned Java 26.2 hash")
executable = cache / "versions/26.2/server-26.2.jar"
with zipfile.ZipFile(source) as bundle:
with bundle.open("META-INF/versions/26.2/server-26.2.jar") as embedded:
expected = hashlib.file_digest(embedded, "sha256").hexdigest()
if digest(executable) != expected:
raise SystemExit("The cached executable differs from the verified official bundle")
classpath = os.pathsep.join(map(str, [executable, *sorted((cache / "libraries").rglob("*.jar"))]))
java = args.java.resolve()
script = ROOT / "scripts/physics_reference.java"
measured = cache / "physics-measurements.json"
commands = [
[str(java.with_name("javac")), "-cp", classpath, "-d", str(cache), str(script)],
[str(java), "-Xmx1G", "-cp", str(cache) + os.pathsep + classpath, "physics_reference", str(measured)],
]
with (cache / "physics-measurements.log").open("w") as log:
for command in commands:
result = subprocess.run(command, cwd=cache, stdout=log, stderr=subprocess.STDOUT)
if result.returncode:
raise SystemExit(f"Reference probe failed. Read {cache / 'physics-measurements.log'}")
fixture = json.loads(measured.read_text())
fixture["provenance"] = {
"source_url": f"https://piston-data.mojang.com/v1/objects/{SOURCE_SHA1}/server.jar",
"source_sha1": SOURCE_SHA1,
"source_sha256": digest(source),
"executable_sha256": digest(executable),
"probe_sha256": digest(script),
"command": "python3 scripts/measure_physics.py --java /path/to/java25/bin/java --output crates/shacraft-physics/tests/fixtures/java26.2.json",
"java": subprocess.run([str(java), "-version"], text=True, capture_output=True, check=True).stderr.strip(),
}
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(json.dumps(fixture, indent=2) + "\n")
print(f"Wrote {args.output}")
if __name__ == "__main__":
main()
+332
View File
@@ -0,0 +1,332 @@
// Copyright Shacraft contributors. MIT OR Apache-2.0.
// Measure the original Java runtime. No original source or assets are exported.
import java.nio.file.*;
import java.lang.reflect.*;
import java.util.*;
import com.google.gson.*;
import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.Identifier;
import net.minecraft.world.entity.*;
import net.minecraft.world.entity.player.*;
import net.minecraft.world.entity.ai.attributes.*;
import net.minecraft.world.effect.*;
import net.minecraft.world.level.*;
import net.minecraft.world.level.block.*;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.material.FluidState;
import net.minecraft.world.level.material.Fluid;
import net.minecraft.world.level.gameevent.GameEvent;
import net.minecraft.world.level.border.WorldBorder;
import net.minecraft.world.phys.*;
import net.minecraft.world.phys.shapes.*;
import net.minecraft.tags.*;
class physics_reference {
static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
static final sun.misc.Unsafe ALLOCATOR;
static final Method COLLIDE;
static {
try {
var field = sun.misc.Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
ALLOCATOR = (sun.misc.Unsafe)field.get(null);
COLLIDE = Entity.class.getDeclaredMethod("collideWithShapes", Vec3.class, AABB.class, List.class);
COLLIDE.setAccessible(true);
} catch (ReflectiveOperationException e) { throw new ExceptionInInitializerError(e); }
}
static void field(Object target, Class<?> owner, String name, Object value) {
try { var f=owner.getDeclaredField(name); f.setAccessible(true); f.set(target,value); }
catch (ReflectiveOperationException e) { throw new RuntimeException(e); }
}
static double sprintModifier() {
try {
var f=LivingEntity.class.getDeclaredField("SPEED_MODIFIER_SPRINTING"); f.setAccessible(true);
return ((AttributeModifier)f.get(null)).amount();
} catch (ReflectiveOperationException e) { throw new RuntimeException(e); }
}
static JsonArray vector(Vec3 v) {
var a=new JsonArray(); a.add(v.x); a.add(v.y); a.add(v.z); return a;
}
static Block block(String name) { return BuiltInRegistries.BLOCK.getValue(Identifier.parse("minecraft:"+name)); }
static class SampleWorld implements BlockGetter {
BlockState state;
SampleWorld(BlockState state) { this.state=state; }
public BlockState getBlockState(BlockPos p) { return p.equals(BlockPos.ZERO)?state:Blocks.AIR.defaultBlockState(); }
public FluidState getFluidState(BlockPos p) { return getBlockState(p).getFluidState(); }
public BlockEntity getBlockEntity(BlockPos p) { return null; }
public int getHeight() { return 384; }
public int getMinY() { return -64; }
}
// Constructors are deliberately never invoked: this registry-only harness
// does not create a running server, world files, connections, or EULA state.
static class PlaneLevel extends ServerLevel {
BlockState surface;
List<VoxelShape> obstacles;
WorldBorder border;
PlaneLevel() { super(null,null,null,null,null,null,false,0,List.of(),false); }
public BlockState getBlockState(BlockPos p) { return p.getY()<0?surface:Blocks.AIR.defaultBlockState(); }
public FluidState getFluidState(BlockPos p) { return getBlockState(p).getFluidState(); }
public WorldBorder getWorldBorder() { return border; }
public List<VoxelShape> getEntityCollisions(Entity entity,AABB bounds) { return List.of(); }
public Iterable<VoxelShape> getBlockCollisions(Entity entity,AABB bounds) {
return obstacles.stream().filter(s->s.bounds().intersects(bounds.inflate(1e-7))).toList();
}
}
static class ProbePlayer extends Player {
PlaneLevel plane;
boolean grounded, sprint;
String medium;
double fluidDepth;
Vec3 coordinates, velocity;
Abilities testAbilities;
AttributeSupplier defaults;
List<VoxelShape> obstacles;
ProbePlayer() { super(null,null); }
public GameType gameMode() { return GameType.SURVIVAL; }
public Level level() { return plane; }
public boolean onGround() { return grounded; }
public boolean isPassenger() { return false; }
public boolean isSwimming() { return false; }
public boolean isSprinting() { return sprint; }
public boolean isInWater() { return "water".equals(medium); }
public boolean isInLava() { return "lava".equals(medium); }
public boolean isFallFlying() { return false; }
public boolean isNoGravity() { return false; }
public boolean onClimbable() { return false; }
public boolean shouldDiscardFriction() { return false; }
public boolean isSteppingCarefully() { return false; }
public boolean isSuppressingBounce() { return false; }
public void gameEvent(Holder<GameEvent> event) {}
public double getFluidHeight(TagKey<Fluid> fluid) { return fluidDepth; }
public double getFluidJumpThreshold() { return .4; }
public boolean hasEffect(Holder<MobEffect> effect) { return false; }
public MobEffectInstance getEffect(Holder<MobEffect> effect) { return null; }
public Abilities getAbilities() { return testAbilities; }
public double getAttributeValue(Holder<Attribute> attribute) {
double value=defaults.getBaseValue(attribute);
return attribute.equals(Attributes.MOVEMENT_SPEED)&&sprint?value*(1+sprintModifier()):value;
}
public Vec3 getDeltaMovement() { return velocity; }
public void setDeltaMovement(Vec3 v) { velocity=v; }
public void setDeltaMovement(double x,double y,double z) { velocity=new Vec3(x,y,z); }
protected float getBlockJumpFactor() { return plane.surface.getBlock().getJumpFactor(); }
public void liquidJump() { jumpInLiquid(isInWater()?FluidTags.WATER:FluidTags.LAVA); }
public BlockPos getBlockPosBelowThatAffectsMyMovement() { return new BlockPos(0,-1,0); }
public void move(MoverType type,Vec3 requested) {
try {
var dimensions=getDefaultDimensions(Pose.STANDING);
var bounds=dimensions.makeBoundingBox(coordinates);
var moved=(Vec3)COLLIDE.invoke(null,requested,bounds,obstacles);
horizontalCollision=moved.x!=requested.x||moved.z!=requested.z;
verticalCollision=moved.y!=requested.y;
grounded=verticalCollision&&requested.y<0;
coordinates=coordinates.add(moved);
field(this,Entity.class,"position",coordinates);
if(moved.x!=requested.x)velocity=new Vec3(0,velocity.y,velocity.z);
if(moved.y!=requested.y)velocity=new Vec3(velocity.x,0,velocity.z);
if(moved.z!=requested.z)velocity=new Vec3(velocity.x,velocity.y,0);
} catch (ReflectiveOperationException e) { throw new RuntimeException(e); }
}
}
static ProbePlayer player(String surface,boolean sprint,double y) throws Exception {
var level=(PlaneLevel)ALLOCATOR.allocateInstance(PlaneLevel.class);
level.surface=block(surface).defaultBlockState();
var player=(ProbePlayer)ALLOCATOR.allocateInstance(ProbePlayer.class);
player.plane=level; player.grounded=y==0; player.sprint=sprint;
player.coordinates=new Vec3(0,y,0); player.velocity=new Vec3(0,y==0?-0.0784000015258789:0,0);
player.defaults=Player.createAttributes().build();
player.testAbilities=new Abilities();
player.obstacles=List.of(Shapes.create(new AABB(-1000,-1,-1000,1000,0,1000)));
level.obstacles=player.obstacles; level.border=new WorldBorder();
field(player,Entity.class,"position",player.coordinates);
field(player,Entity.class,"type",EntityTypes.PLAYER);
field(player,Entity.class,"level",level);
field(player,Entity.class,"blockPosition",BlockPos.ZERO);
field(player,Player.class,"abilities",player.testAbilities);
return player;
}
static JsonObject trajectory(String surface,boolean sprint,boolean jump,double y,int moveTicks,int ticks) throws Exception {
return trajectory(surface,sprint,jump,y,moveTicks,ticks,"air",0,false);
}
static JsonObject trajectory(String surface,boolean sprint,boolean jump,double y,int moveTicks,int ticks,String medium,double fluidDepth,boolean flying) throws Exception {
var player=player(surface,sprint,y);
player.medium=medium; player.fluidDepth=fluidDepth; player.testAbilities.flying=flying;
var out=new JsonObject(); out.addProperty("surface",surface); out.addProperty("sprint",sprint);
out.addProperty("medium",medium); out.addProperty("fluid_depth",fluidDepth); out.addProperty("flying",flying);
out.addProperty("jump_first_tick",jump); out.addProperty("input_ticks",moveTicks);
out.add("initial_position",vector(player.coordinates)); out.add("initial_velocity",vector(player.velocity));
var samples=new JsonArray();
for(int tick=1;tick<=ticks;tick++) {
// Match the documented aiStep input boundary. These two threshold rules
// are harness preparation, not claimed as a full original aiStep call.
var v=player.velocity;
double x=v.x,z=v.z;
if(v.horizontalDistanceSqr()<9e-6) { x=0; z=0; }
player.velocity=new Vec3(x,Math.abs(v.y)<.003?0:v.y,z);
if(jump&&tick==1) {
if("air".equals(medium)) player.jumpFromGround(); else player.liquidJump();
}
player.travel(new Vec3(0,0,tick<=moveTicks?(double).98f:0));
var sample=new JsonObject(); sample.addProperty("tick",tick);
sample.add("position",vector(player.coordinates)); sample.add("velocity",vector(player.velocity));
sample.addProperty("on_ground",player.grounded); samples.add(sample);
}
out.add("samples",samples); return out;
}
static JsonObject callbacks() throws Exception {
var root=new JsonObject(); var bounces=new JsonArray();
var bounce=Entity.class.getDeclaredMethod("restituteMovementAfterCollisions",BlockState.class,boolean.class,boolean.class,Vec3.class);
bounce.setAccessible(true);
for(String name:List.of("stone","slime_block","white_bed"))
for(double incoming:List.of(-.0784000015258789,-.3,-1.0))
for(double fraction:List.of(0.0,.25,.9)) {
var p=player(name,false,0); p.velocity=new Vec3(.1,incoming,.2);
p.verticalCollision=true; p.verticalCollisionBelow=true;
bounce.invoke(p,block(name).defaultBlockState(),false,false,new Vec3(.1,incoming*fraction,.2));
var v=new JsonObject(); v.addProperty("surface",name); v.addProperty("incoming_y",incoming);
v.addProperty("moved_fraction",fraction); v.add("velocity",vector(p.velocity)); bounces.add(v);
}
root.add("vertical_collision_restitution",bounces);
var slides=new JsonArray();
var slide=HoneyBlock.class.getDeclaredMethod("doSlideMovement",Entity.class); slide.setAccessible(true);
for(double incoming:List.of(-.16,-.3,-1.0)) {
var p=player("honey_block",false,10); p.velocity=new Vec3(.1,incoming,.2);
slide.invoke(Blocks.HONEY_BLOCK,p);
var v=new JsonObject(); v.addProperty("incoming_y",incoming); v.add("velocity",vector(p.velocity)); slides.add(v);
}
root.add("honey_slide_callback",slides);
var slimeSteps=new JsonArray();
for(double incoming:List.of(0.0,-.0784000015258789,.05,.2)) {
var p=player("slime_block",false,0); p.velocity=new Vec3(.1,incoming,.2);
Blocks.SLIME_BLOCK.stepOn(p.plane,BlockPos.ZERO,Blocks.SLIME_BLOCK.defaultBlockState(),p);
var v=new JsonObject(); v.addProperty("incoming_y",incoming); v.add("velocity",vector(p.velocity)); slimeSteps.add(v);
}
root.add("slime_step_callback",slimeSteps);
var bubbles=new JsonArray();
for(boolean above:List.of(false,true))for(boolean down:List.of(false,true))for(double incoming:List.of(-1.0,0.0,1.0)) {
var p=player("stone",false,10); p.velocity=new Vec3(.1,incoming,.2);
// Null private level suppresses the callback's optional server particles.
field(p,Entity.class,"level",null);
if(above)p.onAboveBubbleColumn(down,BlockPos.ZERO); else p.onInsideBubbleColumn(down);
var v=new JsonObject(); v.addProperty("above",above); v.addProperty("down",down);
v.addProperty("incoming_y",incoming); v.add("velocity",vector(p.velocity)); bubbles.add(v);
}
root.add("bubble_column_callback",bubbles); return root;
}
static JsonArray currentCases() throws Exception {
var result=new JsonArray();
var tracker=Class.forName("net.minecraft.world.entity.EntityFluidInteraction$Tracker");
var constructor=tracker.getDeclaredConstructor(); constructor.setAccessible(true);
var accumulate=tracker.getDeclaredMethod("accumulateCurrent",Vec3.class); accumulate.setAccessible(true);
var apply=tracker.getDeclaredMethod("applyCurrentTo",Entity.class,double.class); apply.setAccessible(true);
for(double incomingX:List.of(0.0,.01))for(double currentX:List.of(.001,.01,1.0)) {
var p=player("stone",false,10); p.velocity=new Vec3(incomingX,0,0);
var state=constructor.newInstance(); accumulate.invoke(state,new Vec3(currentX,0,0));
apply.invoke(state,p,.014);
var v=new JsonObject(); v.addProperty("incoming_x",incomingX); v.addProperty("current_x",currentX);
v.addProperty("strength",.014); v.add("velocity",vector(p.velocity)); result.add(v);
}
return result;
}
static JsonObject collisionCase(String label,double y,boolean grounded,Vec3 requested,List<AABB> boxes) throws Exception {
var p=player("stone",false,y); p.grounded=grounded;
p.plane.obstacles=boxes.stream().map(Shapes::create).toList();
field(p,Entity.class,"bb",p.getDefaultDimensions(Pose.STANDING).makeBoundingBox(p.coordinates));
var method=Entity.class.getDeclaredMethod("collide",Vec3.class); method.setAccessible(true);
var result=(Vec3)method.invoke(p,requested);
var out=new JsonObject(); out.addProperty("name",label); out.addProperty("on_ground",grounded);
out.add("position",vector(p.coordinates)); out.add("requested",vector(requested)); out.add("result",vector(result));
out.addProperty("step_height",(double)p.maxUpStep());
var geometry=new JsonArray();
for(var b:boxes) {
var object=new JsonObject(); object.add("min",vector(new Vec3(b.minX,b.minY,b.minZ)));
object.add("max",vector(new Vec3(b.maxX,b.maxY,b.maxZ))); geometry.add(object);
}
out.add("boxes",geometry); return out;
}
static JsonArray collisionCases() throws Exception {
var result=new JsonArray();
var floor=new AABB(-10,-1,-10,10,0,10);
var slab=new AABB(.8,0,-1,1.8,.5,1);
var full=new AABB(.8,0,-1,1.8,1,1);
var thin=new AABB(.8,0,-1,1.8,.0625,1);
var ceiling=new AABB(-1,2.3,-1,2,2.4,1);
var wall=new AABB(-1,0,.8,2,2,1.8);
result.add(collisionCase("half_slab",0,true,new Vec3(.8,-.0784,0),List.of(floor,slab)));
result.add(collisionCase("half_slab_low_ceiling",0,true,new Vec3(.8,-.0784,0),List.of(floor,slab,ceiling)));
result.add(collisionCase("full_block",0,true,new Vec3(.8,-.0784,0),List.of(floor,full)));
result.add(collisionCase("thin_step",0,true,new Vec3(.8,-.0784,0),List.of(floor,thin)));
result.add(collisionCase("lowest_improving_step",0,true,new Vec3(.8,-.0784,0),List.of(floor,new AABB(.4,0,-1,1.4,.125,1),slab)));
result.add(collisionCase("descending_into_step",.2,false,new Vec3(.8,-.4,0),List.of(floor,slab)));
result.add(collisionCase("corner_major_z",0,true,new Vec3(.8,-.0784,.9),List.of(floor,slab,wall)));
result.add(collisionCase("airborne_no_step",0,false,new Vec3(.8,.1,0),List.of(floor,slab)));
return result;
}
public static void main(String[] args) throws Exception {
SharedConstants.tryDetectVersion(); Bootstrap.bootStrap();
var root=new JsonObject(); root.addProperty("version","26.2");
var surfaces=new JsonObject();
for(String name:List.of("stone","ice","packed_ice","blue_ice","frosted_ice","slime_block","honey_block","soul_sand","soul_soil","white_bed","water","lava","cobweb","powder_snow","ladder","scaffolding")) {
var b=block(name); var values=new JsonObject();
values.addProperty("friction",(double)b.getFriction());
values.addProperty("speed_factor",(double)b.getSpeedFactor());
values.addProperty("jump_factor",(double)b.getJumpFactor());
values.addProperty("bounce_restitution",(double)b.getBounceRestitution());
surfaces.add(name,values);
}
root.add("surfaces",surfaces);
var attrs=new JsonObject(); var defaults=Player.createAttributes().build();
for(var holder:BuiltInRegistries.ATTRIBUTE.listElements().toList())
if(defaults.hasAttribute(holder))attrs.addProperty(holder.unwrapKey().get().identifier().toString(),defaults.getBaseValue(holder));
root.add("player_attributes",attrs);
root.addProperty("sprint_attribute_modifier",sprintModifier());
var poses=new JsonObject(); var actor=player("stone",false,0);
for(var pose:List.of(Pose.STANDING,Pose.CROUCHING,Pose.SWIMMING,Pose.FALL_FLYING,Pose.SLEEPING)) {
var d=actor.getDefaultDimensions(pose); var values=new JsonObject();
values.addProperty("width",(double)d.width()); values.addProperty("height",(double)d.height());
values.addProperty("eye_height",(double)d.eyeHeight()); poses.add(pose.name().toLowerCase(Locale.ROOT),values);
}
root.add("poses",poses);
var fluids=new JsonObject();
for(String name:List.of("water","lava")) {
var levels=new JsonArray();
for(int level=0;level<=15;level++) {
var state=block(name).defaultBlockState().setValue(net.minecraft.world.level.block.state.properties.BlockStateProperties.LEVEL,level);
var world=new SampleWorld(state); var fluid=state.getFluidState(); var values=new JsonObject();
values.addProperty("level",level); values.addProperty("height",(double)fluid.getHeight(world,BlockPos.ZERO));
values.addProperty("source",fluid.isSource()); levels.add(values);
}
fluids.add(name,levels);
}
root.add("fluid_levels",fluids);
var trajectories=new JsonObject();
trajectories.add("stone_walk_stop",trajectory("stone",false,false,0,20,40));
trajectories.add("stone_sprint_stop",trajectory("stone",true,false,0,20,40));
trajectories.add("stone_jump",trajectory("stone",false,true,0,0,16));
trajectories.add("stone_sprint_jump",trajectory("stone",true,true,0,15,20));
trajectories.add("ice_walk_stop",trajectory("ice",false,false,0,20,40));
trajectories.add("blue_ice_walk_stop",trajectory("blue_ice",false,false,0,20,40));
trajectories.add("water_move_stop",trajectory("stone",false,false,10,20,40,"water",1,false));
trajectories.add("water_sprint_stop",trajectory("stone",true,false,10,20,40,"water",1,false));
trajectories.add("water_jump",trajectory("stone",false,true,10,0,12,"water",1,false));
trajectories.add("lava_deep_move_stop",trajectory("stone",false,false,10,20,40,"lava",1,false));
trajectories.add("lava_shallow_move_stop",trajectory("stone",false,false,10,20,40,"lava",.2,false));
trajectories.add("creative_fly_move_stop",trajectory("stone",false,false,10,20,40,"air",0,true));
trajectories.add("creative_fly_sprint_stop",trajectory("stone",true,false,10,20,40,"air",0,true));
root.add("travel_kernel_trajectories",trajectories);
root.add("callback_measurements",callbacks());
root.add("collision_cases",collisionCases());
root.add("fluid_current_cases",currentCases());
root.addProperty("measurement_scope","Public block and attribute APIs; original Player.travel, jumpFromGround and Entity.collideWithShapes execute in an isolated flat-plane harness. Player constructors and server constructors are bypassed. The harness supplies inputs, threshold preparation, the measured sprint attribute modifier, constant medium/depth and flat-plane movement bookkeeping. Collision cases call original Entity.collide including step selection on explicit boxes. Collision restitution, honey slide, slime step, bubble column and fluid current application are invoked separately. This is not an original full game tick, multiplayer, fluid-world sampling, or automatic callback-dispatch measurement. The water sprint case intentionally leaves swimming pose false to isolate travelInWater.");
Files.writeString(Path.of(args[0]),GSON.toJson(root)+"\n");
System.out.println("Measured "+surfaces.size()+" surfaces, "+poses.size()+" poses, "+trajectories.size()+" original travel-kernel trajectories.");
}
}
+160
View File
@@ -0,0 +1,160 @@
#!/usr/bin/env python3
"""Bundle selected pixel block textures with the local Shacraft packages.
Uses only the Python standard library. Source images are copied verbatim; this
does not generate art, resample pixels, or infer distribution rights.
"""
import argparse
import hashlib
import json
from pathlib import Path
import re
import shutil
import struct
import tempfile
ROOT = Path(__file__).resolve().parents[1]
DEFAULT_TEXTURES = (
"stone cobblestone oak_planks oak_log oak_log_top dirt grass_block_top "
"grass_block_side sand gravel bricks stone_bricks glass oak_leaves "
"diamond_ore iron_ore coal_ore gold_ore redstone_ore deepslate obsidian "
"snow netherrack oak_door_bottom"
).split()
MARKER = ".shacraft-texture-bundle.json"
def write_json(path, data):
path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
def resource(path, package, role):
data = path.read_bytes()
return {
"path": path.relative_to(package).as_posix(),
"scope": "client",
"role": role,
"size": len(data),
"sha256": hashlib.sha256(data).hexdigest(),
}
def texture_bytes(path, pixel_size=32):
data = path.read_bytes()
if (
len(data) < 33
or data[:16] != b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR"
or struct.unpack(">II", data[16:24]) != (pixel_size, pixel_size)
):
raise ValueError(f"Expected a real {pixel_size}x{pixel_size} PNG: {path}")
if len(data) > 16 * 1024 * 1024:
raise ValueError(f"Texture exceeds the server resource limit: {path}")
return data
def prepare(args):
source = args.source.resolve()
output = args.output.resolve()
if source == output or source.is_relative_to(output):
raise ValueError("The output must not contain the source pack.")
if output == ROOT / "packages" or (ROOT / "packages").is_relative_to(output):
raise ValueError("The output must not replace the bundled core packages.")
if output.exists():
if not args.overwrite:
raise ValueError("Output already exists; use --overwrite to rebuild it.")
marker = output / MARKER
if not marker.is_file() or json.loads(marker.read_text()).get("generator") != "prepare_texture_pack.py":
raise ValueError("Refusing to replace a directory not created by this script.")
if not re.fullmatch(r"[a-z0-9_.-]+", args.id):
raise ValueError("Package id must use lowercase letters, digits, '.', '_' or '-'.")
if args.id in {"shacraft.base", "shacraft.trampoline"}:
raise ValueError("The texture package needs its own id.")
if not re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+", args.version):
raise ValueError("Package version must contain three numeric components.")
if not args.name.strip() or len(args.name) > 120:
raise ValueError("Texture pack name must contain 1 to 120 characters.")
if not args.license or len(args.license) > 256 or any(ord(c) < 32 for c in args.license):
raise ValueError("Supply a nonempty license or local-use notice (at most 256 characters).")
names = args.textures or DEFAULT_TEXTURES
if not 1 <= len(names) <= 127 or len(set(names)) != len(names):
raise ValueError("Choose 1 to 127 unique texture names.")
if any(not re.fullmatch(r"[a-z0-9_]+", name) for name in names):
raise ValueError("Texture names must be plain Minecraft block texture names.")
source_textures = source / "assets/minecraft/textures/block"
# Validate every selected source before touching an existing output bundle.
textures = [(name, texture_bytes(source_textures / f"{name}.png", args.pixel_size)) for name in names]
output.parent.mkdir(parents=True, exist_ok=True)
stage = Path(tempfile.mkdtemp(prefix=f".{output.name}-", dir=output.parent))
backup = None
try:
for name in ("base", "trampoline"):
shutil.copytree(ROOT / "packages" / name, stage / name)
package = stage / f"pixel{args.pixel_size}"
images = package / "client/textures"
images.mkdir(parents=True)
entries, resources = [], []
for name, data in textures:
path = images / f"{name}.png"
path.write_bytes(data)
entries.append({"name": name, "path": path.relative_to(package).as_posix()})
resources.append(resource(path, package, "texture"))
style = package / "client/style.json"
write_json(style, {"schema": 1, "texture_pack": {
"name": args.name, "pixel_size": args.pixel_size, "textures": entries,
}})
resources.append(resource(style, package, "client-style"))
base_manifest = json.loads((stage / "base/manifest.json").read_text())
write_json(package / "manifest.json", {
"schema": 1, "id": args.id, "version": args.version,
"license": args.license,
"dependencies": [{"id": base_manifest["id"], "version": base_manifest["version"]}],
"capabilities": ["client.texture", "client.style"],
"resources": resources,
})
write_json(stage / MARKER, {
"generator": "prepare_texture_pack.py", "schema": 1,
"package": args.id, "name": args.name, "textures": len(entries), "pixel_size": args.pixel_size,
"texture_bytes": sum(len(data) for _, data in textures),
})
if output.exists():
backup = Path(tempfile.mkdtemp(prefix=f".{output.name}-old-", dir=output.parent))
backup.rmdir()
output.rename(backup)
try:
stage.rename(output)
except OSError:
if backup is not None:
backup.rename(output)
backup = None
raise
if backup is not None:
shutil.rmtree(backup)
print(f"Prepared {len(entries)} textures ({sum(len(data) for _, data in textures):,} PNG bytes)")
print(f"Package bundle: {output}")
finally:
if stage.exists():
shutil.rmtree(stage)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--source", type=Path, required=True, help="Unpacked Minecraft-format resource pack")
parser.add_argument("--output", type=Path, required=True, help="Output bundle for the server --packages option")
parser.add_argument("--name", default="Shacraft Pixel32 Study")
parser.add_argument("--id", default="shacraft.pixel32.study")
parser.add_argument("--version", default="1.0.0")
parser.add_argument("--pixel-size", type=int, choices=(16, 32, 64, 128), default=32,
help="Required native PNG dimensions (default: 32)")
parser.add_argument("--license", default="Local reference-derived study; redistribution rights not granted")
parser.add_argument("--textures", nargs="+", help="Selected texture names without .png (defaults to the 24-texture study)")
parser.add_argument("--overwrite", action="store_true", help="Replace a bundle previously created by this script")
args = parser.parse_args()
try:
prepare(args)
except (OSError, ValueError) as error:
parser.exit(1, f"Texture pack preparation failed: {error}\n")
if __name__ == "__main__":
main()
+262
View File
@@ -0,0 +1,262 @@
#!/usr/bin/env python3
"""Prepare a local test atlas from an existing Java client JAR (no downloads).
Keeps original pixel colors; smaller sprites use integer nearest-neighbor
scaling. Animated sprites use their first declared frame. Face materials are
projected onto Shacraft's existing geometry, not a replacement model renderer.
Requires Pillow. Generated assets must not be redistributed without permission.
"""
import argparse
from functools import lru_cache
import hashlib
from io import BytesIO
import json
import math
from pathlib import Path
import shutil
import zipfile
from PIL import Image
ROOT = Path(__file__).resolve().parents[1]
PREFIX = 'assets/minecraft/'
DIRECTIONS = ['east', 'west', 'up', 'down', 'south', 'north']
NORMALS = [(1, 0, 0), (-1, 0, 0), (0, 1, 0), (0, -1, 0), (0, 0, 1), (0, 0, -1)]
def norm(name):
return name.removeprefix('minecraft:')
def projection(direction, p):
x, y, z = p
return [(1-z, 1-y), (z, 1-y), (x, z), (x, 1-z), (x, 1-y), (1-x, 1-y)][direction]
def rotate(p, x, y):
# Java blockstate rotations are clockwise around the block center.
x, y = math.radians(-x), math.radians(-y)
a, b, c = p
b, c = b*math.cos(x)-c*math.sin(x), b*math.sin(x)+c*math.cos(x)
return (a*math.cos(y)+c*math.sin(y), b, -a*math.sin(y)+c*math.cos(y))
def condition(when, props):
if 'OR' in when:
return any(condition(c, props) for c in when['OR'])
if 'AND' in when:
return all(condition(c, props) for c in when['AND'])
return all(props.get(k) in str(v).split('|') for k, v in when.items())
class Compiler:
def __init__(self, jar, images):
self.jar, self.images = jar, images
@lru_cache(None)
def read(self, path):
return json.loads(self.jar.read(PREFIX + path))
@lru_cache(None)
def model(self, name):
data = self.read('models/' + norm(name) + '.json')
parent = data.get('parent')
base = self.model(parent) if parent and 'builtin/' not in parent else {}
return {**base, **data, 'textures': {**base.get('textures', {}), **data.get('textures', {})}}
def resolve(self, value, textures):
seen = set()
while isinstance(value, str) and value.startswith('#') and value not in seen:
seen.add(value)
value = textures.get(value[1:])
if isinstance(value, dict):
value = value.get('sprite')
if isinstance(value, str) and norm(value).startswith('block/'):
name = norm(value).removeprefix('block/')
if name in self.images:
return name
return None
@lru_cache(None)
def model_faces(self, name, rx=0, ry=0, uvlock=False):
model = self.model(name)
textures = model.get('textures', {})
result, areas = [None]*6, [-1]*6
for element in model.get('elements', []):
lo = [v/16 for v in element['from']]
hi = [v/16 for v in element['to']]
for direction, data in element.get('faces', {}).items():
texture = self.resolve(data.get('texture'), textures)
if not texture or texture == 'grass_block_side_overlay':
continue # The opaque grass side already includes its edge.
face = DIRECTIONS.index(direction)
normal = tuple(round(v) for v in rotate(NORMALS[face], rx, ry))
dest = NORMALS.index(normal)
axes = [i for i, v in enumerate(NORMALS[face]) if not v]
area = math.prod(abs(hi[i]-lo[i]) for i in axes)
if area <= areas[dest]:
continue
areas[dest] = area
corners = [projection(face, p) for p in [lo, hi]]
low = [min(c[i] for c in corners) for i in range(2)]
high = [max(c[i] for c in corners) for i in range(2)]
rect = [v/16 for v in data.get('uv', [low[0]*16, low[1]*16, high[0]*16, high[1]*16])]
def uv_at(world):
# Inverse rotation is the transpose of the orthogonal basis.
offset = [v-.5 for v in world]
basis = [rotate(axis, rx, ry) for axis in [(1, 0, 0), (0, 1, 0), (0, 0, 1)]]
local = [.5 + sum(a*b for a, b in zip(offset, axis)) for axis in basis]
uv = projection(face, local)
if uvlock:
return projection(dest, world)
uv = [(uv[i]-low[i])/max(high[i]-low[i], 1e-6) for i in range(2)]
for _ in range(data.get('rotation', 0)//90):
uv = [uv[1], 1-uv[0]]
return [rect[i] + uv[i]*(rect[i+2]-rect[i]) for i in range(2)]
zero = uv_at([0, 0, 0])
unit = [uv_at(p) for p in [[1, 0, 0], [0, 1, 0], [0, 0, 1]]]
transform = [round(v, 6) for i in range(2) for v in [*(p[i]-zero[i] for p in unit), zero[i]]]
result[dest] = {'texture': texture, 'uv': transform, 'tinted': 'tintindex' in data}
particle = self.resolve(textures.get('particle'), textures)
fallback = next((v for v in result if v), None)
for i, face in enumerate(result):
if face is None:
texture = particle or (fallback and fallback['texture'])
result[i] = {'texture': texture, 'tinted': bool(fallback and fallback['tinted'])} if texture else None
return result
def faces(self, block, props):
definition = self.read('blockstates/' + norm(block) + '.json')
models = []
for selector, model in definition.get('variants', {}).items():
when = dict(part.split('=', 1) for part in selector.split(',') if part)
if condition(when, props):
models.append(model[0] if isinstance(model, list) else model)
break
for part in definition.get('multipart', []):
if condition(part.get('when', {}), props):
model = part['apply']
models.append(model[0] if isinstance(model, list) else model)
faces = [None]*6
for model in models:
incoming = self.model_faces(model['model'], model.get('x', 0), model.get('y', 0), model.get('uvlock', False))
faces = [a or b for a, b in zip(faces, incoming)]
result = []
for face in faces:
if not face:
result.append(None)
continue
face = face.copy()
tinted = face.pop('tinted', False)
texture = face['texture']
if tinted:
tint = [0.58, 0.8, 0.34]
if 'leaves' in block or 'vine' in block:
tint = [0.46, 0.7, 0.28]
if 'spruce' in block:
tint = [0.38, 0.60, 0.38]
if block == 'minecraft:redstone_wire':
power = int(props.get('power', '0'))/15
tint = [.3+.7*power, max(0, power*power*.7-.5), 0]
face['tint'] = tint
if block in ('minecraft:water', 'minecraft:bubble_column'):
face['tint'] = [0.25, 0.46, 0.9]
alpha = self.images[texture].getchannel('A').getextrema()
face['cutout'] = alpha[0] == 0 and alpha[1] == 255
result.append(face)
return result
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--jar', type=Path, required=True)
parser.add_argument('--states', type=Path, required=True, help='Matching generated/reports/blocks.json')
parser.add_argument('--output', type=Path, required=True)
args = parser.parse_args()
output = args.output.resolve()
if output.exists():
parser.error('Output already exists; choose a new directory.')
jar = zipfile.ZipFile(args.jar)
images, animated, native_sizes = {}, [], {}
for path in sorted(jar.namelist()):
if not path.startswith(PREFIX + 'textures/block/') or not path.endswith('.png'):
continue
name = Path(path).stem
image = Image.open(BytesIO(jar.read(path))).convert('RGBA')
metadata = json.loads(jar.read(path + '.mcmeta')).get('animation', {}) if path + '.mcmeta' in jar.namelist() else {}
width = metadata.get('width', min(image.size))
height = metadata.get('height', width)
frames = metadata.get('frames', [0])
first = frames[0] if frames else 0
index = first.get('index', 0) if isinstance(first, dict) else first
x, y = index % (image.width//width)*width, index // (image.width//width)*height
frame = image.crop((x, y, x+width, y+height))
native_sizes[name] = [width, height]
if metadata or image.size != frame.size:
animated.append(name)
images[name] = frame.resize((32, 32), Image.Resampling.NEAREST)
compiler = Compiler(jar, images)
report = json.loads(args.states.read_text())
sets, lookup, states, defaults, uncovered = [], {}, {}, {}, []
for block, definition in report.items():
for state in definition['states']:
props = state.get('properties', {})
try:
faces = compiler.faces(block, props)
except KeyError:
faces = [None]*6
key = json.dumps(faces, separators=(',', ':'), sort_keys=True)
if key not in lookup:
lookup[key] = len(sets)
sets.append(faces)
canonical = block + ('['+','.join(f'{k}={v}' for k, v in sorted(props.items()))+']' if props else '')
states[canonical] = lookup[key]
if state.get('default'):
defaults[block] = lookup[key]
if not any(faces):
uncovered.append(block)
output.mkdir(parents=True)
for base in ('base', 'trampoline'):
shutil.copytree(ROOT/'packages'/base, output/base)
package = output/'vanilla'
(package/'client').mkdir(parents=True)
columns = 40
rows = math.ceil(len(images)/columns)
atlas = Image.new('RGBA', (columns*32, rows*32))
for i, image in enumerate(images.values()):
atlas.paste(image, (i % columns*32, i//columns*32))
atlas.save(package/'client/atlas.png', optimize=True)
descriptor = {'schema': 1, 'texture_pack': {
'name': 'Minecraft 26.2 Original — Local Test', 'pixel_size': 32,
'atlas': {'path': 'client/atlas.png', 'columns': columns, 'rows': rows},
'textures': [{'name': name} for name in images],
'grass_overlay': {'base': 'grass_block_side', 'overlay': 'grass_block_side_overlay'},
'block_faces': {'sets': sets, 'states': states, 'defaults': defaults},
}}
(package/'client/style.json').write_text(json.dumps(descriptor, separators=(',', ':'))+'\n')
resources = []
for path, role in [('client/atlas.png', 'texture'), ('client/style.json', 'client-style')]:
data = (package/path).read_bytes()
assert len(data) < 16*1024*1024
resources.append({'path': path, 'scope': 'client', 'role': role, 'size': len(data), 'sha256': hashlib.sha256(data).hexdigest()})
base = json.loads((output/'base/manifest.json').read_text())
manifest = {'schema': 1, 'id': 'shacraft.vanilla.local', 'version': '1.0.1',
'license': 'Minecraft assets copyright Mojang/Microsoft. Local testing only; no redistribution rights granted.',
'dependencies': [{'id': base['id'], 'version': base['version']}],
'capabilities': ['client.texture', 'client.style'], 'resources': resources}
(package/'manifest.json').write_text(json.dumps(manifest, indent=2)+'\n')
receipt = {'textures': len(images), 'states': len(states), 'face_sets': len(sets),
'source_sha256': hashlib.sha256(args.jar.read_bytes()).hexdigest(),
'native_sizes': dict((str(size), list(native_sizes.values()).count(size)) for size in native_sizes.values()),
'animated_first_frame': animated, 'untextured_default_blocks': uncovered,
'atlas_bytes': (package/'client/atlas.png').stat().st_size,
'mapping_bytes': (package/'client/style.json').stat().st_size,
'limitations': ['Static first animation frame', 'Existing engine geometry; complex multipart and entity-rendered blocks are approximated', 'Fixed foliage tint rather than biome colors']}
(output/'receipt.json').write_text(json.dumps(receipt, indent=2)+'\n')
print(json.dumps(receipt, indent=2))
if __name__ == '__main__':
main()
+5 -2
View File
@@ -2,10 +2,13 @@
set -euo pipefail
cd "$(dirname "${BASH_SOURCE[0]}")/.."
cargo fmt --all -- --check
rustfmt --edition 2024 --check crates/shacraft-server/src/control.rs
rustfmt --edition 2024 --check crates/shacraft-server/src/control.rs crates/shacraft-server/src/world_streaming.rs
bash scripts/build_physics.sh
cargo clippy --workspace --all-targets --locked -- -D warnings
cargo test --workspace --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
node scripts/check_server.mjs --binary target/debug/shacraft-server --port "${SHACRAFT_TEST_PORT:-4011}" --output artifacts/server-e2e-debug.json
node scripts/check_chunk_streaming.mjs --binary target/debug/shacraft-server --port "${SHACRAFT_TEST_PORT:-4011}" --output artifacts/chunk-streaming/network-report.json
node scripts/check_player_physics.mjs --binary target/debug/shacraft-server --port "${SHACRAFT_TEST_PORT:-4011}" --output artifacts/physics/network-report.json