Jigsaw junctions and studio tools

- Rotatable jigsaw candidates try their rotations in random order
- Candidate connectors are tried by priority within each rotation
- Planar candidates must agree with every occupied neighbor socket
- Planar assembly falls back to source-only matching before it fails
- Connectors whose branch placed nothing stay open
- Spatial default kit covers all 24 Y-rotation topologies
- Planar start pool includes Blank at weight 1
- Vanilla spatial projects omit Iris themes and placement caps
- Vanilla spatial projects use max depth 4
- 1.21.1 export derives the vertical distance from the structure
- Workcell floors map the active variant's connectors
- Floor froglights mark up, down and up-down connectors
- Connector Tool places, moves, re-orients and removes spatial connectors
- Connector Tool re-seats planar face-center sockets
- Main menu pages workcells five at a time
- Spatial variants can be deleted while another variant remains
- Tool sticks act on the variant they were made for
- Destructive tool confirmation needs two separate clicks
- Pistons cannot extend into protected cells
- Autosave capture failures back off and notify the owner
- Autosave plays the save bell
- Hidden-connector air final states are stored as air
- Reset Connector Blocks clears unsaved markers
- Folia logout releases reset and resize reservations
- Spatial workcell rename relabels its variant
- Workcell teleports land inside the focused workcell
- Replacement layouts keep rendering and hydration working
- Container preview probes stay out of chat
- Non-owners can run commands that are not recognized as mutating
- Spatial resize or rotation past the workcell volume limit is refused
- Tests cover default kit assembly, connector placement, floor glyphs and menu paging
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-28 17:14:14 -04:00
1 parent b3579e623a
commit 7d73c4d150
56 files changed
+4869 -537

No files matched your search

@@ -83,7 +83,7 @@ public class SchemaBuilderIdentityTest {
@ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
private static final String EXPECTED_SCHEMA_DIGEST = "6935e8d3914e5cde852522151d87fe898f79de3b394826e544c5c5bfbfceba57";
private static final String EXPECTED_SCHEMA_DIGEST = "b8dd16d1a3188e8150a68a9d5b2964aed5094d679c0f941e3703aec675f87e20";
private static final List<Class<?>> SCHEMA_ROOTS = List.of(
IrisDimension.class,
@@ -167,6 +167,33 @@ public class VanillaJigsawDatapackExporterTest {
}
}
@Test
public void exports1211WithRuntimeDefaultSettingsDerivedFromTheStructure() throws Exception {
when(IrisPlatforms.get().minecraftVersion()).thenReturn("1.21.1");
try {
TestGraph graph = connectorGraph();
VanillaJigsawExportResult staticDefaults = new VanillaJigsawDatapackExporter().export(
request(graph, temporaryFolder.getRoot().toPath().resolve("static-defaults-1211"))
.settings(VanillaJigsawExportSettings.defaults()).build());
assertTrue(hasCode(staticDefaults, VanillaJigsawExportDiagnostic.Code.INVALID_MAX_DISTANCE));
Path output = temporaryFolder.getRoot().toPath().resolve("runtime-defaults-1211");
VanillaJigsawExportSettings settings = VanillaJigsawExportSettings.runtimeDefaults(graph.structure);
VanillaJigsawExportResult result = new VanillaJigsawDatapackExporter().export(
request(graph, output).namespace("studio").resourcePath("village/test").settings(settings).build());
assertEquals(graph.structure.getMaxSizeChunks() * 16, settings.maxDistanceVertical());
assertTrue(result.diagnostics().toString(), result.isSuccess());
JsonObject structure = json(output.resolve("data/studio/worldgen/structure/village/test.json"));
assertEquals(graph.structure.getMaxSizeChunks() * 16,
structure.get("max_distance_from_center").getAsInt());
} finally {
when(IrisPlatforms.get().minecraftVersion()).thenReturn("26.2");
}
assertEquals(VanillaJigsawExportSettings.defaults().maxDistanceVertical(),
VanillaJigsawExportSettings.runtimeDefaults(connectorGraph().structure).maxDistanceVertical());
}
@Test
public void rejects1211VanillaExportWithDifferentVerticalDistance() throws Exception {
when(IrisPlatforms.get().minecraftVersion()).thenReturn("1.21.1");
@@ -38,7 +38,10 @@ import art.arcane.volmlib.util.math.RNG;
import org.junit.ClassRule;
import org.junit.Test;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -310,6 +313,82 @@ public class StructureAssemblerTest {
assertEquals(west.getMaxX() + 1, start.getMinX());
}
@Test
public void rotatableCandidatesAttachThroughEitherCornerConnectorAcrossSeeds() {
Fixture fixture = new Fixture();
IrisJigsawPiece startPiece = piece("start-object", false,
connector(IrisDirection.NORTH_NEGATIVE_Z, "turns", "door", "door")
.setPosition(new IrisPosition(1, 0, 0))
.setJoint(JigsawJoint.ALIGNED));
IrisJigsawPiece cornerPiece = piece("corner-object", true,
connector(IrisDirection.NORTH_NEGATIVE_Z, "", "door", "door")
.setPosition(new IrisPosition(1, 0, 0))
.setJoint(JigsawJoint.ALIGNED),
connector(IrisDirection.EAST_POSITIVE_X, "", "door", "door")
.setPosition(new IrisPosition(2, 0, 1))
.setJoint(JigsawJoint.ALIGNED));
fixture.pool("start", pool("", "start-piece"));
fixture.pool("turns", pool("", "corner-piece"));
fixture.piece("start-piece", startPiece);
fixture.piece("corner-piece", cornerPiece);
fixture.object("start-object", new IrisObject(3, 1, 3));
fixture.object("corner-object", new IrisObject(3, 1, 3));
StructureAssembler assembler = fixture.assembler(structure("start", 1), 0, 0, 0);
Set<Integer> attachedConnectors = new HashSet<>();
for (long seed = 0; seed < 64; seed++) {
PlacedStructurePiece corner = requirePlacedPiece(completedPieces(assembler, new RNG(seed)), cornerPiece);
for (int index = 0; index < cornerPiece.getConnectors().size(); index++) {
if (corner.getRotation().rotate(cornerPiece.getConnectors().get(index).getDirection())
== IrisDirection.SOUTH_POSITIVE_Z) {
attachedConnectors.add(index);
}
}
}
assertEquals(Set.of(0, 1), attachedConnectors);
}
@Test
public void planarJunctionPlacesAPieceOpenTowardEveryOccupiedNeighbor() {
Fixture fixture = planarJunctionFixture("straight-piece", "corner-piece");
IrisJigsawPiece corner = planarPiece(true,
IrisDirection.NORTH_NEGATIVE_Z, IrisDirection.EAST_POSITIVE_X);
fixture.piece("straight-piece", planarPiece(true,
IrisDirection.NORTH_NEGATIVE_Z, IrisDirection.SOUTH_POSITIVE_Z));
fixture.piece("corner-piece", corner);
StructureAssembler assembler = fixture.assembler(planarJunctionStructure(), 0, 64, 0);
for (long seed = 0; seed < 32; seed++) {
List<PlacedStructurePiece> placed = completedPieces(assembler, new RNG(seed));
PlacedStructurePiece junction = placed.get(3);
assertEquals(4, placed.size());
assertSame("seed " + seed, corner, junction.getPiece());
assertEquals("seed " + seed,
Set.of(IrisDirection.WEST_NEGATIVE_X, IrisDirection.SOUTH_POSITIVE_Z),
worldFacings(junction));
}
}
@Test
public void planarJunctionWithoutAFittingPieceKeepsTheAttachmentOnlyPlacementInsteadOfFailing() {
Fixture fixture = planarJunctionFixture("straight-piece");
IrisJigsawPiece straight = planarPiece(true,
IrisDirection.NORTH_NEGATIVE_Z, IrisDirection.SOUTH_POSITIVE_Z);
fixture.piece("straight-piece", straight);
StructureAssembler assembler = fixture.assembler(planarJunctionStructure(), 0, 64, 0);
for (long seed = 0; seed < 8; seed++) {
List<PlacedStructurePiece> placed = completedPieces(assembler, new RNG(seed));
assertEquals(4, placed.size());
assertSame(straight, placed.get(3).getPiece());
assertEquals(Set.of(IrisDirection.WEST_NEGATIVE_X, IrisDirection.EAST_POSITIVE_X),
worldFacings(placed.get(3)));
}
}
@Test
public void selectionPriorityProcessesHigherConnectorsFirstAndPreservesStableTies() {
Fixture fixture = new Fixture();
@@ -1115,6 +1194,53 @@ public class StructureAssemblerTest {
return fixture.assembler(structure("start", 1), 0, 0, 0).assemble(new RNG(137L));
}
private static Fixture planarJunctionFixture(String... junctionPieces) {
Fixture fixture = new Fixture();
fixture.pool("start", pool("", "start-piece"));
fixture.pool("north", pool("", "north-elbow"));
fixture.pool("east", pool("", "east-elbow"));
fixture.pool("junction", pool("", junctionPieces));
fixture.piece("start-piece", piece("cell-object", false,
planarConnector(IrisDirection.NORTH_NEGATIVE_Z, "north"),
planarConnector(IrisDirection.EAST_POSITIVE_X, "east")));
fixture.piece("north-elbow", piece("cell-object", false,
planarConnector(IrisDirection.SOUTH_POSITIVE_Z, ""),
planarConnector(IrisDirection.EAST_POSITIVE_X, "junction")));
fixture.piece("east-elbow", piece("cell-object", false,
planarConnector(IrisDirection.WEST_NEGATIVE_X, ""),
planarConnector(IrisDirection.NORTH_NEGATIVE_Z, "")));
fixture.object("cell-object", new IrisObject(3, 1, 3));
return fixture;
}
private static IrisJigsawPiece planarPiece(boolean rotatable, IrisDirection... directions) {
IrisJigsawPiece piece = new IrisJigsawPiece().setObject("cell-object").setRotatable(rotatable);
for (IrisDirection direction : directions) {
piece.getConnectors().add(planarConnector(direction, ""));
}
return piece;
}
private static IrisJigsawConnector planarConnector(IrisDirection direction, String pool) {
return connector(direction, pool, "door", "door")
.setPosition(IrisJigsawConnector.canonicalPlanarPosition(new IrisPosition(3, 1, 3), direction))
.setJoint(JigsawJoint.ALIGNED);
}
private static IrisStructure planarJunctionStructure() {
return structure("start", 3)
.setMode(IrisJigsawMode.PLANAR_JIGSAW)
.setCellSize(new IrisPosition(3, 1, 3));
}
private static Set<IrisDirection> worldFacings(PlacedStructurePiece piece) {
Set<IrisDirection> facings = EnumSet.noneOf(IrisDirection.class);
for (IrisJigsawConnector connector : piece.getPiece().getConnectors()) {
facings.add(piece.getRotation().rotate(connector.getDirection()));
}
return facings;
}
private static IrisStructure structure(String startPool, int maxDepth) {
return new IrisStructure().setStartPool(startPool).setMaxDepth(maxDepth).setMaxSizeChunks(8);
}
@@ -2,8 +2,6 @@ package art.arcane.iris.studio.generation;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.pack.loading.ResourceLoader;
import art.arcane.iris.studio.jigsaw.JigsawPlanarArchetype;
import art.arcane.iris.studio.jigsaw.JigsawPlanarDirection;
import art.arcane.iris.studio.jigsaw.JigsawPlanarTopology;
import art.arcane.iris.studio.jigsaw.JigsawStudioActivation;
import art.arcane.iris.studio.jigsaw.JigsawStudioBay;
@@ -52,6 +50,7 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
@@ -168,57 +167,123 @@ public class JigsawStudioGeneratorTest {
}
@Test
public void paintsCanonicalWoolGlyphsBelowCaptureAndOneControlChest() {
@SuppressWarnings("unchecked")
public void paintsActiveVariantConnectorLinesBelowCaptureAndOneControlChest() {
IrisData source = mock(IrisData.class);
ResourceLoader<IrisJigsawPiece> pieceLoader = mock(ResourceLoader.class);
ResourceLoader<IrisObject> objectLoader = mock(ResourceLoader.class);
when(source.getJigsawPieceLoader()).thenReturn(pieceLoader);
when(source.getObjectLoader()).thenReturn(objectLoader);
JigsawStudioCellDimensions dimensions = new JigsawStudioCellDimensions(7, 5, 7);
IrisJigsawPiece piece = new IrisJigsawPiece()
.setObject("village/corner")
.setConnectors(new KList<>());
piece.getConnectors().add(connector(new IrisPosition(3, 2, 0), IrisDirection.NORTH_NEGATIVE_Z));
piece.getConnectors().add(connector(new IrisPosition(6, 2, 3), IrisDirection.EAST_POSITIVE_X));
when(pieceLoader.load("village/corner", false)).thenReturn(piece);
when(objectLoader.load("village/corner", false)).thenReturn(new IrisObject(7, 5, 7));
JigsawStudioVariant variant = planarVariant("village/corner", JigsawPlanarTopology.NORTH_EAST_CORNER);
GeneratorFixture fixture = fixture(
source,
JigsawStudioMode.PLANAR_JIGSAW,
dimensions,
JigsawStudioVariantCatalog.empty());
new JigsawStudioVariantCatalog(List.of(variant)));
JigsawStudioBay corner = fixture.layout().get("workcell/corner");
int glyphY = corner.bounds().originY() - 1;
int floorY = corner.bounds().originY() - 1;
int centerX = dimensions.width() / 2;
int centerZ = dimensions.depth() / 2;
assertSame(fixture.connectorCap(), stateAt(
fixture.generator(),
corner.bounds().originX() + centerX,
glyphY,
corner.bounds().originZ()));
assertSame(fixture.connectorCap(), stateAt(
fixture.generator(),
corner.bounds().maxX(),
glyphY,
corner.bounds().originZ() + centerZ));
assertSame(fixture.topologyPath(), stateAt(
fixture.generator(),
corner.bounds().originX() + centerX,
glyphY,
corner.bounds().originZ() + 1));
assertSame(fixture.topologyPath(), stateAt(
fixture.generator(),
corner.bounds().originX() + centerX + 1,
glyphY,
corner.bounds().originZ() + centerZ));
assertSame(fixture.topologyBase(), stateAt(
fixture.generator(),
corner.bounds().originX(),
glyphY,
corner.bounds().originZ()));
assertFalse(fixture.topologyBase() == stateAt(
fixture.generator(),
corner.bounds().originX() + centerX,
corner.bounds().originY(),
corner.bounds().originZ() + centerZ));
try (MockedStatic<B> ignored = mockStatic(B.class)) {
assertSame(fixture.palette().horizontalSocket(), stateAt(
fixture.generator(), corner.bounds().originX() + centerX, floorY, corner.bounds().originZ()));
assertSame(fixture.palette().horizontalSocket(), stateAt(
fixture.generator(), corner.bounds().maxX(), floorY, corner.bounds().originZ() + centerZ));
assertSame(fixture.palette().line(), stateAt(
fixture.generator(), corner.bounds().originX() + centerX, floorY, corner.bounds().originZ() + 1));
assertSame(fixture.palette().line(), stateAt(
fixture.generator(),
corner.bounds().originX() + centerX + 1,
floorY,
corner.bounds().originZ() + centerZ));
assertSame(fixture.palette().base(), stateAt(
fixture.generator(), corner.bounds().originX(), floorY, corner.bounds().originZ()));
assertFalse(fixture.palette().base() == stateAt(
fixture.generator(),
corner.bounds().originX() + centerX,
corner.bounds().originY(),
corner.bounds().originZ() + centerZ));
JigsawStudioBay blank = fixture.layout().get("workcell/blank");
assertSame(fixture.topologyBase(), stateAt(
fixture.generator(),
blank.bounds().originX() + centerX,
blank.bounds().originY() - 1,
blank.bounds().originZ() + centerZ));
JigsawStudioControlPosition control = fixture.layout().controlPosition();
assertSame(fixture.controlChest(), stateAt(
fixture.generator(), control.worldX(), control.worldY(), control.worldZ()));
JigsawStudioBay blank = fixture.layout().get("workcell/blank");
assertSame(fixture.palette().base(), stateAt(
fixture.generator(),
blank.bounds().originX() + centerX,
blank.bounds().originY() - 1,
blank.bounds().originZ() + centerZ));
JigsawStudioBay tee = fixture.layout().get("workcell/tee");
assertSame(fixture.palette().base(), stateAt(
fixture.generator(),
tee.bounds().originX() + centerX,
tee.bounds().originY() - 1,
tee.bounds().originZ()));
JigsawStudioControlPosition control = fixture.layout().controlPosition();
assertSame(fixture.controlChest(), stateAt(
fixture.generator(), control.worldX(), control.worldY(), control.worldZ()));
}
}
@Test
@SuppressWarnings("unchecked")
public void spatialWorkcellsPaintVerticalSocketsUnderTheirConnectorColumns() {
IrisData source = mock(IrisData.class);
ResourceLoader<IrisJigsawPiece> pieceLoader = mock(ResourceLoader.class);
ResourceLoader<IrisObject> objectLoader = mock(ResourceLoader.class);
when(source.getJigsawPieceLoader()).thenReturn(pieceLoader);
when(source.getObjectLoader()).thenReturn(objectLoader);
IrisJigsawPiece piece = new IrisJigsawPiece()
.setObject("stronghold/shaft")
.setConnectors(new KList<>());
piece.getConnectors().add(connector(new IrisPosition(2, 4, 2), IrisDirection.UP_POSITIVE_Y)
.setTop(IrisDirection.NORTH_NEGATIVE_Z));
piece.getConnectors().add(connector(new IrisPosition(2, 0, 2), IrisDirection.DOWN_NEGATIVE_Y)
.setTop(IrisDirection.NORTH_NEGATIVE_Z));
piece.getConnectors().add(connector(new IrisPosition(0, 1, 4), IrisDirection.DOWN_NEGATIVE_Y)
.setTop(IrisDirection.NORTH_NEGATIVE_Z));
piece.getConnectors().add(connector(new IrisPosition(2, 2, 0), IrisDirection.NORTH_NEGATIVE_Z));
when(pieceLoader.load("stronghold/shaft", false)).thenReturn(piece);
when(objectLoader.load("stronghold/shaft", false)).thenReturn(new IrisObject(5, 5, 5));
JigsawStudioVariant variant = new JigsawStudioVariant(
"stronghold/shaft",
"stronghold/shaft",
"",
Optional.of(new JigsawStudioCellDimensions(5, 5, 5)),
JigsawStudioMode.SPATIAL_JIGSAW,
Optional.empty(),
true,
true,
List.of(),
new JigsawStudioPieceRules(0, 30, 0, 0, false),
List.of());
GeneratorFixture fixture = fixture(
source,
JigsawStudioMode.SPATIAL_JIGSAW,
new JigsawStudioCellDimensions(5, 5, 5),
new JigsawStudioVariantCatalog(List.of(variant)));
JigsawStudioBay workcell = fixture.layout().bays().getFirst();
int originX = workcell.bounds().originX();
int originZ = workcell.bounds().originZ();
int floorY = workcell.bounds().originY() - 1;
try (MockedStatic<B> ignored = mockStatic(B.class)) {
assertSame(fixture.palette().upAndDownSocket(), stateAt(fixture.generator(), originX + 2, floorY, originZ + 2));
assertSame(fixture.palette().downSocket(), stateAt(fixture.generator(), originX, floorY, originZ + 4));
assertSame(fixture.palette().horizontalSocket(), stateAt(fixture.generator(), originX + 2, floorY, originZ));
assertSame(fixture.palette().line(), stateAt(fixture.generator(), originX + 2, floorY, originZ + 1));
assertSame(fixture.palette().line(), stateAt(fixture.generator(), originX + 1, floorY, originZ + 3));
assertSame(fixture.palette().base(), stateAt(fixture.generator(), originX + 4, floorY, originZ + 4));
}
assertSame(
fixture.palette().upAndDownSocket(),
fixture.generator().floorState(fixture.generator().floorGlyph(workcell).cell(2, 2)));
}
@Test
@@ -247,77 +312,6 @@ public class JigsawStudioGeneratorTest {
workcell.bounds().originZ()));
}
@Test
public void glyphMathHandlesAllArchetypesAndSmallEvenAndOddCells() {
int[] sizes = {1, 2, 3, 4, 7, 16};
for (int size : sizes) {
JigsawStudioCellDimensions dimensions = new JigsawStudioCellDimensions(size, 1, size);
int center = size / 2;
for (JigsawPlanarArchetype archetype : JigsawPlanarArchetype.values()) {
JigsawPlanarTopology topology = archetype.canonicalTopology();
boolean centerPath = JigsawStudioGenerator.isTopologyPath(
topology, center, center, center, center);
assertEquals(archetype != JigsawPlanarArchetype.BLANK, centerPath);
if (topology.connects(JigsawPlanarDirection.NORTH)) {
assertTrue(JigsawStudioGenerator.isConnectorCap(
topology, dimensions, center, 0, center, center));
}
if (topology.connects(JigsawPlanarDirection.EAST)) {
assertTrue(JigsawStudioGenerator.isConnectorCap(
topology, dimensions, size - 1, center, center, center));
}
}
}
}
@Test
public void connectorCapsOnlyOccupyConnectedFaceCenters() {
int[] sizes = {3, 4, 7, 16};
for (int size : sizes) {
JigsawStudioCellDimensions dimensions = new JigsawStudioCellDimensions(size, 1, size);
int center = size / 2;
for (JigsawPlanarArchetype archetype : JigsawPlanarArchetype.values()) {
JigsawPlanarTopology topology = archetype.canonicalTopology();
for (int z = 0; z < size; z++) {
for (int x = 0; x < size; x++) {
boolean expected = topology.connects(JigsawPlanarDirection.NORTH)
&& x == center && z == 0
|| topology.connects(JigsawPlanarDirection.EAST)
&& x == size - 1 && z == center
|| topology.connects(JigsawPlanarDirection.SOUTH)
&& x == center && z == size - 1
|| topology.connects(JigsawPlanarDirection.WEST)
&& x == 0 && z == center;
assertEquals(expected, JigsawStudioGenerator.isConnectorCap(
topology, dimensions, x, z, center, center));
}
}
}
}
}
@Test
public void canonicalFiveByFivePathsMatchEveryArchetype() {
Map<JigsawPlanarArchetype, List<String>> expected = Map.of(
JigsawPlanarArchetype.BLANK, List.of(".....", ".....", ".....", ".....", "....."),
JigsawPlanarArchetype.END, List.of("..#..", "..#..", "..#..", ".....", "....."),
JigsawPlanarArchetype.STRAIGHT, List.of("..#..", "..#..", "..#..", "..#..", "..#.."),
JigsawPlanarArchetype.CORNER, List.of("..#..", "..#..", "..###", ".....", "....."),
JigsawPlanarArchetype.TEE, List.of("..#..", "..#..", "#####", ".....", "....."),
JigsawPlanarArchetype.CROSS, List.of("..#..", "..#..", "#####", "..#..", "..#.."));
for (JigsawPlanarArchetype archetype : JigsawPlanarArchetype.values()) {
List<String> rows = expected.get(archetype);
for (int z = 0; z < 5; z++) {
for (int x = 0; x < 5; x++) {
boolean path = JigsawStudioGenerator.isTopologyPath(
archetype.canonicalTopology(), x, z, 2, 2);
assertEquals(rows.get(z).charAt(x) == '#', path);
}
}
}
}
@Test
public void clipsLargeGlyphPaintingToOneChunkFootprint() {
GeneratorFixture fixture = fixture(
@@ -331,10 +325,7 @@ public class JigsawStudioGeneratorTest {
fixture.generator().paintChunk(chunk, chunkX, chunkZ);
int glyphBlocks = chunk.countState(fixture.topologyBase())
+ chunk.countState(fixture.topologyPath())
+ chunk.countState(fixture.connectorCap());
assertEquals(256, glyphBlocks);
assertEquals(256, chunk.countState(fixture.palette().base()));
}
@Test
@@ -467,6 +458,29 @@ public class JigsawStudioGeneratorTest {
assertNotSame(nextGeneration, fixture.generator().renderVariant(workcell, variant, 5L));
}
@Test
public void workcellsFromAnEquivalentReplacedLayoutStillRender() {
JigsawStudioCellDimensions dimensions = new JigsawStudioCellDimensions(5, 5, 5);
GeneratorFixture fixture = fixture(
JigsawStudioMode.PLANAR_JIGSAW,
dimensions,
JigsawStudioVariantCatalog.empty());
JigsawStudioBay stale = fixture.layout().get("workcell/end");
fixture.generator().getSession().replaceLayout(JigsawStudioLayout.create(
JigsawStudioMode.PLANAR_JIGSAW,
dimensions,
JigsawStudioVariantCatalog.empty()));
assertNotSame(stale, fixture.generator().getLayout().get("workcell/end"));
assertTrue(fixture.generator().renderBay(stale).valid());
fixture.generator().getSession().replaceLayout(JigsawStudioLayout.create(
JigsawStudioMode.PLANAR_JIGSAW,
new JigsawStudioCellDimensions(6, 5, 5),
JigsawStudioVariantCatalog.empty()));
assertThrows(IllegalArgumentException.class, () -> fixture.generator().renderBay(stale));
}
@Test
public void mapsAllVanillaJigsawOrientations() {
assertEquals("north_up", JigsawStudioGenerator.orientation(
@@ -535,9 +549,13 @@ public class JigsawStudioGeneratorTest {
NativeBlockState lightFloor = mock(NativeBlockState.class);
NativeBlockState darkFloor = mock(NativeBlockState.class);
NativeBlockState frame = mock(NativeBlockState.class);
NativeBlockState topologyBase = mock(NativeBlockState.class);
NativeBlockState topologyPath = mock(NativeBlockState.class);
NativeBlockState connectorCap = mock(NativeBlockState.class);
JigsawStudioGenerator.FloorPalette palette = new JigsawStudioGenerator.FloorPalette(
mock(NativeBlockState.class),
mock(NativeBlockState.class),
mock(NativeBlockState.class),
mock(NativeBlockState.class),
mock(NativeBlockState.class),
mock(NativeBlockState.class));
NativeBlockState invalidMarker = mock(NativeBlockState.class);
NativeBlockState controlChest = mock(NativeBlockState.class);
JigsawStudioGenerator generator = new JigsawStudioGenerator(
@@ -547,9 +565,7 @@ public class JigsawStudioGeneratorTest {
lightFloor,
darkFloor,
frame,
topologyBase,
topologyPath,
connectorCap,
palette,
invalidMarker,
controlChest,
JigsawStudioGeneratorTest::renderFinalState);
@@ -557,12 +573,22 @@ public class JigsawStudioGeneratorTest {
generator,
layout,
frame,
topologyBase,
topologyPath,
connectorCap,
palette,
controlChest);
}
private static IrisJigsawConnector connector(IrisPosition position, IrisDirection direction) {
return new IrisJigsawConnector()
.setPosition(position)
.setDirection(direction)
.setTop(IrisDirection.UP_POSITIVE_Y)
.setPool("test/pieces")
.setName("iris:planar")
.setTargetName("iris:planar")
.setJoint(JigsawJoint.ALIGNED)
.setFinalState("minecraft:structure_void");
}
private static JigsawStudioVariant planarVariant(String key, JigsawPlanarTopology topology) {
return new JigsawStudioVariant(
key,
@@ -596,9 +622,7 @@ public class JigsawStudioGeneratorTest {
JigsawStudioGenerator generator,
JigsawStudioLayout layout,
NativeBlockState frame,
NativeBlockState topologyBase,
NativeBlockState topologyPath,
NativeBlockState connectorCap,
JigsawStudioGenerator.FloorPalette palette,
NativeBlockState controlChest
) {
}
@@ -0,0 +1,509 @@
package art.arcane.iris.studio.jigsaw;
import art.arcane.iris.pack.value.IrisDirection;
import art.arcane.iris.pack.value.IrisPosition;
import art.arcane.iris.structure.authoring.StructureResourceBundle;
import art.arcane.iris.structure.authoring.StructureWriteResult;
import art.arcane.iris.structure.graph.StructureAssemblyResult;
import art.arcane.iris.structure.graph.StructureAssemblyStatus;
import art.arcane.iris.structure.graph.StructureGraphCompilation;
import art.arcane.iris.structure.graph.StructureResourceBundleGraphCompiler;
import art.arcane.iris.structure.jigsaw.IrisJigsawConnector;
import art.arcane.iris.structure.jigsaw.IrisJigsawPiece;
import art.arcane.iris.structure.jigsaw.IrisJigsawPieceEntry;
import art.arcane.iris.structure.jigsaw.IrisJigsawPool;
import art.arcane.iris.structure.jigsaw.JigsawJoint;
import art.arcane.iris.structure.object.IrisObject;
import art.arcane.iris.structure.placement.IrisStructure;
import art.arcane.iris.structure.placement.PlacedStructurePiece;
import art.arcane.iris.structure.placement.StructureAssembler;
import art.arcane.iris.testsupport.DurabilityMode;
import art.arcane.volmlib.util.math.RNG;
import com.google.gson.Gson;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
public class JigsawDefaultKitAssemblyTest {
@ClassRule
public static final DurabilityMode DURABILITY = DurabilityMode.relaxed();
private static final Gson GSON = new Gson();
private static final int SEEDS = 160;
private static final int SPATIAL_IRIS_PIECE_BOUND = 23 * 4 + 1;
private static final int SPATIAL_PORTABLE_PIECE_BOUND = 129;
private static final List<JigsawStudioCellDimensions> PLANAR_DIMENSIONS = List.of(
new JigsawStudioCellDimensions(16, 16, 16),
new JigsawStudioCellDimensions(15, 9, 15));
private static final List<JigsawStudioCellDimensions> SPATIAL_DIMENSIONS = List.of(
new JigsawStudioCellDimensions(15, 15, 15),
new JigsawStudioCellDimensions(16, 12, 16));
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void planarDefaultKitOpensEveryOccupiedNeighborOnTheCellGrid() throws IOException {
for (JigsawStudioCellDimensions dimensions : PLANAR_DIMENSIONS) {
for (JigsawStudioCompatibilityTarget target : JigsawStudioCompatibilityTarget.values()) {
StructureGraphCompilation compilation = compile(JigsawStudioMode.PLANAR_JIGSAW, target, dimensions);
int closedFaces = 0;
for (long seed = 0; seed < SEEDS; seed++) {
List<PlacedStructurePiece> pieces = complete(compilation, seed);
for (PlacedStructurePiece piece : pieces) {
for (IrisJigsawConnector connector : piece.getPiece().getConnectors()) {
IrisDirection facing = piece.getRotation().rotate(connector.getDirection());
IrisPosition targetCell = offset(planarSocket(piece, facing), facing);
PlacedStructurePiece neighbor = containing(pieces, targetCell);
if (neighbor == null) {
continue;
}
String context = target + " " + dimensions + " seed " + seed + ": "
+ piece.getPiece().getObject() + " faces " + facing + " into "
+ neighbor.getPiece().getObject();
assertTrue(context, hasPlanarSocket(
neighbor,
facing.reverse(),
targetCell,
piece.getRotation().rotate(connector.getTop())));
assertSharedFace(context, piece, neighbor, facing);
closedFaces++;
}
}
}
assertTrue(closedFaces > SEEDS);
}
}
}
@Test
public void planarDefaultKitTurnsBothWaysThroughCornersAndTees() throws IOException {
StructureGraphCompilation compilation = compile(
JigsawStudioMode.PLANAR_JIGSAW,
JigsawStudioCompatibilityTarget.IRIS_EXTENDED,
PLANAR_DIMENSIONS.getFirst());
Map<String, Set<Integer>> attachments = new TreeMap<>();
for (long seed = 0; seed < SEEDS; seed++) {
List<PlacedStructurePiece> pieces = complete(compilation, seed);
for (int index = 1; index < pieces.size(); index++) {
PlacedStructurePiece piece = pieces.get(index);
int attachment = soleEarlierPlanarAttachment(pieces, index);
if (attachment >= 0) {
attachments.computeIfAbsent(baseName(piece), ignored -> new HashSet<>()).add(attachment);
}
}
}
assertEquals(attachments.toString(), Set.of(0, 1), attachments.get("corner"));
Set<Integer> teeAttachments = attachments.get("tee");
assertNotNull(attachments.toString(), teeAttachments);
assertTrue(attachments.toString(), teeAttachments.contains(0));
assertTrue(attachments.toString(), teeAttachments.contains(1) || teeAttachments.contains(2));
}
@Test
public void connectorlessBlankPiecesGenerateAsStandaloneStructures() throws IOException {
for (JigsawStudioMode mode : JigsawStudioMode.values()) {
JigsawStudioCellDimensions dimensions = mode == JigsawStudioMode.PLANAR_JIGSAW
? PLANAR_DIMENSIONS.getFirst()
: SPATIAL_DIMENSIONS.getFirst();
StructureGraphCompilation compilation = compile(
mode,
JigsawStudioCompatibilityTarget.IRIS_EXTENDED,
dimensions);
int standaloneBlanks = 0;
int multiPieceAssemblies = 0;
for (long seed = 0; seed < SEEDS * 8 && (standaloneBlanks == 0 || seed < SEEDS); seed++) {
List<PlacedStructurePiece> pieces = complete(compilation, seed);
if (pieces.size() == 1 && pieces.getFirst().getPiece().getConnectors().isEmpty()) {
assertEquals("blank", baseName(pieces.getFirst()));
standaloneBlanks++;
continue;
}
multiPieceAssemblies++;
for (PlacedStructurePiece piece : pieces) {
assertFalse(mode + " seed " + seed, piece.getPiece().getConnectors().isEmpty());
}
}
assertTrue(mode + " never generated its connectorless piece", standaloneBlanks > 0);
assertTrue(mode + " generated only standalone pieces", multiPieceAssemblies > standaloneBlanks);
}
StructureResourceBundle planar = JigsawStudioProjectCreator.bundle(options(
JigsawStudioMode.PLANAR_JIGSAW,
JigsawStudioCompatibilityTarget.VANILLA_PORTABLE,
PLANAR_DIMENSIONS.getLast()));
IrisJigsawPool start = pool(planar, "kit/test/start");
assertEquals(2, start.getPieces().size());
assertEquals("kit/test/cross", start.getPieces().get(0).getPiece());
assertEquals(3, start.getPieces().get(0).getWeight());
assertEquals("kit/test/blank", start.getPieces().get(1).getPiece());
assertEquals(1, start.getPieces().get(1).getWeight());
for (IrisJigsawPieceEntry entry : pool(planar, "kit/test/pieces").getPieces()) {
assertFalse("kit/test/blank".equals(entry.getPiece()));
}
}
@Test
public void spatialDefaultKitCoversEveryYRotationTopologyOnce() throws IOException {
for (JigsawStudioCompatibilityTarget target : JigsawStudioCompatibilityTarget.values()) {
JigsawStudioCellDimensions dimensions = SPATIAL_DIMENSIONS.getLast();
StructureResourceBundle bundle = JigsawStudioProjectCreator.bundle(
options(JigsawStudioMode.SPATIAL_JIGSAW, target, dimensions));
Map<String, IrisJigsawPiece> pieces = pieces(bundle);
Map<Integer, String> bySignature = new HashMap<>();
for (Map.Entry<String, IrisJigsawPiece> entry : pieces.entrySet()) {
IrisJigsawPiece piece = entry.getValue();
int horizontal = 0;
int vertical = 0;
for (IrisJigsawConnector connector : piece.getConnectors()) {
IrisDirection direction = connector.getDirection();
assertEquals(spatialFaceCenter(dimensions, direction), connector.getPosition());
assertEquals(direction.isVertical() ? IrisDirection.NORTH_NEGATIVE_Z : IrisDirection.UP_POSITIVE_Y,
connector.getTop());
assertEquals(JigsawJoint.ROLLABLE, connector.getJoint());
assertEquals("kit/test/pieces", connector.getPool());
horizontal |= horizontalBit(direction);
vertical |= verticalBit(direction);
}
assertTrue(piece.isRotatable());
assertEquals(entry.getKey(), piece.getObject());
assertEquals(expectedSpatialKey(canonicalMask(horizontal), vertical), entry.getKey());
String previous = bySignature.put(canonicalMask(horizontal) | vertical, entry.getKey());
assertEquals("duplicate topology " + entry.getKey() + " and " + previous, null, previous);
if (target == JigsawStudioCompatibilityTarget.IRIS_EXTENDED) {
assertEquals(List.of("variant-1"), List.copyOf(piece.getThemes()));
assertEquals(4, piece.resolvedRules().getMaximumPlacements());
} else {
assertTrue(piece.getThemes().isEmpty());
assertEquals(0, piece.resolvedRules().getMaximumPlacements());
}
}
assertEquals(24, pieces.size());
assertEquals(24, bySignature.size());
IrisJigsawPool start = pool(bundle, "kit/test/start");
assertEquals(24, start.getPieces().size());
IrisJigsawPool piecePool = pool(bundle, "kit/test/pieces");
assertEquals(24, piecePool.getPieces().size());
assertTrue(piecePool.getPieces().getLast().isEmpty());
for (IrisJigsawPieceEntry entry : piecePool.getPieces()) {
assertFalse("kit/test/blank".equals(entry.getPiece()));
}
}
}
@Test
public void spatialDefaultKitAttachesEveryPieceThroughAlignedConnectorsAndTurnsBothWays() throws IOException {
for (JigsawStudioCellDimensions dimensions : SPATIAL_DIMENSIONS) {
for (JigsawStudioCompatibilityTarget target : JigsawStudioCompatibilityTarget.values()) {
StructureGraphCompilation compilation = compile(JigsawStudioMode.SPATIAL_JIGSAW, target, dimensions);
int bound = target == JigsawStudioCompatibilityTarget.IRIS_EXTENDED
? SPATIAL_IRIS_PIECE_BOUND
: SPATIAL_PORTABLE_PIECE_BOUND;
Set<String> placedKinds = new HashSet<>();
Set<Integer> cornerAttachments = new HashSet<>();
for (long seed = 0; seed < SEEDS; seed++) {
List<PlacedStructurePiece> pieces = complete(compilation, seed);
String context = target + " " + dimensions + " seed " + seed;
assertTrue(context + " placed " + pieces.size(), pieces.size() <= bound);
for (int index = 0; index < pieces.size(); index++) {
placedKinds.add(baseName(pieces.get(index)));
if (index == 0) {
continue;
}
List<Integer> attachments = earlierSpatialAttachments(pieces, index);
assertFalse(context + " left " + baseName(pieces.get(index)) + " unattached",
attachments.isEmpty());
if (attachments.size() == 1 && baseName(pieces.get(index)).startsWith("corner")
&& attachments.getFirst() < 2) {
cornerAttachments.add(attachments.getFirst());
}
}
}
assertEquals(target + " " + dimensions, Set.of(0, 1), cornerAttachments);
assertFalse(placedKinds.contains("blank"));
assertEquals(target + " " + dimensions + " placed " + placedKinds, 23, placedKinds.size());
}
}
}
@Test
public void vanillaPortableSpatialProjectIsCreatedWithAViableGraph() throws IOException {
Path packRoot = temporaryFolder.getRoot().toPath();
JigsawStudioProjectCreator.Options options = options(
JigsawStudioMode.SPATIAL_JIGSAW,
JigsawStudioCompatibilityTarget.VANILLA_PORTABLE,
SPATIAL_DIMENSIONS.getFirst());
StructureWriteResult result = JigsawStudioProjectCreator.create(packRoot, options);
assertTrue(result.successful());
IrisStructure structure = GSON.fromJson(Files.readString(
packRoot.resolve("structures/kit/test.json"), StandardCharsets.UTF_8), IrisStructure.class);
assertEquals(4, structure.getMaxDepth());
assertTrue(Files.isRegularFile(packRoot.resolve("jigsaw-pieces/kit/test/corner-up-down.json")));
assertTrue(Files.isRegularFile(packRoot.resolve("objects/kit/test/blank.iob")));
}
private static JigsawStudioProjectCreator.Options options(
JigsawStudioMode mode,
JigsawStudioCompatibilityTarget target,
JigsawStudioCellDimensions dimensions
) {
return new JigsawStudioProjectCreator.Options("kit/test", mode, target, dimensions);
}
private static StructureGraphCompilation compile(
JigsawStudioMode mode,
JigsawStudioCompatibilityTarget target,
JigsawStudioCellDimensions dimensions
) throws IOException {
StructureGraphCompilation compilation = StructureResourceBundleGraphCompiler.compile(
JigsawStudioProjectCreator.bundle(options(mode, target, dimensions))).getFirst();
assertFalse(compilation.getDiagnostics().toString(), compilation.hasErrors());
return compilation;
}
private static List<PlacedStructurePiece> complete(StructureGraphCompilation compilation, long seed) {
StructureAssemblyResult result = StructureAssembler.forCompilation(
compilation,
new IrisPosition(0, 64, 0)).assemble(new RNG(seed));
assertEquals("seed " + seed + ": " + result.detail(), StructureAssemblyStatus.COMPLETE, result.status());
return result.pieces();
}
private static int soleEarlierPlanarAttachment(List<PlacedStructurePiece> pieces, int index) {
PlacedStructurePiece piece = pieces.get(index);
int attachment = -1;
int pairings = 0;
for (int connectorIndex = 0; connectorIndex < piece.getPiece().getConnectors().size(); connectorIndex++) {
IrisJigsawConnector connector = piece.getPiece().getConnectors().get(connectorIndex);
IrisDirection facing = piece.getRotation().rotate(connector.getDirection());
IrisPosition targetCell = offset(planarSocket(piece, facing), facing);
for (int earlier = 0; earlier < index; earlier++) {
PlacedStructurePiece neighbor = pieces.get(earlier);
if (contains(neighbor, targetCell) && hasPlanarSocket(
neighbor,
facing.reverse(),
targetCell,
piece.getRotation().rotate(connector.getTop()))) {
attachment = connectorIndex;
pairings++;
}
}
}
return pairings == 1 ? attachment : -1;
}
private static List<Integer> earlierSpatialAttachments(List<PlacedStructurePiece> pieces, int index) {
PlacedStructurePiece piece = pieces.get(index);
List<Integer> attachments = new ArrayList<>();
for (int connectorIndex = 0; connectorIndex < piece.getPiece().getConnectors().size(); connectorIndex++) {
IrisJigsawConnector connector = piece.getPiece().getConnectors().get(connectorIndex);
IrisPosition socket = spatialSocket(piece, connector);
IrisDirection facing = piece.getRotation().rotate(connector.getDirection());
for (int earlier = 0; earlier < index; earlier++) {
PlacedStructurePiece neighbor = pieces.get(earlier);
for (IrisJigsawConnector other : neighbor.getPiece().getConnectors()) {
IrisDirection otherFacing = neighbor.getRotation().rotate(other.getDirection());
if (otherFacing == facing.reverse()
&& offset(spatialSocket(neighbor, other), otherFacing).equals(socket)) {
attachments.add(connectorIndex);
}
}
}
}
return attachments;
}
private static boolean hasPlanarSocket(
PlacedStructurePiece piece,
IrisDirection facing,
IrisPosition position,
IrisDirection top
) {
if (!planarSocket(piece, facing).equals(position)) {
return false;
}
for (IrisJigsawConnector connector : piece.getPiece().getConnectors()) {
if (piece.getRotation().rotate(connector.getDirection()) == facing
&& piece.getRotation().rotate(connector.getTop()) == top) {
return true;
}
}
return false;
}
private static void assertSharedFace(
String context,
PlacedStructurePiece piece,
PlacedStructurePiece neighbor,
IrisDirection facing
) {
assertEquals(context, piece.getMinY(), neighbor.getMinY());
assertEquals(context, piece.getMaxY(), neighbor.getMaxY());
switch (facing) {
case EAST_POSITIVE_X -> assertEquals(context, piece.getMaxX() + 1, neighbor.getMinX());
case WEST_NEGATIVE_X -> assertEquals(context, piece.getMinX() - 1, neighbor.getMaxX());
case SOUTH_POSITIVE_Z -> assertEquals(context, piece.getMaxZ() + 1, neighbor.getMinZ());
case NORTH_NEGATIVE_Z -> assertEquals(context, piece.getMinZ() - 1, neighbor.getMaxZ());
case UP_POSITIVE_Y, DOWN_NEGATIVE_Y -> throw new AssertionError(context + " uses a vertical socket");
}
if (facing.x() != 0) {
assertEquals(context, piece.getMinZ(), neighbor.getMinZ());
assertEquals(context, piece.getMaxZ(), neighbor.getMaxZ());
} else {
assertEquals(context, piece.getMinX(), neighbor.getMinX());
assertEquals(context, piece.getMaxX(), neighbor.getMaxX());
}
}
private static IrisPosition planarSocket(PlacedStructurePiece piece, IrisDirection facing) {
IrisPosition size = new IrisPosition(
piece.getMaxX() - piece.getMinX() + 1,
piece.getMaxY() - piece.getMinY() + 1,
piece.getMaxZ() - piece.getMinZ() + 1);
IrisPosition local = IrisJigsawConnector.canonicalPlanarPosition(size, facing);
return new IrisPosition(
piece.getMinX() + local.getX(),
piece.getMinY() + local.getY(),
piece.getMinZ() + local.getZ());
}
private static IrisPosition spatialSocket(PlacedStructurePiece piece, IrisJigsawConnector connector) {
IrisObject object = piece.getObject();
IrisPosition relative = new IrisPosition(
connector.getPosition().getX() - object.getW() / 2,
connector.getPosition().getY() - object.getH() / 2,
connector.getPosition().getZ() - object.getD() / 2);
IrisPosition rotated = piece.getRotation().rotate(relative);
return new IrisPosition(
piece.getX() + rotated.getX(),
piece.getY() + rotated.getY(),
piece.getZ() + rotated.getZ());
}
private static IrisPosition spatialFaceCenter(JigsawStudioCellDimensions dimensions, IrisDirection direction) {
int centerX = dimensions.width() / 2;
int centerY = dimensions.height() / 2;
int centerZ = dimensions.depth() / 2;
return switch (direction) {
case NORTH_NEGATIVE_Z -> new IrisPosition(centerX, centerY, 0);
case SOUTH_POSITIVE_Z -> new IrisPosition(centerX, centerY, dimensions.depth() - 1);
case EAST_POSITIVE_X -> new IrisPosition(dimensions.width() - 1, centerY, centerZ);
case WEST_NEGATIVE_X -> new IrisPosition(0, centerY, centerZ);
case UP_POSITIVE_Y -> new IrisPosition(centerX, dimensions.height() - 1, centerZ);
case DOWN_NEGATIVE_Y -> new IrisPosition(centerX, 0, centerZ);
};
}
private static IrisPosition offset(IrisPosition position, IrisDirection direction) {
return new IrisPosition(
position.getX() + direction.x(),
position.getY() + direction.y(),
position.getZ() + direction.z());
}
private static PlacedStructurePiece containing(List<PlacedStructurePiece> pieces, IrisPosition position) {
for (PlacedStructurePiece piece : pieces) {
if (contains(piece, position)) {
return piece;
}
}
return null;
}
private static boolean contains(PlacedStructurePiece piece, IrisPosition position) {
return position.getX() >= piece.getMinX() && position.getX() <= piece.getMaxX()
&& position.getY() >= piece.getMinY() && position.getY() <= piece.getMaxY()
&& position.getZ() >= piece.getMinZ() && position.getZ() <= piece.getMaxZ();
}
private static String baseName(PlacedStructurePiece piece) {
String key = piece.getPiece().getObject();
return key.substring(key.lastIndexOf('/') + 1);
}
private static int horizontalBit(IrisDirection direction) {
return switch (direction) {
case NORTH_NEGATIVE_Z -> 1;
case EAST_POSITIVE_X -> 2;
case SOUTH_POSITIVE_Z -> 4;
case WEST_NEGATIVE_X -> 8;
case UP_POSITIVE_Y, DOWN_NEGATIVE_Y -> 0;
};
}
private static int verticalBit(IrisDirection direction) {
return switch (direction) {
case UP_POSITIVE_Y -> 16;
case DOWN_NEGATIVE_Y -> 32;
case NORTH_NEGATIVE_Z, EAST_POSITIVE_X, SOUTH_POSITIVE_Z, WEST_NEGATIVE_X -> 0;
};
}
private static int canonicalMask(int mask) {
int canonical = mask;
for (int quarterTurns = 1; quarterTurns < 4; quarterTurns++) {
canonical = Math.min(canonical, ((mask << quarterTurns) | (mask >>> (4 - quarterTurns))) & 15);
}
return canonical;
}
private static String expectedSpatialKey(int canonicalHorizontal, int vertical) {
String horizontal = switch (canonicalHorizontal) {
case 0 -> "blank";
case 1 -> "end";
case 3 -> "corner";
case 5 -> "straight";
case 7 -> "tee";
case 15 -> "cross";
default -> throw new AssertionError("Unexpected horizontal topology " + canonicalHorizontal);
};
String suffix = switch (vertical) {
case 0 -> "";
case 16 -> "-up";
case 32 -> "-down";
case 48 -> "-up-down";
default -> throw new AssertionError("Unexpected vertical topology " + vertical);
};
return "kit/test/" + horizontal + suffix;
}
private static Map<String, IrisJigsawPiece> pieces(StructureResourceBundle bundle) {
Map<String, IrisJigsawPiece> pieces = new TreeMap<>();
for (StructureResourceBundle.Resource resource : bundle.resources().values()) {
String path = resource.relativePath();
if (path.startsWith("jigsaw-pieces/") && path.endsWith(".json")) {
pieces.put(
path.substring("jigsaw-pieces/".length(), path.length() - ".json".length()),
GSON.fromJson(new String(resource.content(), StandardCharsets.UTF_8), IrisJigsawPiece.class));
}
}
return pieces;
}
private static IrisJigsawPool pool(StructureResourceBundle bundle, String key) {
StructureResourceBundle.Resource resource = bundle.resources().get("jigsaw-pools/" + key + ".json");
assertNotNull(key, resource);
return GSON.fromJson(new String(resource.content(), StandardCharsets.UTF_8), IrisJigsawPool.class);
}
}
@@ -0,0 +1,101 @@
package art.arcane.iris.studio.jigsaw;
import art.arcane.iris.pack.value.IrisDirection;
import art.arcane.iris.pack.value.IrisPosition;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.structure.jigsaw.IrisJigsawConnector;
import art.arcane.iris.structure.jigsaw.IrisJigsawPiece;
import art.arcane.iris.structure.jigsaw.JigsawJoint;
import art.arcane.iris.structure.object.IrisObject;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.collection.KList;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.data.BlockData;
import org.bukkit.entity.Player;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.io.ByteArrayInputStream;
import java.util.List;
import java.util.UUID;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
public class JigsawStudioCaptureFinalStateTest {
@Test
public void hiddenConnectorOverDugOutAirStoresExplicitAirLikeVisibleMarkers() throws Throwable {
JigsawStudioBounds bounds = new JigsawStudioBounds(0, 64, 0, new JigsawStudioCellDimensions(1, 1, 1));
IrisJigsawPiece piece = new IrisJigsawPiece().setConnectors(new KList<>());
piece.getConnectors().add(new IrisJigsawConnector()
.setPosition(new IrisPosition(0, 0, 0))
.setDirection(IrisDirection.NORTH_NEGATIVE_Z)
.setTop(IrisDirection.UP_POSITIVE_Y)
.setPool("fort/pieces")
.setName("iris:door")
.setTargetName("iris:door")
.setJoint(JigsawJoint.ALIGNED)
.setFinalState("minecraft:structure_void"));
BlockData air = mock(BlockData.class);
when(air.getMaterial()).thenReturn(Material.AIR);
when(air.getAsString()).thenReturn("minecraft:air");
Block block = mock(Block.class);
when(block.getBlockData()).thenReturn(air);
World world = mock(World.class);
when(world.getBlockAt(0, 64, 0)).thenReturn(block);
JigsawStudioCapture.ChunkCaptureArea area = JigsawStudioCapture.chunkIntersections(bounds).getFirst();
JigsawStudioCapture.ChunkSnapshot snapshot = JigsawStudioCapture.captureChunkIntersection(
world, bounds, piece, new IrisObject(1, 1, 1), area, 0, false);
JigsawStudioCapture.Capture capture = JigsawStudioCapture.aggregateSnapshots(
bounds, List.of(area), List.of(snapshot));
assertEquals("minecraft:air", capture.connectors().getFirst().getFinalState());
NativeBlockState restoredAir = mock(NativeBlockState.class);
when(restoredAir.key()).thenReturn("minecraft:air");
IrisObject restored = read(capture.objectContent(), restoredAir);
assertTrue(restored.getBlocks().containsKey(restored.getSigned(0, 0, 0)));
assertEquals("minecraft:air", restored.getBlocks().get(restored.getSigned(0, 0, 0)).key());
}
@Test
public void autosaveNotificationsFallBackToTheOnlineOwner() {
Player explicit = mock(Player.class);
Player owner = mock(Player.class);
UUID ownerId = UUID.randomUUID();
try (MockedStatic<Bukkit> server = mockStatic(Bukkit.class)) {
server.when(() -> Bukkit.getPlayer(ownerId)).thenReturn(owner);
assertSame(explicit, JigsawStudioCaptureScheduler.notificationTarget(explicit, ownerId));
assertSame(owner, JigsawStudioCaptureScheduler.notificationTarget(null, ownerId));
assertNull(JigsawStudioCaptureScheduler.notificationTarget(null, null));
}
}
private static IrisObject read(byte[] content, NativeBlockState restoredState) throws Throwable {
PlatformRegistries registries = mock(PlatformRegistries.class);
when(registries.block(anyString())).thenReturn(restoredState);
IrisPlatform platform = mock(IrisPlatform.class);
when(platform.registries()).thenReturn(registries);
IrisPlatforms.unbind();
IrisPlatforms.bind(platform);
try {
IrisObject restored = new IrisObject();
restored.read(new ByteArrayInputStream(content));
return restored;
} finally {
IrisPlatforms.unbind();
}
}
}
@@ -0,0 +1,383 @@
package art.arcane.iris.studio.jigsaw;
import art.arcane.iris.pack.value.IrisDirection;
import art.arcane.iris.pack.value.IrisPosition;
import art.arcane.iris.structure.jigsaw.IrisJigsawConnector;
import art.arcane.iris.structure.jigsaw.JigsawJoint;
import art.arcane.iris.studio.jigsaw.JigsawStudioCapture.LocalPosition;
import art.arcane.iris.studio.jigsaw.JigsawStudioConnectorPlacement.Box;
import art.arcane.iris.studio.jigsaw.JigsawStudioConnectorPlacement.BlockClick;
import art.arcane.iris.studio.jigsaw.JigsawStudioConnectorPlacement.Cell;
import art.arcane.iris.studio.jigsaw.JigsawStudioConnectorPlacement.CellContent;
import art.arcane.iris.studio.jigsaw.JigsawStudioConnectorPlacement.CellContents;
import art.arcane.iris.studio.jigsaw.JigsawStudioConnectorPlacement.Decision;
import art.arcane.iris.studio.jigsaw.JigsawStudioConnectorPlacement.Frame;
import art.arcane.iris.studio.jigsaw.JigsawStudioConnectorPlacement.Identity;
import art.arcane.iris.studio.jigsaw.JigsawStudioConnectorPlacement.Operation;
import art.arcane.iris.studio.jigsaw.JigsawStudioConnectorPlacement.Ray;
import art.arcane.iris.studio.jigsaw.JigsawStudioConnectorPlacement.ReseatResult;
import art.arcane.iris.studio.jigsaw.JigsawStudioConnectorPlacement.Target;
import org.junit.Test;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class JigsawStudioConnectorPlacementTest {
private static final Box BOX = new Box(16, 65, 16, 20, 69, 20);
private static final CellContents EMPTY = position -> Optional.empty();
private static final Identity SPATIAL = new Identity("demo/pieces", "iris:spatial", "iris:spatial", JigsawJoint.ROLLABLE);
@Test
public void clickedBlockInsideTheVariantBecomesTheConnectorFacingOutOfTheClickedFace() {
Optional<Target> target = JigsawStudioConnectorPlacement.target(
BOX,
new Ray(18.5D, 67.5D, 25.0D, 0.0D, 0.0D, -1.0D),
Optional.of(new BlockClick(18, 67, 20, IrisDirection.SOUTH_POSITIVE_Z)),
Set.of(),
false);
assertEquals(Optional.of(new Target(18, 67, 20, IrisDirection.SOUTH_POSITIVE_Z)), target);
}
@Test
public void sneakingFacesTheConnectorIntoTheClickedFace() {
Optional<Target> target = JigsawStudioConnectorPlacement.target(
BOX,
new Ray(18.5D, 67.5D, 25.0D, 0.0D, 0.0D, -1.0D),
Optional.of(new BlockClick(18, 67, 20, IrisDirection.SOUTH_POSITIVE_Z)),
Set.of(),
true);
assertEquals(Optional.of(new Target(18, 67, 20, IrisDirection.NORTH_NEGATIVE_Z)), target);
}
@Test
public void clickingTheStudioFloorPlacesADownConnectorOnTheBottomLayer() {
Optional<Target> target = JigsawStudioConnectorPlacement.target(
BOX,
new Ray(18.5D, 70.0D, 18.5D, 0.0D, -1.0D, 0.0D),
Optional.of(new BlockClick(18, 64, 18, IrisDirection.UP_POSITIVE_Y)),
Set.of(),
false);
assertEquals(Optional.of(new Target(18, 65, 18, IrisDirection.DOWN_NEGATIVE_Y)), target);
}
@Test
public void pointingAtAirFromInsideUsesTheBoundaryCellWhereTheRayLeaves() {
Optional<Target> target = JigsawStudioConnectorPlacement.target(
BOX,
new Ray(18.5D, 67.5D, 18.5D, 0.0D, 0.0D, -1.0D),
Optional.empty(),
Set.of(),
false);
assertEquals(Optional.of(new Target(18, 67, 16, IrisDirection.NORTH_NEGATIVE_Z)), target);
}
@Test
public void pointingAtAirFromOutsideUsesTheBoundaryCellWhereTheRayEnters() {
Optional<Target> target = JigsawStudioConnectorPlacement.target(
BOX,
new Ray(18.5D, 67.5D, 30.0D, 0.0D, 0.0D, -1.0D),
Optional.empty(),
Set.of(),
false);
assertEquals(Optional.of(new Target(18, 67, 20, IrisDirection.SOUTH_POSITIVE_Z)), target);
}
@Test
public void connectorCellsInFrontOfTheClickedBlockWinSoHiddenConnectorsStaySelectable() {
Optional<Target> target = JigsawStudioConnectorPlacement.target(
BOX,
new Ray(18.5D, 67.5D, 18.5D, 0.0D, 0.0D, -1.0D),
Optional.of(new BlockClick(18, 67, 16, IrisDirection.SOUTH_POSITIVE_Z)),
Set.of(new Cell(18, 67, 17)),
false);
assertEquals(Optional.of(new Target(18, 67, 17, IrisDirection.SOUTH_POSITIVE_Z)), target);
}
@Test
public void raysThatMissTheVariantResolveNothing() {
Optional<Target> target = JigsawStudioConnectorPlacement.target(
BOX,
new Ray(30.5D, 67.5D, 30.5D, 0.0D, 0.0D, 1.0D),
Optional.empty(),
Set.of(),
false);
assertTrue(target.isEmpty());
}
@Test
public void spatialRightClicksAddSelectMoveReorientAndDeselect() {
LocalPosition door = new LocalPosition(2, 1, 0);
LocalPosition other = new LocalPosition(4, 1, 2);
LocalPosition empty = new LocalPosition(0, 1, 2);
Map<LocalPosition, IrisDirection> connectors = Map.of(
door, IrisDirection.NORTH_NEGATIVE_Z,
other, IrisDirection.EAST_POSITIVE_X);
assertEquals(Operation.ADD, spatial(false, empty, IrisDirection.WEST_NEGATIVE_X, connectors, null));
assertEquals(Operation.SELECT, spatial(false, door, IrisDirection.NORTH_NEGATIVE_Z, connectors, null));
assertEquals(Operation.MOVE, spatial(false, empty, IrisDirection.WEST_NEGATIVE_X, connectors, door));
assertEquals(Operation.DESELECT, spatial(false, door, IrisDirection.NORTH_NEGATIVE_Z, connectors, door));
assertEquals(Operation.REORIENT, spatial(false, door, IrisDirection.UP_POSITIVE_Y, connectors, door));
assertEquals(Operation.SELECT, spatial(false, other, IrisDirection.EAST_POSITIVE_X, connectors, door));
assertEquals(Operation.ADD, spatial(false, empty, IrisDirection.WEST_NEGATIVE_X, connectors, empty));
}
@Test
public void spatialLeftClicksRemoveOrClearTheSelection() {
LocalPosition door = new LocalPosition(2, 1, 0);
Map<LocalPosition, IrisDirection> connectors = Map.of(door, IrisDirection.NORTH_NEGATIVE_Z);
LocalPosition empty = new LocalPosition(1, 1, 1);
assertEquals(Operation.REMOVE, spatial(true, door, IrisDirection.NORTH_NEGATIVE_Z, connectors, null));
assertEquals(Operation.CLEAR_SELECTION, spatial(true, empty, IrisDirection.UP_POSITIVE_Y, connectors, door));
assertEquals(Operation.NONE, spatial(true, empty, IrisDirection.UP_POSITIVE_Y, connectors, null));
}
@Test
public void planarRightClicksReseatOnlyTheWorkcellsTopologySides() {
JigsawStudioCellDimensions dimensions = new JigsawStudioCellDimensions(5, 5, 5);
Map<LocalPosition, IrisDirection> connectors = Map.of();
Decision east = JigsawStudioConnectorPlacement.decidePlanar(false, new LocalPosition(4, 2, 2),
IrisDirection.EAST_POSITIVE_X, dimensions, JigsawPlanarTopology.NORTH_EAST_WEST_TEE, connectors, "T Junction");
Decision south = JigsawStudioConnectorPlacement.decidePlanar(false, new LocalPosition(2, 2, 4),
IrisDirection.SOUTH_POSITIVE_Z, dimensions, JigsawPlanarTopology.NORTH_EAST_WEST_TEE, connectors, "T Junction");
Decision floor = JigsawStudioConnectorPlacement.decidePlanar(false, new LocalPosition(0, 0, 3),
IrisDirection.DOWN_NEGATIVE_Y, dimensions, JigsawPlanarTopology.NORTH_EAST_WEST_TEE, connectors, "T Junction");
assertEquals(Operation.RESEAT, east.operation());
assertEquals(JigsawPlanarDirection.EAST, east.side());
assertEquals(Operation.REFUSE, south.operation());
assertTrue(south.message().contains("north, east, and west"));
assertEquals(Operation.RESEAT, floor.operation());
assertEquals(JigsawPlanarDirection.WEST, floor.side());
}
@Test
public void planarLeftClicksRefuseTheOnlySocketOfAConnectedSideButRemoveStrays() {
JigsawStudioCellDimensions dimensions = new JigsawStudioCellDimensions(5, 5, 5);
LocalPosition socket = new LocalPosition(2, 2, 0);
LocalPosition stray = new LocalPosition(1, 2, 0);
Map<LocalPosition, IrisDirection> single = Map.of(socket, IrisDirection.NORTH_NEGATIVE_Z);
Map<LocalPosition, IrisDirection> doubled = Map.of(
socket, IrisDirection.NORTH_NEGATIVE_Z,
stray, IrisDirection.NORTH_NEGATIVE_Z);
Decision refused = JigsawStudioConnectorPlacement.decidePlanar(true, socket,
IrisDirection.NORTH_NEGATIVE_Z, dimensions, JigsawPlanarTopology.NORTH_END, single, "End Cap");
Decision removed = JigsawStudioConnectorPlacement.decidePlanar(true, stray,
IrisDirection.NORTH_NEGATIVE_Z, dimensions, JigsawPlanarTopology.NORTH_END, doubled, "End Cap");
assertEquals(Operation.REFUSE, refused.operation());
assertEquals(Operation.REMOVE, removed.operation());
}
@Test
public void framesRoundTripPositionsAndDirectionsForEveryQuarterTurn() {
JigsawStudioCellDimensions source = new JigsawStudioCellDimensions(6, 3, 4);
for (int quarterTurns = 0; quarterTurns < 4; quarterTurns++) {
Frame frame = new Frame(source, quarterTurns);
for (int x = 0; x < 6; x++) {
for (int z = 0; z < 4; z++) {
IrisPosition position = new IrisPosition(x, 1, z);
LocalPosition display = frame.display(position);
JigsawStudioCellDimensions dimensions = frame.displayDimensions();
assertTrue(display.x() >= 0 && display.x() < dimensions.width());
assertTrue(display.z() >= 0 && display.z() < dimensions.depth());
assertEquals(position, frame.source(display));
}
}
for (IrisDirection direction : IrisDirection.values()) {
assertEquals(direction, frame.sourceDirection(frame.displayDirection(direction)));
}
}
}
@Test
public void addConvertsDisplayPlacementIntoSourceSpaceWithDefaultIdentity() {
Frame frame = new Frame(new JigsawStudioCellDimensions(6, 3, 4), 1);
LocalPosition target = new LocalPosition(0, 1, 2);
List<IrisJigsawConnector> added = JigsawStudioConnectorPlacement.add(
List.of(), frame, target, IrisDirection.WEST_NEGATIVE_X, IrisDirection.UP_POSITIVE_Y, SPATIAL, EMPTY);
IrisJigsawConnector connector = added.getFirst();
assertEquals(target, frame.display(connector.getPosition()));
assertEquals(IrisDirection.WEST_NEGATIVE_X, frame.displayDirection(connector.getDirection()));
assertEquals(IrisDirection.UP_POSITIVE_Y, connector.getTop());
assertEquals("demo/pieces", connector.getPool());
assertEquals("iris:spatial", connector.getName());
assertEquals("iris:spatial", connector.getTargetName());
assertEquals(JigsawJoint.ROLLABLE, connector.getJoint());
assertEquals("minecraft:structure_void", connector.getFinalState());
assertEquals("", connector.getChannel());
}
@Test
public void connectorsPlacedOnBuiltBlocksKeepThatBlockAsTheirFinalState() {
Frame frame = new Frame(new JigsawStudioCellDimensions(5, 5, 5), 0);
CellContents wall = position -> Optional.of(new CellContent("minecraft:stone_bricks", false));
CellContents chest = position -> Optional.of(new CellContent("minecraft:chest[facing=north]", true));
List<IrisJigsawConnector> added = JigsawStudioConnectorPlacement.add(
List.of(), frame, new LocalPosition(2, 1, 0), IrisDirection.NORTH_NEGATIVE_Z,
IrisDirection.UP_POSITIVE_Y, SPATIAL, wall);
assertEquals("minecraft:stone_bricks", added.getFirst().getFinalState());
assertThrows(IllegalArgumentException.class, () -> JigsawStudioConnectorPlacement.add(
List.of(), frame, new LocalPosition(2, 1, 0), IrisDirection.NORTH_NEGATIVE_Z,
IrisDirection.UP_POSITIVE_Y, SPATIAL, chest));
}
@Test
public void addingOntoAnExistingConnectorIsRejected() {
Frame frame = new Frame(new JigsawStudioCellDimensions(5, 5, 5), 0);
List<IrisJigsawConnector> source = List.of(connector(new IrisPosition(2, 1, 0), IrisDirection.NORTH_NEGATIVE_Z));
assertThrows(IllegalArgumentException.class, () -> JigsawStudioConnectorPlacement.add(
source, frame, new LocalPosition(2, 1, 0), IrisDirection.NORTH_NEGATIVE_Z,
IrisDirection.UP_POSITIVE_Y, SPATIAL, EMPTY));
}
@Test
public void moveKeepsIdentityAndOrderAndOnlyReplacesTheFinalStateOnBuiltBlocks() {
Frame frame = new Frame(new JigsawStudioCellDimensions(5, 5, 5), 0);
IrisJigsawConnector first = connector(new IrisPosition(2, 1, 0), IrisDirection.NORTH_NEGATIVE_Z)
.setChannel("hall").setFinalState("minecraft:air");
IrisJigsawConnector second = connector(new IrisPosition(4, 1, 2), IrisDirection.EAST_POSITIVE_X);
List<IrisJigsawConnector> source = List.of(first, second);
List<IrisJigsawConnector> moved = JigsawStudioConnectorPlacement.move(
source, frame, new LocalPosition(2, 1, 0), new LocalPosition(0, 1, 2),
IrisDirection.WEST_NEGATIVE_X, IrisDirection.UP_POSITIVE_Y, EMPTY);
List<IrisJigsawConnector> onWall = JigsawStudioConnectorPlacement.move(
source, frame, new LocalPosition(2, 1, 0), new LocalPosition(0, 1, 2),
IrisDirection.WEST_NEGATIVE_X, IrisDirection.UP_POSITIVE_Y,
position -> Optional.of(new CellContent("minecraft:stone", false)));
assertEquals(2, moved.size());
assertEquals(new IrisPosition(0, 1, 2), moved.getFirst().getPosition());
assertEquals(IrisDirection.WEST_NEGATIVE_X, moved.getFirst().getDirection());
assertEquals("hall", moved.getFirst().getChannel());
assertEquals("demo/pieces", moved.getFirst().getPool());
assertEquals("minecraft:air", moved.getFirst().getFinalState());
assertEquals("minecraft:stone", onWall.getFirst().getFinalState());
assertEquals(second.getPosition(), moved.get(1).getPosition());
assertEquals(new IrisPosition(2, 1, 0), first.getPosition());
assertThrows(IllegalArgumentException.class, () -> JigsawStudioConnectorPlacement.move(
source, frame, new LocalPosition(2, 1, 0), new LocalPosition(4, 1, 2),
IrisDirection.EAST_POSITIVE_X, IrisDirection.UP_POSITIVE_Y, EMPTY));
}
@Test
public void removeDropsOnlyTheConnectorAtThatDisplayPosition() {
Frame frame = new Frame(new JigsawStudioCellDimensions(5, 5, 5), 2);
IrisJigsawConnector north = connector(new IrisPosition(2, 1, 0), IrisDirection.NORTH_NEGATIVE_Z);
IrisJigsawConnector east = connector(new IrisPosition(4, 1, 2), IrisDirection.EAST_POSITIVE_X);
List<IrisJigsawConnector> remaining = JigsawStudioConnectorPlacement.remove(
List.of(north, east), frame, frame.display(north.getPosition()));
assertEquals(1, remaining.size());
assertEquals(east.getPosition(), remaining.getFirst().getPosition());
}
@Test
public void reseatSnapsAMovedSocketBackToTheSourceFaceCenterAndDropsStrays() {
JigsawStudioCellDimensions source = new JigsawStudioCellDimensions(4, 4, 6);
Frame frame = new Frame(source, 1);
IrisJigsawConnector moved = connector(new IrisPosition(0, 1, 0), IrisDirection.NORTH_NEGATIVE_Z).setChannel("road");
IrisJigsawConnector stray = connector(new IrisPosition(3, 1, 0), IrisDirection.NORTH_NEGATIVE_Z);
IrisJigsawConnector south = connector(
IrisJigsawConnector.canonicalPlanarPosition(new IrisPosition(4, 4, 6), IrisDirection.SOUTH_POSITIVE_Z),
IrisDirection.SOUTH_POSITIVE_Z);
JigsawPlanarDirection displaySide = JigsawPlanarDirection.EAST;
assertEquals(IrisDirection.NORTH_NEGATIVE_Z, frame.sourceDirection(displaySide.irisDirection()));
ReseatResult result = JigsawStudioConnectorPlacement.reseat(
List.of(moved, stray, south), frame, displaySide, SPATIAL, EMPTY);
assertTrue(result.changed());
assertEquals(1, result.removedStrays());
assertEquals(2, result.connectors().size());
IrisJigsawConnector seated = result.connectors().getFirst();
assertEquals(IrisJigsawConnector.canonicalPlanarPosition(
new IrisPosition(4, 4, 6), IrisDirection.NORTH_NEGATIVE_Z), seated.getPosition());
assertEquals("road", seated.getChannel());
assertEquals(south.getPosition(), result.connectors().get(1).getPosition());
ReseatResult unchanged = JigsawStudioConnectorPlacement.reseat(
result.connectors(), frame, displaySide, SPATIAL, EMPTY);
assertFalse(unchanged.changed());
}
@Test
public void reseatAddsAMissingSocketWithTheDefaultIdentity() {
Frame frame = new Frame(new JigsawStudioCellDimensions(5, 5, 5), 0);
Identity planar = new Identity("demo/pieces", "iris:planar", "iris:planar", JigsawJoint.ALIGNED);
ReseatResult result = JigsawStudioConnectorPlacement.reseat(
List.of(), frame, JigsawPlanarDirection.WEST, planar, EMPTY);
assertTrue(result.changed());
IrisJigsawConnector added = result.connectors().getFirst();
assertEquals(new IrisPosition(0, 2, 2), added.getPosition());
assertEquals(IrisDirection.WEST_NEGATIVE_X, added.getDirection());
assertEquals(IrisDirection.UP_POSITIVE_Y, added.getTop());
assertEquals(JigsawJoint.ALIGNED, added.getJoint());
assertEquals("iris:planar", added.getName());
}
@Test
public void changedPositionsListEveryDisplayCellWhoseConnectorDiffers() {
Frame frame = new Frame(new JigsawStudioCellDimensions(5, 5, 5), 0);
IrisJigsawConnector north = connector(new IrisPosition(2, 1, 0), IrisDirection.NORTH_NEGATIVE_Z);
IrisJigsawConnector east = connector(new IrisPosition(4, 1, 2), IrisDirection.EAST_POSITIVE_X);
List<IrisJigsawConnector> before = List.of(north, east);
List<IrisJigsawConnector> after = JigsawStudioConnectorPlacement.move(
before, frame, new LocalPosition(2, 1, 0), new LocalPosition(0, 1, 2),
IrisDirection.WEST_NEGATIVE_X, IrisDirection.UP_POSITIVE_Y, EMPTY);
Set<LocalPosition> changed = JigsawStudioConnectorPlacement.changedPositions(before, after, frame);
assertEquals(Set.of(new LocalPosition(2, 1, 0), new LocalPosition(0, 1, 2)), changed);
}
private static Operation spatial(
boolean primary,
LocalPosition target,
IrisDirection facing,
Map<LocalPosition, IrisDirection> connectors,
LocalPosition selection
) {
return JigsawStudioConnectorPlacement.decideSpatial(
primary, target, facing, new LinkedHashMap<>(connectors), Optional.ofNullable(selection)).operation();
}
private static IrisJigsawConnector connector(IrisPosition position, IrisDirection direction) {
return new IrisJigsawConnector()
.setPosition(position)
.setDirection(direction)
.setTop(IrisDirection.UP_POSITIVE_Y)
.setPool("demo/pieces")
.setName("iris:spatial")
.setTargetName("iris:spatial")
.setJoint(JigsawJoint.ROLLABLE)
.setFinalState("minecraft:structure_void");
}
}
@@ -0,0 +1,40 @@
package art.arcane.iris.studio.jigsaw;
import art.arcane.iris.studio.jigsaw.JigsawStudioCapture.LocalPosition;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.junit.Test;
import java.util.Set;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class JigsawStudioConnectorRepairTest {
@Test
public void resetFindsUnsavedMarkersSoTopologyErrorsCanBeRepaired() {
JigsawStudioBounds bounds = new JigsawStudioBounds(0, 65, 0, new JigsawStudioCellDimensions(3, 2, 3));
World world = mock(World.class);
Block air = mock(Block.class);
when(air.getType()).thenReturn(Material.AIR);
Block saved = mock(Block.class);
when(saved.getType()).thenReturn(Material.JIGSAW);
Block stray = mock(Block.class);
when(stray.getType()).thenReturn(Material.JIGSAW);
when(world.getBlockAt(anyInt(), anyInt(), anyInt())).thenReturn(air);
when(world.getBlockAt(1, 65, 0)).thenReturn(saved);
when(world.getBlockAt(1, 66, 1)).thenReturn(stray);
JigsawStudioCapture.ChunkCaptureArea area = JigsawStudioCapture.chunkIntersections(bounds).getFirst();
Set<LocalPosition> savedConnectors = Set.of(new LocalPosition(1, 0, 0));
assertEquals(
Set.of(new LocalPosition(1, 1, 1)),
JigsawStudioMaterializer.strayMarkerPositions(world, bounds, area, savedConnectors, true));
assertEquals(
Set.of(new LocalPosition(1, 0, 0), new LocalPosition(1, 1, 1)),
JigsawStudioMaterializer.strayMarkerPositions(world, bounds, area, savedConnectors, false));
}
}
@@ -0,0 +1,143 @@
package art.arcane.iris.studio.jigsaw;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.studio.generation.JigsawStudioGenerator;
import art.arcane.iris.world.task.J;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.entity.Player;
import org.bukkit.event.block.BlockPistonExtendEvent;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class JigsawStudioEditorRoutingTest {
private static final UUID REQUEST = UUID.fromString("66666666-6666-6666-6666-666666666666");
@Test
public void renamingASpatialWorkcellRenamesTheVariantThatLabelsIt() throws Exception {
JigsawStudioVariant variant = new JigsawStudioVariant(
"demo/cross",
"demo/cross",
"",
Optional.of(new JigsawStudioCellDimensions(15, 15, 15)),
JigsawStudioMode.SPATIAL_JIGSAW,
Optional.empty(),
true,
true,
List.of(),
new JigsawStudioPieceRules(0, 30, 0, 0, false),
List.of());
JigsawStudioLayout layout = JigsawStudioLayout.create(
JigsawStudioMode.SPATIAL_JIGSAW,
new JigsawStudioCellDimensions(15, 15, 15),
new JigsawStudioVariantCatalog(List.of(variant)));
JigsawStudioSession session = new JigsawStudioSession(REQUEST, "overworld", "demo", layout);
JigsawStudioService service = new JigsawStudioService();
JigsawStudioVariantProperties properties = mock(JigsawStudioVariantProperties.class);
setField(service, "variantProperties", properties);
Player player = register(service, session);
try (MockedStatic<J> scheduling = mockStatic(J.class)) {
scheduling.when(() -> J.isOwnedByCurrentRegion(any(Player.class))).thenReturn(true);
service.workcellEditor.updateWorkcellDisplayName(player, JigsawStudioLayout.SPATIAL_WORKCELL_ID, "Great Hall");
verify(properties).updateVariantDisplayName(
player, JigsawStudioLayout.SPATIAL_WORKCELL_ID, "demo/cross", "Great Hall");
}
}
@Test
public void pistonHeadsCannotExtendIntoProtectedStudioCells() throws Exception {
JigsawStudioService service = new JigsawStudioService();
JigsawStudioProtection protection = mock(JigsawStudioProtection.class);
setField(service, "protection", protection);
World world = mock(World.class);
when(world.getUID()).thenReturn(UUID.randomUUID());
Block piston = mock(Block.class);
Block head = mock(Block.class);
when(piston.getWorld()).thenReturn(world);
when(piston.getRelative(BlockFace.EAST)).thenReturn(head);
when(protection.isImmutableStudioBlock(head)).thenReturn(true);
BlockPistonExtendEvent event = new BlockPistonExtendEvent(piston, List.of(), BlockFace.EAST);
service.protectionListener.onProtectedPistonExtend(event);
assertTrue(event.isCancelled());
}
private static Player register(JigsawStudioService service, JigsawStudioSession session) throws Exception {
UUID worldId = UUID.randomUUID();
World world = mock(World.class);
when(world.getUID()).thenReturn(worldId);
Player player = mock(Player.class);
when(player.getWorld()).thenReturn(world);
when(player.getUniqueId()).thenReturn(UUID.randomUUID());
JigsawStudioActivation.Request request = new JigsawStudioActivation.Request(
REQUEST,
"overworld",
"demo",
JigsawStudioMode.SPATIAL_JIGSAW,
JigsawStudioCompatibilityTarget.IRIS_EXTENDED,
new JigsawStudioCellDimensions(15, 15, 15),
mock(IrisData.class),
null);
JigsawStudioGenerator generator = mock(JigsawStudioGenerator.class);
when(generator.getRequest()).thenReturn(request);
when(generator.getSession()).thenReturn(session);
when(generator.getLayout()).thenAnswer(invocation -> session.layout());
Class<?> studioType = Class.forName(JigsawStudioService.class.getName() + "$ActiveStudio");
Constructor<?> constructor = studioType.getDeclaredConstructor(
UUID.class,
World.class,
Engine.class,
JigsawStudioGenerator.class,
ConcurrentHashMap.class,
Set.class,
AtomicLong.class);
constructor.setAccessible(true);
Object studio = constructor.newInstance(
worldId,
world,
mock(Engine.class),
generator,
new ConcurrentHashMap<>(),
ConcurrentHashMap.newKeySet(),
new AtomicLong());
@SuppressWarnings("unchecked")
Map<UUID, Object> studios = (Map<UUID, Object>) getField(service, "studios");
studios.put(worldId, studio);
return player;
}
private static void setField(Object target, String name, Object value) throws Exception {
Field field = target.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(target, value);
}
private static Object getField(Object target, String name) throws Exception {
Field field = target.getClass().getDeclaredField(name);
field.setAccessible(true);
return field.get(target);
}
}
@@ -0,0 +1,139 @@
package art.arcane.iris.studio.jigsaw;
import art.arcane.iris.pack.value.IrisDirection;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
public class JigsawStudioFloorGlyphTest {
@Test
public void emptyConnectorListPaintsOnlyTheBase() {
JigsawStudioFloorGlyph glyph = JigsawStudioFloorGlyph.of(5, 5, List.of());
assertRows(glyph, 5, 5, List.of(".....", ".....", ".....", ".....", "....."));
}
@Test
public void canonicalPlanarSocketsDrawStraightLinesFromTheCenter() {
Map<JigsawPlanarArchetype, List<String>> expected = Map.of(
JigsawPlanarArchetype.END, List.of("..o..", "..#..", "..#..", ".....", "....."),
JigsawPlanarArchetype.STRAIGHT, List.of("..o..", "..#..", "..#..", "..#..", "..o.."),
JigsawPlanarArchetype.CORNER, List.of("..o..", "..#..", "..##o", ".....", "....."),
JigsawPlanarArchetype.TEE, List.of("..o..", "..#..", "o###o", ".....", "....."),
JigsawPlanarArchetype.CROSS, List.of("..o..", "..#..", "o###o", "..#..", "..o.."));
for (Map.Entry<JigsawPlanarArchetype, List<String>> entry : expected.entrySet()) {
List<JigsawStudioFloorGlyph.Marker> markers = new ArrayList<>();
for (JigsawPlanarDirection direction : entry.getKey().canonicalTopology().directions()) {
markers.add(canonicalMarker(5, 5, direction.irisDirection()));
}
assertRows(JigsawStudioFloorGlyph.of(5, 5, markers), 5, 5, entry.getValue());
}
}
@Test
public void evenFootprintsUseFloorDivisionForTheCenter() {
JigsawStudioFloorGlyph glyph = JigsawStudioFloorGlyph.of(4, 4, List.of(
canonicalMarker(4, 4, IrisDirection.NORTH_NEGATIVE_Z),
canonicalMarker(4, 4, IrisDirection.WEST_NEGATIVE_X)));
assertRows(glyph, 4, 4, List.of("..o.", "..#.", "o##.", "...."));
}
@Test
public void verticalConnectorsUseDistinctSocketsAndMergeOnOneColumn() {
JigsawStudioFloorGlyph up = JigsawStudioFloorGlyph.of(5, 5, List.of(
new JigsawStudioFloorGlyph.Marker(2, 2, IrisDirection.UP_POSITIVE_Y)));
JigsawStudioFloorGlyph down = JigsawStudioFloorGlyph.of(5, 5, List.of(
new JigsawStudioFloorGlyph.Marker(2, 2, IrisDirection.DOWN_NEGATIVE_Y)));
JigsawStudioFloorGlyph both = JigsawStudioFloorGlyph.of(5, 5, List.of(
new JigsawStudioFloorGlyph.Marker(2, 2, IrisDirection.UP_POSITIVE_Y),
new JigsawStudioFloorGlyph.Marker(2, 2, IrisDirection.DOWN_NEGATIVE_Y),
canonicalMarker(5, 5, IrisDirection.EAST_POSITIVE_X)));
assertEquals(JigsawStudioFloorGlyph.Cell.UP_SOCKET, up.cell(2, 2));
assertEquals(JigsawStudioFloorGlyph.Cell.DOWN_SOCKET, down.cell(2, 2));
assertRows(both, 5, 5, List.of(".....", ".....", "..B#o", ".....", "....."));
}
@Test
public void offCenterVerticalConnectorsGetALineToTheirColumn() {
JigsawStudioFloorGlyph glyph = JigsawStudioFloorGlyph.of(5, 5, List.of(
new JigsawStudioFloorGlyph.Marker(0, 2, IrisDirection.DOWN_NEGATIVE_Y)));
assertRows(glyph, 5, 5, List.of(".....", ".....", "v##..", ".....", "....."));
}
@Test
public void interiorHorizontalConnectorsExtendTheirLineOutOfTheFaceTheyOpen() {
JigsawStudioFloorGlyph glyph = JigsawStudioFloorGlyph.of(5, 5, List.of(
new JigsawStudioFloorGlyph.Marker(3, 2, IrisDirection.SOUTH_POSITIVE_Z)));
assertRows(glyph, 5, 5, List.of(".....", ".....", "..#o.", "...#.", "...#."));
}
@Test
public void diagonalLinesStayConnectedAndInsideTheFootprint() {
JigsawStudioFloorGlyph glyph = JigsawStudioFloorGlyph.of(7, 5, List.of(
new JigsawStudioFloorGlyph.Marker(6, 0, IrisDirection.EAST_POSITIVE_X)));
assertEquals(JigsawStudioFloorGlyph.Cell.HORIZONTAL_SOCKET, glyph.cell(6, 0));
assertEquals(JigsawStudioFloorGlyph.Cell.LINE, glyph.cell(3, 2));
int lineCells = 0;
for (int z = 0; z < 5; z++) {
for (int x = 0; x < 7; x++) {
if (glyph.cell(x, z) == JigsawStudioFloorGlyph.Cell.LINE) {
lineCells++;
}
}
}
assertEquals(3, lineCells);
assertEquals(JigsawStudioFloorGlyph.Cell.BASE, glyph.cell(-1, 0));
assertEquals(JigsawStudioFloorGlyph.Cell.BASE, glyph.cell(7, 0));
}
@Test
public void markersOutsideTheFootprintAreClampedToItsEdge() {
JigsawStudioFloorGlyph glyph = JigsawStudioFloorGlyph.of(3, 3, List.of(
new JigsawStudioFloorGlyph.Marker(9, 1, IrisDirection.EAST_POSITIVE_X)));
assertRows(glyph, 3, 3, List.of("...", ".#o", "..."));
}
private static JigsawStudioFloorGlyph.Marker canonicalMarker(int width, int depth, IrisDirection direction) {
int centerX = width / 2;
int centerZ = depth / 2;
return switch (direction) {
case NORTH_NEGATIVE_Z -> new JigsawStudioFloorGlyph.Marker(centerX, 0, direction);
case SOUTH_POSITIVE_Z -> new JigsawStudioFloorGlyph.Marker(centerX, depth - 1, direction);
case EAST_POSITIVE_X -> new JigsawStudioFloorGlyph.Marker(width - 1, centerZ, direction);
case WEST_NEGATIVE_X -> new JigsawStudioFloorGlyph.Marker(0, centerZ, direction);
case UP_POSITIVE_Y, DOWN_NEGATIVE_Y -> new JigsawStudioFloorGlyph.Marker(centerX, centerZ, direction);
};
}
private static void assertRows(JigsawStudioFloorGlyph glyph, int width, int depth, List<String> rows) {
for (int z = 0; z < depth; z++) {
StringBuilder actual = new StringBuilder(width);
for (int x = 0; x < width; x++) {
actual.append(symbol(glyph.cell(x, z)));
}
assertEquals("row " + z, rows.get(z), actual.toString());
}
}
private static char symbol(JigsawStudioFloorGlyph.Cell cell) {
return switch (cell) {
case BASE -> '.';
case LINE -> '#';
case HORIZONTAL_SOCKET -> 'o';
case UP_SOCKET -> '^';
case DOWN_SOCKET -> 'v';
case UP_AND_DOWN_SOCKET -> 'B';
};
}
}
@@ -234,6 +234,60 @@ public class JigsawStudioGraphMapperTest {
assertEquals(new JigsawStudioCellDimensions(24, 32, 24), dimensions);
}
@Test
@SuppressWarnings("unchecked")
public void spatialFootprintRejectsChangesThatExceedTheWorkcellVolume() {
IrisData data = mock(IrisData.class);
ResourceLoader<IrisObject> objectLoader = mock(ResourceLoader.class);
when(data.getObjectLoader()).thenReturn(objectLoader);
when(objectLoader.load("stronghold/tower")).thenReturn(new IrisObject(16, 192, 16));
when(objectLoader.load("stronghold/hall")).thenReturn(new IrisObject(128, 10, 20));
JigsawStudioVariantCatalog catalog = new JigsawStudioVariantCatalog(List.of(
spatialVariant("stronghold/tower", new JigsawStudioCellDimensions(16, 192, 16), false),
spatialVariant("stronghold/hall", new JigsawStudioCellDimensions(128, 10, 20), false)));
JigsawStudioCellDimensions configured = new JigsawStudioCellDimensions(16, 16, 16);
assertEquals("", JigsawStudioGraphMapper.spatialFootprintFailure(
data,
configured,
catalog,
new JigsawStudioGraphMapper.SpatialChange("stronghold/hall", new JigsawStudioCellDimensions(128, 10, 20), false)));
assertFalse(JigsawStudioGraphMapper.spatialFootprintFailure(
data,
configured,
catalog,
new JigsawStudioGraphMapper.SpatialChange("stronghold/hall", new JigsawStudioCellDimensions(128, 10, 20), true)).isEmpty());
assertFalse(JigsawStudioGraphMapper.spatialFootprintFailure(
data,
configured,
catalog,
new JigsawStudioGraphMapper.SpatialChange("stronghold/hall", new JigsawStudioCellDimensions(128, 10, 128), false)).isEmpty());
assertEquals("", JigsawStudioGraphMapper.spatialFootprintFailure(
data,
configured,
catalog,
new JigsawStudioGraphMapper.SpatialChange("stronghold/tower", new JigsawStudioCellDimensions(16, 64, 16), false)));
}
private static JigsawStudioVariant spatialVariant(
String key,
JigsawStudioCellDimensions dimensions,
boolean rotatable
) {
return new JigsawStudioVariant(
key,
key,
"",
Optional.of(dimensions),
JigsawStudioMode.SPATIAL_JIGSAW,
Optional.empty(),
rotatable,
true,
List.of(),
new JigsawStudioPieceRules(0, 30, 0, 0, false),
List.of());
}
@Test
@SuppressWarnings("unchecked")
public void mapsAuthorLabelsAndCanonicalPerVariantDimensionsIndependently() {
@@ -0,0 +1,27 @@
package art.arcane.iris.studio.jigsaw;
import org.junit.Test;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
public class JigsawStudioLayoutColumnTest {
@Test
public void columnLookupFocusesTheWorkcellFromItsFloorToTheTeleportHeight() {
JigsawStudioLayout layout = JigsawStudioLayout.create(
JigsawStudioMode.PLANAR_JIGSAW,
new JigsawStudioCellDimensions(7, 5, 7),
JigsawStudioVariantCatalog.empty());
JigsawStudioBay cross = layout.get("workcell/cross");
JigsawStudioBounds bounds = cross.bounds();
int x = bounds.originX() + 3;
int z = bounds.originZ() + 3;
assertSame(cross, layout.findColumn(x, bounds.originY() - 1, z));
assertSame(cross, layout.findColumn(x, bounds.maxY() + 2, z));
assertSame(cross, layout.findColumn(x, bounds.maxY() + JigsawStudioLayout.COLUMN_HEADROOM, z));
assertNull(layout.findColumn(x, bounds.maxY() + JigsawStudioLayout.COLUMN_HEADROOM + 1, z));
assertNull(layout.findColumn(bounds.originX() - 1, bounds.originY(), z));
assertNull(layout.findAt(x, bounds.maxY() + 2, z));
}
}
@@ -0,0 +1,47 @@
package art.arcane.iris.studio.jigsaw;
import org.junit.Test;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class JigsawStudioMainMenuRowTest {
@Test
public void planarRowKeepsAllSixWorkcellsLeftOfTheRowControls() {
JigsawStudioMainMenu.WorkcellRow row = JigsawStudioMainMenu.workcellRow(6, 4);
assertEquals(List.of(0, 1, 2, 3, 4, 5), row.indices());
assertEquals(List.of(-3, -2, -1, 0, 1, 2), row.slots());
assertEquals(-1, row.previousTarget());
assertEquals(-1, row.nextTarget());
}
@Test
public void largeSpatialRowsPageWithoutHidingOrOverlappingWorkcells() {
for (int count : new int[]{7, 24}) {
Set<Integer> reachable = new HashSet<>();
for (int viewed = 0; viewed < count; viewed++) {
JigsawStudioMainMenu.WorkcellRow row = JigsawStudioMainMenu.workcellRow(count, viewed);
assertTrue(row.indices().contains(viewed));
assertEquals(row.indices().size(), new HashSet<>(row.slots()).size());
for (int slot : row.slots()) {
assertTrue(slot >= -3 && slot <= 1);
}
reachable.addAll(row.indices());
if (row.nextTarget() >= 0) {
assertTrue(JigsawStudioMainMenu.workcellRow(count, row.nextTarget()).indices()
.contains(row.nextTarget()));
}
}
assertEquals(count, reachable.size());
}
JigsawStudioMainMenu.WorkcellRow last = JigsawStudioMainMenu.workcellRow(24, 23);
assertEquals(List.of(20, 21, 22, 23), last.indices());
assertEquals(15, last.previousTarget());
assertEquals(-1, last.nextTarget());
}
}
@@ -491,7 +491,7 @@ public class JigsawStudioPersistenceEditorsTest {
JigsawStudioGraphEditor.VariantFamilyCreation creation = JigsawStudioGraphEditor.duplicateActiveFamily(
packRoot,
"studio/spatial",
Map.of(JigsawStudioLayout.SPATIAL_WORKCELL_ID, "studio/spatial/start"),
Map.of(JigsawStudioLayout.SPATIAL_WORKCELL_ID, "studio/spatial/blank"),
"variant-2");
assertTrue(creation.writeResult().successful());
@@ -499,7 +499,7 @@ public class JigsawStudioPersistenceEditorsTest {
"studio/spatial/variants/spatial/variant-2",
creation.pieceKeysByWorkcell().get(JigsawStudioLayout.SPATIAL_WORKCELL_ID));
JsonObject pool = readJson(packRoot.resolve("jigsaw-pools/studio/spatial/start.json"));
assertEquals(8, pool.getAsJsonArray("pieces").size());
assertEquals(25, pool.getAsJsonArray("pieces").size());
}
@Test
@@ -507,10 +507,13 @@ public class JigsawStudioPersistenceEditorsTest {
Path packRoot = createSpatialProject("spatial-theme-row");
Map<String, String> sources = new LinkedHashMap<>();
sources.put(JigsawStudioLayout.SPATIAL_WORKCELL_ID, "studio/spatial/start");
for (int connectorCount = 1; connectorCount <= 6; connectorCount++) {
String pieceKey = "studio/spatial/connectors-" + connectorCount;
sources.put(JigsawStudioLayout.SPATIAL_WORKCELL_ID + "/" + pieceKey, pieceKey);
for (String vertical : List.of("", "-up", "-down", "-up-down")) {
for (String horizontal : List.of("blank", "end", "straight", "corner", "tee", "cross")) {
String pieceKey = "studio/spatial/" + horizontal + vertical;
sources.put(sources.isEmpty()
? JigsawStudioLayout.SPATIAL_WORKCELL_ID
: JigsawStudioLayout.SPATIAL_WORKCELL_ID + "/" + pieceKey, pieceKey);
}
}
JigsawStudioGraphEditor.VariantFamilyCreation creation = JigsawStudioGraphEditor.duplicateActiveFamily(
packRoot,
@@ -519,13 +522,13 @@ public class JigsawStudioPersistenceEditorsTest {
"variant-2");
assertTrue(creation.writeResult().successful());
assertEquals(7, creation.pieceKeysByWorkcell().size());
assertEquals(24, creation.pieceKeysByWorkcell().size());
for (String targetPieceKey : creation.pieceKeysByWorkcell().values()) {
assertTrue(Files.isRegularFile(packRoot.resolve("jigsaw-pieces/" + targetPieceKey + ".json")));
assertTrue(Files.isRegularFile(packRoot.resolve("objects/" + targetPieceKey + ".iob")));
}
JsonObject pool = readJson(packRoot.resolve("jigsaw-pools/studio/spatial/start.json"));
assertEquals(14, pool.getAsJsonArray("pieces").size());
assertEquals(48, pool.getAsJsonArray("pieces").size());
}
@Test
@@ -557,7 +560,7 @@ public class JigsawStudioPersistenceEditorsTest {
"END").get("displayName").getAsString());
Path spatialRoot = createSpatialProject("labels-spatial");
Path spatialObject = spatialRoot.resolve("objects/studio/spatial/start.iob");
Path spatialObject = spatialRoot.resolve("objects/studio/spatial/blank.iob");
byte[] spatialBefore = Files.readAllBytes(spatialObject);
assertTrue(JigsawStudioStructureEditor.updateSpatialWorkcellDisplayName(
@@ -34,7 +34,7 @@ public class JigsawStudioPersistenceIdentityTest {
private static final String PIECE_POOL_KEY = "studio/identity/pieces";
private static final String RECORDED_AT_FIELD = "recordedAtEpochMilli";
private static final String SCRIPTED_SESSION_DIGEST =
"318d9338a63a44063c85e59a9e74f869473d6e0f15c362536172f7723d960faa";
"42b5d3bed8d74b2ef7a010a58302544d0d425c83faba0dc86efb0e41e69ba627";
@Rule
public final TemporaryFolder temporaryFolder = new TemporaryFolder();
@@ -102,9 +102,15 @@ public class JigsawStudioProjectCreatorTest {
JsonObject startPool = JsonParser.parseString(Files.readString(
temporaryDirectory.resolve("jigsaw-pools/settlement/test/start.json"),
StandardCharsets.UTF_8)).getAsJsonObject();
assertEquals(1, startPool.getAsJsonArray("pieces").size());
assertEquals(2, startPool.getAsJsonArray("pieces").size());
assertEquals("settlement/test/cross", startPool.getAsJsonArray("pieces").get(0)
.getAsJsonObject().get("piece").getAsString());
assertEquals(3, startPool.getAsJsonArray("pieces").get(0)
.getAsJsonObject().get("weight").getAsInt());
assertEquals("settlement/test/blank", startPool.getAsJsonArray("pieces").get(1)
.getAsJsonObject().get("piece").getAsString());
assertEquals(1, startPool.getAsJsonArray("pieces").get(1)
.getAsJsonObject().get("weight").getAsInt());
JsonObject piecePool = JsonParser.parseString(Files.readString(
temporaryDirectory.resolve("jigsaw-pools/settlement/test/pieces.json"),
StandardCharsets.UTF_8)).getAsJsonObject();
@@ -229,7 +235,7 @@ public class JigsawStudioProjectCreatorTest {
}
@Test
public void spatialProjectSeedsZeroThroughSixConnectorVariants() throws Exception {
public void spatialProjectSeedsEveryYRotationTopologyVariant() throws Exception {
Path temporaryDirectory = temporaryFolder.getRoot().toPath();
JigsawStudioProjectCreator.Options options = new JigsawStudioProjectCreator.Options(
"stronghold/gallery",
@@ -246,35 +252,48 @@ public class JigsawStudioProjectCreatorTest {
JsonObject piecePool = JsonParser.parseString(Files.readString(
temporaryDirectory.resolve("jigsaw-pools/stronghold/gallery/pieces.json"),
StandardCharsets.UTF_8)).getAsJsonObject();
assertEquals(7, startPool.getAsJsonArray("pieces").size());
assertEquals(7, piecePool.getAsJsonArray("pieces").size());
assertEquals("stronghold/gallery/connectors-1", piecePool.getAsJsonArray("pieces").get(0)
assertEquals(24, startPool.getAsJsonArray("pieces").size());
assertEquals("stronghold/gallery/blank", startPool.getAsJsonArray("pieces").get(0)
.getAsJsonObject().get("piece").getAsString());
assertTrue(piecePool.getAsJsonArray("pieces").get(6).getAsJsonObject()
assertEquals(24, piecePool.getAsJsonArray("pieces").size());
assertEquals("stronghold/gallery/end", piecePool.getAsJsonArray("pieces").get(0)
.getAsJsonObject().get("piece").getAsString());
assertTrue(piecePool.getAsJsonArray("pieces").get(23).getAsJsonObject()
.get("empty").getAsBoolean());
for (int connectorCount = 0; connectorCount <= 6; connectorCount++) {
String pieceName = connectorCount == 0 ? "start" : "connectors-" + connectorCount;
JsonObject piece = JsonParser.parseString(Files.readString(
temporaryDirectory.resolve("jigsaw-pieces/stronghold/gallery/"
+ pieceName + ".json"),
StandardCharsets.UTF_8)).getAsJsonObject();
assertEquals(connectorCount, piece.getAsJsonArray("connectors").size());
assertEquals(connectorCount + (connectorCount == 1 ? " Connector" : " Connectors"),
piece.get("displayName").getAsString());
assertEquals(16, piece.getAsJsonObject("rules").get("maximumPlacements").getAsInt());
assertEquals(new IrisBlockVector(15, 15, 15), IrisObject.sampleSize(
temporaryDirectory.resolve("objects/stronghold/gallery/"
+ pieceName + ".iob").toFile()));
List<String> horizontalClasses = List.of("blank", "end", "straight", "corner", "tee", "cross");
List<String> verticalClasses = List.of("", "-up", "-down", "-up-down");
List<Integer> horizontalCounts = List.of(0, 1, 2, 2, 3, 4);
for (String vertical : verticalClasses) {
for (int index = 0; index < horizontalClasses.size(); index++) {
String pieceName = horizontalClasses.get(index) + vertical;
JsonObject piece = JsonParser.parseString(Files.readString(
temporaryDirectory.resolve("jigsaw-pieces/stronghold/gallery/" + pieceName + ".json"),
StandardCharsets.UTF_8)).getAsJsonObject();
int verticalCount = vertical.isEmpty() ? 0 : vertical.equals("-up-down") ? 2 : 1;
assertEquals(pieceName, horizontalCounts.get(index) + verticalCount,
piece.getAsJsonArray("connectors").size());
assertEquals(4, piece.getAsJsonObject("rules").get("maximumPlacements").getAsInt());
assertEquals(new IrisBlockVector(15, 15, 15), IrisObject.sampleSize(
temporaryDirectory.resolve("objects/stronghold/gallery/"
+ pieceName + ".iob").toFile()));
}
}
JsonObject six = JsonParser.parseString(Files.readString(
temporaryDirectory.resolve("jigsaw-pieces/stronghold/gallery/connectors-6.json"),
JsonObject cornerShaft = JsonParser.parseString(Files.readString(
temporaryDirectory.resolve("jigsaw-pieces/stronghold/gallery/corner-up-down.json"),
StandardCharsets.UTF_8)).getAsJsonObject();
assertEquals("NORTH_NEGATIVE_Z", six.getAsJsonArray("connectors").get(0)
assertEquals("L Junction + Up/Down", cornerShaft.get("displayName").getAsString());
assertEquals("NORTH_NEGATIVE_Z", cornerShaft.getAsJsonArray("connectors").get(0)
.getAsJsonObject().get("direction").getAsString());
assertEquals("DOWN_NEGATIVE_Y", six.getAsJsonArray("connectors").get(5)
assertEquals("EAST_POSITIVE_X", cornerShaft.getAsJsonArray("connectors").get(1)
.getAsJsonObject().get("direction").getAsString());
assertEquals(0, six.getAsJsonArray("connectors").get(5)
assertEquals("UP_POSITIVE_Y", cornerShaft.getAsJsonArray("connectors").get(2)
.getAsJsonObject().get("direction").getAsString());
assertEquals("DOWN_NEGATIVE_Y", cornerShaft.getAsJsonArray("connectors").get(3)
.getAsJsonObject().get("direction").getAsString());
assertEquals(0, cornerShaft.getAsJsonArray("connectors").get(3)
.getAsJsonObject().getAsJsonObject("position").get("y").getAsInt());
assertEquals("NORTH_NEGATIVE_Z", cornerShaft.getAsJsonArray("connectors").get(3)
.getAsJsonObject().get("top").getAsString());
StructureGraphCompilation compilation = StructureResourceBundleGraphCompiler.compile(
JigsawStudioProjectCreator.bundle(options)).getFirst();
@@ -283,7 +302,7 @@ public class JigsawStudioProjectCreatorTest {
new IrisPosition(0, 0, 0)).assemble(new RNG(1337L));
assertEquals(preview.detail(), StructureAssemblyStatus.COMPLETE, preview.status());
assertTrue(preview.pieces().size() > 1);
assertTrue(preview.pieces().size() <= 96);
assertTrue(preview.pieces().size() <= 93);
}
@Test
@@ -411,10 +430,10 @@ public class JigsawStudioProjectCreatorTest {
JsonObject pool = JsonParser.parseString(Files.readString(
temporaryDirectory.resolve("jigsaw-pools/stronghold/test/start.json"),
StandardCharsets.UTF_8)).getAsJsonObject();
assertEquals(8, pool.getAsJsonArray("pieces").size());
assertEquals("stronghold/test/hall", pool.getAsJsonArray("pieces").get(7)
assertEquals(25, pool.getAsJsonArray("pieces").size());
assertEquals("stronghold/test/hall", pool.getAsJsonArray("pieces").get(24)
.getAsJsonObject().get("piece").getAsString());
assertEquals(3, pool.getAsJsonArray("pieces").get(7)
assertEquals(3, pool.getAsJsonArray("pieces").get(24)
.getAsJsonObject().get("weight").getAsInt());
StructureWriteResult resized = JigsawStudioStructureEditor.updateCellSize(
@@ -435,7 +454,7 @@ public class JigsawStudioProjectCreatorTest {
IrisBlockVector hallSize = IrisObject.sampleSize(
temporaryDirectory.resolve("objects/stronghold/test/hall.iob").toFile());
IrisBlockVector starterSize = IrisObject.sampleSize(
temporaryDirectory.resolve("objects/stronghold/test/start.iob").toFile());
temporaryDirectory.resolve("objects/stronghold/test/blank.iob").toFile());
assertEquals(new IrisBlockVector(16, 12, 18), hallSize);
assertEquals(new IrisBlockVector(12, 10, 14), starterSize);
@@ -56,6 +56,21 @@ public class JigsawStudioProtectionProbeTest {
}
}
@Test
public void protectedWorkcellProbesAreDeniedWithoutChatMessages() throws Exception {
JigsawStudioService service = new JigsawStudioService();
JigsawStudioProtection protection = mock(JigsawStudioProtection.class);
setField(service, "protection", protection);
PlayerInteractEvent event = probe();
when(protection.studioEditRefusal(event.getPlayer(), event.getClickedBlock()))
.thenReturn("This Jigsaw Studio variant is read-only.");
service.protectionListener.onUnauthorizedInteract(event);
assertTrue(event.isCancelled());
verify(protection, never()).isUnauthorizedStudioEdit(any(Player.class), any(Block.class));
}
private static PlayerInteractEvent probe() {
Player player = mock(Player.class);
World world = mock(World.class);
@@ -80,20 +80,20 @@ public class JigsawStudioResourceBundleAssemblerTest {
JigsawStudioResourceBundleAssembler.assemble(
packRoot,
"fort",
"fort/start",
"fort/blank",
emptyObject(),
List.of(connector),
false);
assertEquals(17, assembly.bundle().resources().size());
assertEquals("fort/start", assembly.objectKey());
assertEquals(51, assembly.bundle().resources().size());
assertEquals("fort/blank", assembly.objectKey());
assertEquals(1, assembly.piece().getConnectors().size());
StructureWriteResult result = new StructureTransactionWriter(packRoot)
.write(assembly.bundle(), StructureWriteMode.OVERWRITE);
assertTrue(result.successful());
assertTrue(result.committed());
Path piecePath = packRoot.resolve("jigsaw-pieces/fort/start.json");
Path piecePath = packRoot.resolve("jigsaw-pieces/fort/blank.json");
IrisJigsawPiece persisted = new Gson().fromJson(
Files.readString(piecePath, StandardCharsets.UTF_8), IrisJigsawPiece.class);
assertEquals(1, persisted.getConnectors().size());
@@ -143,7 +143,7 @@ public class JigsawStudioResourceBundleAssemblerTest {
@Test
public void capturePreservesAbsentDefaultsAndUnknownPieceMetadata() throws Exception {
Path packRoot = createProject();
String pieceResource = "jigsaw-pieces/fort/start.json";
String pieceResource = "jigsaw-pieces/fort/blank.json";
Path piecePath = packRoot.resolve(pieceResource);
JsonObject source = JsonParser.parseString(
Files.readString(piecePath, StandardCharsets.UTF_8)).getAsJsonObject();
@@ -158,7 +158,7 @@ public class JigsawStudioResourceBundleAssemblerTest {
JigsawStudioResourceBundleAssembler.assemble(
packRoot,
STRUCTURE_KEY,
"fort/start",
"fort/blank",
emptyObject(),
List.copyOf(sourcePiece.getConnectors()),
false);
@@ -173,7 +173,7 @@ public class JigsawStudioResourceBundleAssemblerTest {
JigsawStudioResourceBundleAssembler.assemble(
packRoot,
STRUCTURE_KEY,
"fort/start",
"fort/blank",
emptyObject(),
List.of(updated),
false);
@@ -202,7 +202,7 @@ public class JigsawStudioResourceBundleAssemblerTest {
() -> JigsawStudioResourceBundleAssembler.assemble(
packRoot,
STRUCTURE_KEY,
"fort/start",
"fort/blank",
emptyObject(),
List.of(connector()),
false));
@@ -213,7 +213,7 @@ public class JigsawStudioResourceBundleAssemblerTest {
@Test
public void refusesManagedDatapackOwnershipWithoutAssemblingAStudioSave() throws Exception {
Path packRoot = createProject();
Path objectPath = packRoot.resolve("objects/fort/start.iob");
Path objectPath = packRoot.resolve("objects/fort/blank.iob");
byte[] before = Files.readAllBytes(objectPath);
markManagedDatapack(packRoot, STRUCTURE_KEY);
@@ -221,7 +221,7 @@ public class JigsawStudioResourceBundleAssemblerTest {
() -> JigsawStudioResourceBundleAssembler.assemble(
packRoot,
STRUCTURE_KEY,
"fort/start",
"fort/blank",
emptyObject(),
List.of(connector()),
false));
@@ -234,13 +234,13 @@ public class JigsawStudioResourceBundleAssemblerTest {
@Test
public void writerRejectsAResourceModifiedOutsideTheOwnershipTransaction() throws Exception {
Path packRoot = createProject();
Path piecePath = packRoot.resolve("jigsaw-pieces/fort/start.json");
Path piecePath = packRoot.resolve("jigsaw-pieces/fort/blank.json");
Files.writeString(piecePath, Files.readString(piecePath, StandardCharsets.UTF_8) + "\n");
JigsawStudioResourceBundleAssembler.Assembly assembly =
JigsawStudioResourceBundleAssembler.assemble(
packRoot,
STRUCTURE_KEY,
"fort/start",
"fort/blank",
emptyObject(),
List.of(connector().setPool("fort/start")),
false);
@@ -181,11 +181,13 @@ public class JigsawStudioServiceCaptureTest {
when(state.isCustom()).thenReturn(false);
when(state.nativeHandle()).thenReturn(blockData);
JigsawStudioChunkWriter.restoreConnectorChunk(
JigsawStudioCapture.LocalPosition position = new JigsawStudioCapture.LocalPosition(1, 2, 3);
JigsawStudioChunkWriter.restoreChunkPositions(
world,
workcell,
List.of(renderedConnector),
Map.of(new JigsawStudioCapture.LocalPosition(1, 2, 3), renderedBlock),
Set.of(position),
Map.of(position, renderedConnector),
Map.of(position, renderedBlock),
false);
verify(world).getBlockAt(11, 22, 33);
@@ -293,7 +295,9 @@ public class JigsawStudioServiceCaptureTest {
assertFalse(JigsawStudioProtection.blocksMutatingCommand(ownerId, otherId, "/iris jigsaw status"));
assertFalse(JigsawStudioProtection.blocksMutatingCommand(ownerId, otherId, "/msg owner hello"));
assertTrue(JigsawStudioProtection.blocksMutatingCommand(ownerId, otherId, "/execute run say bypass"));
assertTrue(JigsawStudioProtection.blocksMutatingCommand(ownerId, otherId, "/unknownplugin mutate"));
assertTrue(JigsawStudioProtection.blocksMutatingCommand(ownerId, otherId, "/setblock 0 0 0 stone"));
assertFalse(JigsawStudioProtection.blocksMutatingCommand(ownerId, otherId, "/unknownplugin status"));
assertFalse(JigsawStudioProtection.blocksMutatingCommand(ownerId, otherId, "/spawn"));
assertTrue(JigsawStudioProtection.blocksNonEditableWorkcellMutation(
true, "/fill 0 0 0 1 1 1 stone"));
assertTrue(JigsawStudioProtection.blocksNonEditableWorkcellMutation(true, "//paste -a"));
@@ -0,0 +1,172 @@
package art.arcane.iris.studio.jigsaw;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.studio.generation.JigsawStudioGenerator;
import art.arcane.iris.world.task.J;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class JigsawStudioToolbeltTest {
private static final UUID REQUEST = UUID.fromString("55555555-5555-5555-5555-555555555555");
private static final JigsawStudioToolPayload DELETE = JigsawStudioToolPayload.variant(
JigsawStudioToolAction.DELETE_VARIANT, REQUEST, "workcell/end", "test/end");
@Test
public void heldUseRepeatsCannotConfirmADestructiveTool() {
JigsawStudioToolbelt.ToolConfirmation armed =
new JigsawStudioToolbelt.ToolConfirmation(DELETE, 1_000_000_000L, 11_000_000_000L);
assertEquals(JigsawStudioToolbelt.ToolConfirmationState.ARMED,
JigsawStudioToolbelt.confirmationState(null, DELETE, 1_000_000_000L));
assertEquals(JigsawStudioToolbelt.ToolConfirmationState.HELD,
JigsawStudioToolbelt.confirmationState(armed, DELETE, 1_200_000_000L));
assertEquals(JigsawStudioToolbelt.ToolConfirmationState.CONFIRMED,
JigsawStudioToolbelt.confirmationState(armed, DELETE, 1_600_000_000L));
assertEquals(JigsawStudioToolbelt.ToolConfirmationState.ARMED,
JigsawStudioToolbelt.confirmationState(armed, DELETE, 12_000_000_000L));
assertEquals(JigsawStudioToolbelt.ToolConfirmationState.ARMED,
JigsawStudioToolbelt.confirmationState(
armed,
JigsawStudioToolPayload.request(JigsawStudioToolAction.DELETE_PROJECT, REQUEST),
1_600_000_000L));
}
@Test
public void variantBoundSticksRequireTheirBoundVariant() {
assertTrue(JigsawStudioToolbelt.requiresBoundVariant(JigsawStudioToolPayload.variant(
JigsawStudioToolAction.TOGGLE_ROTATION, REQUEST, "workcell/end", "test/end")));
assertTrue(JigsawStudioToolbelt.requiresBoundVariant(JigsawStudioToolPayload.variant(
JigsawStudioToolAction.EXPAND_TO_CELL, REQUEST, "workcell/end", "test/end")));
assertTrue(JigsawStudioToolbelt.requiresBoundVariant(JigsawStudioToolPayload.variant(
JigsawStudioToolAction.DUPLICATE_VARIANT, REQUEST, "workcell/end", "test/end")));
assertFalse(JigsawStudioToolbelt.requiresBoundVariant(JigsawStudioToolPayload.workcell(
JigsawStudioToolAction.CREATE_VARIANT, REQUEST, "workcell/end")));
assertFalse(JigsawStudioToolbelt.requiresBoundVariant(JigsawStudioToolPayload.request(
JigsawStudioToolAction.CONNECTOR_TOOL, REQUEST)));
}
@Test
public void staleRotationStickDoesNotToggleTheNewlyLoadedVariant() throws Exception {
JigsawStudioVariant first = variant("test/end");
JigsawStudioVariant second = variant("test/end-b");
JigsawStudioLayout layout = JigsawStudioLayout.create(
JigsawStudioMode.PLANAR_JIGSAW,
new JigsawStudioCellDimensions(16, 16, 16),
new JigsawStudioVariantCatalog(List.of(second, first)));
JigsawStudioSession session = new JigsawStudioSession(REQUEST, "overworld", "test/project", layout);
assertEquals("test/end-b", session.activeVariant("workcell/end").orElseThrow().pieceKey());
JigsawStudioService service = new JigsawStudioService();
JigsawStudioVariantProperties properties = mock(JigsawStudioVariantProperties.class);
setField(service, "variantProperties", properties);
Player player = player(service, session);
try (MockedStatic<J> scheduling = mockStatic(J.class)) {
scheduling.when(() -> J.isOwnedByCurrentRegion(any(Player.class))).thenReturn(true);
boolean used = service.toolbelt.useTool(
player,
JigsawStudioToolPayload.variant(
JigsawStudioToolAction.TOGGLE_ROTATION, REQUEST, "workcell/end", "test/end"),
null,
false);
assertFalse(used);
verify(properties, never()).toggleVariantRotatable(any(Player.class), anyString());
}
}
private static Player player(JigsawStudioService service, JigsawStudioSession session) throws Exception {
UUID worldId = UUID.randomUUID();
World world = mock(World.class);
when(world.getUID()).thenReturn(worldId);
Player player = mock(Player.class);
when(player.getWorld()).thenReturn(world);
when(player.getUniqueId()).thenReturn(UUID.randomUUID());
JigsawStudioActivation.Request request = new JigsawStudioActivation.Request(
REQUEST,
"overworld",
"test/project",
JigsawStudioMode.PLANAR_JIGSAW,
JigsawStudioCompatibilityTarget.IRIS_EXTENDED,
new JigsawStudioCellDimensions(16, 16, 16),
mock(IrisData.class),
null);
JigsawStudioGenerator generator = mock(JigsawStudioGenerator.class);
when(generator.getRequest()).thenReturn(request);
when(generator.getSession()).thenReturn(session);
when(generator.getLayout()).thenAnswer(invocation -> session.layout());
Class<?> studioType = Class.forName(JigsawStudioService.class.getName() + "$ActiveStudio");
Constructor<?> constructor = studioType.getDeclaredConstructor(
UUID.class,
World.class,
Engine.class,
JigsawStudioGenerator.class,
ConcurrentHashMap.class,
Set.class,
AtomicLong.class);
constructor.setAccessible(true);
Object studio = constructor.newInstance(
worldId,
world,
mock(Engine.class),
generator,
new ConcurrentHashMap<>(),
ConcurrentHashMap.newKeySet(),
new AtomicLong());
@SuppressWarnings("unchecked")
Map<UUID, Object> studios = (Map<UUID, Object>) getField(service, "studios");
studios.put(worldId, studio);
return player;
}
private static JigsawStudioVariant variant(String pieceKey) {
return new JigsawStudioVariant(
pieceKey,
pieceKey,
"",
Optional.of(new JigsawStudioCellDimensions(16, 16, 16)),
JigsawStudioMode.PLANAR_JIGSAW,
Optional.of(JigsawPlanarTopology.NORTH_END),
true,
true,
List.of(),
new JigsawStudioPieceRules(0, 30, 0, 0, false),
List.of());
}
private static void setField(Object target, String name, Object value) throws Exception {
Field field = target.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(target, value);
}
private static Object getField(Object target, String name) throws Exception {
Field field = target.getClass().getDeclaredField(name);
field.setAccessible(true);
return field.get(target);
}
}
@@ -0,0 +1,99 @@
package art.arcane.iris.studio.jigsaw;
import org.junit.Test;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class JigsawStudioVariantDeletionRuleTest {
private static final UUID WORLD = UUID.randomUUID();
private static final UUID REQUEST = UUID.randomUUID();
@Test
public void spatialVariantsCanBeDeletedWhileLoadedUnlessTheyAreTheLastOne() {
JigsawStudioMenuState.Variant first = variant("demo/end", true, true);
JigsawStudioMenuState.Variant second = variant("demo/cross", true, true);
JigsawStudioMenuState.Workcell firstCell = workcell("workcell/spatial", first);
JigsawStudioMenuState.Workcell secondCell = workcell("workcell/spatial/demo/cross", second);
JigsawStudioMenuState twoCells = state(JigsawStudioMode.SPATIAL_JIGSAW, List.of(firstCell, secondCell));
JigsawStudioMenuState oneCell = state(JigsawStudioMode.SPATIAL_JIGSAW, List.of(firstCell));
assertTrue(twoCells.canDeleteVariant(firstCell, first));
assertFalse(oneCell.canDeleteVariant(firstCell, first));
List<JigsawStudioToolboxMenu.ToolboxTool> tools = JigsawStudioToolboxMenu.toolboxTools(twoCells, firstCell);
assertTrue(tools.stream().anyMatch(tool -> tool.payload().action() == JigsawStudioToolAction.DELETE_VARIANT
&& tool.payload().pieceKey().equals("demo/end")));
}
@Test
public void planarVariantsStillRequireAnotherLoadedVariantFirst() {
JigsawStudioMenuState.Variant loaded = variant("demo/end", true, true);
JigsawStudioMenuState.Variant spare = variant("demo/end-b", false, true);
JigsawStudioMenuState.Variant readOnly = variant("demo/end-c", false, false);
JigsawStudioMenuState.Workcell cell = new JigsawStudioMenuState.Workcell(
"workcell/end",
"End Cap",
"End Cap",
new JigsawStudioCellDimensions(16, 16, 16),
true,
loaded.pieceKey(),
false,
false,
false,
false,
List.of(loaded, spare, readOnly));
JigsawStudioMenuState state = state(JigsawStudioMode.PLANAR_JIGSAW, List.of(cell));
assertFalse(state.canDeleteVariant(cell, loaded));
assertTrue(state.canDeleteVariant(cell, spare));
assertFalse(state.canDeleteVariant(cell, readOnly));
}
private static JigsawStudioMenuState state(JigsawStudioMode mode, List<JigsawStudioMenuState.Workcell> workcells) {
return new JigsawStudioMenuState(
WORLD,
REQUEST,
"demo",
mode,
JigsawStudioCompatibilityTarget.IRIS_EXTENDED,
false,
List.of(new JigsawStudioMenuState.ThemeSet("variant-1", 1)),
workcells.getFirst().stableId(),
JigsawStudioMenuState.Evaluation.pending(),
workcells);
}
private static JigsawStudioMenuState.Workcell workcell(String stableId, JigsawStudioMenuState.Variant active) {
return new JigsawStudioMenuState.Workcell(
stableId,
"Spatial",
active.displayName(),
new JigsawStudioCellDimensions(15, 15, 15),
true,
active.pieceKey(),
false,
false,
false,
false,
List.of(active));
}
private static JigsawStudioMenuState.Variant variant(String pieceKey, boolean active, boolean owned) {
return new JigsawStudioMenuState.Variant(
pieceKey,
pieceKey.substring(pieceKey.lastIndexOf('/') + 1),
Optional.of(new JigsawStudioCellDimensions(15, 15, 15)),
active,
owned,
true,
owned,
active,
List.of(),
new JigsawStudioPieceRules(0, 30, 0, 0, false),
List.of());
}
}