mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-30 07:50:03 +00:00
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:
1 parent
b3579e623a
commit
7d73c4d150
56 files changed
+4869
-537
No files matched your search
@@ -1,5 +1,8 @@
|
||||
package art.arcane.iris.structure.export;
|
||||
|
||||
import art.arcane.iris.pack.datapack.DataVersion;
|
||||
import art.arcane.iris.structure.placement.IrisStructure;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
@@ -41,6 +44,13 @@ public final class VanillaJigsawExportSettings {
|
||||
return builder().build();
|
||||
}
|
||||
|
||||
public static VanillaJigsawExportSettings runtimeDefaults(IrisStructure structure) {
|
||||
if (structure == null || DataVersion.getRuntime() != DataVersion.V1_21_1) {
|
||||
return defaults();
|
||||
}
|
||||
return builder().maxDistanceVertical(structure.getMaxSizeChunks() * 16).build();
|
||||
}
|
||||
|
||||
public List<String> biomes() {
|
||||
return biomes;
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ public class IrisJigsawConnector {
|
||||
@Description("Block state placed where this jigsaw marker was authored after assembly. Used when exporting or capturing Mojang-style jigsaw templates.")
|
||||
private String finalState = "minecraft:air";
|
||||
|
||||
@Description("Signed priority for selecting this connector within its piece. Higher values are processed before lower values; equal priorities preserve authored order.")
|
||||
@Description("Signed priority for selecting this connector within its piece. Higher values are processed before lower values; equal priorities preserve authored order. A rotatable candidate piece applies this order within each of its cardinal rotations, which it tries in random order.")
|
||||
private int selectionPriority = 0;
|
||||
|
||||
@Description("Signed priority assigned to a child piece attached through this connector. Higher-priority child pieces expand first; equal priorities preserve attachment order.")
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ import art.arcane.volmlib.util.documentation.Description;
|
||||
|
||||
@Description("The orientation-independent connector shape represented by a planar jigsaw workcell.")
|
||||
public enum IrisJigsawWorkcellArchetype {
|
||||
@Description("Pieces with no horizontal connectors: sealed filler cells that terminate nothing and are excluded from the default piece pool.")
|
||||
@Description("Pieces with no horizontal connectors. Nothing can attach to them, so they generate only as standalone start pieces; new projects list the default Blank in the start pool and exclude it from the piece pool.")
|
||||
BLANK(0),
|
||||
|
||||
@Description("One-connector dead-end pieces; the Studio bay seeds a single north connector and new projects mark these as terminal caps.")
|
||||
|
||||
@@ -218,7 +218,7 @@ public final class StructureAssembler {
|
||||
+ "' references missing connector pool '" + connector.pool + "'");
|
||||
}
|
||||
|
||||
AttachmentResult attachment = attachOne(
|
||||
AttachmentResult attachment = attach(
|
||||
pending,
|
||||
placed,
|
||||
placementCounts,
|
||||
@@ -229,7 +229,9 @@ public final class StructureAssembler {
|
||||
depth,
|
||||
selectedTheme,
|
||||
rng);
|
||||
consume(consumedConnectors, connector.sourcePiece(), connector.sourceConnector());
|
||||
if (attachment.state() != AttachmentState.TERMINATED) {
|
||||
consume(consumedConnectors, connector.sourcePiece(), connector.sourceConnector());
|
||||
}
|
||||
if (attachment.state() == AttachmentState.FAILED) {
|
||||
return StructureAssemblyResult.failed(
|
||||
StructureAssemblyStatus.FAILED_UNCAPPED,
|
||||
@@ -250,6 +252,49 @@ public final class StructureAssembler {
|
||||
return StructureAssemblyResult.complete(placed, selectedTheme);
|
||||
}
|
||||
|
||||
private AttachmentResult attach(
|
||||
Deque<PieceExpansion> pending,
|
||||
KList<PlacedStructurePiece> placed,
|
||||
Map<String, Integer> placementCounts,
|
||||
Map<PlacedStructurePiece, Set<IrisJigsawConnector>> consumedConnectors,
|
||||
Map<PlacedStructurePiece, Map<IrisJigsawConnector, OpenConnector>> openConnectors,
|
||||
IrisJigsawPool pool,
|
||||
OpenConnector connector,
|
||||
int depth,
|
||||
String selectedTheme,
|
||||
RNG rng
|
||||
) {
|
||||
boolean planarNeighborClosures = structure.resolvedMode() == IrisJigsawMode.PLANAR_JIGSAW
|
||||
&& !structure.isRequireCaps();
|
||||
AttachmentResult agreed = attachOne(
|
||||
pending,
|
||||
placed,
|
||||
placementCounts,
|
||||
consumedConnectors,
|
||||
openConnectors,
|
||||
pool,
|
||||
connector,
|
||||
depth,
|
||||
selectedTheme,
|
||||
structure.isRequireCaps() || planarNeighborClosures,
|
||||
rng);
|
||||
if (agreed.state() != AttachmentState.FAILED || !planarNeighborClosures) {
|
||||
return agreed;
|
||||
}
|
||||
return attachOne(
|
||||
pending,
|
||||
placed,
|
||||
placementCounts,
|
||||
consumedConnectors,
|
||||
openConnectors,
|
||||
pool,
|
||||
connector,
|
||||
depth,
|
||||
selectedTheme,
|
||||
false,
|
||||
rng);
|
||||
}
|
||||
|
||||
private AttachmentResult attachOne(
|
||||
Deque<PieceExpansion> pending,
|
||||
KList<PlacedStructurePiece> placed,
|
||||
@@ -260,6 +305,7 @@ public final class StructureAssembler {
|
||||
OpenConnector connector,
|
||||
int depth,
|
||||
String selectedTheme,
|
||||
boolean neighborClosures,
|
||||
RNG rng
|
||||
) {
|
||||
if (pool.getPieces() == null) {
|
||||
@@ -281,6 +327,7 @@ public final class StructureAssembler {
|
||||
selectedTheme,
|
||||
false,
|
||||
!fallbackRequired,
|
||||
neighborClosures,
|
||||
rng);
|
||||
if (primary != AttachmentState.NO_MATCH) {
|
||||
return AttachmentResult.success(primary);
|
||||
@@ -322,6 +369,7 @@ public final class StructureAssembler {
|
||||
selectedTheme,
|
||||
fallbackRequired,
|
||||
!fallbackRequired,
|
||||
neighborClosures,
|
||||
rng);
|
||||
if (fallbackState != AttachmentState.NO_MATCH) {
|
||||
return AttachmentResult.success(fallbackState);
|
||||
@@ -352,9 +400,14 @@ public final class StructureAssembler {
|
||||
String selectedTheme,
|
||||
boolean terminalOnly,
|
||||
boolean allowEmpty,
|
||||
boolean neighborClosures,
|
||||
RNG rng
|
||||
) {
|
||||
int ruleDepth = Math.min(connector.pieceDepth, structure.getMaxDepth());
|
||||
if (connector.targetName == null) {
|
||||
throw assemblyFailure("open connector from pool '" + connector.pool + "' has no target name");
|
||||
}
|
||||
IrisDirection needed = connector.facing.reverse();
|
||||
for (IrisJigsawPieceEntry entry : weightedOrder(
|
||||
pool,
|
||||
selectedTheme,
|
||||
@@ -391,21 +444,16 @@ public final class StructureAssembler {
|
||||
requireConnector(pieceConnector, pieceName);
|
||||
}
|
||||
List<IrisJigsawConnector> candidateConnectors = orderedConnectors(piece.getConnectors(), null);
|
||||
for (IrisJigsawConnector cb : candidateConnectors) {
|
||||
if (connector.targetName == null) {
|
||||
throw assemblyFailure("open connector from pool '" + connector.pool + "' has no target name");
|
||||
}
|
||||
if (!Objects.equals(connector.channel, cb.getChannel())) {
|
||||
continue;
|
||||
}
|
||||
if (!cb.getName().equals(connector.targetName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
IrisDirection needed = connector.facing.reverse();
|
||||
int[] rotations = piece.isRotatable() ? rotationCandidates(connector.joint, rng) : NO_ROTATION;
|
||||
for (int yDeg : rotations) {
|
||||
IrisObjectRotation rot = IrisObjectRotation.of(0, yDeg, 0);
|
||||
int[] rotations = piece.isRotatable() ? shuffledRotations(rng) : NO_ROTATION;
|
||||
for (int yDeg : rotations) {
|
||||
IrisObjectRotation rot = IrisObjectRotation.of(0, yDeg, 0);
|
||||
for (IrisJigsawConnector cb : candidateConnectors) {
|
||||
if (!Objects.equals(connector.channel, cb.getChannel())) {
|
||||
continue;
|
||||
}
|
||||
if (!cb.getName().equals(connector.targetName)) {
|
||||
continue;
|
||||
}
|
||||
IrisDirection rotatedFace = rot.rotate(cb.getDirection());
|
||||
if (rotatedFace != needed) {
|
||||
continue;
|
||||
@@ -431,7 +479,8 @@ public final class StructureAssembler {
|
||||
consumedConnectors,
|
||||
connector,
|
||||
candidate,
|
||||
cb);
|
||||
cb,
|
||||
neighborClosures);
|
||||
if (closures == null) {
|
||||
continue;
|
||||
}
|
||||
@@ -504,9 +553,10 @@ public final class StructureAssembler {
|
||||
Map<PlacedStructurePiece, Set<IrisJigsawConnector>> consumedConnectors,
|
||||
OpenConnector attachmentSource,
|
||||
PlacedStructurePiece candidatePiece,
|
||||
IrisJigsawConnector attachmentConnector
|
||||
IrisJigsawConnector attachmentConnector,
|
||||
boolean neighborClosures
|
||||
) {
|
||||
if (!structure.isRequireCaps()) {
|
||||
if (!neighborClosures) {
|
||||
return List.of();
|
||||
}
|
||||
List<ConnectorClosure> closures = new ArrayList<>();
|
||||
@@ -1025,10 +1075,7 @@ public final class StructureAssembler {
|
||||
return false;
|
||||
}
|
||||
|
||||
private int[] rotationCandidates(JigsawJoint joint, RNG rng) {
|
||||
if (joint == JigsawJoint.ALIGNED) {
|
||||
return Y_DEGREES;
|
||||
}
|
||||
private int[] shuffledRotations(RNG rng) {
|
||||
int[] shuffled = {0, 90, 180, 270};
|
||||
for (int i = shuffled.length - 1; i > 0; i--) {
|
||||
int j = rng.i(i + 1);
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
package art.arcane.iris.studio.generation;
|
||||
|
||||
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;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioBounds;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioCellDimensions;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioControlPosition;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioFloorGlyph;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioLayout;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioSession;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioVariant;
|
||||
@@ -51,9 +50,7 @@ public final class JigsawStudioGenerator extends EnginedStudioGenerator {
|
||||
private final NativeBlockState lightFloor;
|
||||
private final NativeBlockState darkFloor;
|
||||
private final NativeBlockState frame;
|
||||
private final NativeBlockState topologyBase;
|
||||
private final NativeBlockState topologyPath;
|
||||
private final NativeBlockState connectorCap;
|
||||
private final FloorPalette floorPalette;
|
||||
private final NativeBlockState invalidMarker;
|
||||
private final NativeBlockState controlChest;
|
||||
private final FinalStateRenderer finalStateRenderer;
|
||||
@@ -79,9 +76,13 @@ public final class JigsawStudioGenerator extends EnginedStudioGenerator {
|
||||
B.getState("minecraft:smooth_stone"),
|
||||
B.getState("minecraft:polished_deepslate"),
|
||||
B.getState("minecraft:white_concrete"),
|
||||
B.getState("minecraft:light_gray_wool"),
|
||||
B.getState("minecraft:red_wool"),
|
||||
B.getState("minecraft:sea_lantern"),
|
||||
new FloorPalette(
|
||||
B.getState("minecraft:light_gray_wool"),
|
||||
B.getState("minecraft:red_wool"),
|
||||
B.getState("minecraft:sea_lantern"),
|
||||
B.getState("minecraft:verdant_froglight"),
|
||||
B.getState("minecraft:ochre_froglight"),
|
||||
B.getState("minecraft:pearlescent_froglight")),
|
||||
B.getState("minecraft:red_stained_glass"),
|
||||
B.getState("minecraft:chest[facing=south,type=single,waterlogged=false]"),
|
||||
JigsawStudioGenerator::rotatedFinalState
|
||||
@@ -95,9 +96,7 @@ public final class JigsawStudioGenerator extends EnginedStudioGenerator {
|
||||
NativeBlockState lightFloor,
|
||||
NativeBlockState darkFloor,
|
||||
NativeBlockState frame,
|
||||
NativeBlockState topologyBase,
|
||||
NativeBlockState topologyPath,
|
||||
NativeBlockState connectorCap,
|
||||
FloorPalette floorPalette,
|
||||
NativeBlockState invalidMarker,
|
||||
NativeBlockState controlChest,
|
||||
FinalStateRenderer finalStateRenderer
|
||||
@@ -108,9 +107,7 @@ public final class JigsawStudioGenerator extends EnginedStudioGenerator {
|
||||
this.lightFloor = Objects.requireNonNull(lightFloor, "Jigsaw Studio light floor block");
|
||||
this.darkFloor = Objects.requireNonNull(darkFloor, "Jigsaw Studio dark floor block");
|
||||
this.frame = Objects.requireNonNull(frame, "Jigsaw Studio frame block");
|
||||
this.topologyBase = Objects.requireNonNull(topologyBase, "Jigsaw Studio topology base block");
|
||||
this.topologyPath = Objects.requireNonNull(topologyPath, "Jigsaw Studio topology path block");
|
||||
this.connectorCap = Objects.requireNonNull(connectorCap, "Jigsaw Studio connector cap block");
|
||||
this.floorPalette = Objects.requireNonNull(floorPalette, "Jigsaw Studio floor palette");
|
||||
this.invalidMarker = Objects.requireNonNull(invalidMarker, "Jigsaw Studio invalid marker block");
|
||||
this.controlChest = Objects.requireNonNull(controlChest, "Jigsaw Studio control chest block");
|
||||
this.finalStateRenderer = Objects.requireNonNull(
|
||||
@@ -165,6 +162,21 @@ public final class JigsawStudioGenerator extends EnginedStudioGenerator {
|
||||
return renderedBays.computeIfAbsent(key, ignored -> buildRenderedBay(activeWorkcell, variant));
|
||||
}
|
||||
|
||||
public JigsawStudioFloorGlyph floorGlyph(JigsawStudioBay workcell) {
|
||||
return glyphFor(renderBay(workcell));
|
||||
}
|
||||
|
||||
public NativeBlockState floorState(JigsawStudioFloorGlyph.Cell cell) {
|
||||
return switch (Objects.requireNonNull(cell, "Jigsaw Studio floor glyph cell")) {
|
||||
case BASE -> floorPalette.base();
|
||||
case LINE -> floorPalette.line();
|
||||
case HORIZONTAL_SOCKET -> floorPalette.horizontalSocket();
|
||||
case UP_SOCKET -> floorPalette.upSocket();
|
||||
case DOWN_SOCKET -> floorPalette.downSocket();
|
||||
case UP_AND_DOWN_SOCKET -> floorPalette.upAndDownSocket();
|
||||
};
|
||||
}
|
||||
|
||||
public void invalidateRender(String workcellId) {
|
||||
if (workcellId != null) {
|
||||
renderedBays.keySet().removeIf(key -> key.workcellId().equals(workcellId));
|
||||
@@ -215,10 +227,8 @@ public final class JigsawStudioGenerator extends EnginedStudioGenerator {
|
||||
continue;
|
||||
}
|
||||
paintCage(terrainChunk, workcell, chunkWorldX, chunkWorldZ);
|
||||
if (workcell.archetype().isPresent()) {
|
||||
paintTopologyGlyph(terrainChunk, workcell, chunkWorldX, chunkWorldZ);
|
||||
}
|
||||
RenderedBay renderedBay = renderBay(workcell);
|
||||
paintFloorGlyph(terrainChunk, workcell, glyphFor(renderedBay), chunkWorldX, chunkWorldZ);
|
||||
if (!renderedBay.valid()) {
|
||||
paintInvalidBay(terrainChunk, workcell, chunkWorldX, chunkWorldZ);
|
||||
continue;
|
||||
@@ -345,85 +355,44 @@ public final class JigsawStudioGenerator extends EnginedStudioGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
private void paintTopologyGlyph(
|
||||
private void paintFloorGlyph(
|
||||
TerrainChunk terrainChunk,
|
||||
JigsawStudioBay workcell,
|
||||
JigsawStudioFloorGlyph glyph,
|
||||
int chunkWorldX,
|
||||
int chunkWorldZ
|
||||
) {
|
||||
JigsawPlanarTopology topology = workcell.topology().orElseThrow();
|
||||
JigsawStudioBounds bounds = workcell.bounds();
|
||||
int minimumX = Math.max(bounds.originX(), chunkWorldX);
|
||||
int maximumX = Math.min(bounds.maxX(), chunkWorldX + 15);
|
||||
int minimumZ = Math.max(bounds.originZ(), chunkWorldZ);
|
||||
int maximumZ = Math.min(bounds.maxZ(), chunkWorldZ + 15);
|
||||
if (minimumX > maximumX || minimumZ > maximumZ) {
|
||||
return;
|
||||
}
|
||||
int glyphY = bounds.originY() - 1;
|
||||
int centerX = bounds.dimensions().width() / 2;
|
||||
int centerZ = bounds.dimensions().depth() / 2;
|
||||
int floorY = bounds.originY() - 1;
|
||||
for (int worldX = minimumX; worldX <= maximumX; worldX++) {
|
||||
int localX = worldX - bounds.originX();
|
||||
for (int worldZ = minimumZ; worldZ <= maximumZ; worldZ++) {
|
||||
int localZ = worldZ - bounds.originZ();
|
||||
NativeBlockState state = topologyGlyphState(topology, bounds.dimensions(), localX, localZ,
|
||||
centerX, centerZ);
|
||||
setWorldBlock(terrainChunk, worldX, glyphY, worldZ, state, chunkWorldX, chunkWorldZ);
|
||||
NativeBlockState state = floorState(glyph.cell(
|
||||
worldX - bounds.originX(),
|
||||
worldZ - bounds.originZ()));
|
||||
setWorldBlock(terrainChunk, worldX, floorY, worldZ, state, chunkWorldX, chunkWorldZ);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private NativeBlockState topologyGlyphState(
|
||||
JigsawPlanarTopology topology,
|
||||
JigsawStudioCellDimensions dimensions,
|
||||
int localX,
|
||||
int localZ,
|
||||
int centerX,
|
||||
int centerZ
|
||||
) {
|
||||
if (isConnectorCap(topology, dimensions, localX, localZ, centerX, centerZ)) {
|
||||
return connectorCap;
|
||||
private static JigsawStudioFloorGlyph glyphFor(RenderedBay renderedBay) {
|
||||
if (!renderedBay.valid() || renderedBay.connectors().isEmpty()) {
|
||||
return JigsawStudioFloorGlyph.empty();
|
||||
}
|
||||
if (isTopologyPath(topology, localX, localZ, centerX, centerZ)) {
|
||||
return topologyPath;
|
||||
List<JigsawStudioFloorGlyph.Marker> markers = new ArrayList<>(renderedBay.connectors().size());
|
||||
for (RenderedConnector connector : renderedBay.connectors()) {
|
||||
markers.add(new JigsawStudioFloorGlyph.Marker(
|
||||
connector.x(),
|
||||
connector.z(),
|
||||
connector.connector().getDirection()));
|
||||
}
|
||||
return topologyBase;
|
||||
}
|
||||
|
||||
static boolean isConnectorCap(
|
||||
JigsawPlanarTopology topology,
|
||||
JigsawStudioCellDimensions dimensions,
|
||||
int localX,
|
||||
int localZ,
|
||||
int centerX,
|
||||
int centerZ
|
||||
) {
|
||||
return topology.connects(JigsawPlanarDirection.NORTH) && localX == centerX && localZ == 0
|
||||
|| topology.connects(JigsawPlanarDirection.EAST)
|
||||
&& localX == dimensions.width() - 1 && localZ == centerZ
|
||||
|| topology.connects(JigsawPlanarDirection.SOUTH)
|
||||
&& localX == centerX && localZ == dimensions.depth() - 1
|
||||
|| topology.connects(JigsawPlanarDirection.WEST) && localX == 0 && localZ == centerZ;
|
||||
}
|
||||
|
||||
static boolean isTopologyPath(
|
||||
JigsawPlanarTopology topology,
|
||||
int localX,
|
||||
int localZ,
|
||||
int centerX,
|
||||
int centerZ
|
||||
) {
|
||||
if (topology == JigsawPlanarTopology.BLANK) {
|
||||
return false;
|
||||
}
|
||||
boolean vertical = localX == centerX
|
||||
&& (topology.connects(JigsawPlanarDirection.NORTH) && localZ <= centerZ
|
||||
|| topology.connects(JigsawPlanarDirection.SOUTH) && localZ >= centerZ);
|
||||
boolean horizontal = localZ == centerZ
|
||||
&& (topology.connects(JigsawPlanarDirection.WEST) && localX <= centerX
|
||||
|| topology.connects(JigsawPlanarDirection.EAST) && localX >= centerX);
|
||||
return vertical || horizontal;
|
||||
return JigsawStudioFloorGlyph.of(
|
||||
renderedBay.dimensions().width(),
|
||||
renderedBay.dimensions().depth(),
|
||||
markers);
|
||||
}
|
||||
|
||||
private RenderedBay buildRenderedBay(JigsawStudioBay workcell, JigsawStudioVariant variant) {
|
||||
@@ -720,7 +689,7 @@ public final class JigsawStudioGenerator extends EnginedStudioGenerator {
|
||||
|
||||
private JigsawStudioBay requireWorkcell(JigsawStudioBay workcell) {
|
||||
JigsawStudioBay activeWorkcell = Objects.requireNonNull(workcell, "Jigsaw Studio workcell");
|
||||
if (getLayout().get(activeWorkcell.stableId()) != activeWorkcell) {
|
||||
if (!activeWorkcell.equals(getLayout().get(activeWorkcell.stableId()))) {
|
||||
throw new IllegalArgumentException("Workcell does not belong to this Jigsaw Studio layout");
|
||||
}
|
||||
return activeWorkcell;
|
||||
@@ -884,6 +853,24 @@ public final class JigsawStudioGenerator extends EnginedStudioGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
public record FloorPalette(
|
||||
NativeBlockState base,
|
||||
NativeBlockState line,
|
||||
NativeBlockState horizontalSocket,
|
||||
NativeBlockState upSocket,
|
||||
NativeBlockState downSocket,
|
||||
NativeBlockState upAndDownSocket
|
||||
) {
|
||||
public FloorPalette {
|
||||
Objects.requireNonNull(base, "Jigsaw Studio floor base block");
|
||||
Objects.requireNonNull(line, "Jigsaw Studio floor line block");
|
||||
Objects.requireNonNull(horizontalSocket, "Jigsaw Studio horizontal socket block");
|
||||
Objects.requireNonNull(upSocket, "Jigsaw Studio upward socket block");
|
||||
Objects.requireNonNull(downSocket, "Jigsaw Studio downward socket block");
|
||||
Objects.requireNonNull(upAndDownSocket, "Jigsaw Studio vertical socket block");
|
||||
}
|
||||
}
|
||||
|
||||
public record RenderedBlock(int x, int y, int z, NativeBlockState state, TileData tileData) {
|
||||
public RenderedBlock {
|
||||
state = Objects.requireNonNull(state, "Jigsaw Studio rendered block state");
|
||||
|
||||
@@ -141,6 +141,10 @@ final class JigsawStudioCapture {
|
||||
if (hiddenConnector != null) {
|
||||
hiddenConnector.setFinalState(blockData.getAsString());
|
||||
connectors.add(CapturedConnector.from(hiddenConnector));
|
||||
if (isAir(blockData.getMaterial())) {
|
||||
blocks.add(new CapturedBlock(x, y, z, BukkitBlockState.of(blockData), null));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (hiddenConnector == null && blockData instanceof Jigsaw jigsaw) {
|
||||
KMap<String, Object> nbt = BukkitPlatform.serializeTile(block.getLocation());
|
||||
@@ -490,7 +494,7 @@ final class JigsawStudioCapture {
|
||||
};
|
||||
}
|
||||
|
||||
private static LocalPosition forwardPosition(
|
||||
static LocalPosition forwardPosition(
|
||||
int x,
|
||||
int y,
|
||||
int z,
|
||||
|
||||
@@ -18,6 +18,7 @@ import art.arcane.iris.structure.object.IrisObject;
|
||||
import art.arcane.iris.studio.generation.JigsawStudioGenerator;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.world.task.J;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
@@ -122,7 +123,7 @@ final class JigsawStudioCaptureScheduler {
|
||||
}
|
||||
coordinator.accept(snapshot);
|
||||
} catch (Throwable exception) {
|
||||
coordinator.fail("Jigsaw Studio capture failed in chunk "
|
||||
coordinator.failPersistent("Jigsaw Studio capture failed in chunk "
|
||||
+ area.chunkX() + "," + area.chunkZ() + ": " + failureMessage(exception), exception);
|
||||
}
|
||||
}
|
||||
@@ -219,7 +220,7 @@ final class JigsawStudioCaptureScheduler {
|
||||
if (!result.successful()) {
|
||||
String failure = writeFailure(result);
|
||||
coordinator.recordPersistentFailure(failure, result.failure().orElse(null));
|
||||
message(player, failure);
|
||||
message(coordinator.notifiedPlayer(), failure);
|
||||
return;
|
||||
}
|
||||
source.invalidateStructureResources();
|
||||
@@ -234,13 +235,14 @@ final class JigsawStudioCaptureScheduler {
|
||||
+ "Close and reopen this project before continuing: "
|
||||
+ failureMessage(refreshFailure));
|
||||
}
|
||||
service.floorPainter.refresh(studio, bay.stableId());
|
||||
String cleanup = result.status() == StructureWriteResult.Status.COMMITTED_CLEANUP_REQUIRED
|
||||
? " The graph committed, but transaction cleanup requires operator attention in the console."
|
||||
: "";
|
||||
String mutationNotice = unchanged ? "" : " Newer edits remain unsaved.";
|
||||
message(player, "Saved piece '" + coordinator.saveIdentity().variantKey() + "' and object '"
|
||||
+ assembly.objectKey() + "' atomically." + mutationNotice + cleanup);
|
||||
playSaveSound(player);
|
||||
playSaveSound(coordinator.notifiedPlayer());
|
||||
if (unchanged) {
|
||||
service.evaluator.scheduleEvaluation(studio);
|
||||
}
|
||||
@@ -250,12 +252,19 @@ final class JigsawStudioCaptureScheduler {
|
||||
} catch (Throwable exception) {
|
||||
String failure = "Jigsaw Studio save failed: " + failureMessage(exception);
|
||||
coordinator.recordPersistentFailure(failure, exception);
|
||||
message(player, failure);
|
||||
message(coordinator.notifiedPlayer(), failure);
|
||||
} finally {
|
||||
coordinator.complete();
|
||||
}
|
||||
}
|
||||
|
||||
static Player notificationTarget(Player player, UUID ownerId) {
|
||||
if (player != null || ownerId == null) {
|
||||
return player;
|
||||
}
|
||||
return Bukkit.getPlayer(ownerId);
|
||||
}
|
||||
|
||||
static Set<Long> requiredChunks(
|
||||
JigsawStudioBay bay,
|
||||
JigsawStudioGenerator.RenderedBay rendered
|
||||
@@ -368,6 +377,10 @@ final class JigsawStudioCaptureScheduler {
|
||||
return work.player();
|
||||
}
|
||||
|
||||
private Player notifiedPlayer() {
|
||||
return notificationTarget(work.player(), work.studio().generator().getRequest().ownerId());
|
||||
}
|
||||
|
||||
private UUID requestId() {
|
||||
return work.requestId();
|
||||
}
|
||||
@@ -427,7 +440,7 @@ final class JigsawStudioCaptureScheduler {
|
||||
}
|
||||
recordPersistentFailure(failure, exception);
|
||||
complete();
|
||||
message(player(), failure);
|
||||
message(notifiedPlayer(), failure);
|
||||
}
|
||||
|
||||
private void recordPersistentFailure(String failure, Throwable exception) {
|
||||
|
||||
@@ -40,58 +40,84 @@ final class JigsawStudioChunkWriter {
|
||||
private JigsawStudioChunkWriter() {
|
||||
}
|
||||
|
||||
static void restoreConnectorChunk(
|
||||
static void restoreChunkPositions(
|
||||
World world,
|
||||
JigsawStudioBay workcell,
|
||||
List<JigsawStudioGenerator.RenderedConnector> connectors,
|
||||
Set<LocalPosition> positions,
|
||||
Map<LocalPosition, JigsawStudioGenerator.RenderedConnector> renderedConnectors,
|
||||
Map<LocalPosition, JigsawStudioGenerator.RenderedBlock> renderedBlocks,
|
||||
boolean connectorsVisible
|
||||
) throws IOException {
|
||||
JigsawStudioBounds bounds = workcell.bounds();
|
||||
for (JigsawStudioGenerator.RenderedConnector connector : connectors) {
|
||||
LocalPosition position = new LocalPosition(connector.x(), connector.y(), connector.z());
|
||||
BlockData air = null;
|
||||
for (LocalPosition position : positions) {
|
||||
Block target = world.getBlockAt(
|
||||
bounds.originX() + connector.x(),
|
||||
bounds.originY() + connector.y(),
|
||||
bounds.originZ() + connector.z());
|
||||
if (connectorsVisible) {
|
||||
BlockData marker;
|
||||
try {
|
||||
marker = Bukkit.createBlockData(
|
||||
"minecraft:jigsaw[orientation=" + connector.orientation() + "]");
|
||||
} catch (IllegalArgumentException exception) {
|
||||
throw new IOException("Invalid saved connector orientation '"
|
||||
+ connector.orientation() + "'", exception);
|
||||
}
|
||||
target.setBlockData(marker, false);
|
||||
BukkitPlatform.deserializeTile(markerNbt(connector.connector()), target.getLocation());
|
||||
continue;
|
||||
}
|
||||
bounds.originX() + position.x(),
|
||||
bounds.originY() + position.y(),
|
||||
bounds.originZ() + position.z());
|
||||
JigsawStudioGenerator.RenderedConnector connector = renderedConnectors.get(position);
|
||||
JigsawStudioGenerator.RenderedBlock renderedBlock = renderedBlocks.get(position);
|
||||
BlockData restored;
|
||||
if (renderedBlock != null) {
|
||||
if (renderedBlock.state().isCustom()
|
||||
|| !(renderedBlock.state().nativeHandle() instanceof BlockData blockData)) {
|
||||
throw new IOException("Saved connector block '" + renderedBlock.state().key()
|
||||
+ "' cannot be restored directly in Bukkit Studio");
|
||||
}
|
||||
restored = blockData;
|
||||
if (connector != null) {
|
||||
restoreConnector(target, connector, renderedBlock, connectorsVisible);
|
||||
} else if (renderedBlock != null) {
|
||||
restoreRenderedBlock(target, renderedBlock);
|
||||
} else {
|
||||
try {
|
||||
restored = Bukkit.createBlockData(connector.connector().getFinalState());
|
||||
} catch (IllegalArgumentException exception) {
|
||||
throw new IOException("Invalid saved connector final state '"
|
||||
+ connector.connector().getFinalState() + "'", exception);
|
||||
if (air == null) {
|
||||
air = Material.AIR.createBlockData();
|
||||
}
|
||||
target.setBlockData(air, false);
|
||||
}
|
||||
target.setBlockData(restored, false);
|
||||
if (renderedBlock != null && renderedBlock.tileData() != null) {
|
||||
TileData tileData = renderedBlock.tileData();
|
||||
if (!tileData.isApplicable(target.getBlockData()) || !tileData.toBukkitTry(target)) {
|
||||
throw new IOException("Saved connector tile data could not be restored at "
|
||||
+ target.getX() + "," + target.getY() + "," + target.getZ());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void restoreConnector(
|
||||
Block target,
|
||||
JigsawStudioGenerator.RenderedConnector connector,
|
||||
JigsawStudioGenerator.RenderedBlock renderedBlock,
|
||||
boolean connectorsVisible
|
||||
) throws IOException {
|
||||
if (connectorsVisible) {
|
||||
BlockData marker;
|
||||
try {
|
||||
marker = Bukkit.createBlockData(
|
||||
"minecraft:jigsaw[orientation=" + connector.orientation() + "]");
|
||||
} catch (IllegalArgumentException exception) {
|
||||
throw new IOException("Invalid saved connector orientation '"
|
||||
+ connector.orientation() + "'", exception);
|
||||
}
|
||||
target.setBlockData(marker, false);
|
||||
BukkitPlatform.deserializeTile(markerNbt(connector.connector()), target.getLocation());
|
||||
return;
|
||||
}
|
||||
if (renderedBlock != null) {
|
||||
restoreRenderedBlock(target, renderedBlock);
|
||||
return;
|
||||
}
|
||||
BlockData restored;
|
||||
try {
|
||||
restored = Bukkit.createBlockData(connector.connector().getFinalState());
|
||||
} catch (IllegalArgumentException exception) {
|
||||
throw new IOException("Invalid saved connector final state '"
|
||||
+ connector.connector().getFinalState() + "'", exception);
|
||||
}
|
||||
target.setBlockData(restored, false);
|
||||
}
|
||||
|
||||
private static void restoreRenderedBlock(
|
||||
Block target,
|
||||
JigsawStudioGenerator.RenderedBlock renderedBlock
|
||||
) throws IOException {
|
||||
if (renderedBlock.state().isCustom()
|
||||
|| !(renderedBlock.state().nativeHandle() instanceof BlockData blockData)) {
|
||||
throw new IOException("Saved block '" + renderedBlock.state().key()
|
||||
+ "' cannot be restored directly in Bukkit Studio");
|
||||
}
|
||||
target.setBlockData(blockData, false);
|
||||
TileData tileData = renderedBlock.tileData();
|
||||
if (tileData != null
|
||||
&& (!tileData.isApplicable(target.getBlockData()) || !tileData.toBukkitTry(target))) {
|
||||
throw new IOException("Saved tile data could not be restored at "
|
||||
+ target.getX() + "," + target.getY() + "," + target.getZ());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,682 @@
|
||||
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.structure.object.IrisObjectRotation;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioCapture.CapturedConnector;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioCapture.LocalPosition;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
final class JigsawStudioConnectorPlacement {
|
||||
static final double MAX_REACH = 64.0D;
|
||||
static final String EMPTY_FINAL_STATE = "minecraft:structure_void";
|
||||
|
||||
private static final double EPSILON = 1.0E-4D;
|
||||
private static final int MAX_TRAVERSAL_STEPS = 512;
|
||||
private static final List<JigsawPlanarDirection> SIDE_ORDER = List.of(
|
||||
JigsawPlanarDirection.NORTH,
|
||||
JigsawPlanarDirection.EAST,
|
||||
JigsawPlanarDirection.SOUTH,
|
||||
JigsawPlanarDirection.WEST);
|
||||
|
||||
private JigsawStudioConnectorPlacement() {
|
||||
}
|
||||
|
||||
static Optional<Target> target(
|
||||
Box box,
|
||||
Ray ray,
|
||||
Optional<BlockClick> click,
|
||||
Set<Cell> connectorCells,
|
||||
boolean sneaking
|
||||
) {
|
||||
Objects.requireNonNull(box, "Jigsaw connector target box");
|
||||
Objects.requireNonNull(ray, "Jigsaw connector target ray");
|
||||
Objects.requireNonNull(click, "Jigsaw connector target click");
|
||||
Objects.requireNonNull(connectorCells, "Jigsaw connector cells");
|
||||
double limit = click.map(value -> entryDistance(
|
||||
ray,
|
||||
new Box(value.x(), value.y(), value.z(), value.x(), value.y(), value.z()))).orElse(MAX_REACH);
|
||||
Optional<Target> resolved = connectorTarget(ray, connectorCells, limit + EPSILON);
|
||||
if (resolved.isEmpty() && click.isPresent()) {
|
||||
resolved = clickTarget(box, click.get());
|
||||
}
|
||||
if (resolved.isEmpty()) {
|
||||
resolved = boundaryTarget(box, ray, limit);
|
||||
}
|
||||
return resolved.map(target -> sneaking
|
||||
? new Target(target.x(), target.y(), target.z(), target.facing().reverse())
|
||||
: target);
|
||||
}
|
||||
|
||||
static Decision decideSpatial(
|
||||
boolean primary,
|
||||
LocalPosition target,
|
||||
IrisDirection facing,
|
||||
Map<LocalPosition, IrisDirection> connectors,
|
||||
Optional<LocalPosition> selection
|
||||
) {
|
||||
boolean occupied = connectors.containsKey(target);
|
||||
Optional<LocalPosition> activeSelection = selection.filter(connectors::containsKey);
|
||||
if (primary) {
|
||||
if (occupied) {
|
||||
return Decision.of(Operation.REMOVE, target);
|
||||
}
|
||||
return activeSelection.isPresent()
|
||||
? Decision.of(Operation.CLEAR_SELECTION, activeSelection.get())
|
||||
: Decision.message(Operation.NONE, "No connector here. Right-click to place one.");
|
||||
}
|
||||
if (activeSelection.isEmpty()) {
|
||||
return Decision.of(occupied ? Operation.SELECT : Operation.ADD, target);
|
||||
}
|
||||
LocalPosition selected = activeSelection.get();
|
||||
if (selected.equals(target)) {
|
||||
return Decision.of(connectors.get(target) == facing ? Operation.DESELECT : Operation.REORIENT, target);
|
||||
}
|
||||
return Decision.of(occupied ? Operation.SELECT : Operation.MOVE, target);
|
||||
}
|
||||
|
||||
static Decision decidePlanar(
|
||||
boolean primary,
|
||||
LocalPosition target,
|
||||
IrisDirection facing,
|
||||
JigsawStudioCellDimensions displayDimensions,
|
||||
JigsawPlanarTopology topology,
|
||||
Map<LocalPosition, IrisDirection> connectors,
|
||||
String workcellName
|
||||
) {
|
||||
if (primary) {
|
||||
IrisDirection connectorFacing = connectors.get(target);
|
||||
if (connectorFacing == null) {
|
||||
return Decision.message(Operation.NONE, "No connector here. Right-click a side to seat its socket.");
|
||||
}
|
||||
Optional<JigsawPlanarDirection> side = planarSide(connectorFacing);
|
||||
if (side.isPresent() && topology.connects(side.get())
|
||||
&& countFacing(connectors, connectorFacing) == 1) {
|
||||
return Decision.message(Operation.REFUSE, workcellName + " needs its "
|
||||
+ sideName(side.get()) + " socket; planar topology is fixed by the workcell.");
|
||||
}
|
||||
return Decision.of(Operation.REMOVE, target);
|
||||
}
|
||||
if (topology.directions().isEmpty()) {
|
||||
return Decision.message(Operation.REFUSE, workcellName + " has no connector sockets.");
|
||||
}
|
||||
Optional<JigsawPlanarDirection> clicked = planarSide(facing);
|
||||
JigsawPlanarDirection side = clicked.orElseGet(() -> nearestSide(target, displayDimensions, topology));
|
||||
if (!topology.connects(side)) {
|
||||
return Decision.message(Operation.REFUSE, workcellName + " connects "
|
||||
+ describeSides(topology) + " only; its planar topology is fixed.");
|
||||
}
|
||||
return new Decision(Operation.RESEAT, target, side, "");
|
||||
}
|
||||
|
||||
static int indexAt(List<IrisJigsawConnector> connectors, Frame frame, LocalPosition display) {
|
||||
for (int index = 0; index < connectors.size(); index++) {
|
||||
if (frame.display(connectors.get(index).getPosition()).equals(display)) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static List<IrisJigsawConnector> add(
|
||||
List<IrisJigsawConnector> connectors,
|
||||
Frame frame,
|
||||
LocalPosition target,
|
||||
IrisDirection facing,
|
||||
IrisDirection top,
|
||||
Identity identity,
|
||||
CellContents contents
|
||||
) {
|
||||
if (indexAt(connectors, frame, target) >= 0) {
|
||||
throw new IllegalArgumentException("A connector already occupies " + describe(target));
|
||||
}
|
||||
IrisPosition position = frame.source(target);
|
||||
IrisJigsawConnector added = new IrisJigsawConnector()
|
||||
.setPosition(position)
|
||||
.setDirection(frame.sourceDirection(facing))
|
||||
.setTop(frame.sourceDirection(top))
|
||||
.setPool(identity.pool())
|
||||
.setName(identity.name())
|
||||
.setTargetName(identity.targetName())
|
||||
.setChannel("")
|
||||
.setJoint(identity.joint())
|
||||
.setFinalState(finalState(position, EMPTY_FINAL_STATE, contents))
|
||||
.setSelectionPriority(0)
|
||||
.setPlacementPriority(0);
|
||||
List<IrisJigsawConnector> updated = copy(connectors);
|
||||
updated.add(added);
|
||||
return List.copyOf(updated);
|
||||
}
|
||||
|
||||
static List<IrisJigsawConnector> move(
|
||||
List<IrisJigsawConnector> connectors,
|
||||
Frame frame,
|
||||
LocalPosition from,
|
||||
LocalPosition to,
|
||||
IrisDirection facing,
|
||||
IrisDirection top,
|
||||
CellContents contents
|
||||
) {
|
||||
int index = indexAt(connectors, frame, from);
|
||||
if (index < 0) {
|
||||
throw new IllegalArgumentException("No connector exists at " + describe(from));
|
||||
}
|
||||
if (!from.equals(to) && indexAt(connectors, frame, to) >= 0) {
|
||||
throw new IllegalArgumentException("A connector already occupies " + describe(to));
|
||||
}
|
||||
List<IrisJigsawConnector> updated = copy(connectors);
|
||||
IrisJigsawConnector moved = updated.get(index);
|
||||
IrisPosition position = frame.source(to);
|
||||
moved.setPosition(position)
|
||||
.setDirection(frame.sourceDirection(facing))
|
||||
.setTop(frame.sourceDirection(top));
|
||||
if (!from.equals(to)) {
|
||||
moved.setFinalState(finalState(position, moved.getFinalState(), contents));
|
||||
}
|
||||
return List.copyOf(updated);
|
||||
}
|
||||
|
||||
static List<IrisJigsawConnector> remove(
|
||||
List<IrisJigsawConnector> connectors,
|
||||
Frame frame,
|
||||
LocalPosition at
|
||||
) {
|
||||
int index = indexAt(connectors, frame, at);
|
||||
if (index < 0) {
|
||||
throw new IllegalArgumentException("No connector exists at " + describe(at));
|
||||
}
|
||||
List<IrisJigsawConnector> updated = copy(connectors);
|
||||
updated.remove(index);
|
||||
return List.copyOf(updated);
|
||||
}
|
||||
|
||||
static ReseatResult reseat(
|
||||
List<IrisJigsawConnector> connectors,
|
||||
Frame frame,
|
||||
JigsawPlanarDirection displaySide,
|
||||
Identity identity,
|
||||
CellContents contents
|
||||
) {
|
||||
IrisDirection sourceDirection = frame.sourceDirection(displaySide.irisDirection());
|
||||
JigsawStudioCellDimensions dimensions = frame.sourceDimensions();
|
||||
IrisPosition socket = IrisJigsawConnector.canonicalPlanarPosition(
|
||||
new IrisPosition(dimensions.width(), dimensions.height(), dimensions.depth()),
|
||||
sourceDirection);
|
||||
List<IrisJigsawConnector> updated = copy(connectors);
|
||||
int seatedIndex = -1;
|
||||
int strays = 0;
|
||||
for (int index = 0; index < updated.size(); index++) {
|
||||
if (updated.get(index).getDirection() != sourceDirection) {
|
||||
continue;
|
||||
}
|
||||
if (seatedIndex < 0) {
|
||||
seatedIndex = index;
|
||||
} else {
|
||||
strays++;
|
||||
}
|
||||
}
|
||||
if (seatedIndex < 0) {
|
||||
updated.add(new IrisJigsawConnector()
|
||||
.setPosition(socket)
|
||||
.setDirection(sourceDirection)
|
||||
.setTop(IrisDirection.UP_POSITIVE_Y)
|
||||
.setPool(identity.pool())
|
||||
.setName(identity.name())
|
||||
.setTargetName(identity.targetName())
|
||||
.setChannel("")
|
||||
.setJoint(identity.joint())
|
||||
.setFinalState(finalState(socket, EMPTY_FINAL_STATE, contents))
|
||||
.setSelectionPriority(0)
|
||||
.setPlacementPriority(0));
|
||||
return new ReseatResult(List.copyOf(updated), true, 0);
|
||||
}
|
||||
IrisJigsawConnector seated = updated.get(seatedIndex);
|
||||
boolean alreadySeated = socket.equals(seated.getPosition())
|
||||
&& seated.getTop() == IrisDirection.UP_POSITIVE_Y;
|
||||
if (alreadySeated && strays == 0) {
|
||||
return new ReseatResult(List.copyOf(connectors), false, 0);
|
||||
}
|
||||
if (!socket.equals(seated.getPosition())) {
|
||||
seated.setPosition(socket)
|
||||
.setFinalState(finalState(socket, seated.getFinalState(), contents));
|
||||
}
|
||||
seated.setTop(IrisDirection.UP_POSITIVE_Y);
|
||||
List<IrisJigsawConnector> retained = new ArrayList<>(updated.size() - strays);
|
||||
for (int index = 0; index < updated.size(); index++) {
|
||||
IrisJigsawConnector connector = updated.get(index);
|
||||
if (index == seatedIndex || connector.getDirection() != sourceDirection) {
|
||||
retained.add(connector);
|
||||
}
|
||||
}
|
||||
return new ReseatResult(List.copyOf(retained), true, strays);
|
||||
}
|
||||
|
||||
static Set<LocalPosition> changedPositions(
|
||||
List<IrisJigsawConnector> before,
|
||||
List<IrisJigsawConnector> after,
|
||||
Frame frame
|
||||
) {
|
||||
Map<LocalPosition, CapturedConnector> previous = displayed(before, frame);
|
||||
Map<LocalPosition, CapturedConnector> current = displayed(after, frame);
|
||||
Set<LocalPosition> changed = new HashSet<>();
|
||||
for (Map.Entry<LocalPosition, CapturedConnector> entry : previous.entrySet()) {
|
||||
if (!entry.getValue().equals(current.get(entry.getKey()))) {
|
||||
changed.add(entry.getKey());
|
||||
}
|
||||
}
|
||||
for (Map.Entry<LocalPosition, CapturedConnector> entry : current.entrySet()) {
|
||||
if (!entry.getValue().equals(previous.get(entry.getKey()))) {
|
||||
changed.add(entry.getKey());
|
||||
}
|
||||
}
|
||||
return Set.copyOf(changed);
|
||||
}
|
||||
|
||||
static String describe(LocalPosition position) {
|
||||
return position.x() + "," + position.y() + "," + position.z();
|
||||
}
|
||||
|
||||
static String sideName(JigsawPlanarDirection side) {
|
||||
return side.name().toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
private static Map<LocalPosition, CapturedConnector> displayed(List<IrisJigsawConnector> connectors, Frame frame) {
|
||||
Map<LocalPosition, CapturedConnector> displayed = new HashMap<>(connectors.size());
|
||||
for (IrisJigsawConnector connector : connectors) {
|
||||
displayed.put(frame.display(connector.getPosition()), CapturedConnector.from(connector));
|
||||
}
|
||||
return displayed;
|
||||
}
|
||||
|
||||
private static List<IrisJigsawConnector> copy(List<IrisJigsawConnector> connectors) {
|
||||
List<IrisJigsawConnector> copied = new ArrayList<>(connectors.size() + 1);
|
||||
for (IrisJigsawConnector connector : connectors) {
|
||||
copied.add(CapturedConnector.from(connector).toConnector());
|
||||
}
|
||||
return copied;
|
||||
}
|
||||
|
||||
private static String finalState(IrisPosition position, String whenEmpty, CellContents contents) {
|
||||
Optional<CellContent> content = contents.at(position);
|
||||
if (content.isEmpty()) {
|
||||
return whenEmpty;
|
||||
}
|
||||
if (content.get().blockEntity()) {
|
||||
throw new IllegalArgumentException("Connectors cannot replace the block entity at "
|
||||
+ position.getX() + "," + position.getY() + "," + position.getZ()
|
||||
+ "; clear that block first.");
|
||||
}
|
||||
return content.get().state();
|
||||
}
|
||||
|
||||
private static int countFacing(Map<LocalPosition, IrisDirection> connectors, IrisDirection facing) {
|
||||
int count = 0;
|
||||
for (IrisDirection direction : connectors.values()) {
|
||||
if (direction == facing) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private static Optional<JigsawPlanarDirection> planarSide(IrisDirection direction) {
|
||||
return switch (direction) {
|
||||
case NORTH_NEGATIVE_Z -> Optional.of(JigsawPlanarDirection.NORTH);
|
||||
case EAST_POSITIVE_X -> Optional.of(JigsawPlanarDirection.EAST);
|
||||
case SOUTH_POSITIVE_Z -> Optional.of(JigsawPlanarDirection.SOUTH);
|
||||
case WEST_NEGATIVE_X -> Optional.of(JigsawPlanarDirection.WEST);
|
||||
case UP_POSITIVE_Y, DOWN_NEGATIVE_Y -> Optional.empty();
|
||||
};
|
||||
}
|
||||
|
||||
private static JigsawPlanarDirection nearestSide(
|
||||
LocalPosition target,
|
||||
JigsawStudioCellDimensions dimensions,
|
||||
JigsawPlanarTopology topology
|
||||
) {
|
||||
JigsawPlanarDirection nearest = null;
|
||||
int nearestDistance = Integer.MAX_VALUE;
|
||||
for (JigsawPlanarDirection side : SIDE_ORDER) {
|
||||
if (!topology.connects(side)) {
|
||||
continue;
|
||||
}
|
||||
int distance = switch (side) {
|
||||
case NORTH -> target.z();
|
||||
case EAST -> dimensions.width() - 1 - target.x();
|
||||
case SOUTH -> dimensions.depth() - 1 - target.z();
|
||||
case WEST -> target.x();
|
||||
};
|
||||
if (distance < nearestDistance) {
|
||||
nearest = side;
|
||||
nearestDistance = distance;
|
||||
}
|
||||
}
|
||||
return Objects.requireNonNull(nearest, "Planar topology has no sides");
|
||||
}
|
||||
|
||||
private static String describeSides(JigsawPlanarTopology topology) {
|
||||
List<String> names = new ArrayList<>(4);
|
||||
for (JigsawPlanarDirection side : SIDE_ORDER) {
|
||||
if (topology.connects(side)) {
|
||||
names.add(sideName(side));
|
||||
}
|
||||
}
|
||||
if (names.size() == 1) {
|
||||
return names.getFirst();
|
||||
}
|
||||
if (names.size() == 2) {
|
||||
return names.get(0) + " and " + names.get(1);
|
||||
}
|
||||
return String.join(", ", names.subList(0, names.size() - 1)) + ", and " + names.getLast();
|
||||
}
|
||||
|
||||
static double entryDistance(Ray ray, Box box) {
|
||||
double near = 0.0D;
|
||||
double far = MAX_REACH;
|
||||
double[] origin = {ray.originX(), ray.originY(), ray.originZ()};
|
||||
double[] direction = {ray.directionX(), ray.directionY(), ray.directionZ()};
|
||||
double[] minimum = {box.minX(), box.minY(), box.minZ()};
|
||||
double[] maximum = {box.maxX() + 1.0D, box.maxY() + 1.0D, box.maxZ() + 1.0D};
|
||||
for (int axis = 0; axis < 3; axis++) {
|
||||
if (Math.abs(direction[axis]) < 1.0E-12D) {
|
||||
if (origin[axis] < minimum[axis] || origin[axis] >= maximum[axis]) {
|
||||
return MAX_REACH;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
double first = (minimum[axis] - origin[axis]) / direction[axis];
|
||||
double second = (maximum[axis] - origin[axis]) / direction[axis];
|
||||
near = Math.max(near, Math.min(first, second));
|
||||
far = Math.min(far, Math.max(first, second));
|
||||
}
|
||||
return near <= far ? near : MAX_REACH;
|
||||
}
|
||||
|
||||
private static Optional<Target> connectorTarget(Ray ray, Set<Cell> cells, double limit) {
|
||||
if (cells.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
int x = (int) Math.floor(ray.originX());
|
||||
int y = (int) Math.floor(ray.originY());
|
||||
int z = (int) Math.floor(ray.originZ());
|
||||
int stepX = (int) Math.signum(ray.directionX());
|
||||
int stepY = (int) Math.signum(ray.directionY());
|
||||
int stepZ = (int) Math.signum(ray.directionZ());
|
||||
double maxX = boundaryDistance(ray.originX(), ray.directionX(), x);
|
||||
double maxY = boundaryDistance(ray.originY(), ray.directionY(), y);
|
||||
double maxZ = boundaryDistance(ray.originZ(), ray.directionZ(), z);
|
||||
double deltaX = stepX == 0 ? Double.POSITIVE_INFINITY : 1.0D / Math.abs(ray.directionX());
|
||||
double deltaY = stepY == 0 ? Double.POSITIVE_INFINITY : 1.0D / Math.abs(ray.directionY());
|
||||
double deltaZ = stepZ == 0 ? Double.POSITIVE_INFINITY : 1.0D / Math.abs(ray.directionZ());
|
||||
for (int step = 0; step < MAX_TRAVERSAL_STEPS; step++) {
|
||||
double distance;
|
||||
IrisDirection entered;
|
||||
if (maxX < maxY && maxX < maxZ) {
|
||||
distance = maxX;
|
||||
x += stepX;
|
||||
maxX += deltaX;
|
||||
entered = stepX > 0 ? IrisDirection.WEST_NEGATIVE_X : IrisDirection.EAST_POSITIVE_X;
|
||||
} else if (maxY < maxZ) {
|
||||
distance = maxY;
|
||||
y += stepY;
|
||||
maxY += deltaY;
|
||||
entered = stepY > 0 ? IrisDirection.DOWN_NEGATIVE_Y : IrisDirection.UP_POSITIVE_Y;
|
||||
} else {
|
||||
distance = maxZ;
|
||||
z += stepZ;
|
||||
maxZ += deltaZ;
|
||||
entered = stepZ > 0 ? IrisDirection.NORTH_NEGATIVE_Z : IrisDirection.SOUTH_POSITIVE_Z;
|
||||
}
|
||||
if (distance > limit) {
|
||||
return Optional.empty();
|
||||
}
|
||||
if (cells.contains(new Cell(x, y, z))) {
|
||||
return Optional.of(new Target(x, y, z, entered));
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
private static double boundaryDistance(double origin, double direction, int cell) {
|
||||
if (direction > 0.0D) {
|
||||
return (cell + 1 - origin) / direction;
|
||||
}
|
||||
if (direction < 0.0D) {
|
||||
return (origin - cell) / -direction;
|
||||
}
|
||||
return Double.POSITIVE_INFINITY;
|
||||
}
|
||||
|
||||
private static Optional<Target> clickTarget(Box box, BlockClick click) {
|
||||
if (box.contains(click.x(), click.y(), click.z())) {
|
||||
return Optional.of(new Target(click.x(), click.y(), click.z(), click.face()));
|
||||
}
|
||||
int adjacentX = click.x() + click.face().x();
|
||||
int adjacentY = click.y() + click.face().y();
|
||||
int adjacentZ = click.z() + click.face().z();
|
||||
if (box.contains(adjacentX, adjacentY, adjacentZ)) {
|
||||
return Optional.of(new Target(adjacentX, adjacentY, adjacentZ, click.face().reverse()));
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
private static Optional<Target> boundaryTarget(Box box, Ray ray, double limit) {
|
||||
double[] origin = {ray.originX(), ray.originY(), ray.originZ()};
|
||||
double[] direction = {ray.directionX(), ray.directionY(), ray.directionZ()};
|
||||
double[] minimum = {box.minX(), box.minY(), box.minZ()};
|
||||
double[] maximum = {box.maxX() + 1.0D, box.maxY() + 1.0D, box.maxZ() + 1.0D};
|
||||
double near = Double.NEGATIVE_INFINITY;
|
||||
double far = Double.POSITIVE_INFINITY;
|
||||
int nearAxis = -1;
|
||||
int farAxis = -1;
|
||||
for (int axis = 0; axis < 3; axis++) {
|
||||
if (Math.abs(direction[axis]) < 1.0E-12D) {
|
||||
if (origin[axis] < minimum[axis] || origin[axis] >= maximum[axis]) {
|
||||
return Optional.empty();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
double first = (minimum[axis] - origin[axis]) / direction[axis];
|
||||
double second = (maximum[axis] - origin[axis]) / direction[axis];
|
||||
double axisNear = Math.min(first, second);
|
||||
double axisFar = Math.max(first, second);
|
||||
if (axisNear > near) {
|
||||
near = axisNear;
|
||||
nearAxis = axis;
|
||||
}
|
||||
if (axisFar < far) {
|
||||
far = axisFar;
|
||||
farAxis = axis;
|
||||
}
|
||||
}
|
||||
if (near > far || far < 0.0D) {
|
||||
return Optional.empty();
|
||||
}
|
||||
boolean inside = near < 0.0D;
|
||||
double distance = inside ? far - EPSILON : near + EPSILON;
|
||||
int axis = inside ? farAxis : nearAxis;
|
||||
if (axis < 0 || !inside && near > limit) {
|
||||
return Optional.empty();
|
||||
}
|
||||
boolean positive = direction[axis] > 0.0D;
|
||||
IrisDirection facing = axisDirection(axis, inside == positive);
|
||||
int x = clamp((int) Math.floor(origin[0] + direction[0] * distance), box.minX(), box.maxX());
|
||||
int y = clamp((int) Math.floor(origin[1] + direction[1] * distance), box.minY(), box.maxY());
|
||||
int z = clamp((int) Math.floor(origin[2] + direction[2] * distance), box.minZ(), box.maxZ());
|
||||
return Optional.of(new Target(x, y, z, facing));
|
||||
}
|
||||
|
||||
private static IrisDirection axisDirection(int axis, boolean positive) {
|
||||
return switch (axis) {
|
||||
case 0 -> positive ? IrisDirection.EAST_POSITIVE_X : IrisDirection.WEST_NEGATIVE_X;
|
||||
case 1 -> positive ? IrisDirection.UP_POSITIVE_Y : IrisDirection.DOWN_NEGATIVE_Y;
|
||||
case 2 -> positive ? IrisDirection.SOUTH_POSITIVE_Z : IrisDirection.NORTH_NEGATIVE_Z;
|
||||
default -> throw new IllegalStateException("Unreachable Jigsaw connector axis " + axis);
|
||||
};
|
||||
}
|
||||
|
||||
private static int clamp(int value, int minimum, int maximum) {
|
||||
return Math.max(minimum, Math.min(maximum, value));
|
||||
}
|
||||
|
||||
enum Operation {
|
||||
SELECT,
|
||||
DESELECT,
|
||||
CLEAR_SELECTION,
|
||||
ADD,
|
||||
MOVE,
|
||||
REORIENT,
|
||||
REMOVE,
|
||||
RESEAT,
|
||||
REFUSE,
|
||||
NONE
|
||||
}
|
||||
|
||||
record Box(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
|
||||
static Box of(JigsawStudioBounds bounds, JigsawStudioCellDimensions dimensions) {
|
||||
return new Box(
|
||||
bounds.originX(),
|
||||
bounds.originY(),
|
||||
bounds.originZ(),
|
||||
bounds.originX() + dimensions.width() - 1,
|
||||
bounds.originY() + dimensions.height() - 1,
|
||||
bounds.originZ() + dimensions.depth() - 1);
|
||||
}
|
||||
|
||||
boolean contains(int x, int y, int z) {
|
||||
return x >= minX && x <= maxX && y >= minY && y <= maxY && z >= minZ && z <= maxZ;
|
||||
}
|
||||
|
||||
LocalPosition local(Target target) {
|
||||
return new LocalPosition(target.x() - minX, target.y() - minY, target.z() - minZ);
|
||||
}
|
||||
|
||||
Cell world(LocalPosition position) {
|
||||
return new Cell(minX + position.x(), minY + position.y(), minZ + position.z());
|
||||
}
|
||||
}
|
||||
|
||||
record Ray(double originX, double originY, double originZ, double directionX, double directionY, double directionZ) {
|
||||
Ray {
|
||||
double length = Math.sqrt(directionX * directionX + directionY * directionY + directionZ * directionZ);
|
||||
if (!(length > 1.0E-9D)) {
|
||||
throw new IllegalArgumentException("Jigsaw connector ray direction cannot be zero");
|
||||
}
|
||||
directionX /= length;
|
||||
directionY /= length;
|
||||
directionZ /= length;
|
||||
}
|
||||
}
|
||||
|
||||
record BlockClick(int x, int y, int z, IrisDirection face) {
|
||||
BlockClick {
|
||||
Objects.requireNonNull(face, "Jigsaw connector clicked face");
|
||||
}
|
||||
}
|
||||
|
||||
record Cell(int x, int y, int z) {
|
||||
}
|
||||
|
||||
record Target(int x, int y, int z, IrisDirection facing) {
|
||||
Target {
|
||||
Objects.requireNonNull(facing, "Jigsaw connector target facing");
|
||||
}
|
||||
}
|
||||
|
||||
record Decision(Operation operation, LocalPosition position, JigsawPlanarDirection side, String message) {
|
||||
Decision {
|
||||
Objects.requireNonNull(operation, "Jigsaw connector decision operation");
|
||||
message = message == null ? "" : message;
|
||||
}
|
||||
|
||||
static Decision of(Operation operation, LocalPosition position) {
|
||||
return new Decision(operation, position, null, "");
|
||||
}
|
||||
|
||||
static Decision message(Operation operation, String message) {
|
||||
return new Decision(operation, null, null, message);
|
||||
}
|
||||
}
|
||||
|
||||
record Frame(JigsawStudioCellDimensions sourceDimensions, int quarterTurns) {
|
||||
Frame {
|
||||
Objects.requireNonNull(sourceDimensions, "Jigsaw connector frame dimensions");
|
||||
quarterTurns = Math.floorMod(quarterTurns, 4);
|
||||
}
|
||||
|
||||
JigsawStudioCellDimensions displayDimensions() {
|
||||
return (quarterTurns & 1) == 0
|
||||
? sourceDimensions
|
||||
: new JigsawStudioCellDimensions(
|
||||
sourceDimensions.depth(),
|
||||
sourceDimensions.height(),
|
||||
sourceDimensions.width());
|
||||
}
|
||||
|
||||
LocalPosition display(IrisPosition source) {
|
||||
return JigsawStudioCapture.forwardPosition(
|
||||
source.getX(),
|
||||
source.getY(),
|
||||
source.getZ(),
|
||||
sourceDimensions.width(),
|
||||
sourceDimensions.depth(),
|
||||
quarterTurns);
|
||||
}
|
||||
|
||||
IrisPosition source(LocalPosition display) {
|
||||
LocalPosition position = JigsawStudioCapture.inversePosition(
|
||||
display.x(),
|
||||
display.y(),
|
||||
display.z(),
|
||||
sourceDimensions.width(),
|
||||
sourceDimensions.depth(),
|
||||
quarterTurns);
|
||||
return new IrisPosition(position.x(), position.y(), position.z());
|
||||
}
|
||||
|
||||
IrisDirection sourceDirection(IrisDirection display) {
|
||||
return quarterTurns == 0 ? display : IrisObjectRotation.of(0, 90.0D * quarterTurns, 0).rotate(display);
|
||||
}
|
||||
|
||||
IrisDirection displayDirection(IrisDirection source) {
|
||||
return quarterTurns == 0 ? source : IrisObjectRotation.of(0, -90.0D * quarterTurns, 0).rotate(source);
|
||||
}
|
||||
}
|
||||
|
||||
record Identity(String pool, String name, String targetName, JigsawJoint joint) {
|
||||
Identity {
|
||||
Objects.requireNonNull(pool, "Jigsaw connector default pool");
|
||||
Objects.requireNonNull(name, "Jigsaw connector default name");
|
||||
Objects.requireNonNull(targetName, "Jigsaw connector default target");
|
||||
Objects.requireNonNull(joint, "Jigsaw connector default joint");
|
||||
}
|
||||
}
|
||||
|
||||
record CellContent(String state, boolean blockEntity) {
|
||||
CellContent {
|
||||
Objects.requireNonNull(state, "Jigsaw connector cell state");
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
interface CellContents {
|
||||
Optional<CellContent> at(IrisPosition source);
|
||||
}
|
||||
|
||||
record ReseatResult(List<IrisJigsawConnector> connectors, boolean changed, int removedStrays) {
|
||||
ReseatResult {
|
||||
connectors = List.copyOf(connectors);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,842 @@
|
||||
package art.arcane.iris.studio.jigsaw;
|
||||
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.pack.value.IrisDirection;
|
||||
import art.arcane.iris.pack.value.IrisPosition;
|
||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.structure.authoring.StructureTransactionWriter;
|
||||
import art.arcane.iris.structure.authoring.StructureWriteOptions;
|
||||
import art.arcane.iris.structure.authoring.StructureWriteResult;
|
||||
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.JigsawJoint;
|
||||
import art.arcane.iris.structure.object.IrisObject;
|
||||
import art.arcane.iris.studio.generation.JigsawStudioGenerator;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioCapture.LocalPosition;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioConnectorPlacement.BlockClick;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioConnectorPlacement.Box;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioConnectorPlacement.Cell;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioConnectorPlacement.CellContent;
|
||||
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 art.arcane.iris.studio.jigsaw.JigsawStudioMaterializer.ConnectorEditReservation;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioService.ActiveStudio;
|
||||
import art.arcane.iris.world.task.J;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.volmlib.util.hud.HudPriority;
|
||||
import art.arcane.volmlib.util.hud.HudSegment;
|
||||
import art.arcane.volmlib.util.hud.HudSlot;
|
||||
import art.arcane.volmlib.util.plugin.ComponentMessenger;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.BlockFace;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.util.RayTraceResult;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static art.arcane.iris.studio.jigsaw.JigsawStudioGraphMutations.loadMappedLayout;
|
||||
import static art.arcane.iris.studio.jigsaw.JigsawStudioProtection.authorizeOwner;
|
||||
import static art.arcane.iris.studio.jigsaw.JigsawStudioService.AUTOSAVE_RETRY_TICKS;
|
||||
import static art.arcane.iris.studio.jigsaw.JigsawStudioService.chunkKey;
|
||||
import static art.arcane.iris.studio.jigsaw.JigsawStudioService.failureMessage;
|
||||
import static art.arcane.iris.studio.jigsaw.JigsawStudioService.message;
|
||||
import static art.arcane.iris.studio.jigsaw.JigsawStudioService.writeFailure;
|
||||
|
||||
final class JigsawStudioConnectorTool {
|
||||
private static final int MAX_QUEUED_ATTEMPTS = 48;
|
||||
private static final double HOVER_REACH = 5.0D;
|
||||
private static final String HUD_PURPOSE = "iris:jigsaw-connector";
|
||||
private static final long HUD_MILLIS = 4000L;
|
||||
|
||||
private final JigsawStudioService service;
|
||||
final Map<UUID, Selection> selections = new ConcurrentHashMap<>();
|
||||
final Map<UUID, PendingClick> pendingClicks = new ConcurrentHashMap<>();
|
||||
|
||||
JigsawStudioConnectorTool(JigsawStudioService service) {
|
||||
this.service = Objects.requireNonNull(service, "Jigsaw Studio service");
|
||||
}
|
||||
|
||||
boolean handleClick(
|
||||
Player player,
|
||||
JigsawStudioToolPayload payload,
|
||||
boolean primary,
|
||||
Block clicked,
|
||||
BlockFace face
|
||||
) {
|
||||
if (player == null || payload == null) {
|
||||
return false;
|
||||
}
|
||||
if (!J.isOwnedByCurrentRegion(player)) {
|
||||
return J.runEntity(player, () -> handleClick(player, payload, primary, clicked, face));
|
||||
}
|
||||
ActiveStudio studio = service.studios.get(player.getWorld().getUID());
|
||||
if (studio == null || !studio.generator().getRequest().requestId().equals(payload.requestId())) {
|
||||
actionBar(player, ChatColor.RED + "This Connector Tool belongs to a different or closed Jigsaw Studio.");
|
||||
return false;
|
||||
}
|
||||
if (!authorizeOwner(player, studio)) {
|
||||
return false;
|
||||
}
|
||||
if (!primary && clicked != null && clicked.getType() == Material.JIGSAW) {
|
||||
player.closeInventory();
|
||||
}
|
||||
Resolution resolution = resolve(player, studio, primary, blockClick(clicked, face));
|
||||
if (resolution.click().isEmpty()) {
|
||||
actionBar(player, ChatColor.YELLOW + resolution.failure());
|
||||
return false;
|
||||
}
|
||||
process(player, studio, resolution.click().get(), 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
Optional<Feedback> feedback(Player player, ActiveStudio studio, boolean holdingTool) {
|
||||
Optional<Cell> selected = selectedCell(player, studio);
|
||||
Optional<Hover> hover = Optional.empty();
|
||||
if (holdingTool) {
|
||||
RayTraceResult trace = player.rayTraceBlocks(HOVER_REACH);
|
||||
Optional<BlockClick> click = trace == null
|
||||
? Optional.empty()
|
||||
: blockClick(trace.getHitBlock(), trace.getHitBlockFace());
|
||||
Resolution resolution = resolve(player, studio, false, click);
|
||||
if (resolution.click().isPresent() && resolution.box() != null) {
|
||||
ToolClick toolClick = resolution.click().get();
|
||||
hover = Optional.of(new Hover(
|
||||
resolution.box().world(toolClick.target()),
|
||||
toolClick.facing(),
|
||||
resolution.existingConnector()));
|
||||
}
|
||||
}
|
||||
if (selected.isEmpty() && hover.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(new Feedback(selected, hover));
|
||||
}
|
||||
|
||||
void clearPlayer(UUID playerId) {
|
||||
selections.remove(playerId);
|
||||
pendingClicks.remove(playerId);
|
||||
}
|
||||
|
||||
private Optional<Cell> selectedCell(Player player, ActiveStudio studio) {
|
||||
Selection selection = selections.get(player.getUniqueId());
|
||||
if (selection == null || !selection.requestId().equals(studio.generator().getRequest().requestId())) {
|
||||
return Optional.empty();
|
||||
}
|
||||
JigsawStudioSession session = studio.generator().getSession();
|
||||
JigsawStudioBay workcell = session.layout().get(selection.workcellId());
|
||||
if (workcell == null || session.activeVariant(workcell.stableId())
|
||||
.map(JigsawStudioVariant::pieceKey)
|
||||
.filter(selection.pieceKey()::equals)
|
||||
.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
JigsawStudioGenerator.RenderedBay rendered = studio.generator().renderBay(workcell);
|
||||
if (!rendered.valid() || !displayedConnectors(rendered).containsKey(selection.position())) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(Box.of(workcell.bounds(), rendered.dimensions()).world(selection.position()));
|
||||
}
|
||||
|
||||
private Resolution resolve(
|
||||
Player player,
|
||||
ActiveStudio studio,
|
||||
boolean primary,
|
||||
Optional<BlockClick> click
|
||||
) {
|
||||
JigsawStudioSession session = studio.generator().getSession();
|
||||
JigsawStudioLayout layout = session.layout();
|
||||
Location eye = player.getEyeLocation();
|
||||
Vector direction = eye.getDirection();
|
||||
if (direction.lengthSquared() < 1.0E-9D) {
|
||||
return Resolution.failed("Look at a workcell to use the Connector Tool.");
|
||||
}
|
||||
Ray ray = new Ray(eye.getX(), eye.getY(), eye.getZ(), direction.getX(), direction.getY(), direction.getZ());
|
||||
JigsawStudioBay workcell = targetWorkcell(layout, player.getLocation(), ray, click);
|
||||
if (workcell == null) {
|
||||
return Resolution.failed("Point the Connector Tool at a workcell.");
|
||||
}
|
||||
JigsawStudioVariant variant = session.activeVariant(workcell.stableId()).orElse(null);
|
||||
if (variant == null) {
|
||||
return Resolution.failed(workcell.displayName() + " has no loaded variant.");
|
||||
}
|
||||
if (!variant.owned()) {
|
||||
return Resolution.failed("Variant '" + variant.pieceKey() + "' is read-only.");
|
||||
}
|
||||
JigsawStudioGenerator.RenderedBay rendered = studio.generator().renderBay(workcell);
|
||||
if (!rendered.valid()) {
|
||||
return Resolution.failed(workcell.displayName() + " cannot be edited: " + rendered.failure());
|
||||
}
|
||||
Box box = Box.of(workcell.bounds(), rendered.dimensions());
|
||||
Map<LocalPosition, IrisDirection> connectors = displayedConnectors(rendered);
|
||||
Set<Cell> cells = new HashSet<>(connectors.size());
|
||||
for (LocalPosition position : connectors.keySet()) {
|
||||
cells.add(box.world(position));
|
||||
}
|
||||
Optional<Target> target = JigsawStudioConnectorPlacement.target(box, ray, click, cells, player.isSneaking());
|
||||
if (target.isEmpty()) {
|
||||
return Resolution.failed("Point at a block or wall inside the "
|
||||
+ JigsawStudioObjectResizer.describeDimensions(rendered.dimensions()) + " variant.");
|
||||
}
|
||||
LocalPosition position = box.local(target.get());
|
||||
IrisDirection top = target.get().facing().isVertical()
|
||||
? IrisDirection.getDirection(player.getFacing())
|
||||
: IrisDirection.UP_POSITIVE_Y;
|
||||
ToolClick toolClick = new ToolClick(
|
||||
studio.generator().getRequest().requestId(),
|
||||
workcell.stableId(),
|
||||
variant.pieceKey(),
|
||||
session.workcellSnapshot(workcell.stableId()).loadGeneration(),
|
||||
primary,
|
||||
position,
|
||||
target.get().facing(),
|
||||
top);
|
||||
return new Resolution(Optional.of(toolClick), box, connectors.containsKey(position), "");
|
||||
}
|
||||
|
||||
private void process(Player player, ActiveStudio studio, ToolClick click, int attempt) {
|
||||
if (!service.isCurrentRequest(studio, click.requestId())) {
|
||||
actionBar(player, ChatColor.RED + "This Jigsaw Studio session is no longer active.");
|
||||
return;
|
||||
}
|
||||
JigsawStudioSession session = studio.generator().getSession();
|
||||
JigsawStudioBay workcell = session.layout().get(click.workcellId());
|
||||
JigsawStudioVariant variant = workcell == null ? null : session.activeVariant(workcell.stableId()).orElse(null);
|
||||
if (variant == null
|
||||
|| !variant.pieceKey().equals(click.pieceKey())
|
||||
|| session.workcellSnapshot(workcell.stableId()).loadGeneration() != click.loadGeneration()) {
|
||||
actionBar(player, ChatColor.YELLOW + "The workcell changed before the connector edit ran; click again.");
|
||||
return;
|
||||
}
|
||||
JigsawStudioSession.WorkcellSnapshot snapshot = session.workcellSnapshot(workcell.stableId());
|
||||
if (snapshot.dirty() || snapshot.saveInProgress() || snapshot.switchInProgress()) {
|
||||
queue(player, studio, click, attempt, "Saving your latest edits before changing connectors...");
|
||||
return;
|
||||
}
|
||||
JigsawStudioSaveLifecycle.BayReadiness readiness = studio.population(workcell).readiness();
|
||||
if (!readiness.ready()) {
|
||||
if (!readiness.failure().isEmpty()) {
|
||||
actionBar(player, ChatColor.RED + workcell.displayName() + " is not editable: " + readiness.failure());
|
||||
return;
|
||||
}
|
||||
queue(player, studio, click, attempt, "Waiting for " + workcell.displayName() + " to finish loading...");
|
||||
return;
|
||||
}
|
||||
JigsawStudioGenerator.RenderedBay rendered = studio.generator().renderBay(workcell);
|
||||
if (!rendered.valid()) {
|
||||
actionBar(player, ChatColor.RED + workcell.displayName() + " cannot be edited: " + rendered.failure());
|
||||
return;
|
||||
}
|
||||
Map<LocalPosition, IrisDirection> connectors = displayedConnectors(rendered);
|
||||
Optional<LocalPosition> selection = currentSelection(player, click);
|
||||
Decision decision = workcell.topology().isPresent()
|
||||
? JigsawStudioConnectorPlacement.decidePlanar(
|
||||
click.primary(),
|
||||
click.target(),
|
||||
click.facing(),
|
||||
rendered.dimensions(),
|
||||
workcell.topology().get(),
|
||||
connectors,
|
||||
workcell.displayName())
|
||||
: JigsawStudioConnectorPlacement.decideSpatial(
|
||||
click.primary(),
|
||||
click.target(),
|
||||
click.facing(),
|
||||
connectors,
|
||||
selection);
|
||||
switch (decision.operation()) {
|
||||
case SELECT -> {
|
||||
selections.put(player.getUniqueId(), new Selection(
|
||||
click.requestId(), click.workcellId(), click.pieceKey(), decision.position()));
|
||||
actionBar(player, ChatColor.AQUA + "Selected the connector at "
|
||||
+ JigsawStudioConnectorPlacement.describe(decision.position()) + " facing "
|
||||
+ directionName(connectors.get(decision.position()))
|
||||
+ ChatColor.GRAY + ". Right-click a new spot to move it, the same block to deselect, or left-click to remove it.");
|
||||
}
|
||||
case DESELECT, CLEAR_SELECTION -> {
|
||||
selections.remove(player.getUniqueId());
|
||||
actionBar(player, ChatColor.GRAY + "Connector deselected.");
|
||||
}
|
||||
case REFUSE -> actionBar(player, ChatColor.RED + decision.message());
|
||||
case NONE -> actionBar(player, ChatColor.GRAY + decision.message());
|
||||
case ADD, MOVE, REORIENT, REMOVE, RESEAT -> write(
|
||||
player,
|
||||
studio,
|
||||
workcell,
|
||||
variant,
|
||||
new Edit(decision, click, selection),
|
||||
attempt);
|
||||
}
|
||||
}
|
||||
|
||||
private void write(
|
||||
Player player,
|
||||
ActiveStudio studio,
|
||||
JigsawStudioBay workcell,
|
||||
JigsawStudioVariant variant,
|
||||
Edit edit,
|
||||
int attempt
|
||||
) {
|
||||
ConnectorEditReservation reservation = service.materializer.beginConnectorEdit(studio, workcell.stableId());
|
||||
if (reservation.status() == JigsawStudioMaterializer.ConnectorEditStatus.BUSY) {
|
||||
queue(player, studio, edit.click(), attempt, reservation.message());
|
||||
return;
|
||||
}
|
||||
if (reservation.status() == JigsawStudioMaterializer.ConnectorEditStatus.BLOCKED) {
|
||||
actionBar(player, ChatColor.RED + reservation.message());
|
||||
return;
|
||||
}
|
||||
UUID requestId = edit.click().requestId();
|
||||
J.a(() -> {
|
||||
EditCommit commit = null;
|
||||
Throwable failure = null;
|
||||
try {
|
||||
commit = commitEdit(studio, workcell, variant, edit);
|
||||
} catch (Throwable exception) {
|
||||
failure = exception;
|
||||
}
|
||||
EditCommit completed = commit;
|
||||
Throwable completedFailure = failure;
|
||||
boolean written = completed != null && completed.changed();
|
||||
boolean scheduled = J.runEntity(
|
||||
player,
|
||||
() -> completeEdit(player, studio, edit, completed, completedFailure),
|
||||
0,
|
||||
() -> abandonEdit(requestId, written, completedFailure));
|
||||
if (!scheduled) {
|
||||
abandonEdit(requestId, written, completedFailure);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private EditCommit commitEdit(
|
||||
ActiveStudio studio,
|
||||
JigsawStudioBay workcell,
|
||||
JigsawStudioVariant variant,
|
||||
Edit edit
|
||||
) throws IOException {
|
||||
JigsawStudioActivation.Request request = studio.generator().getRequest();
|
||||
IrisData source = request.source();
|
||||
Path packRoot = source.getDataFolder().toPath();
|
||||
IrisJigsawPiece piece = source.getJigsawPieceLoader().load(variant.pieceKey(), false);
|
||||
if (piece == null || piece.getConnectors() == null || piece.getObject() == null || piece.getObject().isBlank()) {
|
||||
throw new IOException("Piece '" + variant.pieceKey() + "' is missing or has no object");
|
||||
}
|
||||
IrisObject object = source.getObjectLoader().load(piece.getObject(), false);
|
||||
if (object == null || object.getW() < 1 || object.getH() < 1 || object.getD() < 1) {
|
||||
throw new IOException("Object '" + piece.getObject() + "' is missing or has invalid dimensions");
|
||||
}
|
||||
Frame frame = new Frame(
|
||||
new JigsawStudioCellDimensions(object.getW(), object.getH(), object.getD()),
|
||||
variant.sourceToCanonicalQuarterTurns());
|
||||
JigsawStudioConnectorPlacement.CellContents contents = position -> cellContent(object, position);
|
||||
List<IrisJigsawConnector> before = List.copyOf(piece.getConnectors());
|
||||
LocalPosition target = edit.click().target();
|
||||
IrisDirection facing = edit.click().facing();
|
||||
IrisDirection top = edit.click().top();
|
||||
List<IrisJigsawConnector> after;
|
||||
String summary;
|
||||
Selection selection = null;
|
||||
switch (edit.decision().operation()) {
|
||||
case ADD -> {
|
||||
after = JigsawStudioConnectorPlacement.add(
|
||||
before, frame, target, facing, top, defaultIdentity(studio, variant, before, packRoot), contents);
|
||||
summary = "Placed a connector at " + JigsawStudioConnectorPlacement.describe(target)
|
||||
+ " facing " + directionName(facing) + ".";
|
||||
}
|
||||
case MOVE -> {
|
||||
LocalPosition from = edit.selection().orElseThrow();
|
||||
after = JigsawStudioConnectorPlacement.move(before, frame, from, target, facing, top, contents);
|
||||
summary = "Moved the connector from " + JigsawStudioConnectorPlacement.describe(from)
|
||||
+ " to " + JigsawStudioConnectorPlacement.describe(target)
|
||||
+ " facing " + directionName(facing) + ".";
|
||||
selection = new Selection(
|
||||
edit.click().requestId(), edit.click().workcellId(), edit.click().pieceKey(), target);
|
||||
}
|
||||
case REORIENT -> {
|
||||
after = JigsawStudioConnectorPlacement.move(before, frame, target, target, facing, top, contents);
|
||||
summary = "Turned the connector at " + JigsawStudioConnectorPlacement.describe(target)
|
||||
+ " to face " + directionName(facing) + ".";
|
||||
selection = new Selection(
|
||||
edit.click().requestId(), edit.click().workcellId(), edit.click().pieceKey(), target);
|
||||
}
|
||||
case REMOVE -> {
|
||||
after = JigsawStudioConnectorPlacement.remove(before, frame, target);
|
||||
summary = "Removed the connector at " + JigsawStudioConnectorPlacement.describe(target) + ".";
|
||||
}
|
||||
case RESEAT -> {
|
||||
JigsawPlanarDirection side = Objects.requireNonNull(edit.decision().side(), "Planar socket side");
|
||||
ReseatResult result = JigsawStudioConnectorPlacement.reseat(
|
||||
before, frame, side, defaultIdentity(studio, variant, before, packRoot), contents);
|
||||
if (!result.changed()) {
|
||||
return EditCommit.unchanged("The " + JigsawStudioConnectorPlacement.sideName(side)
|
||||
+ " socket is already seated at its face center.");
|
||||
}
|
||||
after = result.connectors();
|
||||
summary = "Seated the " + JigsawStudioConnectorPlacement.sideName(side) + " socket at its face center"
|
||||
+ (result.removedStrays() == 0 ? "." : " and removed " + result.removedStrays()
|
||||
+ " stray connector" + (result.removedStrays() == 1 ? "." : "s."));
|
||||
}
|
||||
default -> throw new IllegalStateException("Unsupported connector edit " + edit.decision().operation());
|
||||
}
|
||||
JigsawStudioCapture.requireWorkcellTopology(workcell, after, frame.quarterTurns());
|
||||
JigsawStudioResourceBundleAssembler.Assembly assembly = JigsawStudioResourceBundleAssembler.assemble(
|
||||
packRoot,
|
||||
request.structureKey(),
|
||||
variant.pieceKey(),
|
||||
JigsawStudioResourceBundleAssembler.ownedResourceContent(
|
||||
packRoot,
|
||||
"objects/" + piece.getObject() + ".iob"),
|
||||
after,
|
||||
false);
|
||||
StructureResourceBundleGraphCompiler.requireViable(assembly.bundle());
|
||||
JigsawStudioHistoryStore historyStore = new JigsawStudioHistoryStore(packRoot, request.structureKey());
|
||||
JigsawStudioHistoryStore.Snapshot previous = historyStore.snapshotCurrent(variant.pieceKey());
|
||||
if (!previous.matches(assembly.bundle())) {
|
||||
historyStore.append(previous);
|
||||
}
|
||||
StructureWriteResult result;
|
||||
synchronized (service.saveLifecycleLock) {
|
||||
result = new StructureTransactionWriter(packRoot)
|
||||
.write(assembly.bundle(), StructureWriteOptions.overwriteExpected(assembly.expectedManifestHash()));
|
||||
}
|
||||
if (!result.successful()) {
|
||||
throw new IOException(writeFailure(result));
|
||||
}
|
||||
source.invalidateStructureResources();
|
||||
return new EditCommit(
|
||||
true,
|
||||
loadMappedLayout(studio),
|
||||
JigsawStudioConnectorPlacement.changedPositions(before, after, frame),
|
||||
summary,
|
||||
Optional.ofNullable(selection),
|
||||
edit.decision().operation() == Operation.REMOVE ? Optional.of(target) : Optional.empty());
|
||||
}
|
||||
|
||||
private void completeEdit(
|
||||
Player player,
|
||||
ActiveStudio studio,
|
||||
Edit edit,
|
||||
EditCommit commit,
|
||||
Throwable failure
|
||||
) {
|
||||
UUID requestId = edit.click().requestId();
|
||||
if (failure != null) {
|
||||
service.materializer.finishConnectorEdit(requestId);
|
||||
if (!(failure instanceof IllegalArgumentException)) {
|
||||
IrisLogging.reportError("Jigsaw Studio connector edit failed in workcell "
|
||||
+ edit.click().workcellId(), failure);
|
||||
}
|
||||
message(player, "Connector edit failed: " + failureMessage(failure));
|
||||
actionBar(player, ChatColor.RED + "Connector edit failed: " + failureMessage(failure));
|
||||
return;
|
||||
}
|
||||
if (!commit.changed()) {
|
||||
service.materializer.finishConnectorEdit(requestId);
|
||||
actionBar(player, ChatColor.GRAY + commit.summary());
|
||||
return;
|
||||
}
|
||||
if (!service.isCurrentRequest(studio, requestId)) {
|
||||
service.materializer.finishConnectorEdit(requestId);
|
||||
message(player, "The connector edit saved after this Studio session changed; reopen it to continue.");
|
||||
return;
|
||||
}
|
||||
JigsawStudioSession session = studio.generator().getSession();
|
||||
JigsawStudioBay workcell;
|
||||
JigsawStudioGenerator.RenderedBay rendered;
|
||||
try {
|
||||
session.replaceLayout(commit.layout());
|
||||
for (JigsawStudioBay bay : commit.layout().bays()) {
|
||||
studio.generator().invalidateRender(bay.stableId());
|
||||
}
|
||||
workcell = session.layout().get(edit.click().workcellId());
|
||||
if (workcell == null) {
|
||||
throw new IOException("workcell '" + edit.click().workcellId() + "' left the layout");
|
||||
}
|
||||
rendered = studio.generator().renderBay(workcell);
|
||||
if (!rendered.valid()) {
|
||||
throw new IOException(rendered.failure());
|
||||
}
|
||||
} catch (Throwable exception) {
|
||||
service.saveLifecycle.reopenRequiredRequests.add(requestId);
|
||||
service.materializer.finishConnectorEdit(requestId);
|
||||
IrisLogging.reportError("Jigsaw Studio saved a connector edit but could not refresh workcell "
|
||||
+ edit.click().workcellId(), exception);
|
||||
message(player, "The connector edit saved, but Studio could not refresh: "
|
||||
+ failureMessage(exception) + ". Reopen this project before editing further.");
|
||||
return;
|
||||
}
|
||||
restoreWorld(player, studio, workcell, rendered, commit,
|
||||
session.workcellSnapshot(workcell.stableId()).connectorsVisible());
|
||||
}
|
||||
|
||||
private void restoreWorld(
|
||||
Player player,
|
||||
ActiveStudio studio,
|
||||
JigsawStudioBay workcell,
|
||||
JigsawStudioGenerator.RenderedBay rendered,
|
||||
EditCommit commit,
|
||||
boolean connectorsVisible
|
||||
) {
|
||||
Map<LocalPosition, JigsawStudioGenerator.RenderedConnector> connectors = new HashMap<>();
|
||||
for (JigsawStudioGenerator.RenderedConnector connector : rendered.connectors()) {
|
||||
connectors.put(new LocalPosition(connector.x(), connector.y(), connector.z()), connector);
|
||||
}
|
||||
Map<LocalPosition, JigsawStudioGenerator.RenderedBlock> blocks = new HashMap<>();
|
||||
for (JigsawStudioGenerator.RenderedBlock block : rendered.blocks()) {
|
||||
blocks.put(new LocalPosition(block.x(), block.y(), block.z()), block);
|
||||
}
|
||||
JigsawStudioBounds bounds = workcell.bounds();
|
||||
Map<Long, Set<LocalPosition>> positionsByChunk = new LinkedHashMap<>();
|
||||
for (LocalPosition position : commit.changedPositions()) {
|
||||
positionsByChunk.computeIfAbsent(
|
||||
chunkKey((bounds.originX() + position.x()) >> 4, (bounds.originZ() + position.z()) >> 4),
|
||||
ignored -> new HashSet<>()).add(position);
|
||||
}
|
||||
if (positionsByChunk.isEmpty()) {
|
||||
finishEdit(player, studio, workcell.stableId(), commit, "", null);
|
||||
return;
|
||||
}
|
||||
AtomicInteger remaining = new AtomicInteger(positionsByChunk.size());
|
||||
AtomicReference<String> failure = new AtomicReference<>("");
|
||||
AtomicReference<Throwable> cause = new AtomicReference<>();
|
||||
for (Map.Entry<Long, Set<LocalPosition>> entry : positionsByChunk.entrySet()) {
|
||||
int chunkX = (int) (entry.getKey() >> 32);
|
||||
int chunkZ = (int) entry.getKey().longValue();
|
||||
boolean scheduled = J.runRegion(studio.world(), chunkX, chunkZ, () -> {
|
||||
try {
|
||||
if (!studio.world().isChunkLoaded(chunkX, chunkZ)) {
|
||||
throw new IOException("workcell chunk " + chunkX + "," + chunkZ + " is not loaded");
|
||||
}
|
||||
JigsawStudioChunkWriter.restoreChunkPositions(
|
||||
studio.world(), workcell, entry.getValue(), connectors, blocks, connectorsVisible);
|
||||
} catch (Throwable exception) {
|
||||
failure.compareAndSet("", failureMessage(exception));
|
||||
cause.compareAndSet(null, exception);
|
||||
}
|
||||
if (remaining.decrementAndGet() == 0) {
|
||||
scheduleFinish(player, studio, workcell.stableId(), commit, failure.get(), cause.get());
|
||||
}
|
||||
});
|
||||
if (!scheduled) {
|
||||
failure.compareAndSet("", "workcell chunk " + chunkX + "," + chunkZ
|
||||
+ " could not be scheduled on its owning region");
|
||||
if (remaining.decrementAndGet() == 0) {
|
||||
scheduleFinish(player, studio, workcell.stableId(), commit, failure.get(), cause.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void scheduleFinish(
|
||||
Player player,
|
||||
ActiveStudio studio,
|
||||
String workcellId,
|
||||
EditCommit commit,
|
||||
String failure,
|
||||
Throwable cause
|
||||
) {
|
||||
UUID requestId = studio.generator().getRequest().requestId();
|
||||
boolean scheduled = J.runEntity(
|
||||
player,
|
||||
() -> finishEdit(player, studio, workcellId, commit, failure, cause),
|
||||
0,
|
||||
() -> {
|
||||
service.materializer.finishConnectorEdit(requestId);
|
||||
service.floorPainter.refresh(studio, workcellId);
|
||||
});
|
||||
if (!scheduled) {
|
||||
service.materializer.finishConnectorEdit(requestId);
|
||||
service.floorPainter.refresh(studio, workcellId);
|
||||
}
|
||||
}
|
||||
|
||||
private void finishEdit(
|
||||
Player player,
|
||||
ActiveStudio studio,
|
||||
String workcellId,
|
||||
EditCommit commit,
|
||||
String failure,
|
||||
Throwable cause
|
||||
) {
|
||||
service.materializer.finishConnectorEdit(studio.generator().getRequest().requestId());
|
||||
commit.selection().ifPresentOrElse(
|
||||
selection -> selections.put(player.getUniqueId(), selection),
|
||||
() -> commit.removed().ifPresent(removed -> selections.computeIfPresent(
|
||||
player.getUniqueId(),
|
||||
(ignored, current) -> current.workcellId().equals(workcellId)
|
||||
&& current.position().equals(removed) ? null : current)));
|
||||
service.floorPainter.refresh(studio, workcellId);
|
||||
service.playerContext.refreshWorkcellContext(studio.worldId(), workcellId);
|
||||
service.evaluator.scheduleEvaluation(studio);
|
||||
if (!failure.isEmpty()) {
|
||||
if (cause != null) {
|
||||
IrisLogging.reportError("Jigsaw Studio could not update connector blocks in workcell " + workcellId, cause);
|
||||
}
|
||||
message(player, commit.summary() + " The workcell blocks could not update: " + failure
|
||||
+ ". Visit the whole workcell and use Reset Connector Blocks.");
|
||||
return;
|
||||
}
|
||||
actionBar(player, ChatColor.GREEN + commit.summary());
|
||||
player.playSound(player.getLocation(), "minecraft:block.note_block.hat", 0.6F, 1.4F);
|
||||
}
|
||||
|
||||
private void abandonEdit(UUID requestId, boolean written, Throwable failure) {
|
||||
if (written) {
|
||||
service.saveLifecycle.reopenRequiredRequests.add(requestId);
|
||||
}
|
||||
service.materializer.finishConnectorEdit(requestId);
|
||||
if (failure != null && !(failure instanceof IllegalArgumentException)) {
|
||||
IrisLogging.reportError("Jigsaw Studio connector edit failed after its player left", failure);
|
||||
}
|
||||
}
|
||||
|
||||
private void queue(Player player, ActiveStudio studio, ToolClick click, int attempt, String reason) {
|
||||
int nextAttempt = attempt + 1;
|
||||
if (nextAttempt > MAX_QUEUED_ATTEMPTS) {
|
||||
pendingClicks.remove(player.getUniqueId());
|
||||
actionBar(player, ChatColor.RED + "The connector edit was cancelled: " + reason);
|
||||
return;
|
||||
}
|
||||
PendingClick pending = new PendingClick(click, nextAttempt);
|
||||
PendingClick previous = pendingClicks.put(player.getUniqueId(), pending);
|
||||
service.autosaveScheduler.expediteAutosaves(click.requestId());
|
||||
if (previous == null || attempt == 0) {
|
||||
actionBar(player, ChatColor.GRAY + reason);
|
||||
}
|
||||
boolean scheduled = J.runEntity(
|
||||
player,
|
||||
() -> resumePending(player, studio, pending),
|
||||
AUTOSAVE_RETRY_TICKS,
|
||||
() -> pendingClicks.remove(player.getUniqueId(), pending));
|
||||
if (!scheduled) {
|
||||
pendingClicks.remove(player.getUniqueId(), pending);
|
||||
}
|
||||
}
|
||||
|
||||
private void resumePending(Player player, ActiveStudio studio, PendingClick pending) {
|
||||
if (!pendingClicks.remove(player.getUniqueId(), pending)) {
|
||||
return;
|
||||
}
|
||||
process(player, studio, pending.click(), pending.attempt());
|
||||
}
|
||||
|
||||
private Optional<LocalPosition> currentSelection(Player player, ToolClick click) {
|
||||
Selection selection = selections.get(player.getUniqueId());
|
||||
if (selection == null
|
||||
|| !selection.requestId().equals(click.requestId())
|
||||
|| !selection.workcellId().equals(click.workcellId())
|
||||
|| !selection.pieceKey().equals(click.pieceKey())) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(selection.position());
|
||||
}
|
||||
|
||||
private Identity defaultIdentity(
|
||||
ActiveStudio studio,
|
||||
JigsawStudioVariant variant,
|
||||
List<IrisJigsawConnector> existing,
|
||||
Path packRoot
|
||||
) {
|
||||
JigsawStudioActivation.Request request = studio.generator().getRequest();
|
||||
boolean planar = variant.mode() == JigsawStudioMode.PLANAR_JIGSAW;
|
||||
String generatedPool = request.structureKey() + "/pieces";
|
||||
String pool;
|
||||
if (Files.isRegularFile(packRoot.resolve("jigsaw-pools/" + generatedPool + ".json"))) {
|
||||
pool = generatedPool;
|
||||
} else if (!existing.isEmpty() && existing.getFirst().getPool() != null
|
||||
&& !existing.getFirst().getPool().isBlank()) {
|
||||
pool = existing.getFirst().getPool();
|
||||
} else if (!variant.memberships().isEmpty()) {
|
||||
pool = variant.memberships().getFirst().poolKey();
|
||||
} else {
|
||||
throw new IllegalArgumentException("Variant '" + variant.pieceKey()
|
||||
+ "' has no pool for a new connector; add it to a pool first.");
|
||||
}
|
||||
return new Identity(
|
||||
pool,
|
||||
planar ? "iris:planar" : "iris:spatial",
|
||||
planar ? "iris:planar" : "iris:spatial",
|
||||
planar ? JigsawJoint.ALIGNED : JigsawJoint.ROLLABLE);
|
||||
}
|
||||
|
||||
private static Optional<CellContent> cellContent(IrisObject object, IrisPosition position) {
|
||||
NativeBlockState state = object.getBlocks().get(object.getSigned(position.getX(), position.getY(), position.getZ()));
|
||||
if (state == null || state.isAir()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
boolean blockEntity = object.getStates().get(object.getSigned(position.getX(), position.getY(), position.getZ())) != null;
|
||||
return Optional.of(new CellContent(state.key(), blockEntity));
|
||||
}
|
||||
|
||||
private static JigsawStudioBay targetWorkcell(
|
||||
JigsawStudioLayout layout,
|
||||
Location feet,
|
||||
Ray ray,
|
||||
Optional<BlockClick> click
|
||||
) {
|
||||
if (click.isPresent()) {
|
||||
BlockClick value = click.get();
|
||||
JigsawStudioBay clicked = layout.findAt(value.x(), value.y(), value.z());
|
||||
if (clicked != null) {
|
||||
return clicked;
|
||||
}
|
||||
JigsawStudioBay adjacent = layout.findAt(
|
||||
value.x() + value.face().x(),
|
||||
value.y() + value.face().y(),
|
||||
value.z() + value.face().z());
|
||||
if (adjacent != null) {
|
||||
return adjacent;
|
||||
}
|
||||
}
|
||||
JigsawStudioBay standing = layout.findColumn(feet.getBlockX(), feet.getBlockY(), feet.getBlockZ());
|
||||
if (standing != null) {
|
||||
return standing;
|
||||
}
|
||||
JigsawStudioBay nearest = null;
|
||||
double nearestDistance = JigsawStudioConnectorPlacement.MAX_REACH;
|
||||
for (JigsawStudioBay bay : layout.bays()) {
|
||||
JigsawStudioBounds bounds = bay.bounds();
|
||||
double distance = JigsawStudioConnectorPlacement.entryDistance(ray, Box.of(bounds, bounds.dimensions()));
|
||||
if (distance < nearestDistance) {
|
||||
nearest = bay;
|
||||
nearestDistance = distance;
|
||||
}
|
||||
}
|
||||
return nearest;
|
||||
}
|
||||
|
||||
private static Optional<BlockClick> blockClick(Block clicked, BlockFace face) {
|
||||
if (clicked == null || face == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return switch (face) {
|
||||
case NORTH, SOUTH, EAST, WEST, UP, DOWN -> Optional.of(new BlockClick(
|
||||
clicked.getX(), clicked.getY(), clicked.getZ(), IrisDirection.getDirection(face)));
|
||||
default -> Optional.empty();
|
||||
};
|
||||
}
|
||||
|
||||
private static Map<LocalPosition, IrisDirection> displayedConnectors(JigsawStudioGenerator.RenderedBay rendered) {
|
||||
Map<LocalPosition, IrisDirection> connectors = new LinkedHashMap<>(rendered.connectors().size());
|
||||
for (JigsawStudioGenerator.RenderedConnector connector : rendered.connectors()) {
|
||||
connectors.put(new LocalPosition(connector.x(), connector.y(), connector.z()),
|
||||
connector.connector().getDirection());
|
||||
}
|
||||
return connectors;
|
||||
}
|
||||
|
||||
static String directionName(IrisDirection direction) {
|
||||
if (direction == null) {
|
||||
return "nowhere";
|
||||
}
|
||||
return switch (direction) {
|
||||
case UP_POSITIVE_Y -> "up";
|
||||
case DOWN_NEGATIVE_Y -> "down";
|
||||
case NORTH_NEGATIVE_Z -> "north";
|
||||
case SOUTH_POSITIVE_Z -> "south";
|
||||
case EAST_POSITIVE_X -> "east";
|
||||
case WEST_NEGATIVE_X -> "west";
|
||||
};
|
||||
}
|
||||
|
||||
private static void actionBar(Player player, String text) {
|
||||
if (BukkitPlatform.hasHud()) {
|
||||
BukkitPlatform.hudBar().publish(player, new HudSegment(
|
||||
HUD_PURPOSE,
|
||||
HudPriority.INTERACTIVE,
|
||||
HUD_MILLIS,
|
||||
List.of(HudSlot.CENTER),
|
||||
text));
|
||||
return;
|
||||
}
|
||||
ComponentMessenger.sendActionBarLegacy(player, text);
|
||||
}
|
||||
|
||||
record Selection(UUID requestId, String workcellId, String pieceKey, LocalPosition position) {
|
||||
Selection {
|
||||
Objects.requireNonNull(requestId, "Jigsaw connector selection request");
|
||||
Objects.requireNonNull(workcellId, "Jigsaw connector selection workcell");
|
||||
Objects.requireNonNull(pieceKey, "Jigsaw connector selection piece");
|
||||
Objects.requireNonNull(position, "Jigsaw connector selection position");
|
||||
}
|
||||
}
|
||||
|
||||
record PendingClick(ToolClick click, int attempt) {
|
||||
PendingClick {
|
||||
Objects.requireNonNull(click, "Jigsaw connector pending click");
|
||||
}
|
||||
}
|
||||
|
||||
record ToolClick(
|
||||
UUID requestId,
|
||||
String workcellId,
|
||||
String pieceKey,
|
||||
long loadGeneration,
|
||||
boolean primary,
|
||||
LocalPosition target,
|
||||
IrisDirection facing,
|
||||
IrisDirection top
|
||||
) {
|
||||
ToolClick {
|
||||
Objects.requireNonNull(requestId, "Jigsaw connector click request");
|
||||
Objects.requireNonNull(workcellId, "Jigsaw connector click workcell");
|
||||
Objects.requireNonNull(pieceKey, "Jigsaw connector click piece");
|
||||
Objects.requireNonNull(target, "Jigsaw connector click target");
|
||||
Objects.requireNonNull(facing, "Jigsaw connector click facing");
|
||||
Objects.requireNonNull(top, "Jigsaw connector click top");
|
||||
}
|
||||
}
|
||||
|
||||
record Hover(Cell cell, IrisDirection facing, boolean existingConnector) {
|
||||
}
|
||||
|
||||
record Feedback(Optional<Cell> selected, Optional<Hover> hover) {
|
||||
}
|
||||
|
||||
private record Resolution(Optional<ToolClick> click, Box box, boolean existingConnector, String failure) {
|
||||
private static Resolution failed(String failure) {
|
||||
return new Resolution(Optional.empty(), null, false, failure);
|
||||
}
|
||||
}
|
||||
|
||||
private record Edit(Decision decision, ToolClick click, Optional<LocalPosition> selection) {
|
||||
}
|
||||
|
||||
private record EditCommit(
|
||||
boolean changed,
|
||||
JigsawStudioLayout layout,
|
||||
Set<LocalPosition> changedPositions,
|
||||
String summary,
|
||||
Optional<Selection> selection,
|
||||
Optional<LocalPosition> removed
|
||||
) {
|
||||
private static EditCommit unchanged(String summary) {
|
||||
return new EditCommit(false, null, Set.of(), summary, Optional.empty(), Optional.empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -158,8 +158,11 @@ final class JigsawStudioDetailsMenu {
|
||||
}
|
||||
window.setElement(2, 0, size);
|
||||
|
||||
boolean lastVariant = workcell.variants().size() <= 1;
|
||||
boolean deletionAvailable = variant.owned() && !variant.active() && !lastVariant;
|
||||
boolean spatial = state.mode() == JigsawStudioMode.SPATIAL_JIGSAW;
|
||||
boolean lastVariant = spatial
|
||||
? state.workcells().stream().mapToInt(cell -> cell.variants().size()).sum() <= 1
|
||||
: workcell.variants().size() <= 1;
|
||||
boolean deletionAvailable = state.canDeleteVariant(workcell, variant);
|
||||
boolean confirmed = deletionAvailable && controller.isDeleteConfirmed(
|
||||
window.getViewer().getUniqueId(),
|
||||
state.requestId(),
|
||||
@@ -170,11 +173,11 @@ final class JigsawStudioDetailsMenu {
|
||||
deletionAvailable ? confirmed ? Material.REDSTONE : Material.LAVA_BUCKET : Material.BARRIER,
|
||||
deletionAvailable
|
||||
? confirmed ? ChatColor.RED + "Confirm Delete Variant" : ChatColor.RED + "Delete Variant"
|
||||
: variant.active()
|
||||
? ChatColor.GRAY + "Loaded Variant Cannot Be Deleted"
|
||||
: !variant.owned()
|
||||
? ChatColor.GRAY + "Deletion is Read-only"
|
||||
: lastVariant
|
||||
? ChatColor.GRAY + "Last Variant Cannot Be Deleted"
|
||||
: ChatColor.GRAY + "Deletion is Read-only")
|
||||
: ChatColor.GRAY + "Loaded Variant Cannot Be Deleted")
|
||||
.setEnchanted(confirmed);
|
||||
if (deletionAvailable) {
|
||||
delete.addLore(confirmed
|
||||
@@ -186,10 +189,10 @@ final class JigsawStudioDetailsMenu {
|
||||
workcell.stableId(),
|
||||
variant.pieceKey(),
|
||||
page));
|
||||
} else if (variant.active()) {
|
||||
delete.addLore(ChatColor.GRAY + "Load another variant in this workcell first.");
|
||||
} else if (lastVariant) {
|
||||
} else if (variant.owned() && lastVariant) {
|
||||
delete.addLore(ChatColor.GRAY + "Create another variant before deleting this one.");
|
||||
} else if (variant.owned()) {
|
||||
delete.addLore(ChatColor.GRAY + "Load another variant in this workcell first.");
|
||||
}
|
||||
window.setElement(4, 0, delete);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
package art.arcane.iris.studio.jigsaw;
|
||||
|
||||
import art.arcane.iris.pack.value.IrisDirection;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class JigsawStudioFloorGlyph {
|
||||
private static final JigsawStudioFloorGlyph EMPTY = new JigsawStudioFloorGlyph(0, 0, Map.of());
|
||||
|
||||
private final int width;
|
||||
private final int depth;
|
||||
private final Map<Long, Cell> cells;
|
||||
|
||||
private JigsawStudioFloorGlyph(int width, int depth, Map<Long, Cell> cells) {
|
||||
this.width = width;
|
||||
this.depth = depth;
|
||||
this.cells = cells;
|
||||
}
|
||||
|
||||
public static JigsawStudioFloorGlyph empty() {
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
public static JigsawStudioFloorGlyph of(int width, int depth, List<Marker> markers) {
|
||||
List<Marker> sourceMarkers = List.copyOf(Objects.requireNonNull(markers, "Jigsaw Studio floor markers"));
|
||||
if (width < 1 || depth < 1 || sourceMarkers.isEmpty()) {
|
||||
return EMPTY;
|
||||
}
|
||||
Map<Long, Cell> cells = new HashMap<>();
|
||||
int centerX = width / 2;
|
||||
int centerZ = depth / 2;
|
||||
for (Marker marker : sourceMarkers) {
|
||||
int x = clamp(marker.x(), width);
|
||||
int z = clamp(marker.z(), depth);
|
||||
drawLine(cells, centerX, centerZ, x, z);
|
||||
if (!marker.direction().isVertical()) {
|
||||
drawExtension(cells, x, z, marker.direction(), width, depth);
|
||||
}
|
||||
}
|
||||
for (Marker marker : sourceMarkers) {
|
||||
int x = clamp(marker.x(), width);
|
||||
int z = clamp(marker.z(), depth);
|
||||
Cell socket = switch (marker.direction()) {
|
||||
case UP_POSITIVE_Y -> Cell.UP_SOCKET;
|
||||
case DOWN_NEGATIVE_Y -> Cell.DOWN_SOCKET;
|
||||
case NORTH_NEGATIVE_Z, SOUTH_POSITIVE_Z, EAST_POSITIVE_X, WEST_NEGATIVE_X -> Cell.HORIZONTAL_SOCKET;
|
||||
};
|
||||
cells.merge(key(x, z), socket, JigsawStudioFloorGlyph::merge);
|
||||
}
|
||||
return new JigsawStudioFloorGlyph(width, depth, Map.copyOf(cells));
|
||||
}
|
||||
|
||||
public Cell cell(int localX, int localZ) {
|
||||
if (localX < 0 || localZ < 0 || localX >= width || localZ >= depth) {
|
||||
return Cell.BASE;
|
||||
}
|
||||
return cells.getOrDefault(key(localX, localZ), Cell.BASE);
|
||||
}
|
||||
|
||||
private static void drawLine(Map<Long, Cell> cells, int startX, int startZ, int endX, int endZ) {
|
||||
int deltaX = Math.abs(endX - startX);
|
||||
int deltaZ = -Math.abs(endZ - startZ);
|
||||
int stepX = startX < endX ? 1 : -1;
|
||||
int stepZ = startZ < endZ ? 1 : -1;
|
||||
int error = deltaX + deltaZ;
|
||||
int x = startX;
|
||||
int z = startZ;
|
||||
while (true) {
|
||||
cells.merge(key(x, z), Cell.LINE, JigsawStudioFloorGlyph::merge);
|
||||
if (x == endX && z == endZ) {
|
||||
return;
|
||||
}
|
||||
int doubled = error * 2;
|
||||
if (doubled >= deltaZ) {
|
||||
error += deltaZ;
|
||||
x += stepX;
|
||||
}
|
||||
if (doubled <= deltaX) {
|
||||
error += deltaX;
|
||||
z += stepZ;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void drawExtension(
|
||||
Map<Long, Cell> cells,
|
||||
int startX,
|
||||
int startZ,
|
||||
IrisDirection direction,
|
||||
int width,
|
||||
int depth
|
||||
) {
|
||||
int x = startX + direction.x();
|
||||
int z = startZ + direction.z();
|
||||
while (x >= 0 && z >= 0 && x < width && z < depth) {
|
||||
cells.merge(key(x, z), Cell.LINE, JigsawStudioFloorGlyph::merge);
|
||||
x += direction.x();
|
||||
z += direction.z();
|
||||
}
|
||||
}
|
||||
|
||||
private static Cell merge(Cell current, Cell next) {
|
||||
if (current == Cell.UP_SOCKET && next == Cell.DOWN_SOCKET
|
||||
|| current == Cell.DOWN_SOCKET && next == Cell.UP_SOCKET) {
|
||||
return Cell.UP_AND_DOWN_SOCKET;
|
||||
}
|
||||
return next.priority() > current.priority() ? next : current;
|
||||
}
|
||||
|
||||
private static int clamp(int value, int size) {
|
||||
return Math.max(0, Math.min(size - 1, value));
|
||||
}
|
||||
|
||||
private static long key(int x, int z) {
|
||||
return ((long) x << 32) ^ (z & 0xffffffffL);
|
||||
}
|
||||
|
||||
public enum Cell {
|
||||
BASE(0),
|
||||
LINE(1),
|
||||
HORIZONTAL_SOCKET(2),
|
||||
UP_SOCKET(3),
|
||||
DOWN_SOCKET(3),
|
||||
UP_AND_DOWN_SOCKET(4);
|
||||
|
||||
private final int priority;
|
||||
|
||||
Cell(int priority) {
|
||||
this.priority = priority;
|
||||
}
|
||||
|
||||
int priority() {
|
||||
return priority;
|
||||
}
|
||||
}
|
||||
|
||||
public record Marker(int x, int z, IrisDirection direction) {
|
||||
public Marker {
|
||||
Objects.requireNonNull(direction, "Jigsaw Studio floor marker direction");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package art.arcane.iris.studio.jigsaw;
|
||||
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioCapture.ChunkCaptureArea;
|
||||
import art.arcane.iris.studio.jigsaw.JigsawStudioService.ActiveStudio;
|
||||
import art.arcane.iris.studio.generation.JigsawStudioGenerator;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.world.task.J;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import static art.arcane.iris.studio.jigsaw.JigsawStudioCapture.chunkIntersections;
|
||||
|
||||
final class JigsawStudioFloorPainter {
|
||||
private final JigsawStudioService service;
|
||||
|
||||
JigsawStudioFloorPainter(JigsawStudioService service) {
|
||||
this.service = Objects.requireNonNull(service, "Jigsaw Studio service");
|
||||
}
|
||||
|
||||
void refresh(ActiveStudio studio, String workcellId) {
|
||||
if (studio == null || workcellId == null) {
|
||||
return;
|
||||
}
|
||||
JigsawStudioBay workcell = studio.generator().getLayout().get(workcellId);
|
||||
if (workcell == null) {
|
||||
return;
|
||||
}
|
||||
for (ChunkCaptureArea area : chunkIntersections(workcell.bounds())) {
|
||||
schedule(studio, workcellId, area.chunkX(), area.chunkZ());
|
||||
}
|
||||
}
|
||||
|
||||
void refreshAll(ActiveStudio studio) {
|
||||
if (studio == null) {
|
||||
return;
|
||||
}
|
||||
for (JigsawStudioBay workcell : studio.generator().getLayout().bays()) {
|
||||
refresh(studio, workcell.stableId());
|
||||
}
|
||||
}
|
||||
|
||||
void refreshChunk(ActiveStudio studio, int chunkX, int chunkZ) {
|
||||
if (studio == null) {
|
||||
return;
|
||||
}
|
||||
int chunkMinimumX = chunkX << 4;
|
||||
int chunkMinimumZ = chunkZ << 4;
|
||||
for (JigsawStudioBay workcell : studio.generator().getLayout().bays()) {
|
||||
if (workcell.bounds().intersectsHorizontal(
|
||||
chunkMinimumX,
|
||||
chunkMinimumZ,
|
||||
chunkMinimumX + 15,
|
||||
chunkMinimumZ + 15)) {
|
||||
schedule(studio, workcell.stableId(), chunkX, chunkZ);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static int paintFloorChunk(
|
||||
World world,
|
||||
JigsawStudioGenerator generator,
|
||||
JigsawStudioBay workcell,
|
||||
int chunkX,
|
||||
int chunkZ
|
||||
) {
|
||||
JigsawStudioBounds bounds = workcell.bounds();
|
||||
int minimumX = Math.max(bounds.originX(), chunkX << 4);
|
||||
int maximumX = Math.min(bounds.maxX(), (chunkX << 4) + 15);
|
||||
int minimumZ = Math.max(bounds.originZ(), chunkZ << 4);
|
||||
int maximumZ = Math.min(bounds.maxZ(), (chunkZ << 4) + 15);
|
||||
if (minimumX > maximumX || minimumZ > maximumZ) {
|
||||
return 0;
|
||||
}
|
||||
JigsawStudioFloorGlyph glyph = generator.floorGlyph(workcell);
|
||||
int floorY = bounds.originY() - 1;
|
||||
int changed = 0;
|
||||
for (int worldX = minimumX; worldX <= maximumX; worldX++) {
|
||||
for (int worldZ = minimumZ; worldZ <= maximumZ; worldZ++) {
|
||||
JigsawStudioFloorGlyph.Cell cell = glyph.cell(
|
||||
worldX - bounds.originX(),
|
||||
worldZ - bounds.originZ());
|
||||
NativeBlockState state = generator.floorState(cell);
|
||||
if (!(state.nativeHandle() instanceof BlockData expected)) {
|
||||
throw new IllegalStateException("Jigsaw Studio floor block '" + state.key()
|
||||
+ "' has no Bukkit block-data representation");
|
||||
}
|
||||
Block block = world.getBlockAt(worldX, floorY, worldZ);
|
||||
if (!block.getBlockData().equals(expected)) {
|
||||
block.setBlockData(expected, false);
|
||||
changed++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
private void schedule(ActiveStudio studio, String workcellId, int chunkX, int chunkZ) {
|
||||
J.runRegion(studio.world(), chunkX, chunkZ, () -> paint(studio, workcellId, chunkX, chunkZ));
|
||||
}
|
||||
|
||||
private void paint(ActiveStudio studio, String workcellId, int chunkX, int chunkZ) {
|
||||
if (!service.enabled
|
||||
|| service.studios.get(studio.worldId()) != studio
|
||||
|| !studio.world().isChunkLoaded(chunkX, chunkZ)) {
|
||||
return;
|
||||
}
|
||||
JigsawStudioBay workcell = studio.generator().getLayout().get(workcellId);
|
||||
if (workcell == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
paintFloorChunk(studio.world(), studio.generator(), workcell, chunkX, chunkZ);
|
||||
} catch (Throwable exception) {
|
||||
IrisLogging.reportError("Jigsaw Studio could not refresh the connector floor of workcell "
|
||||
+ workcellId + " in chunk " + chunkX + "," + chunkZ, exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,13 +44,7 @@ public final class JigsawStudioGraphMapper {
|
||||
? JigsawStudioMode.PLANAR_JIGSAW
|
||||
: JigsawStudioMode.SPATIAL_JIGSAW;
|
||||
JigsawStudioVariantCatalog catalog = catalog(data, structure, mode);
|
||||
IrisPosition configuredCell = structure.getCellSize();
|
||||
JigsawStudioCellDimensions dimensions = configuredCell == null
|
||||
? new JigsawStudioCellDimensions(15, 15, 15)
|
||||
: new JigsawStudioCellDimensions(
|
||||
Math.max(1, configuredCell.getX()),
|
||||
Math.max(1, configuredCell.getY()),
|
||||
Math.max(1, configuredCell.getZ()));
|
||||
JigsawStudioCellDimensions dimensions = configuredCellDimensions(structure);
|
||||
if (mode == JigsawStudioMode.SPATIAL_JIGSAW) {
|
||||
dimensions = expandSpatialDimensions(data, catalog, dimensions);
|
||||
return JigsawStudioLayout.createSpatial(
|
||||
@@ -129,26 +123,54 @@ public final class JigsawStudioGraphMapper {
|
||||
if (data.getObjectLoader() == null) {
|
||||
return configured;
|
||||
}
|
||||
int width = configured.width();
|
||||
int height = configured.height();
|
||||
int depth = configured.depth();
|
||||
SpatialExtent extent = SpatialExtent.of(configured);
|
||||
for (JigsawStudioVariant variant : catalog.spatialVariants()) {
|
||||
IrisObject object = data.getObjectLoader().load(variant.objectKey());
|
||||
if (object == null) {
|
||||
if (object != null) {
|
||||
extent = extent.include(object.getW(), object.getH(), object.getD(), variant.rotatable());
|
||||
}
|
||||
}
|
||||
return new JigsawStudioCellDimensions((int) extent.width(), (int) extent.height(), (int) extent.depth());
|
||||
}
|
||||
|
||||
static JigsawStudioCellDimensions configuredCellDimensions(IrisStructure structure) {
|
||||
IrisPosition configuredCell = structure.getCellSize();
|
||||
return configuredCell == null
|
||||
? new JigsawStudioCellDimensions(15, 15, 15)
|
||||
: new JigsawStudioCellDimensions(
|
||||
Math.max(1, configuredCell.getX()),
|
||||
Math.max(1, configuredCell.getY()),
|
||||
Math.max(1, configuredCell.getZ()));
|
||||
}
|
||||
|
||||
static String spatialFootprintFailure(
|
||||
IrisData data,
|
||||
JigsawStudioCellDimensions configured,
|
||||
JigsawStudioVariantCatalog catalog,
|
||||
SpatialChange change
|
||||
) {
|
||||
SpatialExtent extent = SpatialExtent.of(configured);
|
||||
for (JigsawStudioVariant variant : catalog.spatialVariants()) {
|
||||
if (variant.pieceKey().equals(change.pieceKey())) {
|
||||
continue;
|
||||
}
|
||||
int objectWidth = object.getW();
|
||||
int objectDepth = object.getD();
|
||||
if (variant.rotatable()) {
|
||||
int horizontalSpan = Math.max(objectWidth, objectDepth);
|
||||
objectWidth = horizontalSpan;
|
||||
objectDepth = horizontalSpan;
|
||||
IrisObject object = data.getObjectLoader() == null ? null : data.getObjectLoader().load(variant.objectKey());
|
||||
if (object != null) {
|
||||
extent = extent.include(object.getW(), object.getH(), object.getD(), variant.rotatable());
|
||||
}
|
||||
width = Math.max(width, objectWidth);
|
||||
height = Math.max(height, object.getH());
|
||||
depth = Math.max(depth, objectDepth);
|
||||
}
|
||||
return new JigsawStudioCellDimensions(width, height, depth);
|
||||
JigsawStudioCellDimensions changed = change.dimensions();
|
||||
extent = extent.include(changed.width(), changed.height(), changed.depth(), change.rotatable());
|
||||
long volume = extent.width() * extent.height() * extent.depth();
|
||||
if (extent.width() <= JigsawStudioCellDimensions.MAX_HORIZONTAL_AXIS
|
||||
&& extent.depth() <= JigsawStudioCellDimensions.MAX_HORIZONTAL_AXIS
|
||||
&& extent.height() <= JigsawStudioCellDimensions.MAX_HEIGHT
|
||||
&& volume <= JigsawStudioCellDimensions.MAX_VOLUME) {
|
||||
return "";
|
||||
}
|
||||
return "Spatial workcells would grow to " + extent.width() + "x" + extent.height() + "x" + extent.depth()
|
||||
+ " (" + volume + " blocks), past the " + JigsawStudioCellDimensions.MAX_VOLUME
|
||||
+ "-block workcell limit. Make this variant smaller or keep rotation disabled.";
|
||||
}
|
||||
|
||||
private static void traversePools(
|
||||
@@ -378,4 +400,25 @@ public final class JigsawStudioGraphMapper {
|
||||
return new OwnedResources(Set.of(), Set.of(), Set.of(), false);
|
||||
}
|
||||
}
|
||||
|
||||
record SpatialChange(String pieceKey, JigsawStudioCellDimensions dimensions, boolean rotatable) {
|
||||
SpatialChange {
|
||||
Objects.requireNonNull(pieceKey, "Spatial change piece key");
|
||||
Objects.requireNonNull(dimensions, "Spatial change dimensions");
|
||||
}
|
||||
}
|
||||
|
||||
private record SpatialExtent(long width, long height, long depth) {
|
||||
static SpatialExtent of(JigsawStudioCellDimensions dimensions) {
|
||||
return new SpatialExtent(dimensions.width(), dimensions.height(), dimensions.depth());
|
||||
}
|
||||
|
||||
SpatialExtent include(int objectWidth, int objectHeight, int objectDepth, boolean rotatable) {
|
||||
int horizontalSpan = Math.max(objectWidth, objectDepth);
|
||||
return new SpatialExtent(
|
||||
Math.max(width, rotatable ? horizontalSpan : objectWidth),
|
||||
Math.max(height, objectHeight),
|
||||
Math.max(depth, rotatable ? horizontalSpan : objectDepth));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -199,6 +199,7 @@ final class JigsawStudioGraphMutations {
|
||||
for (JigsawStudioBay workcell : result.layout().bays()) {
|
||||
service.playerContext.refreshWorkcellContext(studio.worldId(), workcell.stableId());
|
||||
}
|
||||
service.floorPainter.refreshAll(studio);
|
||||
service.evaluator.scheduleEvaluation(studio);
|
||||
message(player, result.message());
|
||||
finishGraphMutation(requestId);
|
||||
@@ -273,15 +274,20 @@ final class JigsawStudioGraphMutations {
|
||||
String failure,
|
||||
Throwable cause
|
||||
) {
|
||||
boolean scheduled = J.runEntity(
|
||||
player,
|
||||
() -> completeLiveRelayout(player, studio, requestId, result, failure, cause));
|
||||
if (!scheduled) {
|
||||
Runnable abandon = () -> {
|
||||
service.saveLifecycle.reopenRequiredRequests.add(requestId);
|
||||
finishGraphMutation(requestId);
|
||||
if (cause != null) {
|
||||
IrisLogging.reportError(cause);
|
||||
}
|
||||
};
|
||||
boolean scheduled = J.runEntity(
|
||||
player,
|
||||
() -> completeLiveRelayout(player, studio, requestId, result, failure, cause),
|
||||
0,
|
||||
abandon);
|
||||
if (!scheduled) {
|
||||
abandon.run();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ public final class JigsawStudioLayout {
|
||||
public static final int PLANAR_GAP = 1;
|
||||
public static final int MAX_VARIANTS = 512;
|
||||
public static final String SPATIAL_WORKCELL_ID = "workcell/spatial";
|
||||
public static final int COLUMN_HEADROOM = 4;
|
||||
|
||||
private static final int FIRST_ORIGIN = 16;
|
||||
private static final JigsawStudioControlPosition CONTROL_POSITION =
|
||||
@@ -208,6 +209,18 @@ public final class JigsawStudioLayout {
|
||||
return null;
|
||||
}
|
||||
|
||||
public JigsawStudioBay findColumn(int worldX, int worldY, int worldZ) {
|
||||
for (JigsawStudioBay bay : bays) {
|
||||
JigsawStudioBounds bounds = bay.bounds();
|
||||
if (worldX >= bounds.originX() && worldX <= bounds.maxX()
|
||||
&& worldZ >= bounds.originZ() && worldZ <= bounds.maxZ()
|
||||
&& worldY >= bounds.originY() - 1 && worldY <= bounds.maxY() + COLUMN_HEADROOM) {
|
||||
return bay;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public List<JigsawStudioVariant> variants(JigsawStudioBay workcell) {
|
||||
JigsawStudioBay activeWorkcell = requireWorkcell(workcell);
|
||||
if (activeWorkcell.kind() == JigsawStudioBayKind.SPATIAL_WORKCELL) {
|
||||
@@ -276,7 +289,7 @@ public final class JigsawStudioLayout {
|
||||
|
||||
private JigsawStudioBay requireWorkcell(JigsawStudioBay workcell) {
|
||||
JigsawStudioBay activeWorkcell = Objects.requireNonNull(workcell, "Jigsaw Studio workcell");
|
||||
if (byStableId.get(activeWorkcell.stableId()) != activeWorkcell) {
|
||||
if (!activeWorkcell.equals(byStableId.get(activeWorkcell.stableId()))) {
|
||||
throw new IllegalArgumentException("Workcell does not belong to this Jigsaw Studio layout");
|
||||
}
|
||||
return activeWorkcell;
|
||||
|
||||
@@ -6,6 +6,7 @@ import art.arcane.volmlib.util.inventorygui.UIWindow;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.Material;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
@@ -27,6 +28,9 @@ import static art.arcane.iris.studio.jigsaw.JigsawStudioMenuFormat.workcellStatu
|
||||
import static art.arcane.iris.studio.jigsaw.JigsawStudioMenuFormat.yesNo;
|
||||
|
||||
final class JigsawStudioMainMenu {
|
||||
static final int WORKCELL_ROW_CAPACITY = 6;
|
||||
static final int WORKCELLS_PER_PAGE = 5;
|
||||
|
||||
private final JigsawStudioMenuController controller;
|
||||
|
||||
JigsawStudioMainMenu(JigsawStudioMenuController controller) {
|
||||
@@ -103,51 +107,113 @@ final class JigsawStudioMainMenu {
|
||||
}
|
||||
|
||||
private void addWorkcells(UIWindow window, JigsawStudioMenuState state, String selectedWorkcellId) {
|
||||
int count = state.workcells().size();
|
||||
int start = count == 1 ? 0 : -(count / 2);
|
||||
for (int index = 0; index < count; index++) {
|
||||
JigsawStudioMenuState.Workcell workcell = state.workcells().get(index);
|
||||
boolean selected = workcell.stableId().equals(selectedWorkcellId);
|
||||
UIElement element = element(
|
||||
"workcell-" + index,
|
||||
workcellMaterial(workcell, selected),
|
||||
(selected ? ChatColor.AQUA : workcell.enabled() ? ChatColor.WHITE : ChatColor.GRAY)
|
||||
+ safe(workcell.displayName()))
|
||||
.setEnchanted(selected);
|
||||
element.addLore(workcell.enabled()
|
||||
? ChatColor.GREEN + "Enabled"
|
||||
: ChatColor.RED + "Disabled for assembly and export");
|
||||
if (!workcell.canonicalName().equals(workcell.displayName())) {
|
||||
element.addLore(ChatColor.GRAY + "Solver role: " + safe(workcell.canonicalName()));
|
||||
List<JigsawStudioMenuState.Workcell> workcells = state.workcells();
|
||||
int viewedIndex = 0;
|
||||
for (int index = 0; index < workcells.size(); index++) {
|
||||
if (workcells.get(index).stableId().equals(selectedWorkcellId)) {
|
||||
viewedIndex = index;
|
||||
}
|
||||
element.addLore(ChatColor.GRAY + "Capacity: " + dimensions(workcell.capacity()));
|
||||
element.addLore(ChatColor.GRAY + "Variants: " + workcell.variants().size());
|
||||
JigsawStudioMenuState.Variant active = workcell.activeVariant();
|
||||
element.addLore(ChatColor.GRAY + "Loaded: "
|
||||
+ (active == null ? "None" : safe(active.displayName())));
|
||||
element.addLore(workcellStatus(workcell));
|
||||
element.addLore(ChatColor.GRAY + "Connector blocks: "
|
||||
+ (workcell.connectorsVisible() ? "Visible" : "Hidden"));
|
||||
element.addLore(ChatColor.YELLOW + "Left-click to select and teleport");
|
||||
element.addLore(ChatColor.YELLOW + "Right-click for workcell settings");
|
||||
if (workcell.dirty() && !workcell.saving()) {
|
||||
element.addLore(ChatColor.GOLD + "Shift-left: Flush Autosave Now");
|
||||
element.onShiftLeftClick(clicked -> controller.flushNow(
|
||||
window.getViewer(), state.requestId(), workcell.stableId()));
|
||||
}
|
||||
element.onLeftClick(clicked -> controller.selectWorkcell(
|
||||
window.getViewer(),
|
||||
state.requestId(),
|
||||
workcell.stableId()));
|
||||
element.onRightClick(clicked -> J.runEntity(
|
||||
window.getViewer(),
|
||||
() -> controller.openWorkcellSettings(
|
||||
window.getViewer(),
|
||||
state.requestId(),
|
||||
workcell.stableId()),
|
||||
1));
|
||||
window.setElement(start + index, 0, element);
|
||||
}
|
||||
WorkcellRow row = workcellRow(workcells.size(), viewedIndex);
|
||||
for (int position = 0; position < row.indices().size(); position++) {
|
||||
int index = row.indices().get(position);
|
||||
JigsawStudioMenuState.Workcell workcell = workcells.get(index);
|
||||
window.setElement(row.slots().get(position), 0, workcellElement(
|
||||
window,
|
||||
state,
|
||||
workcell,
|
||||
index,
|
||||
workcell.stableId().equals(selectedWorkcellId)));
|
||||
}
|
||||
if (row.previousTarget() >= 0) {
|
||||
JigsawStudioMenuState.Workcell target = workcells.get(row.previousTarget());
|
||||
UIElement previous = element("workcells-previous", Material.ARROW, ChatColor.YELLOW + "Previous Workcells");
|
||||
previous.addLore(ChatColor.GRAY + "Workcells " + (row.previousTarget() + 1) + "-"
|
||||
+ (row.previousTarget() + WORKCELLS_PER_PAGE) + " of " + workcells.size());
|
||||
previous.onLeftClick(clicked -> controller.refreshMain(
|
||||
window.getViewer(), state.requestId(), target.stableId(), 0));
|
||||
window.setElement(-4, 0, previous);
|
||||
}
|
||||
if (row.nextTarget() >= 0) {
|
||||
JigsawStudioMenuState.Workcell target = workcells.get(row.nextTarget());
|
||||
UIElement next = element("workcells-next", Material.ARROW, ChatColor.YELLOW + "Next Workcells");
|
||||
next.addLore(ChatColor.GRAY + "Workcells " + (row.nextTarget() + 1) + "-"
|
||||
+ Math.min(workcells.size(), row.nextTarget() + WORKCELLS_PER_PAGE)
|
||||
+ " of " + workcells.size());
|
||||
next.onLeftClick(clicked -> controller.refreshMain(
|
||||
window.getViewer(), state.requestId(), target.stableId(), 0));
|
||||
window.setElement(2, 0, next);
|
||||
}
|
||||
}
|
||||
|
||||
static WorkcellRow workcellRow(int count, int viewedIndex) {
|
||||
List<Integer> indices = new ArrayList<>(Math.min(count, WORKCELL_ROW_CAPACITY));
|
||||
List<Integer> slots = new ArrayList<>(Math.min(count, WORKCELL_ROW_CAPACITY));
|
||||
if (count <= WORKCELL_ROW_CAPACITY) {
|
||||
int start = count == 1 ? 0 : -(count / 2);
|
||||
for (int index = 0; index < count; index++) {
|
||||
indices.add(index);
|
||||
slots.add(start + index);
|
||||
}
|
||||
return new WorkcellRow(indices, slots, -1, -1);
|
||||
}
|
||||
int page = Math.max(0, Math.min(viewedIndex, count - 1)) / WORKCELLS_PER_PAGE;
|
||||
int first = page * WORKCELLS_PER_PAGE;
|
||||
for (int offset = 0; offset < WORKCELLS_PER_PAGE && first + offset < count; offset++) {
|
||||
indices.add(first + offset);
|
||||
slots.add(-3 + offset);
|
||||
}
|
||||
int previous = page == 0 ? -1 : first - WORKCELLS_PER_PAGE;
|
||||
int next = first + WORKCELLS_PER_PAGE < count ? first + WORKCELLS_PER_PAGE : -1;
|
||||
return new WorkcellRow(indices, slots, previous, next);
|
||||
}
|
||||
|
||||
private UIElement workcellElement(
|
||||
UIWindow window,
|
||||
JigsawStudioMenuState state,
|
||||
JigsawStudioMenuState.Workcell workcell,
|
||||
int index,
|
||||
boolean selected
|
||||
) {
|
||||
UIElement element = element(
|
||||
"workcell-" + index,
|
||||
workcellMaterial(workcell, selected),
|
||||
(selected ? ChatColor.AQUA : workcell.enabled() ? ChatColor.WHITE : ChatColor.GRAY)
|
||||
+ safe(workcell.displayName()))
|
||||
.setEnchanted(selected);
|
||||
element.addLore(workcell.enabled()
|
||||
? ChatColor.GREEN + "Enabled"
|
||||
: ChatColor.RED + "Disabled for assembly and export");
|
||||
if (!workcell.canonicalName().equals(workcell.displayName())) {
|
||||
element.addLore(ChatColor.GRAY + "Solver role: " + safe(workcell.canonicalName()));
|
||||
}
|
||||
element.addLore(ChatColor.GRAY + "Capacity: " + dimensions(workcell.capacity()));
|
||||
element.addLore(ChatColor.GRAY + "Variants: " + workcell.variants().size());
|
||||
JigsawStudioMenuState.Variant active = workcell.activeVariant();
|
||||
element.addLore(ChatColor.GRAY + "Loaded: "
|
||||
+ (active == null ? "None" : safe(active.displayName())));
|
||||
element.addLore(workcellStatus(workcell));
|
||||
element.addLore(ChatColor.GRAY + "Connector blocks: "
|
||||
+ (workcell.connectorsVisible() ? "Visible" : "Hidden"));
|
||||
element.addLore(ChatColor.YELLOW + "Left-click to select and teleport");
|
||||
element.addLore(ChatColor.YELLOW + "Right-click for workcell settings");
|
||||
if (workcell.dirty() && !workcell.saving()) {
|
||||
element.addLore(ChatColor.GOLD + "Shift-left: Flush Autosave Now");
|
||||
element.onShiftLeftClick(clicked -> controller.flushNow(
|
||||
window.getViewer(), state.requestId(), workcell.stableId()));
|
||||
}
|
||||
element.onLeftClick(clicked -> controller.selectWorkcell(
|
||||
window.getViewer(),
|
||||
state.requestId(),
|
||||
workcell.stableId()));
|
||||
element.onRightClick(clicked -> J.runEntity(
|
||||
window.getViewer(),
|
||||
() -> controller.openWorkcellSettings(
|
||||
window.getViewer(),
|
||||
state.requestId(),
|
||||
workcell.stableId()),
|
||||
1));
|
||||
return element;
|
||||
}
|
||||
|
||||
private void addVariants(
|
||||
@@ -283,4 +349,11 @@ final class JigsawStudioMainMenu {
|
||||
window.getViewer(), state.requestId(), workcell.stableId(), 0));
|
||||
window.setElement(4, 5, toolbox);
|
||||
}
|
||||
|
||||
record WorkcellRow(List<Integer> indices, List<Integer> slots, int previousTarget, int nextTarget) {
|
||||
WorkcellRow {
|
||||
indices = List.copyOf(indices);
|
||||
slots = List.copyOf(slots);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,9 @@ import art.arcane.iris.studio.jigsaw.JigsawStudioService.ActiveStudio;
|
||||
import art.arcane.iris.studio.generation.JigsawStudioGenerator;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.world.task.J;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -24,7 +26,7 @@ import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static art.arcane.iris.studio.jigsaw.JigsawStudioCapture.chunkIntersections;
|
||||
import static art.arcane.iris.studio.jigsaw.JigsawStudioChunkWriter.hydrateChunk;
|
||||
import static art.arcane.iris.studio.jigsaw.JigsawStudioChunkWriter.restoreConnectorChunk;
|
||||
import static art.arcane.iris.studio.jigsaw.JigsawStudioChunkWriter.restoreChunkPositions;
|
||||
import static art.arcane.iris.studio.jigsaw.JigsawStudioChunkWriter.validateMaterialization;
|
||||
import static art.arcane.iris.studio.jigsaw.JigsawStudioChunkWriter.writeMaterializedChunk;
|
||||
import static art.arcane.iris.studio.jigsaw.JigsawStudioService.chunkKey;
|
||||
@@ -139,6 +141,51 @@ final class JigsawStudioMaterializer {
|
||||
}
|
||||
}
|
||||
|
||||
ConnectorEditReservation beginConnectorEdit(ActiveStudio studio, String workcellId) {
|
||||
UUID requestId = studio.generator().getRequest().requestId();
|
||||
service.tileWatcher.finalizeJigsawTileWatches(requestId);
|
||||
JigsawStudioSession session = studio.generator().getSession();
|
||||
synchronized (service.saveLifecycleLock) {
|
||||
if (!service.isCurrentRequest(studio, requestId)) {
|
||||
return ConnectorEditReservation.blocked("This Jigsaw Studio session is no longer active.");
|
||||
}
|
||||
if (service.saveLifecycle.closingRequests.contains(requestId)) {
|
||||
return ConnectorEditReservation.blocked("This Jigsaw Studio is closing and cannot edit connectors.");
|
||||
}
|
||||
if (service.saveLifecycle.reopenRequiredRequests.contains(requestId)) {
|
||||
return ConnectorEditReservation.blocked("Close and reopen Jigsaw Studio before editing connectors.");
|
||||
}
|
||||
if (session.layout().get(workcellId) == null) {
|
||||
return ConnectorEditReservation.blocked("The targeted workcell no longer exists.");
|
||||
}
|
||||
JigsawStudioSession.WorkcellSnapshot snapshot = session.workcellSnapshot(workcellId);
|
||||
if (snapshot.dirty() || snapshot.saveInProgress()
|
||||
|| service.saveLifecycle.savesInProgress.contains(requestId)) {
|
||||
return ConnectorEditReservation.busy("Saving your latest edits before changing connectors...");
|
||||
}
|
||||
if (service.graphMutations.graphMutationsInProgress.contains(requestId)) {
|
||||
return ConnectorEditReservation.busy("Waiting for the current graph update before changing connectors...");
|
||||
}
|
||||
if (service.saveLifecycle.exportsInProgress.contains(requestId)) {
|
||||
return ConnectorEditReservation.busy("Waiting for the current export before changing connectors...");
|
||||
}
|
||||
if (session.operationInProgress()) {
|
||||
return ConnectorEditReservation.busy("Waiting for the current workcell operation before changing connectors...");
|
||||
}
|
||||
if (service.tileWatcher.hasJigsawTileWatch(requestId)) {
|
||||
return ConnectorEditReservation.busy("Waiting for the vanilla jigsaw-block editor to close...");
|
||||
}
|
||||
if (!materializationsInProgress.add(requestId)) {
|
||||
return ConnectorEditReservation.busy("Waiting for the current variant load or connector edit to finish...");
|
||||
}
|
||||
return ConnectorEditReservation.started();
|
||||
}
|
||||
}
|
||||
|
||||
void finishConnectorEdit(UUID requestId) {
|
||||
finishMaterialization(requestId);
|
||||
}
|
||||
|
||||
boolean scheduleConnectorRepair(
|
||||
Player player,
|
||||
ActiveStudio studio,
|
||||
@@ -146,39 +193,55 @@ final class JigsawStudioMaterializer {
|
||||
JigsawStudioGenerator.RenderedBay rendered,
|
||||
boolean connectorsVisible
|
||||
) {
|
||||
Map<Long, List<JigsawStudioGenerator.RenderedConnector>> connectorsByChunk = new HashMap<>();
|
||||
JigsawStudioBounds bounds = workcell.bounds();
|
||||
JigsawStudioBounds variantBounds = new JigsawStudioBounds(
|
||||
bounds.originX(),
|
||||
bounds.originY(),
|
||||
bounds.originZ(),
|
||||
rendered.dimensions());
|
||||
Map<LocalPosition, JigsawStudioGenerator.RenderedConnector> renderedConnectors = new HashMap<>();
|
||||
for (JigsawStudioGenerator.RenderedConnector connector : rendered.connectors()) {
|
||||
int worldX = bounds.originX() + connector.x();
|
||||
int worldZ = bounds.originZ() + connector.z();
|
||||
connectorsByChunk.computeIfAbsent(
|
||||
chunkKey(worldX >> 4, worldZ >> 4),
|
||||
ignored -> new ArrayList<>()).add(connector);
|
||||
renderedConnectors.put(new LocalPosition(connector.x(), connector.y(), connector.z()), connector);
|
||||
}
|
||||
AtomicInteger remaining = new AtomicInteger(connectorsByChunk.size());
|
||||
AtomicReference<String> failure = new AtomicReference<>("");
|
||||
AtomicReference<Throwable> cause = new AtomicReference<>();
|
||||
AtomicBoolean scheduledAny = new AtomicBoolean(false);
|
||||
Map<LocalPosition, JigsawStudioGenerator.RenderedBlock> renderedBlocks = new HashMap<>();
|
||||
for (JigsawStudioGenerator.RenderedBlock block : rendered.blocks()) {
|
||||
renderedBlocks.put(new LocalPosition(block.x(), block.y(), block.z()), block);
|
||||
}
|
||||
for (Map.Entry<Long, List<JigsawStudioGenerator.RenderedConnector>> entry
|
||||
: connectorsByChunk.entrySet()) {
|
||||
int chunkX = (int) (entry.getKey() >> 32);
|
||||
int chunkZ = (int) entry.getKey().longValue();
|
||||
List<ChunkCaptureArea> areas = chunkIntersections(variantBounds);
|
||||
AtomicInteger remaining = new AtomicInteger(areas.size());
|
||||
AtomicInteger cleared = new AtomicInteger();
|
||||
AtomicReference<String> failure = new AtomicReference<>("");
|
||||
AtomicReference<Throwable> cause = new AtomicReference<>();
|
||||
AtomicBoolean scheduledAny = new AtomicBoolean(false);
|
||||
for (ChunkCaptureArea area : areas) {
|
||||
int chunkX = area.chunkX();
|
||||
int chunkZ = area.chunkZ();
|
||||
boolean scheduled = J.runRegion(studio.world(), chunkX, chunkZ, () -> {
|
||||
try {
|
||||
if (!studio.world().isChunkLoaded(chunkX, chunkZ)) {
|
||||
throw new IOException("connector chunk " + chunkX + "," + chunkZ
|
||||
+ " is not loaded");
|
||||
}
|
||||
restoreConnectorChunk(
|
||||
Set<LocalPosition> strays = strayMarkerPositions(
|
||||
studio.world(),
|
||||
variantBounds,
|
||||
area,
|
||||
renderedConnectors.keySet(),
|
||||
connectorsVisible);
|
||||
Set<LocalPosition> positions = new HashSet<>(strays);
|
||||
for (LocalPosition position : renderedConnectors.keySet()) {
|
||||
if (area.contains(position.x(), position.z())) {
|
||||
positions.add(position);
|
||||
}
|
||||
}
|
||||
restoreChunkPositions(
|
||||
studio.world(),
|
||||
workcell,
|
||||
entry.getValue(),
|
||||
positions,
|
||||
renderedConnectors,
|
||||
renderedBlocks,
|
||||
connectorsVisible);
|
||||
cleared.addAndGet(strays.size());
|
||||
} catch (Throwable exception) {
|
||||
failure.compareAndSet("", failureMessage(exception));
|
||||
cause.compareAndSet(null, exception);
|
||||
@@ -188,6 +251,7 @@ final class JigsawStudioMaterializer {
|
||||
player,
|
||||
studio,
|
||||
rendered.connectors().size(),
|
||||
cleared.get(),
|
||||
failure.get(),
|
||||
cause.get());
|
||||
}
|
||||
@@ -202,6 +266,7 @@ final class JigsawStudioMaterializer {
|
||||
player,
|
||||
studio,
|
||||
rendered.connectors().size(),
|
||||
cleared.get(),
|
||||
failure.get(),
|
||||
cause.get());
|
||||
}
|
||||
@@ -213,31 +278,61 @@ final class JigsawStudioMaterializer {
|
||||
return scheduledAny.get();
|
||||
}
|
||||
|
||||
static Set<LocalPosition> strayMarkerPositions(
|
||||
World world,
|
||||
JigsawStudioBounds variantBounds,
|
||||
ChunkCaptureArea area,
|
||||
Set<LocalPosition> savedConnectors,
|
||||
boolean connectorsVisible
|
||||
) {
|
||||
Set<LocalPosition> strays = new HashSet<>();
|
||||
for (int x = area.minimumX(); x < area.maximumX(); x++) {
|
||||
for (int y = 0; y < variantBounds.dimensions().height(); y++) {
|
||||
for (int z = area.minimumZ(); z < area.maximumZ(); z++) {
|
||||
Block block = world.getBlockAt(
|
||||
variantBounds.originX() + x,
|
||||
variantBounds.originY() + y,
|
||||
variantBounds.originZ() + z);
|
||||
if (block.getType() != Material.JIGSAW) {
|
||||
continue;
|
||||
}
|
||||
LocalPosition position = new LocalPosition(x, y, z);
|
||||
if (!connectorsVisible || !savedConnectors.contains(position)) {
|
||||
strays.add(position);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return strays;
|
||||
}
|
||||
|
||||
private void completeConnectorRepair(
|
||||
Player player,
|
||||
ActiveStudio studio,
|
||||
int connectorCount,
|
||||
int clearedMarkers,
|
||||
String failure,
|
||||
Throwable cause
|
||||
) {
|
||||
UUID requestId = studio.generator().getRequest().requestId();
|
||||
boolean scheduled = J.runEntity(player, () -> {
|
||||
Runnable release = () -> {
|
||||
finishMaterialization(requestId);
|
||||
if (cause != null) {
|
||||
IrisLogging.reportError(cause);
|
||||
}
|
||||
};
|
||||
boolean scheduled = J.runEntity(player, () -> {
|
||||
release.run();
|
||||
if (!failure.isEmpty()) {
|
||||
message(player, "Connector reset was incomplete: " + failure + ". Retry after visiting the workcell.");
|
||||
return;
|
||||
}
|
||||
message(player, "Restored " + connectorCount
|
||||
+ " connector block(s) from the last saved iteration.");
|
||||
});
|
||||
+ " connector block(s) from the last saved iteration"
|
||||
+ (clearedMarkers == 0 ? "." : " and cleared " + clearedMarkers + " unsaved marker(s)."));
|
||||
}, 0, release);
|
||||
if (!scheduled) {
|
||||
finishMaterialization(requestId);
|
||||
if (cause != null) {
|
||||
IrisLogging.reportError(cause);
|
||||
}
|
||||
release.run();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -474,6 +569,7 @@ final class JigsawStudioMaterializer {
|
||||
try {
|
||||
work.studio().generator().invalidateRender(work.workcell().stableId());
|
||||
work.studio().replacePopulation(work.workcell(), work.target(), "", true);
|
||||
service.floorPainter.refresh(work.studio(), work.workcell().stableId());
|
||||
service.playerContext.refreshWorkcellContext(work.studio().worldId(), work.workcell().stableId());
|
||||
message(work.player(), work.connectorVisibilityChange()
|
||||
? "Connector blocks are now "
|
||||
@@ -564,6 +660,7 @@ final class JigsawStudioMaterializer {
|
||||
work.studio().generator().invalidateRender(work.workcell().stableId());
|
||||
if (rollbackFailure.isEmpty() && released) {
|
||||
work.studio().replacePopulation(work.workcell(), work.previous(), "", true);
|
||||
service.floorPainter.refresh(work.studio(), work.workcell().stableId());
|
||||
service.playerContext.refreshWorkcellContext(work.studio().worldId(), work.workcell().stableId());
|
||||
message(work.player(), candidateFailure + " The previous variant was restored.");
|
||||
return;
|
||||
@@ -572,6 +669,7 @@ final class JigsawStudioMaterializer {
|
||||
? "The previous variant could not be restored because the Studio session changed."
|
||||
: rollbackFailure;
|
||||
work.studio().replacePopulation(work.workcell(), work.previous(), failure, false);
|
||||
service.floorPainter.refresh(work.studio(), work.workcell().stableId());
|
||||
service.playerContext.refreshWorkcellContext(work.studio().worldId(), work.workcell().stableId());
|
||||
message(work.player(), candidateFailure + " Automatic rollback also failed: " + failure
|
||||
+ " Do not save this workcell until it is reopened.");
|
||||
@@ -614,6 +712,31 @@ final class JigsawStudioMaterializer {
|
||||
}
|
||||
}
|
||||
|
||||
enum ConnectorEditStatus {
|
||||
STARTED,
|
||||
BUSY,
|
||||
BLOCKED
|
||||
}
|
||||
|
||||
record ConnectorEditReservation(ConnectorEditStatus status, String message) {
|
||||
ConnectorEditReservation {
|
||||
Objects.requireNonNull(status, "Jigsaw Studio connector edit status");
|
||||
message = message == null ? "" : message;
|
||||
}
|
||||
|
||||
static ConnectorEditReservation started() {
|
||||
return new ConnectorEditReservation(ConnectorEditStatus.STARTED, "");
|
||||
}
|
||||
|
||||
static ConnectorEditReservation busy(String message) {
|
||||
return new ConnectorEditReservation(ConnectorEditStatus.BUSY, message);
|
||||
}
|
||||
|
||||
static ConnectorEditReservation blocked(String message) {
|
||||
return new ConnectorEditReservation(ConnectorEditStatus.BLOCKED, message);
|
||||
}
|
||||
}
|
||||
|
||||
public record VariantReloadRequest(
|
||||
String workcellId,
|
||||
JigsawStudioGenerator.RenderedBay previous
|
||||
|
||||
@@ -857,7 +857,7 @@ public final class JigsawStudioMenuController {
|
||||
}
|
||||
JigsawStudioMenuState.Workcell workcell = current.get().workcell(workcellId);
|
||||
JigsawStudioMenuState.Variant target = variant(workcell, pieceKey);
|
||||
if (target == null || !target.owned() || target.active()) {
|
||||
if (!current.get().canDeleteVariant(workcell, target)) {
|
||||
stale(player);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -54,6 +54,20 @@ public record JigsawStudioMenuState(
|
||||
}
|
||||
}
|
||||
|
||||
public boolean canDeleteVariant(Workcell workcell, Variant variant) {
|
||||
if (workcell == null || variant == null || !variant.owned()) {
|
||||
return false;
|
||||
}
|
||||
if (mode == JigsawStudioMode.SPATIAL_JIGSAW) {
|
||||
int variants = 0;
|
||||
for (Workcell current : workcells) {
|
||||
variants += current.variants().size();
|
||||
}
|
||||
return variants > 1;
|
||||
}
|
||||
return !variant.active() && workcell.variants().size() > 1;
|
||||
}
|
||||
|
||||
public Workcell selectedWorkcell() {
|
||||
if (selectedWorkcellId.isEmpty()) {
|
||||
return null;
|
||||
|
||||
@@ -59,7 +59,7 @@ final class JigsawStudioPlayerContext {
|
||||
return;
|
||||
}
|
||||
JigsawStudioSession session = studio.generator().getSession();
|
||||
JigsawStudioBay workcell = session.layout().findAt(
|
||||
JigsawStudioBay workcell = session.layout().findColumn(
|
||||
target.getBlockX(), target.getBlockY(), target.getBlockZ());
|
||||
boolean owner = ownerMatches(player, studio);
|
||||
selectEnteredWorkcell(session, workcell, owner);
|
||||
|
||||
@@ -36,13 +36,9 @@ import java.util.Objects;
|
||||
|
||||
public final class JigsawStudioProjectCreator {
|
||||
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
||||
private static final List<IrisDirection> SPATIAL_CONNECTOR_ORDER = List.of(
|
||||
IrisDirection.NORTH_NEGATIVE_Z,
|
||||
IrisDirection.SOUTH_POSITIVE_Z,
|
||||
IrisDirection.EAST_POSITIVE_X,
|
||||
IrisDirection.WEST_NEGATIVE_X,
|
||||
IrisDirection.UP_POSITIVE_Y,
|
||||
IrisDirection.DOWN_NEGATIVE_Y);
|
||||
private static final int DEFAULT_MAX_DEPTH = 7;
|
||||
private static final int SPATIAL_PORTABLE_MAX_DEPTH = 4;
|
||||
private static final int SPATIAL_MAXIMUM_PLACEMENTS = 4;
|
||||
|
||||
private JigsawStudioProjectCreator() {
|
||||
}
|
||||
@@ -59,7 +55,7 @@ public final class JigsawStudioProjectCreator {
|
||||
StructureKey ownershipKey = new StructureKey("iris", resourceKey);
|
||||
IrisStructure structure = new IrisStructure()
|
||||
.setStartPool(resourceKey + "/start")
|
||||
.setMaxDepth(7)
|
||||
.setMaxDepth(maxDepth(options))
|
||||
.setMaxSizeChunks(8)
|
||||
.setMode(toModelMode(options.mode()))
|
||||
.setCompatibility(toModelCompatibility(options.compatibilityTarget()))
|
||||
@@ -94,6 +90,13 @@ public final class JigsawStudioProjectCreator {
|
||||
return output.toByteArray();
|
||||
}
|
||||
|
||||
private static int maxDepth(Options options) {
|
||||
return options.mode() == JigsawStudioMode.SPATIAL_JIGSAW
|
||||
&& options.compatibilityTarget() == JigsawStudioCompatibilityTarget.VANILLA_PORTABLE
|
||||
? SPATIAL_PORTABLE_MAX_DEPTH
|
||||
: DEFAULT_MAX_DEPTH;
|
||||
}
|
||||
|
||||
private static IrisJigsawMode toModelMode(JigsawStudioMode mode) {
|
||||
return mode == JigsawStudioMode.PLANAR_JIGSAW
|
||||
? IrisJigsawMode.PLANAR_JIGSAW
|
||||
@@ -129,14 +132,11 @@ public final class JigsawStudioProjectCreator {
|
||||
IrisJigsawPool piecePool = new IrisJigsawPool().setFallback(capPoolKey);
|
||||
IrisJigsawPool capPool = new IrisJigsawPool();
|
||||
for (JigsawPlanarArchetype archetype : JigsawPlanarArchetype.values()) {
|
||||
String key = options.structureKey() + "/" + archetype.name().toLowerCase(Locale.ROOT);
|
||||
String key = archetypeKey(options, archetype);
|
||||
IrisJigsawPiece piece = planarPiece(key, piecePoolKey, size, archetype);
|
||||
if (options.compatibilityTarget() == JigsawStudioCompatibilityTarget.IRIS_EXTENDED) {
|
||||
piece.getThemes().add("variant-1");
|
||||
}
|
||||
if (archetype == JigsawPlanarArchetype.CROSS) {
|
||||
pool.getPieces().add(new IrisJigsawPieceEntry(key, 1));
|
||||
}
|
||||
if (archetype != JigsawPlanarArchetype.BLANK) {
|
||||
piecePool.getPieces().add(new IrisJigsawPieceEntry(key, 1));
|
||||
}
|
||||
@@ -159,6 +159,8 @@ public final class JigsawStudioProjectCreator {
|
||||
dimensions.depth())));
|
||||
bundle.textResource("jigsaw-pieces/" + key + ".json", GSON.toJson(piece) + "\n");
|
||||
}
|
||||
pool.getPieces().add(new IrisJigsawPieceEntry(archetypeKey(options, JigsawPlanarArchetype.CROSS), 3));
|
||||
pool.getPieces().add(new IrisJigsawPieceEntry(archetypeKey(options, JigsawPlanarArchetype.BLANK), 1));
|
||||
capPool.getPieces().add(new IrisJigsawPieceEntry().setEmpty(true));
|
||||
bundle.textResource("jigsaw-pools/" + piecePoolKey + ".json", GSON.toJson(piecePool) + "\n");
|
||||
bundle.textResource("jigsaw-pools/" + capPoolKey + ".json", GSON.toJson(capPool) + "\n");
|
||||
@@ -172,26 +174,25 @@ public final class JigsawStudioProjectCreator {
|
||||
JigsawStudioCellDimensions dimensions = options.cellDimensions();
|
||||
String piecePoolKey = options.structureKey() + "/pieces";
|
||||
IrisJigsawPool piecePool = new IrisJigsawPool();
|
||||
for (int connectorCount = 0; connectorCount <= SPATIAL_CONNECTOR_ORDER.size(); connectorCount++) {
|
||||
String key = options.structureKey() + "/"
|
||||
+ (connectorCount == 0 ? "start" : "connectors-" + connectorCount);
|
||||
IrisJigsawPiece piece = spatialPiece(
|
||||
key,
|
||||
piecePoolKey,
|
||||
dimensions,
|
||||
connectorCount);
|
||||
if (options.compatibilityTarget() == JigsawStudioCompatibilityTarget.IRIS_EXTENDED) {
|
||||
piece.getThemes().add("variant-1");
|
||||
boolean irisExtended = options.compatibilityTarget() == JigsawStudioCompatibilityTarget.IRIS_EXTENDED;
|
||||
for (SpatialVerticalClass vertical : SpatialVerticalClass.values()) {
|
||||
for (JigsawPlanarArchetype horizontal : JigsawPlanarArchetype.values()) {
|
||||
String key = archetypeKey(options, horizontal) + vertical.keySuffix();
|
||||
IrisJigsawPiece piece = spatialPiece(key, piecePoolKey, dimensions, horizontal, vertical);
|
||||
if (irisExtended) {
|
||||
piece.getThemes().add("variant-1");
|
||||
piece.setRules(new IrisJigsawPieceRules().setMaximumPlacements(SPATIAL_MAXIMUM_PLACEMENTS));
|
||||
}
|
||||
pool.getPieces().add(new IrisJigsawPieceEntry(key, 1));
|
||||
if (!piece.getConnectors().isEmpty()) {
|
||||
piecePool.getPieces().add(new IrisJigsawPieceEntry(key, 1));
|
||||
}
|
||||
bundle.resource("objects/" + key + ".iob", serialize(new IrisObject(
|
||||
dimensions.width(),
|
||||
dimensions.height(),
|
||||
dimensions.depth())));
|
||||
bundle.textResource("jigsaw-pieces/" + key + ".json", GSON.toJson(piece) + "\n");
|
||||
}
|
||||
pool.getPieces().add(new IrisJigsawPieceEntry(key, 1));
|
||||
if (connectorCount > 0) {
|
||||
piecePool.getPieces().add(new IrisJigsawPieceEntry(key, 1));
|
||||
}
|
||||
bundle.resource("objects/" + key + ".iob", serialize(new IrisObject(
|
||||
dimensions.width(),
|
||||
dimensions.height(),
|
||||
dimensions.depth())));
|
||||
bundle.textResource("jigsaw-pieces/" + key + ".json", GSON.toJson(piece) + "\n");
|
||||
}
|
||||
piecePool.getPieces().add(new IrisJigsawPieceEntry().setEmpty(true));
|
||||
bundle.textResource("jigsaw-pools/" + piecePoolKey + ".json", GSON.toJson(piecePool) + "\n");
|
||||
@@ -201,30 +202,40 @@ public final class JigsawStudioProjectCreator {
|
||||
String objectKey,
|
||||
String poolKey,
|
||||
JigsawStudioCellDimensions dimensions,
|
||||
int connectorCount
|
||||
JigsawPlanarArchetype horizontal,
|
||||
SpatialVerticalClass vertical
|
||||
) {
|
||||
IrisJigsawPiece piece = new IrisJigsawPiece()
|
||||
.setDisplayName(connectorCount + (connectorCount == 1 ? " Connector" : " Connectors"))
|
||||
.setDisplayName(horizontal.displayName() + vertical.displaySuffix())
|
||||
.setObject(objectKey)
|
||||
.setRotatable(true)
|
||||
.setRules(new IrisJigsawPieceRules().setMaximumPlacements(16));
|
||||
for (int index = 0; index < connectorCount; index++) {
|
||||
IrisDirection direction = SPATIAL_CONNECTOR_ORDER.get(index);
|
||||
piece.getConnectors().add(new IrisJigsawConnector()
|
||||
.setPosition(spatialConnectorPosition(dimensions, direction))
|
||||
.setDirection(direction)
|
||||
.setTop(direction.isVertical()
|
||||
? IrisDirection.NORTH_NEGATIVE_Z
|
||||
: IrisDirection.UP_POSITIVE_Y)
|
||||
.setPool(poolKey)
|
||||
.setName("iris:spatial")
|
||||
.setTargetName("iris:spatial")
|
||||
.setJoint(JigsawJoint.ROLLABLE)
|
||||
.setFinalState("minecraft:structure_void"));
|
||||
.setRotatable(true);
|
||||
for (JigsawPlanarDirection planarDirection : horizontal.canonicalTopology().directions()) {
|
||||
piece.getConnectors().add(spatialConnector(dimensions, planarDirection.irisDirection(), poolKey));
|
||||
}
|
||||
for (IrisDirection direction : vertical.directions()) {
|
||||
piece.getConnectors().add(spatialConnector(dimensions, direction, poolKey));
|
||||
}
|
||||
return piece;
|
||||
}
|
||||
|
||||
private static IrisJigsawConnector spatialConnector(
|
||||
JigsawStudioCellDimensions dimensions,
|
||||
IrisDirection direction,
|
||||
String poolKey
|
||||
) {
|
||||
return new IrisJigsawConnector()
|
||||
.setPosition(spatialConnectorPosition(dimensions, direction))
|
||||
.setDirection(direction)
|
||||
.setTop(direction.isVertical()
|
||||
? IrisDirection.NORTH_NEGATIVE_Z
|
||||
: IrisDirection.UP_POSITIVE_Y)
|
||||
.setPool(poolKey)
|
||||
.setName("iris:spatial")
|
||||
.setTargetName("iris:spatial")
|
||||
.setJoint(JigsawJoint.ROLLABLE)
|
||||
.setFinalState("minecraft:structure_void");
|
||||
}
|
||||
|
||||
private static IrisPosition spatialConnectorPosition(
|
||||
JigsawStudioCellDimensions dimensions,
|
||||
IrisDirection direction
|
||||
@@ -242,6 +253,10 @@ public final class JigsawStudioProjectCreator {
|
||||
};
|
||||
}
|
||||
|
||||
private static String archetypeKey(Options options, JigsawPlanarArchetype archetype) {
|
||||
return options.structureKey() + "/" + archetype.name().toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
private static IrisJigsawPiece planarPiece(
|
||||
String objectKey,
|
||||
String poolKey,
|
||||
@@ -291,4 +306,33 @@ public final class JigsawStudioProjectCreator {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
private enum SpatialVerticalClass {
|
||||
NONE("", "", List.of()),
|
||||
UP("-up", " + Up", List.of(IrisDirection.UP_POSITIVE_Y)),
|
||||
DOWN("-down", " + Down", List.of(IrisDirection.DOWN_NEGATIVE_Y)),
|
||||
UP_DOWN("-up-down", " + Up/Down", List.of(IrisDirection.UP_POSITIVE_Y, IrisDirection.DOWN_NEGATIVE_Y));
|
||||
|
||||
private final String keySuffix;
|
||||
private final String displaySuffix;
|
||||
private final List<IrisDirection> directions;
|
||||
|
||||
SpatialVerticalClass(String keySuffix, String displaySuffix, List<IrisDirection> directions) {
|
||||
this.keySuffix = keySuffix;
|
||||
this.displaySuffix = displaySuffix;
|
||||
this.directions = directions;
|
||||
}
|
||||
|
||||
private String keySuffix() {
|
||||
return keySuffix;
|
||||
}
|
||||
|
||||
private String displaySuffix() {
|
||||
return displaySuffix;
|
||||
}
|
||||
|
||||
private List<IrisDirection> directions() {
|
||||
return directions;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -128,7 +128,7 @@ final class JigsawStudioProtection {
|
||||
|
||||
static boolean blocksMutatingCommand(UUID ownerId, UUID actorId, String commandLine) {
|
||||
return !ownerMatches(ownerId, actorId)
|
||||
&& !isSafeNonOwnerCommand(commandLine);
|
||||
&& isMutatingCommand(commandLine);
|
||||
}
|
||||
|
||||
static boolean blocksNonEditableWorkcellMutation(
|
||||
@@ -231,17 +231,25 @@ final class JigsawStudioProtection {
|
||||
}
|
||||
|
||||
boolean isUnauthorizedStudioEdit(Player player, Block block) {
|
||||
if (player == null || block == null) {
|
||||
String refusal = studioEditRefusal(player, block);
|
||||
if (refusal.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
message(player, refusal);
|
||||
return true;
|
||||
}
|
||||
|
||||
String studioEditRefusal(Player player, Block block) {
|
||||
if (player == null || block == null) {
|
||||
return "";
|
||||
}
|
||||
ActiveStudio studio = service.studios.get(block.getWorld().getUID());
|
||||
if (studio == null) {
|
||||
return false;
|
||||
return "";
|
||||
}
|
||||
UUID requestId = studio.generator().getRequest().requestId();
|
||||
if (service.previewRenderer.contains(requestId, block.getX(), block.getY(), block.getZ())) {
|
||||
message(player, "The seed-1337 Jigsaw preview is read-only and refreshes automatically.");
|
||||
return true;
|
||||
return "The seed-1337 Jigsaw preview is read-only and refreshes automatically.";
|
||||
}
|
||||
JigsawStudioSession session = studio.generator().getSession();
|
||||
JigsawStudioBay bay = session.layout().findAt(block.getX(), block.getY(), block.getZ());
|
||||
@@ -249,24 +257,20 @@ final class JigsawStudioProtection {
|
||||
? null
|
||||
: session.activeVariant(bay.stableId()).orElse(null);
|
||||
if (bay != null && (variant == null || !variant.owned())) {
|
||||
message(player, variant == null
|
||||
return variant == null
|
||||
? "This empty Jigsaw Studio workcell has no loaded editable variant. Load one before editing it."
|
||||
: "This Jigsaw Studio variant is read-only. Adopt or clone its graph before editing it.");
|
||||
return true;
|
||||
: "This Jigsaw Studio variant is read-only. Adopt or clone its graph before editing it.";
|
||||
}
|
||||
if (service.saveLifecycle.reopenRequiredRequests.contains(requestId)) {
|
||||
message(player, "Close and reopen Jigsaw Studio before editing the resized layout.");
|
||||
return true;
|
||||
return "Close and reopen Jigsaw Studio before editing the resized layout.";
|
||||
}
|
||||
if (materializationInProgress(studio)) {
|
||||
message(player, "Wait for the current Jigsaw Studio variant load or rollback to finish.");
|
||||
return true;
|
||||
return "Wait for the current Jigsaw Studio variant load or rollback to finish.";
|
||||
}
|
||||
if (!blocksStudioEdit(studio.generator().getRequest().ownerId(), player.getUniqueId())) {
|
||||
return false;
|
||||
return "";
|
||||
}
|
||||
message(player, "This Jigsaw Studio is owned by another player session.");
|
||||
return true;
|
||||
return "This Jigsaw Studio is owned by another player session.";
|
||||
}
|
||||
|
||||
boolean materializationInProgress(ActiveStudio studio) {
|
||||
|
||||
+40
-7
@@ -105,6 +105,7 @@ final class JigsawStudioProtectionListener implements Listener {
|
||||
ActiveStudio studio = service.studios.get(event.getWorld().getUID());
|
||||
if (studio != null) {
|
||||
service.registry.markChunkAvailable(studio, event.getChunk().getX(), event.getChunk().getZ());
|
||||
service.floorPainter.refreshChunk(studio, event.getChunk().getX(), event.getChunk().getZ());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,6 +120,7 @@ final class JigsawStudioProtectionListener implements Listener {
|
||||
service.toolbelt.closeMenu(event.getPlayer());
|
||||
service.tileWatcher.finalizeJigsawTileWatchesForPlayer(event.getPlayer().getUniqueId());
|
||||
service.tripleSneakTracker.clearPlayer(event.getPlayer().getUniqueId());
|
||||
service.connectorTool.clearPlayer(event.getPlayer().getUniqueId());
|
||||
service.visualization.visualizationLoops.remove(event.getPlayer().getUniqueId());
|
||||
service.visualization.assemblyPreviews.remove(event.getPlayer().getUniqueId());
|
||||
service.playerContext.playerWorkcells.remove(event.getPlayer().getUniqueId());
|
||||
@@ -133,6 +135,7 @@ final class JigsawStudioProtectionListener implements Listener {
|
||||
service.tileWatcher.finalizeJigsawTileWatchesForPlayer(playerId);
|
||||
service.tripleSneakTracker.clearPlayer(playerId);
|
||||
service.toolbelt.toolConfirmations.remove(playerId);
|
||||
service.connectorTool.clearPlayer(playerId);
|
||||
service.visualization.visualizationLoops.remove(playerId);
|
||||
service.particlesDisabled.remove(playerId);
|
||||
service.visualization.assemblyPreviews.remove(playerId);
|
||||
@@ -170,15 +173,29 @@ final class JigsawStudioProtectionListener implements Listener {
|
||||
|| (event.getClickedBlock() != null && event.useInteractedBlock() == Event.Result.DENY)) {
|
||||
return;
|
||||
}
|
||||
if (event.getHand() != EquipmentSlot.HAND
|
||||
|| (event.getAction() != Action.RIGHT_CLICK_AIR
|
||||
&& event.getAction() != Action.RIGHT_CLICK_BLOCK)) {
|
||||
if (event.getHand() != EquipmentSlot.HAND || event.getAction() == Action.PHYSICAL) {
|
||||
return;
|
||||
}
|
||||
boolean leftClick = event.getAction() == Action.LEFT_CLICK_AIR
|
||||
|| event.getAction() == Action.LEFT_CLICK_BLOCK;
|
||||
Optional<JigsawStudioToolPayload> decoded = service.toolbelt.toolCodec.decode(event.getItem());
|
||||
if (decoded.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (decoded.get().action() == JigsawStudioToolAction.CONNECTOR_TOOL) {
|
||||
service.tileWatcher.finalizeJigsawTileWatchesForPlayer(event.getPlayer().getUniqueId());
|
||||
event.setCancelled(true);
|
||||
service.connectorTool.handleClick(
|
||||
event.getPlayer(),
|
||||
decoded.get(),
|
||||
leftClick,
|
||||
event.getClickedBlock(),
|
||||
event.getBlockFace());
|
||||
return;
|
||||
}
|
||||
if (leftClick) {
|
||||
return;
|
||||
}
|
||||
service.tileWatcher.finalizeJigsawTileWatchesForPlayer(event.getPlayer().getUniqueId());
|
||||
event.setCancelled(true);
|
||||
service.toolbelt.useTool(event.getPlayer(), decoded.get(), event.getItem(), event.getPlayer().isSneaking());
|
||||
@@ -237,6 +254,13 @@ final class JigsawStudioProtectionListener implements Listener {
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onProtectedBlockBreak(BlockBreakEvent event) {
|
||||
if (service.studios.containsKey(event.getBlock().getWorld().getUID())
|
||||
&& service.toolbelt.toolCodec.decode(event.getPlayer().getInventory().getItemInMainHand())
|
||||
.filter(payload -> payload.action() == JigsawStudioToolAction.CONNECTOR_TOOL)
|
||||
.isPresent()) {
|
||||
event.setCancelled(true);
|
||||
return;
|
||||
}
|
||||
if (service.protection.isControlChest(event.getBlock())) {
|
||||
event.setCancelled(true);
|
||||
message(event.getPlayer(), "The Jigsaw Studio control chest cannot be removed.");
|
||||
@@ -264,9 +288,17 @@ final class JigsawStudioProtectionListener implements Listener {
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onUnauthorizedInteract(PlayerInteractEvent event) {
|
||||
if (event.getAction() == Action.RIGHT_CLICK_BLOCK
|
||||
&& !service.protection.isControlChest(event.getClickedBlock())
|
||||
&& service.protection.isUnauthorizedStudioEdit(event.getPlayer(), event.getClickedBlock())) {
|
||||
if (event.getAction() != Action.RIGHT_CLICK_BLOCK
|
||||
|| service.protection.isControlChest(event.getClickedBlock())) {
|
||||
return;
|
||||
}
|
||||
if (ProtectionProbe.isProbe(event)) {
|
||||
if (!service.protection.studioEditRefusal(event.getPlayer(), event.getClickedBlock()).isEmpty()) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (service.protection.isUnauthorizedStudioEdit(event.getPlayer(), event.getClickedBlock())) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
@@ -337,7 +369,8 @@ final class JigsawStudioProtectionListener implements Listener {
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onProtectedPistonExtend(BlockPistonExtendEvent event) {
|
||||
if (service.protection.materializationInProgress(service.studios.get(event.getBlock().getWorld().getUID()))
|
||||
|| service.protection.movesProtectedBlocks(event.getBlocks(), event.getDirection())) {
|
||||
|| service.protection.movesProtectedBlocks(event.getBlocks(), event.getDirection())
|
||||
|| service.protection.isImmutableStudioBlock(event.getBlock().getRelative(event.getDirection()))) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
+5
@@ -129,6 +129,11 @@ final class JigsawStudioResourceBundleAssembler {
|
||||
return new Assembly(bundle.build(), objectKey, piece, StructureHash.sha256(manifestContent));
|
||||
}
|
||||
|
||||
static byte[] ownedResourceContent(Path packRoot, String relativePath) throws IOException {
|
||||
Path root = Objects.requireNonNull(packRoot, "Jigsaw Studio pack root").toAbsolutePath().normalize();
|
||||
return Files.readAllBytes(resolveOwnedResource(root, relativePath));
|
||||
}
|
||||
|
||||
private static Set<String> resourcePaths(StructurePackageClosure closure) {
|
||||
Set<String> resources = new LinkedHashSet<>();
|
||||
addPaths(resources, "structures", closure.structures(), ".json");
|
||||
|
||||
@@ -286,7 +286,7 @@ final class JigsawStudioSaveLifecycle {
|
||||
String selected = session.selectedBayId().orElse(null);
|
||||
if (selected == null) {
|
||||
Location location = player.getLocation();
|
||||
JigsawStudioBay underPlayer = session.layout().findAt(
|
||||
JigsawStudioBay underPlayer = session.layout().findColumn(
|
||||
location.getBlockX(), location.getBlockY(), location.getBlockZ());
|
||||
if (underPlayer != null) {
|
||||
selected = underPlayer.stableId();
|
||||
|
||||
@@ -73,6 +73,8 @@ public final class JigsawStudioService implements IrisService, JigsawStudioMenuA
|
||||
final JigsawStudioAutosaveScheduler autosaveScheduler = new JigsawStudioAutosaveScheduler(this);
|
||||
final JigsawStudioTileWatcher tileWatcher = new JigsawStudioTileWatcher(this);
|
||||
final JigsawStudioVisualization visualization = new JigsawStudioVisualization(this);
|
||||
final JigsawStudioFloorPainter floorPainter = new JigsawStudioFloorPainter(this);
|
||||
final JigsawStudioConnectorTool connectorTool = new JigsawStudioConnectorTool(this);
|
||||
final JigsawStudioPlayerContext playerContext = new JigsawStudioPlayerContext(this);
|
||||
|
||||
public static JigsawStudioService get() {
|
||||
|
||||
@@ -4,6 +4,7 @@ import java.util.Objects;
|
||||
|
||||
public enum JigsawStudioToolAction {
|
||||
OPEN_MENU("Open Control Menu", false),
|
||||
CONNECTOR_TOOL("Connector Tool", false),
|
||||
SELECT_WORKCELL("Select Workcell", false),
|
||||
TOGGLE_WORKCELL("Toggle Workcell", false),
|
||||
LOAD_VARIANT("Load Variant", false),
|
||||
|
||||
@@ -140,6 +140,13 @@ public final class JigsawStudioToolCodec {
|
||||
if (payload.amount() != 0) {
|
||||
lore.add(ChatColor.GRAY + "Amount: " + payload.amount());
|
||||
}
|
||||
if (payload.action() == JigsawStudioToolAction.CONNECTOR_TOOL) {
|
||||
lore.add(ChatColor.YELLOW + "Right-click: place, select, or move a connector");
|
||||
lore.add(ChatColor.YELLOW + "Left-click: remove a connector");
|
||||
lore.add(ChatColor.GRAY + "Faces out of the clicked face; sneak to face into it");
|
||||
lore.add(ChatColor.GRAY + "Planar: right-click a side to seat its socket");
|
||||
return List.copyOf(lore);
|
||||
}
|
||||
lore.add(payload.action().destructive()
|
||||
? ChatColor.RED + "Right-click twice to confirm"
|
||||
: ChatColor.YELLOW + "Right-click to use");
|
||||
|
||||
@@ -29,6 +29,7 @@ import static art.arcane.iris.studio.jigsaw.JigsawStudioVariantProperties.canRes
|
||||
|
||||
final class JigsawStudioToolbelt {
|
||||
private static final long TOOL_CONFIRM_NANOS = 10_000_000_000L;
|
||||
private static final long TOOL_CONFIRM_MINIMUM_NANOS = 500_000_000L;
|
||||
|
||||
private final JigsawStudioService service;
|
||||
final Map<UUID, DeferredDuplication> deferredDuplications = new ConcurrentHashMap<>();
|
||||
@@ -184,13 +185,27 @@ final class JigsawStudioToolbelt {
|
||||
message(player, "This Jigsaw Studio tool is stale or belongs to another session.");
|
||||
return false;
|
||||
}
|
||||
if (payload.action().destructive() && !confirmTool(player, payload)) {
|
||||
message(player, "Right-click the same tool again within 10 seconds to confirm "
|
||||
+ payload.action().displayName() + ".");
|
||||
return true;
|
||||
if (payload.action().destructive()) {
|
||||
ToolConfirmationState confirmation = confirmTool(player, payload);
|
||||
if (confirmation == ToolConfirmationState.ARMED) {
|
||||
message(player, "Right-click the same tool again within 10 seconds to confirm "
|
||||
+ payload.action().displayName() + ".");
|
||||
return true;
|
||||
}
|
||||
if (confirmation == ToolConfirmationState.HELD) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (requiresBoundVariant(payload) && activeOwnedVariant(
|
||||
player,
|
||||
studio,
|
||||
payload.workcellId(),
|
||||
payload.pieceKey()) == null) {
|
||||
return false;
|
||||
}
|
||||
return switch (payload.action()) {
|
||||
case OPEN_MENU -> openControlMenu(player);
|
||||
case CONNECTOR_TOOL -> service.connectorTool.handleClick(player, payload, false, null, null);
|
||||
case SELECT_WORKCELL -> selectWorkcell(player, payload.workcellId());
|
||||
case LOAD_VARIANT -> service.variantEditor.switchVariant(
|
||||
player,
|
||||
@@ -349,19 +364,40 @@ final class JigsawStudioToolbelt {
|
||||
return state.isPresent() && service.workcellEditor.setRequireCaps(player, !state.get().requireCaps());
|
||||
}
|
||||
|
||||
private boolean confirmTool(Player player, JigsawStudioToolPayload payload) {
|
||||
private ToolConfirmationState confirmTool(Player player, JigsawStudioToolPayload payload) {
|
||||
long now = System.nanoTime();
|
||||
ToolConfirmation previous = toolConfirmations.get(player.getUniqueId());
|
||||
if (previous != null
|
||||
&& previous.expiresAtNanos() >= now
|
||||
&& previous.payload().equals(payload)) {
|
||||
ToolConfirmationState state = confirmationState(previous, payload, now);
|
||||
if (state == ToolConfirmationState.CONFIRMED) {
|
||||
toolConfirmations.remove(player.getUniqueId(), previous);
|
||||
return true;
|
||||
} else if (state == ToolConfirmationState.ARMED) {
|
||||
toolConfirmations.put(
|
||||
player.getUniqueId(),
|
||||
new ToolConfirmation(payload, now, now + TOOL_CONFIRM_NANOS));
|
||||
}
|
||||
toolConfirmations.put(
|
||||
player.getUniqueId(),
|
||||
new ToolConfirmation(payload, now + TOOL_CONFIRM_NANOS));
|
||||
return false;
|
||||
return state;
|
||||
}
|
||||
|
||||
static ToolConfirmationState confirmationState(
|
||||
ToolConfirmation previous,
|
||||
JigsawStudioToolPayload payload,
|
||||
long nowNanos
|
||||
) {
|
||||
if (previous == null
|
||||
|| previous.expiresAtNanos() < nowNanos
|
||||
|| !previous.payload().equals(payload)) {
|
||||
return ToolConfirmationState.ARMED;
|
||||
}
|
||||
return nowNanos - previous.armedAtNanos() < TOOL_CONFIRM_MINIMUM_NANOS
|
||||
? ToolConfirmationState.HELD
|
||||
: ToolConfirmationState.CONFIRMED;
|
||||
}
|
||||
|
||||
static boolean requiresBoundVariant(JigsawStudioToolPayload payload) {
|
||||
return !payload.pieceKey().isEmpty() && switch (payload.action()) {
|
||||
case DUPLICATE_VARIANT, TOGGLE_ROTATION, EXPAND_TO_CELL -> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
JigsawStudioVariant activeOwnedVariant(
|
||||
@@ -677,7 +713,13 @@ final class JigsawStudioToolbelt {
|
||||
}
|
||||
}
|
||||
|
||||
record ToolConfirmation(JigsawStudioToolPayload payload, long expiresAtNanos) {
|
||||
enum ToolConfirmationState {
|
||||
ARMED,
|
||||
HELD,
|
||||
CONFIRMED
|
||||
}
|
||||
|
||||
record ToolConfirmation(JigsawStudioToolPayload payload, long armedAtNanos, long expiresAtNanos) {
|
||||
ToolConfirmation {
|
||||
Objects.requireNonNull(payload, "Jigsaw Studio tool confirmation payload");
|
||||
}
|
||||
|
||||
@@ -105,6 +105,9 @@ final class JigsawStudioToolboxMenu {
|
||||
"Jigsaw Studio toolbox workcell");
|
||||
UUID requestId = menu.requestId();
|
||||
List<ToolboxTool> tools = new ArrayList<>();
|
||||
tools.add(new ToolboxTool(
|
||||
"Connector Tool",
|
||||
JigsawStudioToolPayload.request(JigsawStudioToolAction.CONNECTOR_TOOL, requestId)));
|
||||
tools.add(new ToolboxTool(
|
||||
"Open Control Menu",
|
||||
JigsawStudioToolPayload.request(JigsawStudioToolAction.OPEN_MENU, requestId)));
|
||||
@@ -216,6 +219,15 @@ final class JigsawStudioToolboxMenu {
|
||||
selected.stableId(),
|
||||
active.pieceKey())));
|
||||
}
|
||||
if (menu.mode() == JigsawStudioMode.SPATIAL_JIGSAW && menu.canDeleteVariant(selected, active)) {
|
||||
tools.add(new ToolboxTool(
|
||||
"Delete " + active.displayName(),
|
||||
JigsawStudioToolPayload.variant(
|
||||
JigsawStudioToolAction.DELETE_VARIANT,
|
||||
requestId,
|
||||
selected.stableId(),
|
||||
active.pieceKey())));
|
||||
}
|
||||
if (active.owned()) {
|
||||
for (JigsawStudioMenuState.Membership membership : active.memberships()) {
|
||||
addMembershipTools(
|
||||
@@ -255,7 +267,7 @@ final class JigsawStudioToolboxMenu {
|
||||
selected.stableId(),
|
||||
variant.pieceKey())));
|
||||
}
|
||||
if (variant.owned() && selected.variants().size() > 1) {
|
||||
if (menu.canDeleteVariant(selected, variant)) {
|
||||
tools.add(new ToolboxTool(
|
||||
"Delete " + variant.displayName(),
|
||||
JigsawStudioToolPayload.variant(
|
||||
|
||||
+34
-1
@@ -174,12 +174,20 @@ final class JigsawStudioVariantProperties {
|
||||
return false;
|
||||
}
|
||||
JigsawStudioSession session = studio.generator().getSession();
|
||||
boolean rotatable = !variant.rotatable();
|
||||
if (variant.dimensions().isPresent()) {
|
||||
String footprintFailure = spatialFootprintFailure(studio, variant,
|
||||
new JigsawStudioGraphMapper.SpatialChange(variant.pieceKey(), variant.dimensions().get(), rotatable));
|
||||
if (!footprintFailure.isEmpty()) {
|
||||
message(player, footprintFailure);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
String reservationFailure = service.graphMutations.beginGraphMutation(studio, session);
|
||||
if (!reservationFailure.isEmpty()) {
|
||||
message(player, reservationFailure);
|
||||
return false;
|
||||
}
|
||||
boolean rotatable = !variant.rotatable();
|
||||
return service.graphMutations.scheduleGraphMutation(
|
||||
player,
|
||||
studio,
|
||||
@@ -268,6 +276,12 @@ final class JigsawStudioVariantProperties {
|
||||
+ dimensions.width() + "x" + dimensions.height() + "x" + dimensions.depth() + ".");
|
||||
return false;
|
||||
}
|
||||
String footprintFailure = spatialFootprintFailure(studio, variant,
|
||||
new JigsawStudioGraphMapper.SpatialChange(variant.pieceKey(), dimensions, variant.rotatable()));
|
||||
if (!footprintFailure.isEmpty()) {
|
||||
message(player, footprintFailure);
|
||||
return false;
|
||||
}
|
||||
boolean active = session.activeVariant(workcellId)
|
||||
.map(activeVariant -> activeVariant.pieceKey().equals(pieceKey))
|
||||
.orElse(false);
|
||||
@@ -536,4 +550,23 @@ final class JigsawStudioVariantProperties {
|
||||
|| canonical.height() < target.height()
|
||||
|| canonical.depth() < target.depth());
|
||||
}
|
||||
|
||||
private static String spatialFootprintFailure(
|
||||
ActiveStudio studio,
|
||||
JigsawStudioVariant variant,
|
||||
JigsawStudioGraphMapper.SpatialChange change
|
||||
) {
|
||||
if (variant.mode() != JigsawStudioMode.SPATIAL_JIGSAW) {
|
||||
return "";
|
||||
}
|
||||
IrisStructure structure = loadStudioStructure(studio);
|
||||
if (structure == null) {
|
||||
return "The structure could not be loaded to check the spatial workcell size.";
|
||||
}
|
||||
return JigsawStudioGraphMapper.spatialFootprintFailure(
|
||||
studio.generator().getRequest().source(),
|
||||
JigsawStudioGraphMapper.configuredCellDimensions(structure),
|
||||
studio.generator().getSession().layout().variantCatalog(),
|
||||
change);
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
@@ -35,6 +36,9 @@ final class JigsawStudioVisualization {
|
||||
private static final Color VALID_CONNECTOR_COLOR = Color.LIME;
|
||||
private static final Color ASSEMBLY_PREVIEW_COLOR = Color.fromRGB(180, 90, 255);
|
||||
private static final Color LIVE_PREVIEW_WARNING_COLOR = Color.fromRGB(255, 180, 55);
|
||||
private static final Color TOOL_SELECTED_COLOR = Color.YELLOW;
|
||||
private static final Color TOOL_PLACE_COLOR = Color.WHITE;
|
||||
private static final Color TOOL_EXISTING_COLOR = Color.ORANGE;
|
||||
private static final long ASSEMBLY_PREVIEW_MILLIS = 10_000L;
|
||||
|
||||
private final JigsawStudioService service;
|
||||
@@ -124,7 +128,7 @@ final class JigsawStudioVisualization {
|
||||
JigsawStudioSession session = studio.generator().getSession();
|
||||
JigsawStudioLayout layout = session.layout();
|
||||
Location playerLocation = player.getLocation();
|
||||
JigsawStudioBay focused = layout.findAt(
|
||||
JigsawStudioBay focused = layout.findColumn(
|
||||
playerLocation.getBlockX(),
|
||||
playerLocation.getBlockY(),
|
||||
playerLocation.getBlockZ());
|
||||
@@ -134,6 +138,7 @@ final class JigsawStudioVisualization {
|
||||
drawConnectors(player, playerLocation, studio, focused, budget);
|
||||
drawBounds(player, playerLocation, focused.bounds(), focusedColor, 1.15F, 1.0D, budget);
|
||||
}
|
||||
drawConnectorToolFeedback(player, playerLocation, studio, budget);
|
||||
drawLivePreview(player, playerLocation, studio, budget);
|
||||
drawAssemblyPreview(player, playerLocation, budget);
|
||||
for (JigsawStudioBay bay : layout.bays()) {
|
||||
@@ -148,6 +153,57 @@ final class JigsawStudioVisualization {
|
||||
}
|
||||
}
|
||||
|
||||
private void drawConnectorToolFeedback(
|
||||
Player player,
|
||||
Location playerLocation,
|
||||
ActiveStudio studio,
|
||||
ParticleBudget budget
|
||||
) {
|
||||
UUID requestId = studio.generator().getRequest().requestId();
|
||||
boolean holdingTool = service.toolbelt.toolCodec.decode(player.getInventory().getItemInMainHand())
|
||||
.filter(payload -> payload.action() == JigsawStudioToolAction.CONNECTOR_TOOL
|
||||
&& payload.requestId().equals(requestId))
|
||||
.isPresent();
|
||||
Optional<JigsawStudioConnectorTool.Feedback> feedback =
|
||||
service.connectorTool.feedback(player, studio, holdingTool);
|
||||
if (feedback.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
feedback.get().selected().ifPresent(cell ->
|
||||
drawCell(player, playerLocation, cell, TOOL_SELECTED_COLOR, budget));
|
||||
feedback.get().hover().ifPresent(hover -> {
|
||||
Color color = hover.existingConnector() ? TOOL_EXISTING_COLOR : TOOL_PLACE_COLOR;
|
||||
drawCell(player, playerLocation, hover.cell(), color, budget);
|
||||
drawConnectorLine(
|
||||
player,
|
||||
playerLocation,
|
||||
hover.cell().x() + 0.5D,
|
||||
hover.cell().y() + 0.5D,
|
||||
hover.cell().z() + 0.5D,
|
||||
hover.facing(),
|
||||
color,
|
||||
budget);
|
||||
});
|
||||
}
|
||||
|
||||
private static void drawCell(
|
||||
Player player,
|
||||
Location playerLocation,
|
||||
JigsawStudioConnectorPlacement.Cell cell,
|
||||
Color color,
|
||||
ParticleBudget budget
|
||||
) {
|
||||
drawBounds(
|
||||
player,
|
||||
playerLocation,
|
||||
new JigsawStudioPreviewRenderer.PreviewBounds(
|
||||
cell.x(), cell.y(), cell.z(), cell.x(), cell.y(), cell.z()),
|
||||
color,
|
||||
0.75F,
|
||||
0.25D,
|
||||
budget);
|
||||
}
|
||||
|
||||
private void drawLivePreview(
|
||||
Player player,
|
||||
Location playerLocation,
|
||||
|
||||
@@ -114,10 +114,6 @@ final class JigsawStudioWorkcellEditor {
|
||||
message(player, "Connector blocks cannot reset: " + validationFailure);
|
||||
return false;
|
||||
}
|
||||
if (rendered.connectors().isEmpty()) {
|
||||
message(player, "The active variant has no saved connectors to reset.");
|
||||
return false;
|
||||
}
|
||||
String reservationFailure = service.materializer.beginConnectorRepair(studio);
|
||||
if (!reservationFailure.isEmpty()) {
|
||||
message(player, reservationFailure);
|
||||
@@ -125,7 +121,7 @@ final class JigsawStudioWorkcellEditor {
|
||||
}
|
||||
boolean connectorsVisible = session.workcellSnapshot(workcellId).connectorsVisible();
|
||||
message(player, "Resetting " + rendered.connectors().size()
|
||||
+ " connector block(s) from the last saved iteration...");
|
||||
+ " connector block(s) from the last saved iteration and clearing unsaved markers...");
|
||||
return service.materializer.scheduleConnectorRepair(
|
||||
player,
|
||||
studio,
|
||||
@@ -383,6 +379,16 @@ final class JigsawStudioWorkcellEditor {
|
||||
message(player, "Unknown Jigsaw Studio workcell '" + workcellId + "'.");
|
||||
return false;
|
||||
}
|
||||
JigsawStudioVariant spatialVariant = workcell.archetype().isPresent()
|
||||
? null
|
||||
: session.activeVariant(workcellId).orElse(null);
|
||||
if (spatialVariant != null) {
|
||||
return service.variantProperties.updateVariantDisplayName(
|
||||
player,
|
||||
workcellId,
|
||||
spatialVariant.pieceKey(),
|
||||
displayName);
|
||||
}
|
||||
String normalizedName;
|
||||
try {
|
||||
normalizedName = JigsawStudioGraphEditor.normalizeDisplayName(displayName);
|
||||
|
||||
@@ -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,
|
||||
|
||||
+27
@@ -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);
|
||||
}
|
||||
|
||||
+155
-131
@@ -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);
|
||||
}
|
||||
}
|
||||
+101
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+383
@@ -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());
|
||||
}
|
||||
}
|
||||
+12
-9
@@ -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(
|
||||
|
||||
+1
-1
@@ -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();
|
||||
|
||||
+48
-29
@@ -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);
|
||||
|
||||
+12
-12
@@ -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);
|
||||
|
||||
+8
-4
@@ -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);
|
||||
}
|
||||
}
|
||||
+99
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user