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,38 @@
|
||||
package io.github.minecraftbuilder.paper;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
final class BlockSnapshotsTest {
|
||||
@Test void payloadSurvivesExactlyButPublicViewOnlyContainsStateAndDigest() {
|
||||
String state = "minecraft:chest[facing=north,type=single,waterlogged=false]";
|
||||
String nbt = "{Items:[{id:\"minecraft:written_book\",components:{text:\"private secret\"}}],id:\"minecraft:chest\"}";
|
||||
String encoded = BlockSnapshots.encode(state, nbt);
|
||||
assertEquals(state, BlockSnapshots.state(encoded));
|
||||
assertEquals(nbt, BlockSnapshots.decode(encoded).nbt());
|
||||
var publicValue = BlockSnapshots.publicView(encoded);
|
||||
assertEquals(2, publicValue.size());
|
||||
assertEquals(state, publicValue.get("state"));
|
||||
assertTrue(publicValue.get("snapshot_id").matches("[0-9a-f]{64}"));
|
||||
assertFalse(publicValue.toString().contains("private secret"));
|
||||
assertNotEquals(BlockSnapshots.snapshotId(encoded), BlockSnapshots.snapshotId(BlockSnapshots.encode(state, nbt + " ")));
|
||||
assertEquals(publicValue, BlockSnapshots.publicView(encoded));
|
||||
}
|
||||
@Test void oldPlainJournalStatesStayUnchanged() {
|
||||
String plain = "minecraft:stone";
|
||||
assertEquals(plain, BlockSnapshots.decode(plain).state());
|
||||
assertNull(BlockSnapshots.decode(plain).nbt());
|
||||
assertNull(BlockSnapshots.snapshotId(plain));
|
||||
assertEquals(java.util.Map.of("state", plain), BlockSnapshots.publicView(plain));
|
||||
}
|
||||
@Test void entityBudgetCountsUtf8BytesAndErrorsNeverIncludePrivateText() {
|
||||
String huge = "секрет".repeat(BlockSnapshots.MAX_SNAPSHOT_BYTES / 7);
|
||||
var error = assertThrows(IllegalArgumentException.class, () -> BlockSnapshots.encode("minecraft:chest", huge));
|
||||
assertTrue(error.getMessage().contains("snapshot_budget_exceeded"));
|
||||
assertFalse(error.getMessage().contains("секрет"));
|
||||
}
|
||||
@Test void malformedKnownSnapshotAndAmbiguousStateAreRejected() {
|
||||
assertThrows(IllegalArgumentException.class, () -> BlockSnapshots.decode("\u0000mcb-block-v1:minecraft:chest"));
|
||||
assertThrows(IllegalArgumentException.class, () -> BlockSnapshots.encode("minecraft:chest\n{}", "{}"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package io.github.minecraftbuilder.paper;
|
||||
|
||||
import io.github.minecraftbuilder.core.*;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.util.*;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import javax.imageio.ImageIO;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class BrushPreviewTest {
|
||||
@Test void nativePreviewShowsActualBeforeAfterWithoutWorldWriteClaim() throws Exception {
|
||||
var r=new TerrainBrush.Result(Map.of(new BlockPos(0,2,0),"minecraft:stone"),Set.of(new BlockPos(0,1,0)),
|
||||
new Region("world",new BlockPos(0,0,0),new BlockPos(1,3,1)),new int[][]{{1,1},{1,1}},new int[][]{{2,1},{0,1}},2,1,1);
|
||||
var result=BrushPreview.render(r);assertEquals("live_snapshot",result.get("source"));assertEquals(false,result.get("world_edited"));
|
||||
var image=ImageIO.read(new ByteArrayInputStream(Base64.getDecoder().decode((String)result.get("imageBase64"))));
|
||||
assertEquals(780,image.getWidth());assertEquals(300,image.getHeight());
|
||||
assertNotEquals(image.getRGB(10,40),image.getRGB(270,40));
|
||||
assertEquals(0x89CC70,image.getRGB(530,40)&0xFFFFFF);assertEquals(0xE5A35B,image.getRGB(530,160)&0xFFFFFF);
|
||||
}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
package io.github.minecraftbuilder.paper;
|
||||
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.block.data.Waterlogged;
|
||||
import org.bukkit.block.data.type.Leaves;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.lang.reflect.Proxy;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/** Parsed BlockData stubs test policy; the live registry integration test validates item rejection. */
|
||||
final class BuildingWorldPolicyTest {
|
||||
private static BlockData state(Material material, Class<? extends BlockData> type,
|
||||
boolean waterlogged, boolean persistent) {
|
||||
return (BlockData) Proxy.newProxyInstance(type.getClassLoader(), new Class<?>[]{type},
|
||||
(proxy, method, arguments) -> switch (method.getName()) {
|
||||
case "getMaterial" -> material;
|
||||
case "isWaterlogged" -> waterlogged;
|
||||
case "isPersistent" -> persistent;
|
||||
default -> throw new UnsupportedOperationException(method.getName());
|
||||
});
|
||||
}
|
||||
@Test void leafAndWaterloggedStatesAreNoLongerArtificiallyRestricted() {
|
||||
for (Material material : new Material[]{Material.SPRUCE_LEAVES, Material.OAK_LEAVES})
|
||||
for (boolean wet : new boolean[]{false,true})
|
||||
for (boolean persistent : new boolean[]{false,true})
|
||||
assertTrue(BuildingWorld.supportsData(state(material, Leaves.class, wet, persistent)));
|
||||
for (Material material : new Material[]{Material.SPRUCE_FENCE, Material.CUT_SANDSTONE_SLAB,
|
||||
Material.BARRIER, Material.CHEST})
|
||||
assertTrue(BuildingWorld.supportsData(state(material, Waterlogged.class, true, true)));
|
||||
}
|
||||
@Test void fluidsGrowthAdministrativeAndBlockEntityTypesAreAccepted() {
|
||||
for (Material material : new Material[]{Material.WATER, Material.LAVA, Material.OAK_SAPLING,
|
||||
Material.PEONY, Material.CUT_COPPER, Material.STRUCTURE_VOID, Material.LIGHT,
|
||||
Material.STRUCTURE_BLOCK, Material.JIGSAW, Material.COMMAND_BLOCK,
|
||||
Material.CHAIN_COMMAND_BLOCK, Material.REPEATING_COMMAND_BLOCK, Material.TNT,
|
||||
Material.CHEST, Material.OAK_SIGN, Material.DECORATED_POT, Material.BEEHIVE,
|
||||
Material.TRIAL_SPAWNER, Material.VAULT})
|
||||
assertTrue(BuildingWorld.supportsData(state(material, BlockData.class, false, true)), material.name());
|
||||
}
|
||||
@Test void missingAndLegacyDataDoNotBypassTheParserPolicy() {
|
||||
assertFalse(BuildingWorld.supportsData(null));
|
||||
assertFalse(BuildingWorld.supportsData(state(null, BlockData.class, false, true)));
|
||||
assertFalse(BuildingWorld.supportsData(state(Material.LEGACY_STONE, BlockData.class, false, true)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package io.github.minecraftbuilder.paper;
|
||||
|
||||
import com.google.gson.*;
|
||||
import io.github.minecraftbuilder.core.BlockPos;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.util.*;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class ExpectedBlocksTest {
|
||||
private final BlockPos a = new BlockPos(0,64,0), b = new BlockPos(1,64,0);
|
||||
private JsonArray entries(String... states) {
|
||||
JsonArray result=new JsonArray();
|
||||
for(int x=0;x<states.length;x++) {
|
||||
JsonObject e=new JsonObject(),p=new JsonObject();
|
||||
p.addProperty("x",x);p.addProperty("y",64);p.addProperty("z",0);
|
||||
e.add("pos",p);e.addProperty("state",states[x]);result.add(e);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@Test void rejectsManualChangeBetweenCallerReadAndPreparation() {
|
||||
var snapshot=entries("minecraft:grass_block","minecraft:air");
|
||||
var live=new HashMap<>(Map.of(a,"minecraft:grass_block",b,"minecraft:air"));
|
||||
ExpectedBlocks.check(snapshot,Set.of(a,b),s->s,live::get);
|
||||
live.put(b,"minecraft:gold_block");
|
||||
var error=assertThrows(RpcServer.Fault.class,()->ExpectedBlocks.check(snapshot,Set.of(a,b),s->s,live::get));
|
||||
assertTrue(error.getMessage().contains("Caller snapshot changed"));
|
||||
assertEquals("minecraft:gold_block",live.get(b));
|
||||
}
|
||||
@Test void rejectsIncompleteDuplicateAndOutsideSnapshots() {
|
||||
assertThrows(RpcServer.Fault.class,()->ExpectedBlocks.check(entries("air"),Set.of(a,b),s->s,p->"air"));
|
||||
var duplicate=entries("air","air");duplicate.set(1,duplicate.get(0));
|
||||
assertThrows(RpcServer.Fault.class,()->ExpectedBlocks.check(duplicate,Set.of(a,b),s->s,p->"air"));
|
||||
assertThrows(RpcServer.Fault.class,()->ExpectedBlocks.check(entries("air","air"),Set.of(a,new BlockPos(2,64,0)),s->s,p->"air"));
|
||||
}
|
||||
@Test void canonicalizesExpectedStatesAndRejectsFractionalCoordinates() {
|
||||
ExpectedBlocks.check(entries("grass_block"),Set.of(a),s->"minecraft:"+s,p->"minecraft:grass_block");
|
||||
var fractional=entries("air");fractional.get(0).getAsJsonObject().getAsJsonObject("pos").addProperty("x",.5);
|
||||
assertThrows(RpcServer.Fault.class,()->ExpectedBlocks.check(fractional,Set.of(a),s->s,p->"air"));
|
||||
}
|
||||
@Test void detectsContentsChangeEvenWhenBlockStateIsUnchanged() {
|
||||
var snapshot=entries("minecraft:chest");
|
||||
snapshot.get(0).getAsJsonObject().addProperty("snapshot_id","a".repeat(64));
|
||||
ExpectedBlocks.check(snapshot,Set.of(a),s->s,p->"minecraft:chest",p->"a".repeat(64));
|
||||
var changed=assertThrows(RpcServer.Fault.class,()->ExpectedBlocks.check(snapshot,Set.of(a),s->s,p->"minecraft:chest",p->"b".repeat(64)));
|
||||
assertTrue(changed.getMessage().contains("block-entity snapshot changed"));
|
||||
assertThrows(RpcServer.Fault.class,()->ExpectedBlocks.check(snapshot,Set.of(a),s->s,p->"minecraft:chest",p->null));
|
||||
}
|
||||
@Test void requiresEntityDigestAndRejectsMalformedDigest() {
|
||||
var snapshot=entries("minecraft:chest");
|
||||
assertThrows(RpcServer.Fault.class,()->ExpectedBlocks.check(snapshot,Set.of(a),s->s,p->"minecraft:chest",p->"a".repeat(64)));
|
||||
snapshot.get(0).getAsJsonObject().addProperty("snapshot_id","not-a-hash");
|
||||
assertThrows(RpcServer.Fault.class,()->ExpectedBlocks.check(snapshot,Set.of(a),s->s,p->"minecraft:chest",p->"a".repeat(64)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
package io.github.minecraftbuilder.paper;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class MaterialCatalogTest {
|
||||
private static List<MaterialCatalog.Entry> fixtures() {
|
||||
List<MaterialCatalog.Entry> entries = new ArrayList<>();
|
||||
IntStream.range(0, 70).forEach(i -> entries.add(new MaterialCatalog.Entry("minecraft:block_%02d".formatted(i), true, true)));
|
||||
entries.add(new MaterialCatalog.Entry("minecraft:water", true, false));
|
||||
entries.add(new MaterialCatalog.Entry("minecraft:iron_pickaxe", false, true));
|
||||
entries.add(new MaterialCatalog.Entry("minecraft:waxed_oxidized_copper_stairs", true, true));
|
||||
entries.add(new MaterialCatalog.Entry("minecraft:oxidized_copper_bulb", true, true));
|
||||
entries.add(new MaterialCatalog.Entry("minecraft:cut_copper_stairs", true, true));
|
||||
return entries;
|
||||
}
|
||||
|
||||
private MaterialCatalog catalog() {
|
||||
return new MaterialCatalog("26.2-test", fixtures(), id ->
|
||||
new MaterialCatalog.StateDescription(id + "[waterlogged=false]", Map.of("waterlogged", List.of("true", "false")), List.of("waterloggable")));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static List<Map<String, Object>> results(Map<String, Object> page) {
|
||||
return (List<Map<String, Object>>) page.get("results");
|
||||
}
|
||||
|
||||
@Test void summaryIsTinyAndContainsCountsRatherThanIds() {
|
||||
var summary = catalog().summary();
|
||||
assertEquals(75, summary.get("materials"));
|
||||
assertEquals(74L, summary.get("blocks"));
|
||||
assertEquals(74L, summary.get("items"));
|
||||
assertEquals(16, summary.get("search_default_limit"));
|
||||
assertEquals(32, summary.get("search_max_limit"));
|
||||
String json = new Gson().toJson(summary);
|
||||
assertTrue(json.length() < 200);
|
||||
assertFalse(json.contains("minecraft:"));
|
||||
}
|
||||
|
||||
@Test void searchDefaultsToBlocksAndSixteenCompactResults() {
|
||||
var page = catalog().search(null, null, null, null);
|
||||
assertEquals("block", page.get("kind"));
|
||||
assertEquals(74, page.get("total"));
|
||||
assertEquals(16, results(page).size());
|
||||
assertTrue(results(page).stream().allMatch(entry -> entry.keySet().equals(Set.of("id", "block", "item"))));
|
||||
assertTrue(((String) page.get("next_cursor")).length() < 100);
|
||||
}
|
||||
|
||||
@Test void allPagesAreStableBoundedAndDoNotDuplicateOrLoseEntries() {
|
||||
var catalog = catalog();
|
||||
String cursor = null;
|
||||
List<String> found = new ArrayList<>();
|
||||
int pages = 0;
|
||||
do {
|
||||
var page = catalog.search("", "all", 32, cursor);
|
||||
assertTrue(results(page).size() <= 32);
|
||||
for (var result : results(page)) found.add((String) result.get("id"));
|
||||
cursor = (String) page.get("next_cursor");
|
||||
pages++;
|
||||
} while (cursor != null);
|
||||
assertEquals(3, pages);
|
||||
assertEquals(75, found.size());
|
||||
assertEquals(75, new HashSet<>(found).size());
|
||||
assertEquals(found.stream().sorted().toList(), found);
|
||||
}
|
||||
|
||||
@Test void tokenSearchRequiresEveryTokenAndIgnoresCase() {
|
||||
var page = catalog().search(" COPPER stair ", "block", 32, null);
|
||||
assertEquals("copper stair", page.get("query"));
|
||||
assertEquals(2, results(page).size());
|
||||
assertTrue(results(page).stream().allMatch(entry -> ((String) entry.get("id")).contains("copper_stairs")));
|
||||
assertEquals(0, catalog().search("copper unknown", "all", 32, null).get("total"));
|
||||
}
|
||||
|
||||
@Test void itemKindIncludesInventoryBlocksButExcludesFluids() {
|
||||
var catalog = catalog();
|
||||
assertEquals(0, catalog.search("water", "item", 16, null).get("total"));
|
||||
assertEquals(1, catalog.search("water", "block", 16, null).get("total"));
|
||||
assertEquals(1, catalog.search("pickaxe", "item", 16, null).get("total"));
|
||||
assertEquals(0, catalog.search("pickaxe", "block", 16, null).get("total"));
|
||||
}
|
||||
|
||||
@Test void rejectsCursorWhenCatalogOrSearchChanges() {
|
||||
var catalog = catalog();
|
||||
String cursor = (String) catalog.search("block", "all", 16, null).get("next_cursor");
|
||||
assertEquals("invalid_cursor", assertThrows(RpcServer.Fault.class,
|
||||
() -> catalog.search("water", "all", 16, cursor)).code);
|
||||
assertEquals("invalid_cursor", assertThrows(RpcServer.Fault.class,
|
||||
() -> catalog.search("block", "item", 16, cursor)).code);
|
||||
var changed = new MaterialCatalog("26.3-test", fixtures(), id -> null);
|
||||
assertEquals("stale_cursor", assertThrows(RpcServer.Fault.class,
|
||||
() -> changed.search("block", "all", 16, cursor)).code);
|
||||
var reordered = new ArrayList<>(fixtures());
|
||||
Collections.reverse(reordered);
|
||||
assertEquals(catalog.summary().get("version"), new MaterialCatalog("26.2-test", reordered, id -> null).summary().get("version"));
|
||||
}
|
||||
|
||||
@Test void rejectsMalformedAndOversizedArguments() {
|
||||
var catalog = catalog();
|
||||
for (int limit : List.of(-1, 0, 33, Integer.MAX_VALUE))
|
||||
assertEquals("invalid_material_query", assertThrows(RpcServer.Fault.class, () -> catalog.search("", "all", limit, null)).code);
|
||||
assertThrows(RpcServer.Fault.class, () -> catalog.search("", "nonsense", null, null));
|
||||
assertThrows(RpcServer.Fault.class, () -> catalog.search("x".repeat(97), "all", null, null));
|
||||
assertEquals(0, catalog.search("x".repeat(96), "all", null, null).get("total"));
|
||||
assertThrows(RpcServer.Fault.class, () -> catalog.search("x\ny", "all", null, null));
|
||||
for (String cursor : List.of("bad", "x".repeat(101), "::", "0".repeat(24) + ":" + "0".repeat(24) + ":2147483647"))
|
||||
assertEquals("invalid_cursor", assertThrows(RpcServer.Fault.class, () -> catalog.search("", "all", null, cursor)).code);
|
||||
String valid = (String) catalog.search("", "all", 16, null).get("next_cursor");
|
||||
String beyond = valid.substring(0, valid.lastIndexOf(':') + 1) + "999999999";
|
||||
assertEquals("invalid_cursor", assertThrows(RpcServer.Fault.class, () -> catalog.search("", "all", 16, beyond)).code);
|
||||
}
|
||||
|
||||
@Test void describeLoadsOnlyTheExactBlockRequested() {
|
||||
AtomicInteger calls = new AtomicInteger();
|
||||
var catalog = new MaterialCatalog("test", fixtures(), id -> {
|
||||
calls.incrementAndGet();
|
||||
return new MaterialCatalog.StateDescription(id, Map.of(), List.of());
|
||||
});
|
||||
catalog.summary();
|
||||
catalog.search("", "all", 16, null);
|
||||
assertEquals(0, calls.get());
|
||||
var description = catalog.describe("water");
|
||||
assertEquals("minecraft:water", description.get("default_state"));
|
||||
assertEquals(true, description.get("placeable"));
|
||||
assertEquals(1, calls.get());
|
||||
var item = catalog.describe("minecraft:iron_pickaxe");
|
||||
assertEquals(false, item.get("placeable"));
|
||||
assertEquals(true, item.get("item"));
|
||||
assertFalse(item.containsKey("default_state"));
|
||||
assertFalse(item.containsKey("properties"));
|
||||
assertEquals(1, calls.get());
|
||||
}
|
||||
|
||||
@Test void rejectsUnknownLegacyAndNonExactIds() {
|
||||
for (String id : List.of("minecraft:missing", "minecraft:legacy_stone", "stone[axis=y]", "stone{}", "STONE", "mod:stone", "", "x".repeat(129)))
|
||||
assertEquals("invalid_material", assertThrows(RpcServer.Fault.class, () -> catalog().describe(id)).code);
|
||||
assertEquals("invalid_material", assertThrows(RpcServer.Fault.class, () -> catalog().describe(null)).code);
|
||||
}
|
||||
|
||||
@Test void describesBooleanIntegerAndEnumDomainsWithoutAStateProduct() throws Exception {
|
||||
var state = new FakeState(List.of(
|
||||
new FakeProperty("waterlogged", List.of(false, true)),
|
||||
new FakeProperty("age", List.of(0, 1, 2, 3)),
|
||||
new FakeProperty("facing", List.of(Facing.N, Facing.S))));
|
||||
var properties = MaterialCatalog.readProperties(state);
|
||||
assertEquals(List.of("false", "true"), properties.get("waterlogged"));
|
||||
assertEquals(List.of("0", "1", "2", "3"), properties.get("age"));
|
||||
assertEquals(List.of("north", "south"), properties.get("facing"));
|
||||
assertEquals(List.of("age", "facing", "waterlogged"), new ArrayList<>(properties.keySet()));
|
||||
assertEquals(8, properties.values().stream().mapToInt(List::size).sum());
|
||||
assertEquals(0, MaterialCatalog.readProperties(new FakeState(List.of())).size());
|
||||
}
|
||||
|
||||
@Test void enormousOrDuplicatePropertyDomainsFailRatherThanTruncate() {
|
||||
var enormous = new FakeState(List.of(new FakeProperty("age", IntStream.range(0, 257).boxed().toList())));
|
||||
assertEquals("material_properties_unavailable", assertThrows(RpcServer.Fault.class,
|
||||
() -> MaterialCatalog.readProperties(enormous)).code);
|
||||
var duplicate = new FakeState(List.of(new FakeProperty("age", List.of(0)), new FakeProperty("age", List.of(0))));
|
||||
assertThrows(RpcServer.Fault.class, () -> MaterialCatalog.readProperties(duplicate));
|
||||
}
|
||||
|
||||
enum Facing { N, S }
|
||||
public record FakeState(Collection<FakeProperty> properties) {
|
||||
public Collection<FakeProperty> getProperties() { return properties; }
|
||||
}
|
||||
public record FakeProperty(String name, Collection<?> values) {
|
||||
public String getName() { return name; }
|
||||
public Collection<?> getPossibleValues() { return values; }
|
||||
public String getName(Comparable<?> value) {
|
||||
return value instanceof Facing facing ? (facing == Facing.N ? "north" : "south") : value.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
+16
-4
@@ -76,10 +76,10 @@ class SchematicAssetsTest {
|
||||
assertEquals(expected, assets.read(exported.assetId(), ZERO, 0));
|
||||
}
|
||||
|
||||
@Test void rejectsSparseSnapshotUnsupportedBlocksAndUnrepresentableOrigin() throws Exception {
|
||||
@Test void rejectsSparseSnapshotMalformedBlocksAndUnrepresentableOrigin() throws Exception {
|
||||
SchematicAssets assets = new SchematicAssets(root);
|
||||
assertThrows(IOException.class, () -> assets.exportSnapshot("Gap", Map.of(ZERO, STONE, new BlockPos(2, 0, 0), STONE), ZERO, 5000));
|
||||
assertThrows(IOException.class, () -> assets.exportSnapshot("Chest", Map.of(ZERO, "minecraft:chest"), ZERO, 5000));
|
||||
assertThrows(IOException.class, () -> assets.exportSnapshot("Chest", Map.of(ZERO, "minecraft:chest{Items:[]}"), ZERO, 5000));
|
||||
assertThrows(IOException.class, () -> assets.exportSnapshot("Offset", Map.of(new BlockPos(Integer.MIN_VALUE, 0, 0), STONE), new BlockPos(Integer.MAX_VALUE, 0, 0), 5000));
|
||||
assertEquals(0, assets.list().size());
|
||||
}
|
||||
@@ -156,10 +156,22 @@ class SchematicAssetsTest {
|
||||
assertThrows(IOException.class, () -> SchematicAssets.rotateState("minecraft:oak_stairs", 90));
|
||||
assertThrows(IOException.class, () -> SchematicAssets.rotateState("minecraft:oak_log", 90));
|
||||
assertThrows(IOException.class, () -> SchematicAssets.rotateState("minecraft:oak_log[axis=x,axis=z]", 0));
|
||||
assertThrows(IOException.class, () -> SchematicAssets.rotateState("minecraft:oak_stairs[facing=up]", 0));
|
||||
assertThrows(IOException.class, () -> SchematicAssets.rotateState("minecraft:oak_slab[type=top,waterlogged=true]", 0));
|
||||
assertEquals("minecraft:oak_slab[type=top,waterlogged=true]", SchematicAssets.rotateState("minecraft:oak_slab[type=top,waterlogged=true]", 0));
|
||||
assertThrows(IOException.class, () -> SchematicAssets.rotateState("minecraft:stone[rotation=4]", 90));
|
||||
}
|
||||
@Test void delegatesNewMaterialValidationAndRotationToRuntimeCodec() throws Exception {
|
||||
List<Integer> rotations=new ArrayList<>();
|
||||
SchematicAssets dynamic=new SchematicAssets(root.resolve("dynamic"),(state,degrees)->{
|
||||
rotations.add(degrees);
|
||||
if(!state.startsWith("minecraft:cherry_trapdoor"))throw new IOException("Unknown runtime block");
|
||||
return degrees==90?state.replace("facing=north","facing=east"):state;
|
||||
});
|
||||
String trapdoor="minecraft:cherry_trapdoor[facing=north,half=bottom,open=true,powered=false,waterlogged=true]";
|
||||
var asset=dynamic.exportSnapshot("New palette",Map.of(ZERO,trapdoor),ZERO,5000);
|
||||
assertEquals(trapdoor.replace("facing=north","facing=east"),dynamic.read(asset.assetId(),ZERO,90).get(ZERO));
|
||||
assertTrue(rotations.contains(90));
|
||||
assertThrows(IOException.class,()->dynamic.exportSnapshot("Invalid",Map.of(ZERO,"minecraft:unknown_block"),ZERO,5000));
|
||||
}
|
||||
|
||||
@Test void metadataAndPlacementRejectFutureDataVersionOnTheActualRead() throws Exception {
|
||||
SchematicAssets assets = new SchematicAssets(root);
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package io.github.minecraftbuilder.paper;
|
||||
|
||||
import com.google.gson.JsonParser;
|
||||
import io.github.minecraftbuilder.core.TerrainRecipe;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.util.Base64;
|
||||
import javax.imageio.ImageIO;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class TerrainPreviewTest {
|
||||
@Test void previewIsBoundedNativePngAndReportsClippingWithoutClaimingWorldVerification() throws Exception {
|
||||
var recipe=new TerrainRecipe(JsonParser.parseString("""
|
||||
{"version":1,"min":{"x":0,"y":0,"z":0},"max":{"x":7,"y":7,"z":7},
|
||||
"base_height":20,"seed":1,"mode":"sculpt","noise":{"amplitude":0,"scale":8},
|
||||
"palette":{"rock":"minecraft:stone","soil":"minecraft:dirt","surface":"minecraft:grass_block","soil_depth":2},"features":[],"preserve":[]}
|
||||
""").getAsJsonObject());
|
||||
var result=TerrainPreview.render(recipe,32,4096);
|
||||
assertEquals(false,result.get("world_verified"));assertEquals(64,result.get("clipped_samples"));assertEquals(1,result.get("tile_count"));
|
||||
var image=ImageIO.read(new ByteArrayInputStream(Base64.getDecoder().decode((String)result.get("imageBase64"))));
|
||||
assertEquals(16,image.getWidth());assertEquals(16,image.getHeight());
|
||||
assertThrows(IllegalArgumentException.class,()->TerrainPreview.render(recipe,1024,4096));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user