Expand material support and checkpoint the completed building toolkit
Expose the runtime block/item registry through compact material search and single-material descriptions. Preserve private block-entity data through checked edits and durable undo without sending payloads to model context. Include the completed terrain tools, isolated world/map plugin, station lift, ACP streaming and guidance fixes, local camera auto-connect, construction scripts, and their public documentation, references and verification records. Active station decoration and private runtime data remain outside this commit. Validation: 145 Maven tests, 47 Bridge tests, successful camera Gradle build, and isolated Paper verification of all 1,196 block defaults plus 5,392 independent property cases for placement, same-material edits and restoration.
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
package probe;
|
||||
import org.bukkit.*;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.command.*;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
import java.lang.reflect.*;
|
||||
import java.nio.file.*;
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
|
||||
/** Test-only, console-only helper; never installed in the user's lobby server. */
|
||||
public final class MaterialRegistryProbe extends JavaPlugin {
|
||||
private final List<Map<String,Object>> failures = new ArrayList<>();
|
||||
private int failureCount;
|
||||
@Override public void onEnable() { getLogger().info("Use console registryprobe to run the isolated registry test"); }
|
||||
@Override public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
|
||||
if (sender instanceof ConsoleCommandSender) runProbe(); return true;
|
||||
}
|
||||
private static Method method(Class<?> c, String name, Class<?>... args) throws Exception {
|
||||
Method result=c.getDeclaredMethod(name,args); result.setAccessible(true); return result;
|
||||
}
|
||||
private static Object call(Method m, Object receiver, Object... args) throws Throwable {
|
||||
try { return m.invoke(receiver,args); } catch(InvocationTargetException e) { throw e.getCause(); }
|
||||
}
|
||||
private static void equal(Object expected,Object actual) { if (!Objects.equals(expected,actual)) throw new AssertionError("snapshot_mismatch"); }
|
||||
private void failure(String id,String state,String phase,Throwable error) {
|
||||
failureCount++;
|
||||
if(failures.size()<128) failures.add(Map.of("block",id,"state",state,"phase",phase,"reason",error.getClass().getSimpleName()));
|
||||
}
|
||||
private void runProbe() {
|
||||
long start=System.nanoTime(); failures.clear(); failureCount=0;
|
||||
int total=0,passed=0,entityDefaults=0,catalogDescribed=0,registryCount=0,maxDescriptionBytes=0,maxProperties=0,maxPropertyValues=0;
|
||||
int propertyCases=0,propertyPassed=0,freshPassed=0,sameMaterialPassed=0,privateUndoPassed=0;
|
||||
World anchorWorld=null; BlockData anchorBefore=null; boolean anchorRestored=false;
|
||||
String fatal=null; Map<?,?> catalogSummary=Map.of();
|
||||
try {
|
||||
var plugin=Bukkit.getPluginManager().getPlugin("MinecraftBuilderMCP"); var loader=plugin.getClass().getClassLoader();
|
||||
Class<?> accessClass=Class.forName("io.github.minecraftbuilder.paper.BuildingWorld",true,loader);
|
||||
Class<?> catalogClass=Class.forName("io.github.minecraftbuilder.paper.MaterialCatalog",true,loader);
|
||||
Constructor<?> catalogConstructor=catalogClass.getDeclaredConstructor(); catalogConstructor.setAccessible(true);
|
||||
Object catalog=catalogConstructor.newInstance(); Method describe=method(catalogClass,"describe",String.class);
|
||||
catalogSummary=(Map<?,?>)call(method(catalogClass,"summary"),catalog); registryCount=(int)Registry.BLOCK.stream().count();
|
||||
Class<?> posClass=Class.forName("io.github.minecraftbuilder.core.BlockPos",true,loader);
|
||||
Constructor<?> constructor=accessClass.getDeclaredConstructor(World.class); constructor.setAccessible(true);
|
||||
World world=Bukkit.getWorld("world");
|
||||
if(world==null||Bukkit.getPort()!=25576) throw new IllegalStateException("IsolatedServerRequired");
|
||||
world.loadChunk(0,0); Object pos=posClass.getConstructor(int.class,int.class,int.class).newInstance(4,100,4);
|
||||
if(!world.getBlockAt(4,100,4).getType().isAir()||!world.getBlockAt(5,100,4).getType().isAir()) throw new IllegalStateException("AirFixtureRequired");
|
||||
anchorWorld=world; anchorBefore=world.getBlockAt(5,100,4).getBlockData();
|
||||
world.getBlockAt(5,100,4).setBlockData(Material.STONE.createBlockData(),false);
|
||||
Object access=constructor.newInstance(world);
|
||||
Method capture=method(accessClass,"captureBlock",posClass),prepare=method(accessClass,"prepareBlock",posClass,String.class,String.class),place=method(accessClass,"setCapturedBlock",posClass,String.class);
|
||||
String before=(String)call(capture,access,pos);
|
||||
for(Material material:Material.values()) {
|
||||
if(material.isLegacy()||!material.isBlock()) continue;
|
||||
total++; String id=material.getKey().toString(); Map<?,?> properties=Map.of();
|
||||
try {
|
||||
Map<?,?> description=(Map<?,?>)call(describe,catalog,id);
|
||||
maxDescriptionBytes=Math.max(maxDescriptionBytes,new com.google.gson.Gson().toJson(description).getBytes(java.nio.charset.StandardCharsets.UTF_8).length);
|
||||
properties=(Map<?,?>)description.get("properties"); maxProperties=Math.max(maxProperties,properties.size());
|
||||
for(Object values:properties.values()) maxPropertyValues=Math.max(maxPropertyValues,((Collection<?>)values).size());
|
||||
catalogDescribed++;
|
||||
} catch(Throwable e) { failure(id,id,"catalog_description",e); }
|
||||
String defaultState=material.createBlockData().getAsString(),phase="prepare_default",base=null;
|
||||
try {
|
||||
base=(String)call(prepare,access,pos,defaultState,before); if(base.startsWith("\u0000")) entityDefaults++;
|
||||
equal(before,call(capture,access,pos));
|
||||
phase="place_default"; call(place,access,pos,base);
|
||||
phase="verify_default"; equal(base,call(capture,access,pos)); passed++;
|
||||
} catch(Throwable e) { failure(id,defaultState,phase,e); }
|
||||
finally { call(place,access,pos,before); equal(before,call(capture,access,pos)); }
|
||||
if(base==null) continue;
|
||||
Set<String> variants=new LinkedHashSet<>();
|
||||
for(var property:properties.entrySet()) for(Object value:(Collection<?>)property.getValue())
|
||||
variants.add(Bukkit.createBlockData(id+"["+property.getKey()+"="+value+"]").getAsString());
|
||||
for(String variant:variants) {
|
||||
if(++propertyCases>30_000||System.nanoTime()-start>35_000_000_000L) throw new IllegalStateException("ProbeBudgetExceeded");
|
||||
phase="prepare_fresh_property";
|
||||
try {
|
||||
String desired=(String)call(prepare,access,pos,variant,before); equal(before,call(capture,access,pos));
|
||||
phase="place_fresh_property"; call(place,access,pos,desired);
|
||||
phase="verify_fresh_property"; equal(desired,call(capture,access,pos)); freshPassed++;
|
||||
call(place,access,pos,before); equal(before,call(capture,access,pos));
|
||||
phase="place_existing_default"; call(place,access,pos,base); equal(base,call(capture,access,pos));
|
||||
phase="prepare_same_material"; String changed=(String)call(prepare,access,pos,variant,base); equal(base,call(capture,access,pos));
|
||||
phase="place_same_material"; call(place,access,pos,changed);
|
||||
phase="verify_same_material"; equal(changed,call(capture,access,pos)); sameMaterialPassed++;
|
||||
phase="private_undo"; call(place,access,pos,base); equal(base,call(capture,access,pos)); privateUndoPassed++; propertyPassed++;
|
||||
} catch(Throwable e) { failure(id,variant,phase,e); }
|
||||
finally { call(place,access,pos,before); equal(before,call(capture,access,pos)); }
|
||||
}
|
||||
}
|
||||
} catch(Throwable e) { fatal=e.getClass().getSimpleName(); }
|
||||
finally {
|
||||
if(anchorWorld!=null&&anchorBefore!=null) {
|
||||
anchorWorld.getBlockAt(5,100,4).setBlockData(anchorBefore,false);
|
||||
anchorRestored=anchorBefore.equals(anchorWorld.getBlockAt(5,100,4).getBlockData());
|
||||
}
|
||||
}
|
||||
double millis=(System.nanoTime()-start)/1_000_000.0;
|
||||
Map<String,Object> result=new LinkedHashMap<>();
|
||||
result.put("timestamp",Instant.now().toString());result.put("server_version",Bukkit.getVersion());
|
||||
result.put("fixture","Air target (4,100,4); temporary stone section anchor (5,100,4)");result.put("anchor_restored",anchorRestored);
|
||||
result.put("catalog_summary",catalogSummary);result.put("catalog_described",catalogDescribed);result.put("registry_blocks",registryCount);
|
||||
result.put("max_description_bytes",maxDescriptionBytes);result.put("max_properties",maxProperties);result.put("max_property_values",maxPropertyValues);
|
||||
result.put("total",total);result.put("passed",passed);result.put("entity_defaults",entityDefaults);
|
||||
result.put("property_cases",propertyCases);result.put("property_passed",propertyPassed);result.put("fresh_property_passed",freshPassed);
|
||||
result.put("same_material_property_passed",sameMaterialPassed);result.put("private_undo_passed",privateUndoPassed);
|
||||
result.put("duration_ms",millis);result.put("fatal",fatal);result.put("failure_count",failureCount);result.put("failures",failures);
|
||||
try { getDataFolder().mkdirs(); Files.writeString(getDataFolder().toPath().resolve("report.json"),new com.google.gson.GsonBuilder().serializeNulls().create().toJson(result)); }
|
||||
catch(Exception e) { getLogger().severe("Could not save registry probe report"); }
|
||||
getLogger().info("Registry probe: "+passed+"/"+total+", property cases "+propertyPassed+"/"+propertyCases+", failures "+failureCount+", fatal "+fatal+", "+Math.round(millis)+" ms");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
# Isolated material registry regression probe
|
||||
|
||||
This optional test plugin exercises the **installed production plugin** through reflection. It is excluded from Maven modules and never installed by the development server launcher or this builder.
|
||||
|
||||
It verifies every registered block default, every catalog description, and each distinct state obtained by varying one property at a time. Property cases cover fresh placement, changing an existing block of the same material, and exact private snapshot restoration. It also verifies that preparation does not modify the world. This is not a Cartesian enumeration of all property combinations or a test of later game ticks.
|
||||
|
||||
Build from the repository root after the development Paper runtime and JDK have been prepared:
|
||||
|
||||
```bash
|
||||
python3 scripts/material-registry-probe/build.py
|
||||
```
|
||||
|
||||
The only build outputs are under `.runtime/material-registry-probe/`. Use `--paper-home PATH` to select a prepared Paper installation for its dependency libraries and `--java-home PATH` to select another JDK 25 or newer.
|
||||
|
||||
Install **only into a disposable isolated test server**, using the pinned Paper version and the production plugin JAR being tested:
|
||||
|
||||
```bash
|
||||
cp .runtime/material-registry-probe/material-registry-probe.jar .runtime/material-test-server/plugins/
|
||||
```
|
||||
|
||||
Start or restart that isolated server, then enter `registryprobe` in its server console. The plugin does nothing on startup and ignores player invocations. It requires port **25576**, world **`world`**, and air at **(4, 100, 4)** and **(5, 100, 4)**. It temporarily uses the second position as a stone anchor, then restores both positions; the anchor makes `cave_air` and `void_air` observable because Minecraft treats entirely empty sections as ordinary air. Do not use a valuable world just because it happens to match these guards.
|
||||
|
||||
Read `plugins/MaterialRegistryProbe/report.json` in the isolated server directory. A successful report has matching default/property/catalog counts, `anchor_restored: true`, `fatal: null`, and zero failures. Reports contain only material/state identifiers, counts, timings and exception types—no block-entity payloads. The probe caps property cases at 30,000, execution at 35 seconds, and detailed failures at 128. The console command runs on the server thread; the test server should have no players. Remove the test plugin after use.
|
||||
|
||||
The initial Paper 26.2 verification covered 1,196 block defaults, 186 block-entity defaults, and 5,392 distinct property cases in approximately three seconds. These counts are observations, not hardcoded expectations; the probe follows the runtime registry.
|
||||
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build the opt-in isolated Paper regression probe; never install or run it."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
OUTPUT = ROOT / ".runtime" / "material-registry-probe"
|
||||
PLUGIN = """name: MaterialRegistryProbe
|
||||
version: '1.0'
|
||||
main: probe.MaterialRegistryProbe
|
||||
api-version: '26.2'
|
||||
depend: [MinecraftBuilderMCP]
|
||||
commands:
|
||||
registryprobe:
|
||||
description: Run isolated all-registry block snapshot verification
|
||||
"""
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--paper-home", type=Path, default=ROOT / ".runtime" / "server",
|
||||
help="Prepared Paper installation whose libraries supply the compile classpath")
|
||||
parser.add_argument("--java-home", type=Path,
|
||||
default=Path(os.environ.get("MCB_JAVA_HOME", str(Path.home() / ".cache" / "minecraft-builder-mcp" / "jdk-25.0.2"))),
|
||||
help="JDK 25 or newer; defaults to the project's downloaded JDK")
|
||||
args = parser.parse_args()
|
||||
libraries = sorted((args.paper_home / "libraries").rglob("*.jar"))
|
||||
if not libraries:
|
||||
parser.error("No Paper dependency jars found; prepare the local development server first")
|
||||
javac, jar = args.java_home / "bin" / "javac", args.java_home / "bin" / "jar"
|
||||
if not javac.is_file() or not jar.is_file():
|
||||
parser.error("JDK javac/jar unavailable; supply --java-home or MCB_JAVA_HOME")
|
||||
classes = OUTPUT / "classes"
|
||||
if classes.exists():
|
||||
shutil.rmtree(classes)
|
||||
classes.mkdir(parents=True)
|
||||
subprocess.run([str(javac), "--release", "25", "-cp", os.pathsep.join(str(p.resolve()) for p in libraries),
|
||||
"-d", str(classes), str(Path(__file__).with_name("MaterialRegistryProbe.java"))], check=True)
|
||||
(classes / "plugin.yml").write_text(PLUGIN, encoding="utf-8")
|
||||
output = OUTPUT / "material-registry-probe.jar"
|
||||
subprocess.run([str(jar), "--create", "--file", str(output), "-C", str(classes), "."], check=True)
|
||||
print(output)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user