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
+4762
-430
No files matched your search
@@ -23,6 +23,7 @@ import art.arcane.iris.structure.authoring.StructureKey;
|
|||||||
import art.arcane.iris.structure.authoring.StructureWriteResult;
|
import art.arcane.iris.structure.authoring.StructureWriteResult;
|
||||||
import art.arcane.iris.structure.export.VanillaJigsawExportFormat;
|
import art.arcane.iris.structure.export.VanillaJigsawExportFormat;
|
||||||
import art.arcane.iris.structure.export.VanillaJigsawExportRequest;
|
import art.arcane.iris.structure.export.VanillaJigsawExportRequest;
|
||||||
|
import art.arcane.iris.structure.export.VanillaJigsawExportSettings;
|
||||||
import art.arcane.iris.structure.export.VanillaJigsawExportSource;
|
import art.arcane.iris.structure.export.VanillaJigsawExportSource;
|
||||||
import art.arcane.iris.structure.graph.PlanarJigsawWorkcellResolver;
|
import art.arcane.iris.structure.graph.PlanarJigsawWorkcellResolver;
|
||||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||||
@@ -437,13 +438,14 @@ public class CommandJigsaw implements DirectorExecutor {
|
|||||||
}
|
}
|
||||||
VanillaJigsawExportRequest request;
|
VanillaJigsawExportRequest request;
|
||||||
try {
|
try {
|
||||||
request = VanillaJigsawExportRequest.builder(
|
VanillaJigsawExportSource source = VanillaJigsawExportSource.forData(
|
||||||
VanillaJigsawExportSource.forData(
|
context.request().source(), context.request().structureKey());
|
||||||
context.request().source(), context.request().structureKey()), destination)
|
request = VanillaJigsawExportRequest.builder(source, destination)
|
||||||
.namespace(namespace)
|
.namespace(namespace)
|
||||||
.resourcePath(context.request().structureKey())
|
.resourcePath(context.request().structureKey())
|
||||||
.format(exportFormat)
|
.format(exportFormat)
|
||||||
.replaceExisting(replace)
|
.replaceExisting(replace)
|
||||||
|
.settings(VanillaJigsawExportSettings.runtimeDefaults(source.loadStructure()))
|
||||||
.build();
|
.build();
|
||||||
} catch (IllegalArgumentException exception) {
|
} catch (IllegalArgumentException exception) {
|
||||||
sendError(exception.getMessage());
|
sendError(exception.getMessage());
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
package art.arcane.iris.structure.export;
|
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.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
@@ -41,6 +44,13 @@ public final class VanillaJigsawExportSettings {
|
|||||||
return builder().build();
|
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() {
|
public List<String> biomes() {
|
||||||
return 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.")
|
@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";
|
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;
|
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.")
|
@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.")
|
@Description("The orientation-independent connector shape represented by a planar jigsaw workcell.")
|
||||||
public enum IrisJigsawWorkcellArchetype {
|
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),
|
BLANK(0),
|
||||||
|
|
||||||
@Description("One-connector dead-end pieces; the Studio bay seeds a single north connector and new projects mark these as terminal caps.")
|
@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 + "'");
|
+ "' references missing connector pool '" + connector.pool + "'");
|
||||||
}
|
}
|
||||||
|
|
||||||
AttachmentResult attachment = attachOne(
|
AttachmentResult attachment = attach(
|
||||||
pending,
|
pending,
|
||||||
placed,
|
placed,
|
||||||
placementCounts,
|
placementCounts,
|
||||||
@@ -229,7 +229,9 @@ public final class StructureAssembler {
|
|||||||
depth,
|
depth,
|
||||||
selectedTheme,
|
selectedTheme,
|
||||||
rng);
|
rng);
|
||||||
|
if (attachment.state() != AttachmentState.TERMINATED) {
|
||||||
consume(consumedConnectors, connector.sourcePiece(), connector.sourceConnector());
|
consume(consumedConnectors, connector.sourcePiece(), connector.sourceConnector());
|
||||||
|
}
|
||||||
if (attachment.state() == AttachmentState.FAILED) {
|
if (attachment.state() == AttachmentState.FAILED) {
|
||||||
return StructureAssemblyResult.failed(
|
return StructureAssemblyResult.failed(
|
||||||
StructureAssemblyStatus.FAILED_UNCAPPED,
|
StructureAssemblyStatus.FAILED_UNCAPPED,
|
||||||
@@ -250,6 +252,49 @@ public final class StructureAssembler {
|
|||||||
return StructureAssemblyResult.complete(placed, selectedTheme);
|
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(
|
private AttachmentResult attachOne(
|
||||||
Deque<PieceExpansion> pending,
|
Deque<PieceExpansion> pending,
|
||||||
KList<PlacedStructurePiece> placed,
|
KList<PlacedStructurePiece> placed,
|
||||||
@@ -260,6 +305,7 @@ public final class StructureAssembler {
|
|||||||
OpenConnector connector,
|
OpenConnector connector,
|
||||||
int depth,
|
int depth,
|
||||||
String selectedTheme,
|
String selectedTheme,
|
||||||
|
boolean neighborClosures,
|
||||||
RNG rng
|
RNG rng
|
||||||
) {
|
) {
|
||||||
if (pool.getPieces() == null) {
|
if (pool.getPieces() == null) {
|
||||||
@@ -281,6 +327,7 @@ public final class StructureAssembler {
|
|||||||
selectedTheme,
|
selectedTheme,
|
||||||
false,
|
false,
|
||||||
!fallbackRequired,
|
!fallbackRequired,
|
||||||
|
neighborClosures,
|
||||||
rng);
|
rng);
|
||||||
if (primary != AttachmentState.NO_MATCH) {
|
if (primary != AttachmentState.NO_MATCH) {
|
||||||
return AttachmentResult.success(primary);
|
return AttachmentResult.success(primary);
|
||||||
@@ -322,6 +369,7 @@ public final class StructureAssembler {
|
|||||||
selectedTheme,
|
selectedTheme,
|
||||||
fallbackRequired,
|
fallbackRequired,
|
||||||
!fallbackRequired,
|
!fallbackRequired,
|
||||||
|
neighborClosures,
|
||||||
rng);
|
rng);
|
||||||
if (fallbackState != AttachmentState.NO_MATCH) {
|
if (fallbackState != AttachmentState.NO_MATCH) {
|
||||||
return AttachmentResult.success(fallbackState);
|
return AttachmentResult.success(fallbackState);
|
||||||
@@ -352,9 +400,14 @@ public final class StructureAssembler {
|
|||||||
String selectedTheme,
|
String selectedTheme,
|
||||||
boolean terminalOnly,
|
boolean terminalOnly,
|
||||||
boolean allowEmpty,
|
boolean allowEmpty,
|
||||||
|
boolean neighborClosures,
|
||||||
RNG rng
|
RNG rng
|
||||||
) {
|
) {
|
||||||
int ruleDepth = Math.min(connector.pieceDepth, structure.getMaxDepth());
|
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(
|
for (IrisJigsawPieceEntry entry : weightedOrder(
|
||||||
pool,
|
pool,
|
||||||
selectedTheme,
|
selectedTheme,
|
||||||
@@ -391,21 +444,16 @@ public final class StructureAssembler {
|
|||||||
requireConnector(pieceConnector, pieceName);
|
requireConnector(pieceConnector, pieceName);
|
||||||
}
|
}
|
||||||
List<IrisJigsawConnector> candidateConnectors = orderedConnectors(piece.getConnectors(), null);
|
List<IrisJigsawConnector> candidateConnectors = orderedConnectors(piece.getConnectors(), null);
|
||||||
|
int[] rotations = piece.isRotatable() ? shuffledRotations(rng) : NO_ROTATION;
|
||||||
|
for (int yDeg : rotations) {
|
||||||
|
IrisObjectRotation rot = IrisObjectRotation.of(0, yDeg, 0);
|
||||||
for (IrisJigsawConnector cb : candidateConnectors) {
|
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())) {
|
if (!Objects.equals(connector.channel, cb.getChannel())) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!cb.getName().equals(connector.targetName)) {
|
if (!cb.getName().equals(connector.targetName)) {
|
||||||
continue;
|
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);
|
|
||||||
IrisDirection rotatedFace = rot.rotate(cb.getDirection());
|
IrisDirection rotatedFace = rot.rotate(cb.getDirection());
|
||||||
if (rotatedFace != needed) {
|
if (rotatedFace != needed) {
|
||||||
continue;
|
continue;
|
||||||
@@ -431,7 +479,8 @@ public final class StructureAssembler {
|
|||||||
consumedConnectors,
|
consumedConnectors,
|
||||||
connector,
|
connector,
|
||||||
candidate,
|
candidate,
|
||||||
cb);
|
cb,
|
||||||
|
neighborClosures);
|
||||||
if (closures == null) {
|
if (closures == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -504,9 +553,10 @@ public final class StructureAssembler {
|
|||||||
Map<PlacedStructurePiece, Set<IrisJigsawConnector>> consumedConnectors,
|
Map<PlacedStructurePiece, Set<IrisJigsawConnector>> consumedConnectors,
|
||||||
OpenConnector attachmentSource,
|
OpenConnector attachmentSource,
|
||||||
PlacedStructurePiece candidatePiece,
|
PlacedStructurePiece candidatePiece,
|
||||||
IrisJigsawConnector attachmentConnector
|
IrisJigsawConnector attachmentConnector,
|
||||||
|
boolean neighborClosures
|
||||||
) {
|
) {
|
||||||
if (!structure.isRequireCaps()) {
|
if (!neighborClosures) {
|
||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
List<ConnectorClosure> closures = new ArrayList<>();
|
List<ConnectorClosure> closures = new ArrayList<>();
|
||||||
@@ -1025,10 +1075,7 @@ public final class StructureAssembler {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private int[] rotationCandidates(JigsawJoint joint, RNG rng) {
|
private int[] shuffledRotations(RNG rng) {
|
||||||
if (joint == JigsawJoint.ALIGNED) {
|
|
||||||
return Y_DEGREES;
|
|
||||||
}
|
|
||||||
int[] shuffled = {0, 90, 180, 270};
|
int[] shuffled = {0, 90, 180, 270};
|
||||||
for (int i = shuffled.length - 1; i > 0; i--) {
|
for (int i = shuffled.length - 1; i > 0; i--) {
|
||||||
int j = rng.i(i + 1);
|
int j = rng.i(i + 1);
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
package art.arcane.iris.studio.generation;
|
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.JigsawStudioActivation;
|
||||||
import art.arcane.iris.studio.jigsaw.JigsawStudioBay;
|
import art.arcane.iris.studio.jigsaw.JigsawStudioBay;
|
||||||
import art.arcane.iris.studio.jigsaw.JigsawStudioBounds;
|
import art.arcane.iris.studio.jigsaw.JigsawStudioBounds;
|
||||||
import art.arcane.iris.studio.jigsaw.JigsawStudioCellDimensions;
|
import art.arcane.iris.studio.jigsaw.JigsawStudioCellDimensions;
|
||||||
import art.arcane.iris.studio.jigsaw.JigsawStudioControlPosition;
|
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.JigsawStudioLayout;
|
||||||
import art.arcane.iris.studio.jigsaw.JigsawStudioSession;
|
import art.arcane.iris.studio.jigsaw.JigsawStudioSession;
|
||||||
import art.arcane.iris.studio.jigsaw.JigsawStudioVariant;
|
import art.arcane.iris.studio.jigsaw.JigsawStudioVariant;
|
||||||
@@ -51,9 +50,7 @@ public final class JigsawStudioGenerator extends EnginedStudioGenerator {
|
|||||||
private final NativeBlockState lightFloor;
|
private final NativeBlockState lightFloor;
|
||||||
private final NativeBlockState darkFloor;
|
private final NativeBlockState darkFloor;
|
||||||
private final NativeBlockState frame;
|
private final NativeBlockState frame;
|
||||||
private final NativeBlockState topologyBase;
|
private final FloorPalette floorPalette;
|
||||||
private final NativeBlockState topologyPath;
|
|
||||||
private final NativeBlockState connectorCap;
|
|
||||||
private final NativeBlockState invalidMarker;
|
private final NativeBlockState invalidMarker;
|
||||||
private final NativeBlockState controlChest;
|
private final NativeBlockState controlChest;
|
||||||
private final FinalStateRenderer finalStateRenderer;
|
private final FinalStateRenderer finalStateRenderer;
|
||||||
@@ -79,9 +76,13 @@ public final class JigsawStudioGenerator extends EnginedStudioGenerator {
|
|||||||
B.getState("minecraft:smooth_stone"),
|
B.getState("minecraft:smooth_stone"),
|
||||||
B.getState("minecraft:polished_deepslate"),
|
B.getState("minecraft:polished_deepslate"),
|
||||||
B.getState("minecraft:white_concrete"),
|
B.getState("minecraft:white_concrete"),
|
||||||
|
new FloorPalette(
|
||||||
B.getState("minecraft:light_gray_wool"),
|
B.getState("minecraft:light_gray_wool"),
|
||||||
B.getState("minecraft:red_wool"),
|
B.getState("minecraft:red_wool"),
|
||||||
B.getState("minecraft:sea_lantern"),
|
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:red_stained_glass"),
|
||||||
B.getState("minecraft:chest[facing=south,type=single,waterlogged=false]"),
|
B.getState("minecraft:chest[facing=south,type=single,waterlogged=false]"),
|
||||||
JigsawStudioGenerator::rotatedFinalState
|
JigsawStudioGenerator::rotatedFinalState
|
||||||
@@ -95,9 +96,7 @@ public final class JigsawStudioGenerator extends EnginedStudioGenerator {
|
|||||||
NativeBlockState lightFloor,
|
NativeBlockState lightFloor,
|
||||||
NativeBlockState darkFloor,
|
NativeBlockState darkFloor,
|
||||||
NativeBlockState frame,
|
NativeBlockState frame,
|
||||||
NativeBlockState topologyBase,
|
FloorPalette floorPalette,
|
||||||
NativeBlockState topologyPath,
|
|
||||||
NativeBlockState connectorCap,
|
|
||||||
NativeBlockState invalidMarker,
|
NativeBlockState invalidMarker,
|
||||||
NativeBlockState controlChest,
|
NativeBlockState controlChest,
|
||||||
FinalStateRenderer finalStateRenderer
|
FinalStateRenderer finalStateRenderer
|
||||||
@@ -108,9 +107,7 @@ public final class JigsawStudioGenerator extends EnginedStudioGenerator {
|
|||||||
this.lightFloor = Objects.requireNonNull(lightFloor, "Jigsaw Studio light floor block");
|
this.lightFloor = Objects.requireNonNull(lightFloor, "Jigsaw Studio light floor block");
|
||||||
this.darkFloor = Objects.requireNonNull(darkFloor, "Jigsaw Studio dark floor block");
|
this.darkFloor = Objects.requireNonNull(darkFloor, "Jigsaw Studio dark floor block");
|
||||||
this.frame = Objects.requireNonNull(frame, "Jigsaw Studio frame block");
|
this.frame = Objects.requireNonNull(frame, "Jigsaw Studio frame block");
|
||||||
this.topologyBase = Objects.requireNonNull(topologyBase, "Jigsaw Studio topology base block");
|
this.floorPalette = Objects.requireNonNull(floorPalette, "Jigsaw Studio floor palette");
|
||||||
this.topologyPath = Objects.requireNonNull(topologyPath, "Jigsaw Studio topology path block");
|
|
||||||
this.connectorCap = Objects.requireNonNull(connectorCap, "Jigsaw Studio connector cap block");
|
|
||||||
this.invalidMarker = Objects.requireNonNull(invalidMarker, "Jigsaw Studio invalid marker block");
|
this.invalidMarker = Objects.requireNonNull(invalidMarker, "Jigsaw Studio invalid marker block");
|
||||||
this.controlChest = Objects.requireNonNull(controlChest, "Jigsaw Studio control chest block");
|
this.controlChest = Objects.requireNonNull(controlChest, "Jigsaw Studio control chest block");
|
||||||
this.finalStateRenderer = Objects.requireNonNull(
|
this.finalStateRenderer = Objects.requireNonNull(
|
||||||
@@ -165,6 +162,21 @@ public final class JigsawStudioGenerator extends EnginedStudioGenerator {
|
|||||||
return renderedBays.computeIfAbsent(key, ignored -> buildRenderedBay(activeWorkcell, variant));
|
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) {
|
public void invalidateRender(String workcellId) {
|
||||||
if (workcellId != null) {
|
if (workcellId != null) {
|
||||||
renderedBays.keySet().removeIf(key -> key.workcellId().equals(workcellId));
|
renderedBays.keySet().removeIf(key -> key.workcellId().equals(workcellId));
|
||||||
@@ -215,10 +227,8 @@ public final class JigsawStudioGenerator extends EnginedStudioGenerator {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
paintCage(terrainChunk, workcell, chunkWorldX, chunkWorldZ);
|
paintCage(terrainChunk, workcell, chunkWorldX, chunkWorldZ);
|
||||||
if (workcell.archetype().isPresent()) {
|
|
||||||
paintTopologyGlyph(terrainChunk, workcell, chunkWorldX, chunkWorldZ);
|
|
||||||
}
|
|
||||||
RenderedBay renderedBay = renderBay(workcell);
|
RenderedBay renderedBay = renderBay(workcell);
|
||||||
|
paintFloorGlyph(terrainChunk, workcell, glyphFor(renderedBay), chunkWorldX, chunkWorldZ);
|
||||||
if (!renderedBay.valid()) {
|
if (!renderedBay.valid()) {
|
||||||
paintInvalidBay(terrainChunk, workcell, chunkWorldX, chunkWorldZ);
|
paintInvalidBay(terrainChunk, workcell, chunkWorldX, chunkWorldZ);
|
||||||
continue;
|
continue;
|
||||||
@@ -345,85 +355,44 @@ public final class JigsawStudioGenerator extends EnginedStudioGenerator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void paintTopologyGlyph(
|
private void paintFloorGlyph(
|
||||||
TerrainChunk terrainChunk,
|
TerrainChunk terrainChunk,
|
||||||
JigsawStudioBay workcell,
|
JigsawStudioBay workcell,
|
||||||
|
JigsawStudioFloorGlyph glyph,
|
||||||
int chunkWorldX,
|
int chunkWorldX,
|
||||||
int chunkWorldZ
|
int chunkWorldZ
|
||||||
) {
|
) {
|
||||||
JigsawPlanarTopology topology = workcell.topology().orElseThrow();
|
|
||||||
JigsawStudioBounds bounds = workcell.bounds();
|
JigsawStudioBounds bounds = workcell.bounds();
|
||||||
int minimumX = Math.max(bounds.originX(), chunkWorldX);
|
int minimumX = Math.max(bounds.originX(), chunkWorldX);
|
||||||
int maximumX = Math.min(bounds.maxX(), chunkWorldX + 15);
|
int maximumX = Math.min(bounds.maxX(), chunkWorldX + 15);
|
||||||
int minimumZ = Math.max(bounds.originZ(), chunkWorldZ);
|
int minimumZ = Math.max(bounds.originZ(), chunkWorldZ);
|
||||||
int maximumZ = Math.min(bounds.maxZ(), chunkWorldZ + 15);
|
int maximumZ = Math.min(bounds.maxZ(), chunkWorldZ + 15);
|
||||||
if (minimumX > maximumX || minimumZ > maximumZ) {
|
int floorY = bounds.originY() - 1;
|
||||||
return;
|
|
||||||
}
|
|
||||||
int glyphY = bounds.originY() - 1;
|
|
||||||
int centerX = bounds.dimensions().width() / 2;
|
|
||||||
int centerZ = bounds.dimensions().depth() / 2;
|
|
||||||
for (int worldX = minimumX; worldX <= maximumX; worldX++) {
|
for (int worldX = minimumX; worldX <= maximumX; worldX++) {
|
||||||
int localX = worldX - bounds.originX();
|
|
||||||
for (int worldZ = minimumZ; worldZ <= maximumZ; worldZ++) {
|
for (int worldZ = minimumZ; worldZ <= maximumZ; worldZ++) {
|
||||||
int localZ = worldZ - bounds.originZ();
|
NativeBlockState state = floorState(glyph.cell(
|
||||||
NativeBlockState state = topologyGlyphState(topology, bounds.dimensions(), localX, localZ,
|
worldX - bounds.originX(),
|
||||||
centerX, centerZ);
|
worldZ - bounds.originZ()));
|
||||||
setWorldBlock(terrainChunk, worldX, glyphY, worldZ, state, chunkWorldX, chunkWorldZ);
|
setWorldBlock(terrainChunk, worldX, floorY, worldZ, state, chunkWorldX, chunkWorldZ);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private NativeBlockState topologyGlyphState(
|
private static JigsawStudioFloorGlyph glyphFor(RenderedBay renderedBay) {
|
||||||
JigsawPlanarTopology topology,
|
if (!renderedBay.valid() || renderedBay.connectors().isEmpty()) {
|
||||||
JigsawStudioCellDimensions dimensions,
|
return JigsawStudioFloorGlyph.empty();
|
||||||
int localX,
|
|
||||||
int localZ,
|
|
||||||
int centerX,
|
|
||||||
int centerZ
|
|
||||||
) {
|
|
||||||
if (isConnectorCap(topology, dimensions, localX, localZ, centerX, centerZ)) {
|
|
||||||
return connectorCap;
|
|
||||||
}
|
}
|
||||||
if (isTopologyPath(topology, localX, localZ, centerX, centerZ)) {
|
List<JigsawStudioFloorGlyph.Marker> markers = new ArrayList<>(renderedBay.connectors().size());
|
||||||
return topologyPath;
|
for (RenderedConnector connector : renderedBay.connectors()) {
|
||||||
|
markers.add(new JigsawStudioFloorGlyph.Marker(
|
||||||
|
connector.x(),
|
||||||
|
connector.z(),
|
||||||
|
connector.connector().getDirection()));
|
||||||
}
|
}
|
||||||
return topologyBase;
|
return JigsawStudioFloorGlyph.of(
|
||||||
}
|
renderedBay.dimensions().width(),
|
||||||
|
renderedBay.dimensions().depth(),
|
||||||
static boolean isConnectorCap(
|
markers);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private RenderedBay buildRenderedBay(JigsawStudioBay workcell, JigsawStudioVariant variant) {
|
private RenderedBay buildRenderedBay(JigsawStudioBay workcell, JigsawStudioVariant variant) {
|
||||||
@@ -720,7 +689,7 @@ public final class JigsawStudioGenerator extends EnginedStudioGenerator {
|
|||||||
|
|
||||||
private JigsawStudioBay requireWorkcell(JigsawStudioBay workcell) {
|
private JigsawStudioBay requireWorkcell(JigsawStudioBay workcell) {
|
||||||
JigsawStudioBay activeWorkcell = Objects.requireNonNull(workcell, "Jigsaw Studio 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");
|
throw new IllegalArgumentException("Workcell does not belong to this Jigsaw Studio layout");
|
||||||
}
|
}
|
||||||
return activeWorkcell;
|
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 record RenderedBlock(int x, int y, int z, NativeBlockState state, TileData tileData) {
|
||||||
public RenderedBlock {
|
public RenderedBlock {
|
||||||
state = Objects.requireNonNull(state, "Jigsaw Studio rendered block state");
|
state = Objects.requireNonNull(state, "Jigsaw Studio rendered block state");
|
||||||
|
|||||||
@@ -141,6 +141,10 @@ final class JigsawStudioCapture {
|
|||||||
if (hiddenConnector != null) {
|
if (hiddenConnector != null) {
|
||||||
hiddenConnector.setFinalState(blockData.getAsString());
|
hiddenConnector.setFinalState(blockData.getAsString());
|
||||||
connectors.add(CapturedConnector.from(hiddenConnector));
|
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) {
|
if (hiddenConnector == null && blockData instanceof Jigsaw jigsaw) {
|
||||||
KMap<String, Object> nbt = BukkitPlatform.serializeTile(block.getLocation());
|
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 x,
|
||||||
int y,
|
int y,
|
||||||
int z,
|
int z,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import art.arcane.iris.structure.object.IrisObject;
|
|||||||
import art.arcane.iris.studio.generation.JigsawStudioGenerator;
|
import art.arcane.iris.studio.generation.JigsawStudioGenerator;
|
||||||
import art.arcane.iris.spi.IrisLogging;
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
import art.arcane.iris.world.task.J;
|
import art.arcane.iris.world.task.J;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
import org.bukkit.World;
|
import org.bukkit.World;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
@@ -122,7 +123,7 @@ final class JigsawStudioCaptureScheduler {
|
|||||||
}
|
}
|
||||||
coordinator.accept(snapshot);
|
coordinator.accept(snapshot);
|
||||||
} catch (Throwable exception) {
|
} 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);
|
+ area.chunkX() + "," + area.chunkZ() + ": " + failureMessage(exception), exception);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -219,7 +220,7 @@ final class JigsawStudioCaptureScheduler {
|
|||||||
if (!result.successful()) {
|
if (!result.successful()) {
|
||||||
String failure = writeFailure(result);
|
String failure = writeFailure(result);
|
||||||
coordinator.recordPersistentFailure(failure, result.failure().orElse(null));
|
coordinator.recordPersistentFailure(failure, result.failure().orElse(null));
|
||||||
message(player, failure);
|
message(coordinator.notifiedPlayer(), failure);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
source.invalidateStructureResources();
|
source.invalidateStructureResources();
|
||||||
@@ -234,13 +235,14 @@ final class JigsawStudioCaptureScheduler {
|
|||||||
+ "Close and reopen this project before continuing: "
|
+ "Close and reopen this project before continuing: "
|
||||||
+ failureMessage(refreshFailure));
|
+ failureMessage(refreshFailure));
|
||||||
}
|
}
|
||||||
|
service.floorPainter.refresh(studio, bay.stableId());
|
||||||
String cleanup = result.status() == StructureWriteResult.Status.COMMITTED_CLEANUP_REQUIRED
|
String cleanup = result.status() == StructureWriteResult.Status.COMMITTED_CLEANUP_REQUIRED
|
||||||
? " The graph committed, but transaction cleanup requires operator attention in the console."
|
? " The graph committed, but transaction cleanup requires operator attention in the console."
|
||||||
: "";
|
: "";
|
||||||
String mutationNotice = unchanged ? "" : " Newer edits remain unsaved.";
|
String mutationNotice = unchanged ? "" : " Newer edits remain unsaved.";
|
||||||
message(player, "Saved piece '" + coordinator.saveIdentity().variantKey() + "' and object '"
|
message(player, "Saved piece '" + coordinator.saveIdentity().variantKey() + "' and object '"
|
||||||
+ assembly.objectKey() + "' atomically." + mutationNotice + cleanup);
|
+ assembly.objectKey() + "' atomically." + mutationNotice + cleanup);
|
||||||
playSaveSound(player);
|
playSaveSound(coordinator.notifiedPlayer());
|
||||||
if (unchanged) {
|
if (unchanged) {
|
||||||
service.evaluator.scheduleEvaluation(studio);
|
service.evaluator.scheduleEvaluation(studio);
|
||||||
}
|
}
|
||||||
@@ -250,12 +252,19 @@ final class JigsawStudioCaptureScheduler {
|
|||||||
} catch (Throwable exception) {
|
} catch (Throwable exception) {
|
||||||
String failure = "Jigsaw Studio save failed: " + failureMessage(exception);
|
String failure = "Jigsaw Studio save failed: " + failureMessage(exception);
|
||||||
coordinator.recordPersistentFailure(failure, exception);
|
coordinator.recordPersistentFailure(failure, exception);
|
||||||
message(player, failure);
|
message(coordinator.notifiedPlayer(), failure);
|
||||||
} finally {
|
} finally {
|
||||||
coordinator.complete();
|
coordinator.complete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Player notificationTarget(Player player, UUID ownerId) {
|
||||||
|
if (player != null || ownerId == null) {
|
||||||
|
return player;
|
||||||
|
}
|
||||||
|
return Bukkit.getPlayer(ownerId);
|
||||||
|
}
|
||||||
|
|
||||||
static Set<Long> requiredChunks(
|
static Set<Long> requiredChunks(
|
||||||
JigsawStudioBay bay,
|
JigsawStudioBay bay,
|
||||||
JigsawStudioGenerator.RenderedBay rendered
|
JigsawStudioGenerator.RenderedBay rendered
|
||||||
@@ -368,6 +377,10 @@ final class JigsawStudioCaptureScheduler {
|
|||||||
return work.player();
|
return work.player();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Player notifiedPlayer() {
|
||||||
|
return notificationTarget(work.player(), work.studio().generator().getRequest().ownerId());
|
||||||
|
}
|
||||||
|
|
||||||
private UUID requestId() {
|
private UUID requestId() {
|
||||||
return work.requestId();
|
return work.requestId();
|
||||||
}
|
}
|
||||||
@@ -427,7 +440,7 @@ final class JigsawStudioCaptureScheduler {
|
|||||||
}
|
}
|
||||||
recordPersistentFailure(failure, exception);
|
recordPersistentFailure(failure, exception);
|
||||||
complete();
|
complete();
|
||||||
message(player(), failure);
|
message(notifiedPlayer(), failure);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void recordPersistentFailure(String failure, Throwable exception) {
|
private void recordPersistentFailure(String failure, Throwable exception) {
|
||||||
|
|||||||
@@ -40,20 +40,42 @@ final class JigsawStudioChunkWriter {
|
|||||||
private JigsawStudioChunkWriter() {
|
private JigsawStudioChunkWriter() {
|
||||||
}
|
}
|
||||||
|
|
||||||
static void restoreConnectorChunk(
|
static void restoreChunkPositions(
|
||||||
World world,
|
World world,
|
||||||
JigsawStudioBay workcell,
|
JigsawStudioBay workcell,
|
||||||
List<JigsawStudioGenerator.RenderedConnector> connectors,
|
Set<LocalPosition> positions,
|
||||||
|
Map<LocalPosition, JigsawStudioGenerator.RenderedConnector> renderedConnectors,
|
||||||
Map<LocalPosition, JigsawStudioGenerator.RenderedBlock> renderedBlocks,
|
Map<LocalPosition, JigsawStudioGenerator.RenderedBlock> renderedBlocks,
|
||||||
boolean connectorsVisible
|
boolean connectorsVisible
|
||||||
) throws IOException {
|
) throws IOException {
|
||||||
JigsawStudioBounds bounds = workcell.bounds();
|
JigsawStudioBounds bounds = workcell.bounds();
|
||||||
for (JigsawStudioGenerator.RenderedConnector connector : connectors) {
|
BlockData air = null;
|
||||||
LocalPosition position = new LocalPosition(connector.x(), connector.y(), connector.z());
|
for (LocalPosition position : positions) {
|
||||||
Block target = world.getBlockAt(
|
Block target = world.getBlockAt(
|
||||||
bounds.originX() + connector.x(),
|
bounds.originX() + position.x(),
|
||||||
bounds.originY() + connector.y(),
|
bounds.originY() + position.y(),
|
||||||
bounds.originZ() + connector.z());
|
bounds.originZ() + position.z());
|
||||||
|
JigsawStudioGenerator.RenderedConnector connector = renderedConnectors.get(position);
|
||||||
|
JigsawStudioGenerator.RenderedBlock renderedBlock = renderedBlocks.get(position);
|
||||||
|
if (connector != null) {
|
||||||
|
restoreConnector(target, connector, renderedBlock, connectorsVisible);
|
||||||
|
} else if (renderedBlock != null) {
|
||||||
|
restoreRenderedBlock(target, renderedBlock);
|
||||||
|
} else {
|
||||||
|
if (air == null) {
|
||||||
|
air = Material.AIR.createBlockData();
|
||||||
|
}
|
||||||
|
target.setBlockData(air, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void restoreConnector(
|
||||||
|
Block target,
|
||||||
|
JigsawStudioGenerator.RenderedConnector connector,
|
||||||
|
JigsawStudioGenerator.RenderedBlock renderedBlock,
|
||||||
|
boolean connectorsVisible
|
||||||
|
) throws IOException {
|
||||||
if (connectorsVisible) {
|
if (connectorsVisible) {
|
||||||
BlockData marker;
|
BlockData marker;
|
||||||
try {
|
try {
|
||||||
@@ -65,35 +87,39 @@ final class JigsawStudioChunkWriter {
|
|||||||
}
|
}
|
||||||
target.setBlockData(marker, false);
|
target.setBlockData(marker, false);
|
||||||
BukkitPlatform.deserializeTile(markerNbt(connector.connector()), target.getLocation());
|
BukkitPlatform.deserializeTile(markerNbt(connector.connector()), target.getLocation());
|
||||||
continue;
|
return;
|
||||||
}
|
}
|
||||||
JigsawStudioGenerator.RenderedBlock renderedBlock = renderedBlocks.get(position);
|
|
||||||
BlockData restored;
|
|
||||||
if (renderedBlock != null) {
|
if (renderedBlock != null) {
|
||||||
if (renderedBlock.state().isCustom()
|
restoreRenderedBlock(target, renderedBlock);
|
||||||
|| !(renderedBlock.state().nativeHandle() instanceof BlockData blockData)) {
|
return;
|
||||||
throw new IOException("Saved connector block '" + renderedBlock.state().key()
|
|
||||||
+ "' cannot be restored directly in Bukkit Studio");
|
|
||||||
}
|
}
|
||||||
restored = blockData;
|
BlockData restored;
|
||||||
} else {
|
|
||||||
try {
|
try {
|
||||||
restored = Bukkit.createBlockData(connector.connector().getFinalState());
|
restored = Bukkit.createBlockData(connector.connector().getFinalState());
|
||||||
} catch (IllegalArgumentException exception) {
|
} catch (IllegalArgumentException exception) {
|
||||||
throw new IOException("Invalid saved connector final state '"
|
throw new IOException("Invalid saved connector final state '"
|
||||||
+ connector.connector().getFinalState() + "'", exception);
|
+ connector.connector().getFinalState() + "'", exception);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
target.setBlockData(restored, false);
|
target.setBlockData(restored, false);
|
||||||
if (renderedBlock != null && renderedBlock.tileData() != null) {
|
}
|
||||||
|
|
||||||
|
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();
|
TileData tileData = renderedBlock.tileData();
|
||||||
if (!tileData.isApplicable(target.getBlockData()) || !tileData.toBukkitTry(target)) {
|
if (tileData != null
|
||||||
throw new IOException("Saved connector tile data could not be restored at "
|
&& (!tileData.isApplicable(target.getBlockData()) || !tileData.toBukkitTry(target))) {
|
||||||
|
throw new IOException("Saved tile data could not be restored at "
|
||||||
+ target.getX() + "," + target.getY() + "," + target.getZ());
|
+ target.getX() + "," + target.getY() + "," + target.getZ());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static String validateMaterialization(JigsawStudioGenerator.RenderedBay rendered) {
|
static String validateMaterialization(JigsawStudioGenerator.RenderedBay rendered) {
|
||||||
if (!rendered.valid()) {
|
if (!rendered.valid()) {
|
||||||
|
|||||||
@@ -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);
|
window.setElement(2, 0, size);
|
||||||
|
|
||||||
boolean lastVariant = workcell.variants().size() <= 1;
|
boolean spatial = state.mode() == JigsawStudioMode.SPATIAL_JIGSAW;
|
||||||
boolean deletionAvailable = variant.owned() && !variant.active() && !lastVariant;
|
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(
|
boolean confirmed = deletionAvailable && controller.isDeleteConfirmed(
|
||||||
window.getViewer().getUniqueId(),
|
window.getViewer().getUniqueId(),
|
||||||
state.requestId(),
|
state.requestId(),
|
||||||
@@ -170,11 +173,11 @@ final class JigsawStudioDetailsMenu {
|
|||||||
deletionAvailable ? confirmed ? Material.REDSTONE : Material.LAVA_BUCKET : Material.BARRIER,
|
deletionAvailable ? confirmed ? Material.REDSTONE : Material.LAVA_BUCKET : Material.BARRIER,
|
||||||
deletionAvailable
|
deletionAvailable
|
||||||
? confirmed ? ChatColor.RED + "Confirm Delete Variant" : ChatColor.RED + "Delete Variant"
|
? confirmed ? ChatColor.RED + "Confirm Delete Variant" : ChatColor.RED + "Delete Variant"
|
||||||
: variant.active()
|
: !variant.owned()
|
||||||
? ChatColor.GRAY + "Loaded Variant Cannot Be Deleted"
|
? ChatColor.GRAY + "Deletion is Read-only"
|
||||||
: lastVariant
|
: lastVariant
|
||||||
? ChatColor.GRAY + "Last Variant Cannot Be Deleted"
|
? ChatColor.GRAY + "Last Variant Cannot Be Deleted"
|
||||||
: ChatColor.GRAY + "Deletion is Read-only")
|
: ChatColor.GRAY + "Loaded Variant Cannot Be Deleted")
|
||||||
.setEnchanted(confirmed);
|
.setEnchanted(confirmed);
|
||||||
if (deletionAvailable) {
|
if (deletionAvailable) {
|
||||||
delete.addLore(confirmed
|
delete.addLore(confirmed
|
||||||
@@ -186,10 +189,10 @@ final class JigsawStudioDetailsMenu {
|
|||||||
workcell.stableId(),
|
workcell.stableId(),
|
||||||
variant.pieceKey(),
|
variant.pieceKey(),
|
||||||
page));
|
page));
|
||||||
} else if (variant.active()) {
|
} else if (variant.owned() && lastVariant) {
|
||||||
delete.addLore(ChatColor.GRAY + "Load another variant in this workcell first.");
|
|
||||||
} else if (lastVariant) {
|
|
||||||
delete.addLore(ChatColor.GRAY + "Create another variant before deleting this one.");
|
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);
|
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.PLANAR_JIGSAW
|
||||||
: JigsawStudioMode.SPATIAL_JIGSAW;
|
: JigsawStudioMode.SPATIAL_JIGSAW;
|
||||||
JigsawStudioVariantCatalog catalog = catalog(data, structure, mode);
|
JigsawStudioVariantCatalog catalog = catalog(data, structure, mode);
|
||||||
IrisPosition configuredCell = structure.getCellSize();
|
JigsawStudioCellDimensions dimensions = configuredCellDimensions(structure);
|
||||||
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()));
|
|
||||||
if (mode == JigsawStudioMode.SPATIAL_JIGSAW) {
|
if (mode == JigsawStudioMode.SPATIAL_JIGSAW) {
|
||||||
dimensions = expandSpatialDimensions(data, catalog, dimensions);
|
dimensions = expandSpatialDimensions(data, catalog, dimensions);
|
||||||
return JigsawStudioLayout.createSpatial(
|
return JigsawStudioLayout.createSpatial(
|
||||||
@@ -129,26 +123,54 @@ public final class JigsawStudioGraphMapper {
|
|||||||
if (data.getObjectLoader() == null) {
|
if (data.getObjectLoader() == null) {
|
||||||
return configured;
|
return configured;
|
||||||
}
|
}
|
||||||
int width = configured.width();
|
SpatialExtent extent = SpatialExtent.of(configured);
|
||||||
int height = configured.height();
|
|
||||||
int depth = configured.depth();
|
|
||||||
for (JigsawStudioVariant variant : catalog.spatialVariants()) {
|
for (JigsawStudioVariant variant : catalog.spatialVariants()) {
|
||||||
IrisObject object = data.getObjectLoader().load(variant.objectKey());
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
int objectWidth = object.getW();
|
IrisObject object = data.getObjectLoader() == null ? null : data.getObjectLoader().load(variant.objectKey());
|
||||||
int objectDepth = object.getD();
|
if (object != null) {
|
||||||
if (variant.rotatable()) {
|
extent = extent.include(object.getW(), object.getH(), object.getD(), variant.rotatable());
|
||||||
int horizontalSpan = Math.max(objectWidth, objectDepth);
|
|
||||||
objectWidth = horizontalSpan;
|
|
||||||
objectDepth = horizontalSpan;
|
|
||||||
}
|
}
|
||||||
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(
|
private static void traversePools(
|
||||||
@@ -378,4 +400,25 @@ public final class JigsawStudioGraphMapper {
|
|||||||
return new OwnedResources(Set.of(), Set.of(), Set.of(), false);
|
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()) {
|
for (JigsawStudioBay workcell : result.layout().bays()) {
|
||||||
service.playerContext.refreshWorkcellContext(studio.worldId(), workcell.stableId());
|
service.playerContext.refreshWorkcellContext(studio.worldId(), workcell.stableId());
|
||||||
}
|
}
|
||||||
|
service.floorPainter.refreshAll(studio);
|
||||||
service.evaluator.scheduleEvaluation(studio);
|
service.evaluator.scheduleEvaluation(studio);
|
||||||
message(player, result.message());
|
message(player, result.message());
|
||||||
finishGraphMutation(requestId);
|
finishGraphMutation(requestId);
|
||||||
@@ -273,15 +274,20 @@ final class JigsawStudioGraphMutations {
|
|||||||
String failure,
|
String failure,
|
||||||
Throwable cause
|
Throwable cause
|
||||||
) {
|
) {
|
||||||
boolean scheduled = J.runEntity(
|
Runnable abandon = () -> {
|
||||||
player,
|
|
||||||
() -> completeLiveRelayout(player, studio, requestId, result, failure, cause));
|
|
||||||
if (!scheduled) {
|
|
||||||
service.saveLifecycle.reopenRequiredRequests.add(requestId);
|
service.saveLifecycle.reopenRequiredRequests.add(requestId);
|
||||||
finishGraphMutation(requestId);
|
finishGraphMutation(requestId);
|
||||||
if (cause != null) {
|
if (cause != null) {
|
||||||
IrisLogging.reportError(cause);
|
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 PLANAR_GAP = 1;
|
||||||
public static final int MAX_VARIANTS = 512;
|
public static final int MAX_VARIANTS = 512;
|
||||||
public static final String SPATIAL_WORKCELL_ID = "workcell/spatial";
|
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 int FIRST_ORIGIN = 16;
|
||||||
private static final JigsawStudioControlPosition CONTROL_POSITION =
|
private static final JigsawStudioControlPosition CONTROL_POSITION =
|
||||||
@@ -208,6 +209,18 @@ public final class JigsawStudioLayout {
|
|||||||
return null;
|
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) {
|
public List<JigsawStudioVariant> variants(JigsawStudioBay workcell) {
|
||||||
JigsawStudioBay activeWorkcell = requireWorkcell(workcell);
|
JigsawStudioBay activeWorkcell = requireWorkcell(workcell);
|
||||||
if (activeWorkcell.kind() == JigsawStudioBayKind.SPATIAL_WORKCELL) {
|
if (activeWorkcell.kind() == JigsawStudioBayKind.SPATIAL_WORKCELL) {
|
||||||
@@ -276,7 +289,7 @@ public final class JigsawStudioLayout {
|
|||||||
|
|
||||||
private JigsawStudioBay requireWorkcell(JigsawStudioBay workcell) {
|
private JigsawStudioBay requireWorkcell(JigsawStudioBay workcell) {
|
||||||
JigsawStudioBay activeWorkcell = Objects.requireNonNull(workcell, "Jigsaw Studio 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");
|
throw new IllegalArgumentException("Workcell does not belong to this Jigsaw Studio layout");
|
||||||
}
|
}
|
||||||
return activeWorkcell;
|
return activeWorkcell;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import art.arcane.volmlib.util.inventorygui.UIWindow;
|
|||||||
import org.bukkit.ChatColor;
|
import org.bukkit.ChatColor;
|
||||||
import org.bukkit.Material;
|
import org.bukkit.Material;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
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;
|
import static art.arcane.iris.studio.jigsaw.JigsawStudioMenuFormat.yesNo;
|
||||||
|
|
||||||
final class JigsawStudioMainMenu {
|
final class JigsawStudioMainMenu {
|
||||||
|
static final int WORKCELL_ROW_CAPACITY = 6;
|
||||||
|
static final int WORKCELLS_PER_PAGE = 5;
|
||||||
|
|
||||||
private final JigsawStudioMenuController controller;
|
private final JigsawStudioMenuController controller;
|
||||||
|
|
||||||
JigsawStudioMainMenu(JigsawStudioMenuController controller) {
|
JigsawStudioMainMenu(JigsawStudioMenuController controller) {
|
||||||
@@ -103,11 +107,74 @@ final class JigsawStudioMainMenu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void addWorkcells(UIWindow window, JigsawStudioMenuState state, String selectedWorkcellId) {
|
private void addWorkcells(UIWindow window, JigsawStudioMenuState state, String selectedWorkcellId) {
|
||||||
int count = state.workcells().size();
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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);
|
int start = count == 1 ? 0 : -(count / 2);
|
||||||
for (int index = 0; index < count; index++) {
|
for (int index = 0; index < count; index++) {
|
||||||
JigsawStudioMenuState.Workcell workcell = state.workcells().get(index);
|
indices.add(index);
|
||||||
boolean selected = workcell.stableId().equals(selectedWorkcellId);
|
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(
|
UIElement element = element(
|
||||||
"workcell-" + index,
|
"workcell-" + index,
|
||||||
workcellMaterial(workcell, selected),
|
workcellMaterial(workcell, selected),
|
||||||
@@ -146,8 +213,7 @@ final class JigsawStudioMainMenu {
|
|||||||
state.requestId(),
|
state.requestId(),
|
||||||
workcell.stableId()),
|
workcell.stableId()),
|
||||||
1));
|
1));
|
||||||
window.setElement(start + index, 0, element);
|
return element;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addVariants(
|
private void addVariants(
|
||||||
@@ -283,4 +349,11 @@ final class JigsawStudioMainMenu {
|
|||||||
window.getViewer(), state.requestId(), workcell.stableId(), 0));
|
window.getViewer(), state.requestId(), workcell.stableId(), 0));
|
||||||
window.setElement(4, 5, toolbox);
|
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.studio.generation.JigsawStudioGenerator;
|
||||||
import art.arcane.iris.spi.IrisLogging;
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
import art.arcane.iris.world.task.J;
|
import art.arcane.iris.world.task.J;
|
||||||
|
import org.bukkit.Material;
|
||||||
import org.bukkit.World;
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.block.Block;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
import java.io.IOException;
|
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.JigsawStudioCapture.chunkIntersections;
|
||||||
import static art.arcane.iris.studio.jigsaw.JigsawStudioChunkWriter.hydrateChunk;
|
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.validateMaterialization;
|
||||||
import static art.arcane.iris.studio.jigsaw.JigsawStudioChunkWriter.writeMaterializedChunk;
|
import static art.arcane.iris.studio.jigsaw.JigsawStudioChunkWriter.writeMaterializedChunk;
|
||||||
import static art.arcane.iris.studio.jigsaw.JigsawStudioService.chunkKey;
|
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(
|
boolean scheduleConnectorRepair(
|
||||||
Player player,
|
Player player,
|
||||||
ActiveStudio studio,
|
ActiveStudio studio,
|
||||||
@@ -146,39 +193,55 @@ final class JigsawStudioMaterializer {
|
|||||||
JigsawStudioGenerator.RenderedBay rendered,
|
JigsawStudioGenerator.RenderedBay rendered,
|
||||||
boolean connectorsVisible
|
boolean connectorsVisible
|
||||||
) {
|
) {
|
||||||
Map<Long, List<JigsawStudioGenerator.RenderedConnector>> connectorsByChunk = new HashMap<>();
|
|
||||||
JigsawStudioBounds bounds = workcell.bounds();
|
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()) {
|
for (JigsawStudioGenerator.RenderedConnector connector : rendered.connectors()) {
|
||||||
int worldX = bounds.originX() + connector.x();
|
renderedConnectors.put(new LocalPosition(connector.x(), connector.y(), connector.z()), connector);
|
||||||
int worldZ = bounds.originZ() + connector.z();
|
|
||||||
connectorsByChunk.computeIfAbsent(
|
|
||||||
chunkKey(worldX >> 4, worldZ >> 4),
|
|
||||||
ignored -> new ArrayList<>()).add(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<>();
|
Map<LocalPosition, JigsawStudioGenerator.RenderedBlock> renderedBlocks = new HashMap<>();
|
||||||
for (JigsawStudioGenerator.RenderedBlock block : rendered.blocks()) {
|
for (JigsawStudioGenerator.RenderedBlock block : rendered.blocks()) {
|
||||||
renderedBlocks.put(new LocalPosition(block.x(), block.y(), block.z()), block);
|
renderedBlocks.put(new LocalPosition(block.x(), block.y(), block.z()), block);
|
||||||
}
|
}
|
||||||
for (Map.Entry<Long, List<JigsawStudioGenerator.RenderedConnector>> entry
|
List<ChunkCaptureArea> areas = chunkIntersections(variantBounds);
|
||||||
: connectorsByChunk.entrySet()) {
|
AtomicInteger remaining = new AtomicInteger(areas.size());
|
||||||
int chunkX = (int) (entry.getKey() >> 32);
|
AtomicInteger cleared = new AtomicInteger();
|
||||||
int chunkZ = (int) entry.getKey().longValue();
|
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, () -> {
|
boolean scheduled = J.runRegion(studio.world(), chunkX, chunkZ, () -> {
|
||||||
try {
|
try {
|
||||||
if (!studio.world().isChunkLoaded(chunkX, chunkZ)) {
|
if (!studio.world().isChunkLoaded(chunkX, chunkZ)) {
|
||||||
throw new IOException("connector chunk " + chunkX + "," + chunkZ
|
throw new IOException("connector chunk " + chunkX + "," + chunkZ
|
||||||
+ " is not loaded");
|
+ " 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(),
|
studio.world(),
|
||||||
workcell,
|
workcell,
|
||||||
entry.getValue(),
|
positions,
|
||||||
|
renderedConnectors,
|
||||||
renderedBlocks,
|
renderedBlocks,
|
||||||
connectorsVisible);
|
connectorsVisible);
|
||||||
|
cleared.addAndGet(strays.size());
|
||||||
} catch (Throwable exception) {
|
} catch (Throwable exception) {
|
||||||
failure.compareAndSet("", failureMessage(exception));
|
failure.compareAndSet("", failureMessage(exception));
|
||||||
cause.compareAndSet(null, exception);
|
cause.compareAndSet(null, exception);
|
||||||
@@ -188,6 +251,7 @@ final class JigsawStudioMaterializer {
|
|||||||
player,
|
player,
|
||||||
studio,
|
studio,
|
||||||
rendered.connectors().size(),
|
rendered.connectors().size(),
|
||||||
|
cleared.get(),
|
||||||
failure.get(),
|
failure.get(),
|
||||||
cause.get());
|
cause.get());
|
||||||
}
|
}
|
||||||
@@ -202,6 +266,7 @@ final class JigsawStudioMaterializer {
|
|||||||
player,
|
player,
|
||||||
studio,
|
studio,
|
||||||
rendered.connectors().size(),
|
rendered.connectors().size(),
|
||||||
|
cleared.get(),
|
||||||
failure.get(),
|
failure.get(),
|
||||||
cause.get());
|
cause.get());
|
||||||
}
|
}
|
||||||
@@ -213,31 +278,61 @@ final class JigsawStudioMaterializer {
|
|||||||
return scheduledAny.get();
|
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(
|
private void completeConnectorRepair(
|
||||||
Player player,
|
Player player,
|
||||||
ActiveStudio studio,
|
ActiveStudio studio,
|
||||||
int connectorCount,
|
int connectorCount,
|
||||||
|
int clearedMarkers,
|
||||||
String failure,
|
String failure,
|
||||||
Throwable cause
|
Throwable cause
|
||||||
) {
|
) {
|
||||||
UUID requestId = studio.generator().getRequest().requestId();
|
UUID requestId = studio.generator().getRequest().requestId();
|
||||||
boolean scheduled = J.runEntity(player, () -> {
|
Runnable release = () -> {
|
||||||
finishMaterialization(requestId);
|
finishMaterialization(requestId);
|
||||||
if (cause != null) {
|
if (cause != null) {
|
||||||
IrisLogging.reportError(cause);
|
IrisLogging.reportError(cause);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
boolean scheduled = J.runEntity(player, () -> {
|
||||||
|
release.run();
|
||||||
if (!failure.isEmpty()) {
|
if (!failure.isEmpty()) {
|
||||||
message(player, "Connector reset was incomplete: " + failure + ". Retry after visiting the workcell.");
|
message(player, "Connector reset was incomplete: " + failure + ". Retry after visiting the workcell.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
message(player, "Restored " + connectorCount
|
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) {
|
if (!scheduled) {
|
||||||
finishMaterialization(requestId);
|
release.run();
|
||||||
if (cause != null) {
|
|
||||||
IrisLogging.reportError(cause);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -474,6 +569,7 @@ final class JigsawStudioMaterializer {
|
|||||||
try {
|
try {
|
||||||
work.studio().generator().invalidateRender(work.workcell().stableId());
|
work.studio().generator().invalidateRender(work.workcell().stableId());
|
||||||
work.studio().replacePopulation(work.workcell(), work.target(), "", true);
|
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());
|
service.playerContext.refreshWorkcellContext(work.studio().worldId(), work.workcell().stableId());
|
||||||
message(work.player(), work.connectorVisibilityChange()
|
message(work.player(), work.connectorVisibilityChange()
|
||||||
? "Connector blocks are now "
|
? "Connector blocks are now "
|
||||||
@@ -564,6 +660,7 @@ final class JigsawStudioMaterializer {
|
|||||||
work.studio().generator().invalidateRender(work.workcell().stableId());
|
work.studio().generator().invalidateRender(work.workcell().stableId());
|
||||||
if (rollbackFailure.isEmpty() && released) {
|
if (rollbackFailure.isEmpty() && released) {
|
||||||
work.studio().replacePopulation(work.workcell(), work.previous(), "", true);
|
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());
|
service.playerContext.refreshWorkcellContext(work.studio().worldId(), work.workcell().stableId());
|
||||||
message(work.player(), candidateFailure + " The previous variant was restored.");
|
message(work.player(), candidateFailure + " The previous variant was restored.");
|
||||||
return;
|
return;
|
||||||
@@ -572,6 +669,7 @@ final class JigsawStudioMaterializer {
|
|||||||
? "The previous variant could not be restored because the Studio session changed."
|
? "The previous variant could not be restored because the Studio session changed."
|
||||||
: rollbackFailure;
|
: rollbackFailure;
|
||||||
work.studio().replacePopulation(work.workcell(), work.previous(), failure, false);
|
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());
|
service.playerContext.refreshWorkcellContext(work.studio().worldId(), work.workcell().stableId());
|
||||||
message(work.player(), candidateFailure + " Automatic rollback also failed: " + failure
|
message(work.player(), candidateFailure + " Automatic rollback also failed: " + failure
|
||||||
+ " Do not save this workcell until it is reopened.");
|
+ " 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(
|
public record VariantReloadRequest(
|
||||||
String workcellId,
|
String workcellId,
|
||||||
JigsawStudioGenerator.RenderedBay previous
|
JigsawStudioGenerator.RenderedBay previous
|
||||||
|
|||||||
@@ -857,7 +857,7 @@ public final class JigsawStudioMenuController {
|
|||||||
}
|
}
|
||||||
JigsawStudioMenuState.Workcell workcell = current.get().workcell(workcellId);
|
JigsawStudioMenuState.Workcell workcell = current.get().workcell(workcellId);
|
||||||
JigsawStudioMenuState.Variant target = variant(workcell, pieceKey);
|
JigsawStudioMenuState.Variant target = variant(workcell, pieceKey);
|
||||||
if (target == null || !target.owned() || target.active()) {
|
if (!current.get().canDeleteVariant(workcell, target)) {
|
||||||
stale(player);
|
stale(player);
|
||||||
return;
|
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() {
|
public Workcell selectedWorkcell() {
|
||||||
if (selectedWorkcellId.isEmpty()) {
|
if (selectedWorkcellId.isEmpty()) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ final class JigsawStudioPlayerContext {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
JigsawStudioSession session = studio.generator().getSession();
|
JigsawStudioSession session = studio.generator().getSession();
|
||||||
JigsawStudioBay workcell = session.layout().findAt(
|
JigsawStudioBay workcell = session.layout().findColumn(
|
||||||
target.getBlockX(), target.getBlockY(), target.getBlockZ());
|
target.getBlockX(), target.getBlockY(), target.getBlockZ());
|
||||||
boolean owner = ownerMatches(player, studio);
|
boolean owner = ownerMatches(player, studio);
|
||||||
selectEnteredWorkcell(session, workcell, owner);
|
selectEnteredWorkcell(session, workcell, owner);
|
||||||
|
|||||||
@@ -36,13 +36,9 @@ import java.util.Objects;
|
|||||||
|
|
||||||
public final class JigsawStudioProjectCreator {
|
public final class JigsawStudioProjectCreator {
|
||||||
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
||||||
private static final List<IrisDirection> SPATIAL_CONNECTOR_ORDER = List.of(
|
private static final int DEFAULT_MAX_DEPTH = 7;
|
||||||
IrisDirection.NORTH_NEGATIVE_Z,
|
private static final int SPATIAL_PORTABLE_MAX_DEPTH = 4;
|
||||||
IrisDirection.SOUTH_POSITIVE_Z,
|
private static final int SPATIAL_MAXIMUM_PLACEMENTS = 4;
|
||||||
IrisDirection.EAST_POSITIVE_X,
|
|
||||||
IrisDirection.WEST_NEGATIVE_X,
|
|
||||||
IrisDirection.UP_POSITIVE_Y,
|
|
||||||
IrisDirection.DOWN_NEGATIVE_Y);
|
|
||||||
|
|
||||||
private JigsawStudioProjectCreator() {
|
private JigsawStudioProjectCreator() {
|
||||||
}
|
}
|
||||||
@@ -59,7 +55,7 @@ public final class JigsawStudioProjectCreator {
|
|||||||
StructureKey ownershipKey = new StructureKey("iris", resourceKey);
|
StructureKey ownershipKey = new StructureKey("iris", resourceKey);
|
||||||
IrisStructure structure = new IrisStructure()
|
IrisStructure structure = new IrisStructure()
|
||||||
.setStartPool(resourceKey + "/start")
|
.setStartPool(resourceKey + "/start")
|
||||||
.setMaxDepth(7)
|
.setMaxDepth(maxDepth(options))
|
||||||
.setMaxSizeChunks(8)
|
.setMaxSizeChunks(8)
|
||||||
.setMode(toModelMode(options.mode()))
|
.setMode(toModelMode(options.mode()))
|
||||||
.setCompatibility(toModelCompatibility(options.compatibilityTarget()))
|
.setCompatibility(toModelCompatibility(options.compatibilityTarget()))
|
||||||
@@ -94,6 +90,13 @@ public final class JigsawStudioProjectCreator {
|
|||||||
return output.toByteArray();
|
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) {
|
private static IrisJigsawMode toModelMode(JigsawStudioMode mode) {
|
||||||
return mode == JigsawStudioMode.PLANAR_JIGSAW
|
return mode == JigsawStudioMode.PLANAR_JIGSAW
|
||||||
? IrisJigsawMode.PLANAR_JIGSAW
|
? IrisJigsawMode.PLANAR_JIGSAW
|
||||||
@@ -129,14 +132,11 @@ public final class JigsawStudioProjectCreator {
|
|||||||
IrisJigsawPool piecePool = new IrisJigsawPool().setFallback(capPoolKey);
|
IrisJigsawPool piecePool = new IrisJigsawPool().setFallback(capPoolKey);
|
||||||
IrisJigsawPool capPool = new IrisJigsawPool();
|
IrisJigsawPool capPool = new IrisJigsawPool();
|
||||||
for (JigsawPlanarArchetype archetype : JigsawPlanarArchetype.values()) {
|
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);
|
IrisJigsawPiece piece = planarPiece(key, piecePoolKey, size, archetype);
|
||||||
if (options.compatibilityTarget() == JigsawStudioCompatibilityTarget.IRIS_EXTENDED) {
|
if (options.compatibilityTarget() == JigsawStudioCompatibilityTarget.IRIS_EXTENDED) {
|
||||||
piece.getThemes().add("variant-1");
|
piece.getThemes().add("variant-1");
|
||||||
}
|
}
|
||||||
if (archetype == JigsawPlanarArchetype.CROSS) {
|
|
||||||
pool.getPieces().add(new IrisJigsawPieceEntry(key, 1));
|
|
||||||
}
|
|
||||||
if (archetype != JigsawPlanarArchetype.BLANK) {
|
if (archetype != JigsawPlanarArchetype.BLANK) {
|
||||||
piecePool.getPieces().add(new IrisJigsawPieceEntry(key, 1));
|
piecePool.getPieces().add(new IrisJigsawPieceEntry(key, 1));
|
||||||
}
|
}
|
||||||
@@ -159,6 +159,8 @@ public final class JigsawStudioProjectCreator {
|
|||||||
dimensions.depth())));
|
dimensions.depth())));
|
||||||
bundle.textResource("jigsaw-pieces/" + key + ".json", GSON.toJson(piece) + "\n");
|
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));
|
capPool.getPieces().add(new IrisJigsawPieceEntry().setEmpty(true));
|
||||||
bundle.textResource("jigsaw-pools/" + piecePoolKey + ".json", GSON.toJson(piecePool) + "\n");
|
bundle.textResource("jigsaw-pools/" + piecePoolKey + ".json", GSON.toJson(piecePool) + "\n");
|
||||||
bundle.textResource("jigsaw-pools/" + capPoolKey + ".json", GSON.toJson(capPool) + "\n");
|
bundle.textResource("jigsaw-pools/" + capPoolKey + ".json", GSON.toJson(capPool) + "\n");
|
||||||
@@ -172,19 +174,17 @@ public final class JigsawStudioProjectCreator {
|
|||||||
JigsawStudioCellDimensions dimensions = options.cellDimensions();
|
JigsawStudioCellDimensions dimensions = options.cellDimensions();
|
||||||
String piecePoolKey = options.structureKey() + "/pieces";
|
String piecePoolKey = options.structureKey() + "/pieces";
|
||||||
IrisJigsawPool piecePool = new IrisJigsawPool();
|
IrisJigsawPool piecePool = new IrisJigsawPool();
|
||||||
for (int connectorCount = 0; connectorCount <= SPATIAL_CONNECTOR_ORDER.size(); connectorCount++) {
|
boolean irisExtended = options.compatibilityTarget() == JigsawStudioCompatibilityTarget.IRIS_EXTENDED;
|
||||||
String key = options.structureKey() + "/"
|
for (SpatialVerticalClass vertical : SpatialVerticalClass.values()) {
|
||||||
+ (connectorCount == 0 ? "start" : "connectors-" + connectorCount);
|
for (JigsawPlanarArchetype horizontal : JigsawPlanarArchetype.values()) {
|
||||||
IrisJigsawPiece piece = spatialPiece(
|
String key = archetypeKey(options, horizontal) + vertical.keySuffix();
|
||||||
key,
|
IrisJigsawPiece piece = spatialPiece(key, piecePoolKey, dimensions, horizontal, vertical);
|
||||||
piecePoolKey,
|
if (irisExtended) {
|
||||||
dimensions,
|
|
||||||
connectorCount);
|
|
||||||
if (options.compatibilityTarget() == JigsawStudioCompatibilityTarget.IRIS_EXTENDED) {
|
|
||||||
piece.getThemes().add("variant-1");
|
piece.getThemes().add("variant-1");
|
||||||
|
piece.setRules(new IrisJigsawPieceRules().setMaximumPlacements(SPATIAL_MAXIMUM_PLACEMENTS));
|
||||||
}
|
}
|
||||||
pool.getPieces().add(new IrisJigsawPieceEntry(key, 1));
|
pool.getPieces().add(new IrisJigsawPieceEntry(key, 1));
|
||||||
if (connectorCount > 0) {
|
if (!piece.getConnectors().isEmpty()) {
|
||||||
piecePool.getPieces().add(new IrisJigsawPieceEntry(key, 1));
|
piecePool.getPieces().add(new IrisJigsawPieceEntry(key, 1));
|
||||||
}
|
}
|
||||||
bundle.resource("objects/" + key + ".iob", serialize(new IrisObject(
|
bundle.resource("objects/" + key + ".iob", serialize(new IrisObject(
|
||||||
@@ -193,6 +193,7 @@ public final class JigsawStudioProjectCreator {
|
|||||||
dimensions.depth())));
|
dimensions.depth())));
|
||||||
bundle.textResource("jigsaw-pieces/" + key + ".json", GSON.toJson(piece) + "\n");
|
bundle.textResource("jigsaw-pieces/" + key + ".json", GSON.toJson(piece) + "\n");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
piecePool.getPieces().add(new IrisJigsawPieceEntry().setEmpty(true));
|
piecePool.getPieces().add(new IrisJigsawPieceEntry().setEmpty(true));
|
||||||
bundle.textResource("jigsaw-pools/" + piecePoolKey + ".json", GSON.toJson(piecePool) + "\n");
|
bundle.textResource("jigsaw-pools/" + piecePoolKey + ".json", GSON.toJson(piecePool) + "\n");
|
||||||
}
|
}
|
||||||
@@ -201,16 +202,28 @@ public final class JigsawStudioProjectCreator {
|
|||||||
String objectKey,
|
String objectKey,
|
||||||
String poolKey,
|
String poolKey,
|
||||||
JigsawStudioCellDimensions dimensions,
|
JigsawStudioCellDimensions dimensions,
|
||||||
int connectorCount
|
JigsawPlanarArchetype horizontal,
|
||||||
|
SpatialVerticalClass vertical
|
||||||
) {
|
) {
|
||||||
IrisJigsawPiece piece = new IrisJigsawPiece()
|
IrisJigsawPiece piece = new IrisJigsawPiece()
|
||||||
.setDisplayName(connectorCount + (connectorCount == 1 ? " Connector" : " Connectors"))
|
.setDisplayName(horizontal.displayName() + vertical.displaySuffix())
|
||||||
.setObject(objectKey)
|
.setObject(objectKey)
|
||||||
.setRotatable(true)
|
.setRotatable(true);
|
||||||
.setRules(new IrisJigsawPieceRules().setMaximumPlacements(16));
|
for (JigsawPlanarDirection planarDirection : horizontal.canonicalTopology().directions()) {
|
||||||
for (int index = 0; index < connectorCount; index++) {
|
piece.getConnectors().add(spatialConnector(dimensions, planarDirection.irisDirection(), poolKey));
|
||||||
IrisDirection direction = SPATIAL_CONNECTOR_ORDER.get(index);
|
}
|
||||||
piece.getConnectors().add(new IrisJigsawConnector()
|
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))
|
.setPosition(spatialConnectorPosition(dimensions, direction))
|
||||||
.setDirection(direction)
|
.setDirection(direction)
|
||||||
.setTop(direction.isVertical()
|
.setTop(direction.isVertical()
|
||||||
@@ -220,9 +233,7 @@ public final class JigsawStudioProjectCreator {
|
|||||||
.setName("iris:spatial")
|
.setName("iris:spatial")
|
||||||
.setTargetName("iris:spatial")
|
.setTargetName("iris:spatial")
|
||||||
.setJoint(JigsawJoint.ROLLABLE)
|
.setJoint(JigsawJoint.ROLLABLE)
|
||||||
.setFinalState("minecraft:structure_void"));
|
.setFinalState("minecraft:structure_void");
|
||||||
}
|
|
||||||
return piece;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IrisPosition spatialConnectorPosition(
|
private static IrisPosition spatialConnectorPosition(
|
||||||
@@ -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(
|
private static IrisJigsawPiece planarPiece(
|
||||||
String objectKey,
|
String objectKey,
|
||||||
String poolKey,
|
String poolKey,
|
||||||
@@ -291,4 +306,33 @@ public final class JigsawStudioProjectCreator {
|
|||||||
return key;
|
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) {
|
static boolean blocksMutatingCommand(UUID ownerId, UUID actorId, String commandLine) {
|
||||||
return !ownerMatches(ownerId, actorId)
|
return !ownerMatches(ownerId, actorId)
|
||||||
&& !isSafeNonOwnerCommand(commandLine);
|
&& isMutatingCommand(commandLine);
|
||||||
}
|
}
|
||||||
|
|
||||||
static boolean blocksNonEditableWorkcellMutation(
|
static boolean blocksNonEditableWorkcellMutation(
|
||||||
@@ -231,17 +231,25 @@ final class JigsawStudioProtection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
boolean isUnauthorizedStudioEdit(Player player, Block block) {
|
boolean isUnauthorizedStudioEdit(Player player, Block block) {
|
||||||
if (player == null || block == null) {
|
String refusal = studioEditRefusal(player, block);
|
||||||
|
if (refusal.isEmpty()) {
|
||||||
return false;
|
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());
|
ActiveStudio studio = service.studios.get(block.getWorld().getUID());
|
||||||
if (studio == null) {
|
if (studio == null) {
|
||||||
return false;
|
return "";
|
||||||
}
|
}
|
||||||
UUID requestId = studio.generator().getRequest().requestId();
|
UUID requestId = studio.generator().getRequest().requestId();
|
||||||
if (service.previewRenderer.contains(requestId, block.getX(), block.getY(), block.getZ())) {
|
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 "The seed-1337 Jigsaw preview is read-only and refreshes automatically.";
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
JigsawStudioSession session = studio.generator().getSession();
|
JigsawStudioSession session = studio.generator().getSession();
|
||||||
JigsawStudioBay bay = session.layout().findAt(block.getX(), block.getY(), block.getZ());
|
JigsawStudioBay bay = session.layout().findAt(block.getX(), block.getY(), block.getZ());
|
||||||
@@ -249,24 +257,20 @@ final class JigsawStudioProtection {
|
|||||||
? null
|
? null
|
||||||
: session.activeVariant(bay.stableId()).orElse(null);
|
: session.activeVariant(bay.stableId()).orElse(null);
|
||||||
if (bay != null && (variant == null || !variant.owned())) {
|
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 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.");
|
: "This Jigsaw Studio variant is read-only. Adopt or clone its graph before editing it.";
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
if (service.saveLifecycle.reopenRequiredRequests.contains(requestId)) {
|
if (service.saveLifecycle.reopenRequiredRequests.contains(requestId)) {
|
||||||
message(player, "Close and reopen Jigsaw Studio before editing the resized layout.");
|
return "Close and reopen Jigsaw Studio before editing the resized layout.";
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
if (materializationInProgress(studio)) {
|
if (materializationInProgress(studio)) {
|
||||||
message(player, "Wait for the current Jigsaw Studio variant load or rollback to finish.");
|
return "Wait for the current Jigsaw Studio variant load or rollback to finish.";
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
if (!blocksStudioEdit(studio.generator().getRequest().ownerId(), player.getUniqueId())) {
|
if (!blocksStudioEdit(studio.generator().getRequest().ownerId(), player.getUniqueId())) {
|
||||||
return false;
|
return "";
|
||||||
}
|
}
|
||||||
message(player, "This Jigsaw Studio is owned by another player session.");
|
return "This Jigsaw Studio is owned by another player session.";
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean materializationInProgress(ActiveStudio studio) {
|
boolean materializationInProgress(ActiveStudio studio) {
|
||||||
|
|||||||
+40
-7
@@ -105,6 +105,7 @@ final class JigsawStudioProtectionListener implements Listener {
|
|||||||
ActiveStudio studio = service.studios.get(event.getWorld().getUID());
|
ActiveStudio studio = service.studios.get(event.getWorld().getUID());
|
||||||
if (studio != null) {
|
if (studio != null) {
|
||||||
service.registry.markChunkAvailable(studio, event.getChunk().getX(), event.getChunk().getZ());
|
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.toolbelt.closeMenu(event.getPlayer());
|
||||||
service.tileWatcher.finalizeJigsawTileWatchesForPlayer(event.getPlayer().getUniqueId());
|
service.tileWatcher.finalizeJigsawTileWatchesForPlayer(event.getPlayer().getUniqueId());
|
||||||
service.tripleSneakTracker.clearPlayer(event.getPlayer().getUniqueId());
|
service.tripleSneakTracker.clearPlayer(event.getPlayer().getUniqueId());
|
||||||
|
service.connectorTool.clearPlayer(event.getPlayer().getUniqueId());
|
||||||
service.visualization.visualizationLoops.remove(event.getPlayer().getUniqueId());
|
service.visualization.visualizationLoops.remove(event.getPlayer().getUniqueId());
|
||||||
service.visualization.assemblyPreviews.remove(event.getPlayer().getUniqueId());
|
service.visualization.assemblyPreviews.remove(event.getPlayer().getUniqueId());
|
||||||
service.playerContext.playerWorkcells.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.tileWatcher.finalizeJigsawTileWatchesForPlayer(playerId);
|
||||||
service.tripleSneakTracker.clearPlayer(playerId);
|
service.tripleSneakTracker.clearPlayer(playerId);
|
||||||
service.toolbelt.toolConfirmations.remove(playerId);
|
service.toolbelt.toolConfirmations.remove(playerId);
|
||||||
|
service.connectorTool.clearPlayer(playerId);
|
||||||
service.visualization.visualizationLoops.remove(playerId);
|
service.visualization.visualizationLoops.remove(playerId);
|
||||||
service.particlesDisabled.remove(playerId);
|
service.particlesDisabled.remove(playerId);
|
||||||
service.visualization.assemblyPreviews.remove(playerId);
|
service.visualization.assemblyPreviews.remove(playerId);
|
||||||
@@ -170,15 +173,29 @@ final class JigsawStudioProtectionListener implements Listener {
|
|||||||
|| (event.getClickedBlock() != null && event.useInteractedBlock() == Event.Result.DENY)) {
|
|| (event.getClickedBlock() != null && event.useInteractedBlock() == Event.Result.DENY)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (event.getHand() != EquipmentSlot.HAND
|
if (event.getHand() != EquipmentSlot.HAND || event.getAction() == Action.PHYSICAL) {
|
||||||
|| (event.getAction() != Action.RIGHT_CLICK_AIR
|
|
||||||
&& event.getAction() != Action.RIGHT_CLICK_BLOCK)) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
boolean leftClick = event.getAction() == Action.LEFT_CLICK_AIR
|
||||||
|
|| event.getAction() == Action.LEFT_CLICK_BLOCK;
|
||||||
Optional<JigsawStudioToolPayload> decoded = service.toolbelt.toolCodec.decode(event.getItem());
|
Optional<JigsawStudioToolPayload> decoded = service.toolbelt.toolCodec.decode(event.getItem());
|
||||||
if (decoded.isEmpty()) {
|
if (decoded.isEmpty()) {
|
||||||
return;
|
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());
|
service.tileWatcher.finalizeJigsawTileWatchesForPlayer(event.getPlayer().getUniqueId());
|
||||||
event.setCancelled(true);
|
event.setCancelled(true);
|
||||||
service.toolbelt.useTool(event.getPlayer(), decoded.get(), event.getItem(), event.getPlayer().isSneaking());
|
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)
|
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||||
public void onProtectedBlockBreak(BlockBreakEvent event) {
|
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())) {
|
if (service.protection.isControlChest(event.getBlock())) {
|
||||||
event.setCancelled(true);
|
event.setCancelled(true);
|
||||||
message(event.getPlayer(), "The Jigsaw Studio control chest cannot be removed.");
|
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)
|
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||||
public void onUnauthorizedInteract(PlayerInteractEvent event) {
|
public void onUnauthorizedInteract(PlayerInteractEvent event) {
|
||||||
if (event.getAction() == Action.RIGHT_CLICK_BLOCK
|
if (event.getAction() != Action.RIGHT_CLICK_BLOCK
|
||||||
&& !service.protection.isControlChest(event.getClickedBlock())
|
|| service.protection.isControlChest(event.getClickedBlock())) {
|
||||||
&& service.protection.isUnauthorizedStudioEdit(event.getPlayer(), 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);
|
event.setCancelled(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -337,7 +369,8 @@ final class JigsawStudioProtectionListener implements Listener {
|
|||||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||||
public void onProtectedPistonExtend(BlockPistonExtendEvent event) {
|
public void onProtectedPistonExtend(BlockPistonExtendEvent event) {
|
||||||
if (service.protection.materializationInProgress(service.studios.get(event.getBlock().getWorld().getUID()))
|
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);
|
event.setCancelled(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
@@ -129,6 +129,11 @@ final class JigsawStudioResourceBundleAssembler {
|
|||||||
return new Assembly(bundle.build(), objectKey, piece, StructureHash.sha256(manifestContent));
|
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) {
|
private static Set<String> resourcePaths(StructurePackageClosure closure) {
|
||||||
Set<String> resources = new LinkedHashSet<>();
|
Set<String> resources = new LinkedHashSet<>();
|
||||||
addPaths(resources, "structures", closure.structures(), ".json");
|
addPaths(resources, "structures", closure.structures(), ".json");
|
||||||
|
|||||||
@@ -286,7 +286,7 @@ final class JigsawStudioSaveLifecycle {
|
|||||||
String selected = session.selectedBayId().orElse(null);
|
String selected = session.selectedBayId().orElse(null);
|
||||||
if (selected == null) {
|
if (selected == null) {
|
||||||
Location location = player.getLocation();
|
Location location = player.getLocation();
|
||||||
JigsawStudioBay underPlayer = session.layout().findAt(
|
JigsawStudioBay underPlayer = session.layout().findColumn(
|
||||||
location.getBlockX(), location.getBlockY(), location.getBlockZ());
|
location.getBlockX(), location.getBlockY(), location.getBlockZ());
|
||||||
if (underPlayer != null) {
|
if (underPlayer != null) {
|
||||||
selected = underPlayer.stableId();
|
selected = underPlayer.stableId();
|
||||||
|
|||||||
@@ -73,6 +73,8 @@ public final class JigsawStudioService implements IrisService, JigsawStudioMenuA
|
|||||||
final JigsawStudioAutosaveScheduler autosaveScheduler = new JigsawStudioAutosaveScheduler(this);
|
final JigsawStudioAutosaveScheduler autosaveScheduler = new JigsawStudioAutosaveScheduler(this);
|
||||||
final JigsawStudioTileWatcher tileWatcher = new JigsawStudioTileWatcher(this);
|
final JigsawStudioTileWatcher tileWatcher = new JigsawStudioTileWatcher(this);
|
||||||
final JigsawStudioVisualization visualization = new JigsawStudioVisualization(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);
|
final JigsawStudioPlayerContext playerContext = new JigsawStudioPlayerContext(this);
|
||||||
|
|
||||||
public static JigsawStudioService get() {
|
public static JigsawStudioService get() {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import java.util.Objects;
|
|||||||
|
|
||||||
public enum JigsawStudioToolAction {
|
public enum JigsawStudioToolAction {
|
||||||
OPEN_MENU("Open Control Menu", false),
|
OPEN_MENU("Open Control Menu", false),
|
||||||
|
CONNECTOR_TOOL("Connector Tool", false),
|
||||||
SELECT_WORKCELL("Select Workcell", false),
|
SELECT_WORKCELL("Select Workcell", false),
|
||||||
TOGGLE_WORKCELL("Toggle Workcell", false),
|
TOGGLE_WORKCELL("Toggle Workcell", false),
|
||||||
LOAD_VARIANT("Load Variant", false),
|
LOAD_VARIANT("Load Variant", false),
|
||||||
|
|||||||
@@ -140,6 +140,13 @@ public final class JigsawStudioToolCodec {
|
|||||||
if (payload.amount() != 0) {
|
if (payload.amount() != 0) {
|
||||||
lore.add(ChatColor.GRAY + "Amount: " + payload.amount());
|
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()
|
lore.add(payload.action().destructive()
|
||||||
? ChatColor.RED + "Right-click twice to confirm"
|
? ChatColor.RED + "Right-click twice to confirm"
|
||||||
: ChatColor.YELLOW + "Right-click to use");
|
: ChatColor.YELLOW + "Right-click to use");
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import static art.arcane.iris.studio.jigsaw.JigsawStudioVariantProperties.canRes
|
|||||||
|
|
||||||
final class JigsawStudioToolbelt {
|
final class JigsawStudioToolbelt {
|
||||||
private static final long TOOL_CONFIRM_NANOS = 10_000_000_000L;
|
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;
|
private final JigsawStudioService service;
|
||||||
final Map<UUID, DeferredDuplication> deferredDuplications = new ConcurrentHashMap<>();
|
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.");
|
message(player, "This Jigsaw Studio tool is stale or belongs to another session.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (payload.action().destructive() && !confirmTool(player, payload)) {
|
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 "
|
message(player, "Right-click the same tool again within 10 seconds to confirm "
|
||||||
+ payload.action().displayName() + ".");
|
+ payload.action().displayName() + ".");
|
||||||
return true;
|
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()) {
|
return switch (payload.action()) {
|
||||||
case OPEN_MENU -> openControlMenu(player);
|
case OPEN_MENU -> openControlMenu(player);
|
||||||
|
case CONNECTOR_TOOL -> service.connectorTool.handleClick(player, payload, false, null, null);
|
||||||
case SELECT_WORKCELL -> selectWorkcell(player, payload.workcellId());
|
case SELECT_WORKCELL -> selectWorkcell(player, payload.workcellId());
|
||||||
case LOAD_VARIANT -> service.variantEditor.switchVariant(
|
case LOAD_VARIANT -> service.variantEditor.switchVariant(
|
||||||
player,
|
player,
|
||||||
@@ -349,19 +364,40 @@ final class JigsawStudioToolbelt {
|
|||||||
return state.isPresent() && service.workcellEditor.setRequireCaps(player, !state.get().requireCaps());
|
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();
|
long now = System.nanoTime();
|
||||||
ToolConfirmation previous = toolConfirmations.get(player.getUniqueId());
|
ToolConfirmation previous = toolConfirmations.get(player.getUniqueId());
|
||||||
if (previous != null
|
ToolConfirmationState state = confirmationState(previous, payload, now);
|
||||||
&& previous.expiresAtNanos() >= now
|
if (state == ToolConfirmationState.CONFIRMED) {
|
||||||
&& previous.payload().equals(payload)) {
|
|
||||||
toolConfirmations.remove(player.getUniqueId(), previous);
|
toolConfirmations.remove(player.getUniqueId(), previous);
|
||||||
return true;
|
} else if (state == ToolConfirmationState.ARMED) {
|
||||||
}
|
|
||||||
toolConfirmations.put(
|
toolConfirmations.put(
|
||||||
player.getUniqueId(),
|
player.getUniqueId(),
|
||||||
new ToolConfirmation(payload, now + TOOL_CONFIRM_NANOS));
|
new ToolConfirmation(payload, now, 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(
|
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 {
|
ToolConfirmation {
|
||||||
Objects.requireNonNull(payload, "Jigsaw Studio tool confirmation payload");
|
Objects.requireNonNull(payload, "Jigsaw Studio tool confirmation payload");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,6 +105,9 @@ final class JigsawStudioToolboxMenu {
|
|||||||
"Jigsaw Studio toolbox workcell");
|
"Jigsaw Studio toolbox workcell");
|
||||||
UUID requestId = menu.requestId();
|
UUID requestId = menu.requestId();
|
||||||
List<ToolboxTool> tools = new ArrayList<>();
|
List<ToolboxTool> tools = new ArrayList<>();
|
||||||
|
tools.add(new ToolboxTool(
|
||||||
|
"Connector Tool",
|
||||||
|
JigsawStudioToolPayload.request(JigsawStudioToolAction.CONNECTOR_TOOL, requestId)));
|
||||||
tools.add(new ToolboxTool(
|
tools.add(new ToolboxTool(
|
||||||
"Open Control Menu",
|
"Open Control Menu",
|
||||||
JigsawStudioToolPayload.request(JigsawStudioToolAction.OPEN_MENU, requestId)));
|
JigsawStudioToolPayload.request(JigsawStudioToolAction.OPEN_MENU, requestId)));
|
||||||
@@ -216,6 +219,15 @@ final class JigsawStudioToolboxMenu {
|
|||||||
selected.stableId(),
|
selected.stableId(),
|
||||||
active.pieceKey())));
|
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()) {
|
if (active.owned()) {
|
||||||
for (JigsawStudioMenuState.Membership membership : active.memberships()) {
|
for (JigsawStudioMenuState.Membership membership : active.memberships()) {
|
||||||
addMembershipTools(
|
addMembershipTools(
|
||||||
@@ -255,7 +267,7 @@ final class JigsawStudioToolboxMenu {
|
|||||||
selected.stableId(),
|
selected.stableId(),
|
||||||
variant.pieceKey())));
|
variant.pieceKey())));
|
||||||
}
|
}
|
||||||
if (variant.owned() && selected.variants().size() > 1) {
|
if (menu.canDeleteVariant(selected, variant)) {
|
||||||
tools.add(new ToolboxTool(
|
tools.add(new ToolboxTool(
|
||||||
"Delete " + variant.displayName(),
|
"Delete " + variant.displayName(),
|
||||||
JigsawStudioToolPayload.variant(
|
JigsawStudioToolPayload.variant(
|
||||||
|
|||||||
+34
-1
@@ -174,12 +174,20 @@ final class JigsawStudioVariantProperties {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
JigsawStudioSession session = studio.generator().getSession();
|
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);
|
String reservationFailure = service.graphMutations.beginGraphMutation(studio, session);
|
||||||
if (!reservationFailure.isEmpty()) {
|
if (!reservationFailure.isEmpty()) {
|
||||||
message(player, reservationFailure);
|
message(player, reservationFailure);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
boolean rotatable = !variant.rotatable();
|
|
||||||
return service.graphMutations.scheduleGraphMutation(
|
return service.graphMutations.scheduleGraphMutation(
|
||||||
player,
|
player,
|
||||||
studio,
|
studio,
|
||||||
@@ -268,6 +276,12 @@ final class JigsawStudioVariantProperties {
|
|||||||
+ dimensions.width() + "x" + dimensions.height() + "x" + dimensions.depth() + ".");
|
+ dimensions.width() + "x" + dimensions.height() + "x" + dimensions.depth() + ".");
|
||||||
return false;
|
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)
|
boolean active = session.activeVariant(workcellId)
|
||||||
.map(activeVariant -> activeVariant.pieceKey().equals(pieceKey))
|
.map(activeVariant -> activeVariant.pieceKey().equals(pieceKey))
|
||||||
.orElse(false);
|
.orElse(false);
|
||||||
@@ -536,4 +550,23 @@ final class JigsawStudioVariantProperties {
|
|||||||
|| canonical.height() < target.height()
|
|| canonical.height() < target.height()
|
||||||
|| canonical.depth() < target.depth());
|
|| 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.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
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 VALID_CONNECTOR_COLOR = Color.LIME;
|
||||||
private static final Color ASSEMBLY_PREVIEW_COLOR = Color.fromRGB(180, 90, 255);
|
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 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 static final long ASSEMBLY_PREVIEW_MILLIS = 10_000L;
|
||||||
|
|
||||||
private final JigsawStudioService service;
|
private final JigsawStudioService service;
|
||||||
@@ -124,7 +128,7 @@ final class JigsawStudioVisualization {
|
|||||||
JigsawStudioSession session = studio.generator().getSession();
|
JigsawStudioSession session = studio.generator().getSession();
|
||||||
JigsawStudioLayout layout = session.layout();
|
JigsawStudioLayout layout = session.layout();
|
||||||
Location playerLocation = player.getLocation();
|
Location playerLocation = player.getLocation();
|
||||||
JigsawStudioBay focused = layout.findAt(
|
JigsawStudioBay focused = layout.findColumn(
|
||||||
playerLocation.getBlockX(),
|
playerLocation.getBlockX(),
|
||||||
playerLocation.getBlockY(),
|
playerLocation.getBlockY(),
|
||||||
playerLocation.getBlockZ());
|
playerLocation.getBlockZ());
|
||||||
@@ -134,6 +138,7 @@ final class JigsawStudioVisualization {
|
|||||||
drawConnectors(player, playerLocation, studio, focused, budget);
|
drawConnectors(player, playerLocation, studio, focused, budget);
|
||||||
drawBounds(player, playerLocation, focused.bounds(), focusedColor, 1.15F, 1.0D, budget);
|
drawBounds(player, playerLocation, focused.bounds(), focusedColor, 1.15F, 1.0D, budget);
|
||||||
}
|
}
|
||||||
|
drawConnectorToolFeedback(player, playerLocation, studio, budget);
|
||||||
drawLivePreview(player, playerLocation, studio, budget);
|
drawLivePreview(player, playerLocation, studio, budget);
|
||||||
drawAssemblyPreview(player, playerLocation, budget);
|
drawAssemblyPreview(player, playerLocation, budget);
|
||||||
for (JigsawStudioBay bay : layout.bays()) {
|
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(
|
private void drawLivePreview(
|
||||||
Player player,
|
Player player,
|
||||||
Location playerLocation,
|
Location playerLocation,
|
||||||
|
|||||||
@@ -114,10 +114,6 @@ final class JigsawStudioWorkcellEditor {
|
|||||||
message(player, "Connector blocks cannot reset: " + validationFailure);
|
message(player, "Connector blocks cannot reset: " + validationFailure);
|
||||||
return false;
|
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);
|
String reservationFailure = service.materializer.beginConnectorRepair(studio);
|
||||||
if (!reservationFailure.isEmpty()) {
|
if (!reservationFailure.isEmpty()) {
|
||||||
message(player, reservationFailure);
|
message(player, reservationFailure);
|
||||||
@@ -125,7 +121,7 @@ final class JigsawStudioWorkcellEditor {
|
|||||||
}
|
}
|
||||||
boolean connectorsVisible = session.workcellSnapshot(workcellId).connectorsVisible();
|
boolean connectorsVisible = session.workcellSnapshot(workcellId).connectorsVisible();
|
||||||
message(player, "Resetting " + rendered.connectors().size()
|
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(
|
return service.materializer.scheduleConnectorRepair(
|
||||||
player,
|
player,
|
||||||
studio,
|
studio,
|
||||||
@@ -383,6 +379,16 @@ final class JigsawStudioWorkcellEditor {
|
|||||||
message(player, "Unknown Jigsaw Studio workcell '" + workcellId + "'.");
|
message(player, "Unknown Jigsaw Studio workcell '" + workcellId + "'.");
|
||||||
return false;
|
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;
|
String normalizedName;
|
||||||
try {
|
try {
|
||||||
normalizedName = JigsawStudioGraphEditor.normalizeDisplayName(displayName);
|
normalizedName = JigsawStudioGraphEditor.normalizeDisplayName(displayName);
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ public class SchemaBuilderIdentityTest {
|
|||||||
@ClassRule
|
@ClassRule
|
||||||
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
|
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(
|
private static final List<Class<?>> SCHEMA_ROOTS = List.of(
|
||||||
IrisDimension.class,
|
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
|
@Test
|
||||||
public void rejects1211VanillaExportWithDifferentVerticalDistance() throws Exception {
|
public void rejects1211VanillaExportWithDifferentVerticalDistance() throws Exception {
|
||||||
when(IrisPlatforms.get().minecraftVersion()).thenReturn("1.21.1");
|
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.ClassRule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.EnumSet;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
@@ -310,6 +313,82 @@ public class StructureAssemblerTest {
|
|||||||
assertEquals(west.getMaxX() + 1, start.getMinX());
|
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
|
@Test
|
||||||
public void selectionPriorityProcessesHigherConnectorsFirstAndPreservesStableTies() {
|
public void selectionPriorityProcessesHigherConnectorsFirstAndPreservesStableTies() {
|
||||||
Fixture fixture = new Fixture();
|
Fixture fixture = new Fixture();
|
||||||
@@ -1115,6 +1194,53 @@ public class StructureAssemblerTest {
|
|||||||
return fixture.assembler(structure("start", 1), 0, 0, 0).assemble(new RNG(137L));
|
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) {
|
private static IrisStructure structure(String startPool, int maxDepth) {
|
||||||
return new IrisStructure().setStartPool(startPool).setMaxDepth(maxDepth).setMaxSizeChunks(8);
|
return new IrisStructure().setStartPool(startPool).setMaxDepth(maxDepth).setMaxSizeChunks(8);
|
||||||
}
|
}
|
||||||
|
|||||||
+140
-116
@@ -2,8 +2,6 @@ package art.arcane.iris.studio.generation;
|
|||||||
|
|
||||||
import art.arcane.iris.pack.loading.IrisData;
|
import art.arcane.iris.pack.loading.IrisData;
|
||||||
import art.arcane.iris.pack.loading.ResourceLoader;
|
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.JigsawPlanarTopology;
|
||||||
import art.arcane.iris.studio.jigsaw.JigsawStudioActivation;
|
import art.arcane.iris.studio.jigsaw.JigsawStudioActivation;
|
||||||
import art.arcane.iris.studio.jigsaw.JigsawStudioBay;
|
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.assertFalse;
|
||||||
import static org.junit.Assert.assertNotSame;
|
import static org.junit.Assert.assertNotSame;
|
||||||
import static org.junit.Assert.assertSame;
|
import static org.junit.Assert.assertSame;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.mockStatic;
|
import static org.mockito.Mockito.mockStatic;
|
||||||
@@ -168,58 +167,124 @@ public class JigsawStudioGeneratorTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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);
|
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(
|
GeneratorFixture fixture = fixture(
|
||||||
|
source,
|
||||||
JigsawStudioMode.PLANAR_JIGSAW,
|
JigsawStudioMode.PLANAR_JIGSAW,
|
||||||
dimensions,
|
dimensions,
|
||||||
JigsawStudioVariantCatalog.empty());
|
new JigsawStudioVariantCatalog(List.of(variant)));
|
||||||
JigsawStudioBay corner = fixture.layout().get("workcell/corner");
|
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 centerX = dimensions.width() / 2;
|
||||||
int centerZ = dimensions.depth() / 2;
|
int centerZ = dimensions.depth() / 2;
|
||||||
|
|
||||||
assertSame(fixture.connectorCap(), stateAt(
|
try (MockedStatic<B> ignored = mockStatic(B.class)) {
|
||||||
fixture.generator(),
|
assertSame(fixture.palette().horizontalSocket(), stateAt(
|
||||||
corner.bounds().originX() + centerX,
|
fixture.generator(), corner.bounds().originX() + centerX, floorY, corner.bounds().originZ()));
|
||||||
glyphY,
|
assertSame(fixture.palette().horizontalSocket(), stateAt(
|
||||||
corner.bounds().originZ()));
|
fixture.generator(), corner.bounds().maxX(), floorY, corner.bounds().originZ() + centerZ));
|
||||||
assertSame(fixture.connectorCap(), stateAt(
|
assertSame(fixture.palette().line(), stateAt(
|
||||||
fixture.generator(),
|
fixture.generator(), corner.bounds().originX() + centerX, floorY, corner.bounds().originZ() + 1));
|
||||||
corner.bounds().maxX(),
|
assertSame(fixture.palette().line(), stateAt(
|
||||||
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(),
|
fixture.generator(),
|
||||||
corner.bounds().originX() + centerX + 1,
|
corner.bounds().originX() + centerX + 1,
|
||||||
glyphY,
|
floorY,
|
||||||
corner.bounds().originZ() + centerZ));
|
corner.bounds().originZ() + centerZ));
|
||||||
assertSame(fixture.topologyBase(), stateAt(
|
assertSame(fixture.palette().base(), stateAt(
|
||||||
fixture.generator(),
|
fixture.generator(), corner.bounds().originX(), floorY, corner.bounds().originZ()));
|
||||||
corner.bounds().originX(),
|
assertFalse(fixture.palette().base() == stateAt(
|
||||||
glyphY,
|
|
||||||
corner.bounds().originZ()));
|
|
||||||
assertFalse(fixture.topologyBase() == stateAt(
|
|
||||||
fixture.generator(),
|
fixture.generator(),
|
||||||
corner.bounds().originX() + centerX,
|
corner.bounds().originX() + centerX,
|
||||||
corner.bounds().originY(),
|
corner.bounds().originY(),
|
||||||
corner.bounds().originZ() + centerZ));
|
corner.bounds().originZ() + centerZ));
|
||||||
|
|
||||||
JigsawStudioBay blank = fixture.layout().get("workcell/blank");
|
JigsawStudioBay blank = fixture.layout().get("workcell/blank");
|
||||||
assertSame(fixture.topologyBase(), stateAt(
|
assertSame(fixture.palette().base(), stateAt(
|
||||||
fixture.generator(),
|
fixture.generator(),
|
||||||
blank.bounds().originX() + centerX,
|
blank.bounds().originX() + centerX,
|
||||||
blank.bounds().originY() - 1,
|
blank.bounds().originY() - 1,
|
||||||
blank.bounds().originZ() + centerZ));
|
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();
|
JigsawStudioControlPosition control = fixture.layout().controlPosition();
|
||||||
assertSame(fixture.controlChest(), stateAt(
|
assertSame(fixture.controlChest(), stateAt(
|
||||||
fixture.generator(), control.worldX(), control.worldY(), control.worldZ()));
|
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
|
@Test
|
||||||
public void paintsEveryWorkcellAsAWhiteConcreteEdgeCuboid() {
|
public void paintsEveryWorkcellAsAWhiteConcreteEdgeCuboid() {
|
||||||
@@ -247,77 +312,6 @@ public class JigsawStudioGeneratorTest {
|
|||||||
workcell.bounds().originZ()));
|
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
|
@Test
|
||||||
public void clipsLargeGlyphPaintingToOneChunkFootprint() {
|
public void clipsLargeGlyphPaintingToOneChunkFootprint() {
|
||||||
GeneratorFixture fixture = fixture(
|
GeneratorFixture fixture = fixture(
|
||||||
@@ -331,10 +325,7 @@ public class JigsawStudioGeneratorTest {
|
|||||||
|
|
||||||
fixture.generator().paintChunk(chunk, chunkX, chunkZ);
|
fixture.generator().paintChunk(chunk, chunkX, chunkZ);
|
||||||
|
|
||||||
int glyphBlocks = chunk.countState(fixture.topologyBase())
|
assertEquals(256, chunk.countState(fixture.palette().base()));
|
||||||
+ chunk.countState(fixture.topologyPath())
|
|
||||||
+ chunk.countState(fixture.connectorCap());
|
|
||||||
assertEquals(256, glyphBlocks);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -467,6 +458,29 @@ public class JigsawStudioGeneratorTest {
|
|||||||
assertNotSame(nextGeneration, fixture.generator().renderVariant(workcell, variant, 5L));
|
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
|
@Test
|
||||||
public void mapsAllVanillaJigsawOrientations() {
|
public void mapsAllVanillaJigsawOrientations() {
|
||||||
assertEquals("north_up", JigsawStudioGenerator.orientation(
|
assertEquals("north_up", JigsawStudioGenerator.orientation(
|
||||||
@@ -535,9 +549,13 @@ public class JigsawStudioGeneratorTest {
|
|||||||
NativeBlockState lightFloor = mock(NativeBlockState.class);
|
NativeBlockState lightFloor = mock(NativeBlockState.class);
|
||||||
NativeBlockState darkFloor = mock(NativeBlockState.class);
|
NativeBlockState darkFloor = mock(NativeBlockState.class);
|
||||||
NativeBlockState frame = mock(NativeBlockState.class);
|
NativeBlockState frame = mock(NativeBlockState.class);
|
||||||
NativeBlockState topologyBase = mock(NativeBlockState.class);
|
JigsawStudioGenerator.FloorPalette palette = new JigsawStudioGenerator.FloorPalette(
|
||||||
NativeBlockState topologyPath = mock(NativeBlockState.class);
|
mock(NativeBlockState.class),
|
||||||
NativeBlockState connectorCap = mock(NativeBlockState.class);
|
mock(NativeBlockState.class),
|
||||||
|
mock(NativeBlockState.class),
|
||||||
|
mock(NativeBlockState.class),
|
||||||
|
mock(NativeBlockState.class),
|
||||||
|
mock(NativeBlockState.class));
|
||||||
NativeBlockState invalidMarker = mock(NativeBlockState.class);
|
NativeBlockState invalidMarker = mock(NativeBlockState.class);
|
||||||
NativeBlockState controlChest = mock(NativeBlockState.class);
|
NativeBlockState controlChest = mock(NativeBlockState.class);
|
||||||
JigsawStudioGenerator generator = new JigsawStudioGenerator(
|
JigsawStudioGenerator generator = new JigsawStudioGenerator(
|
||||||
@@ -547,9 +565,7 @@ public class JigsawStudioGeneratorTest {
|
|||||||
lightFloor,
|
lightFloor,
|
||||||
darkFloor,
|
darkFloor,
|
||||||
frame,
|
frame,
|
||||||
topologyBase,
|
palette,
|
||||||
topologyPath,
|
|
||||||
connectorCap,
|
|
||||||
invalidMarker,
|
invalidMarker,
|
||||||
controlChest,
|
controlChest,
|
||||||
JigsawStudioGeneratorTest::renderFinalState);
|
JigsawStudioGeneratorTest::renderFinalState);
|
||||||
@@ -557,12 +573,22 @@ public class JigsawStudioGeneratorTest {
|
|||||||
generator,
|
generator,
|
||||||
layout,
|
layout,
|
||||||
frame,
|
frame,
|
||||||
topologyBase,
|
palette,
|
||||||
topologyPath,
|
|
||||||
connectorCap,
|
|
||||||
controlChest);
|
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) {
|
private static JigsawStudioVariant planarVariant(String key, JigsawPlanarTopology topology) {
|
||||||
return new JigsawStudioVariant(
|
return new JigsawStudioVariant(
|
||||||
key,
|
key,
|
||||||
@@ -596,9 +622,7 @@ public class JigsawStudioGeneratorTest {
|
|||||||
JigsawStudioGenerator generator,
|
JigsawStudioGenerator generator,
|
||||||
JigsawStudioLayout layout,
|
JigsawStudioLayout layout,
|
||||||
NativeBlockState frame,
|
NativeBlockState frame,
|
||||||
NativeBlockState topologyBase,
|
JigsawStudioGenerator.FloorPalette palette,
|
||||||
NativeBlockState topologyPath,
|
|
||||||
NativeBlockState connectorCap,
|
|
||||||
NativeBlockState controlChest
|
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);
|
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
|
@Test
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public void mapsAuthorLabelsAndCanonicalPerVariantDimensionsIndependently() {
|
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(
|
JigsawStudioGraphEditor.VariantFamilyCreation creation = JigsawStudioGraphEditor.duplicateActiveFamily(
|
||||||
packRoot,
|
packRoot,
|
||||||
"studio/spatial",
|
"studio/spatial",
|
||||||
Map.of(JigsawStudioLayout.SPATIAL_WORKCELL_ID, "studio/spatial/start"),
|
Map.of(JigsawStudioLayout.SPATIAL_WORKCELL_ID, "studio/spatial/blank"),
|
||||||
"variant-2");
|
"variant-2");
|
||||||
|
|
||||||
assertTrue(creation.writeResult().successful());
|
assertTrue(creation.writeResult().successful());
|
||||||
@@ -499,7 +499,7 @@ public class JigsawStudioPersistenceEditorsTest {
|
|||||||
"studio/spatial/variants/spatial/variant-2",
|
"studio/spatial/variants/spatial/variant-2",
|
||||||
creation.pieceKeysByWorkcell().get(JigsawStudioLayout.SPATIAL_WORKCELL_ID));
|
creation.pieceKeysByWorkcell().get(JigsawStudioLayout.SPATIAL_WORKCELL_ID));
|
||||||
JsonObject pool = readJson(packRoot.resolve("jigsaw-pools/studio/spatial/start.json"));
|
JsonObject pool = readJson(packRoot.resolve("jigsaw-pools/studio/spatial/start.json"));
|
||||||
assertEquals(8, pool.getAsJsonArray("pieces").size());
|
assertEquals(25, pool.getAsJsonArray("pieces").size());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -507,10 +507,13 @@ public class JigsawStudioPersistenceEditorsTest {
|
|||||||
Path packRoot = createSpatialProject("spatial-theme-row");
|
Path packRoot = createSpatialProject("spatial-theme-row");
|
||||||
|
|
||||||
Map<String, String> sources = new LinkedHashMap<>();
|
Map<String, String> sources = new LinkedHashMap<>();
|
||||||
sources.put(JigsawStudioLayout.SPATIAL_WORKCELL_ID, "studio/spatial/start");
|
for (String vertical : List.of("", "-up", "-down", "-up-down")) {
|
||||||
for (int connectorCount = 1; connectorCount <= 6; connectorCount++) {
|
for (String horizontal : List.of("blank", "end", "straight", "corner", "tee", "cross")) {
|
||||||
String pieceKey = "studio/spatial/connectors-" + connectorCount;
|
String pieceKey = "studio/spatial/" + horizontal + vertical;
|
||||||
sources.put(JigsawStudioLayout.SPATIAL_WORKCELL_ID + "/" + pieceKey, pieceKey);
|
sources.put(sources.isEmpty()
|
||||||
|
? JigsawStudioLayout.SPATIAL_WORKCELL_ID
|
||||||
|
: JigsawStudioLayout.SPATIAL_WORKCELL_ID + "/" + pieceKey, pieceKey);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
JigsawStudioGraphEditor.VariantFamilyCreation creation = JigsawStudioGraphEditor.duplicateActiveFamily(
|
JigsawStudioGraphEditor.VariantFamilyCreation creation = JigsawStudioGraphEditor.duplicateActiveFamily(
|
||||||
packRoot,
|
packRoot,
|
||||||
@@ -519,13 +522,13 @@ public class JigsawStudioPersistenceEditorsTest {
|
|||||||
"variant-2");
|
"variant-2");
|
||||||
|
|
||||||
assertTrue(creation.writeResult().successful());
|
assertTrue(creation.writeResult().successful());
|
||||||
assertEquals(7, creation.pieceKeysByWorkcell().size());
|
assertEquals(24, creation.pieceKeysByWorkcell().size());
|
||||||
for (String targetPieceKey : creation.pieceKeysByWorkcell().values()) {
|
for (String targetPieceKey : creation.pieceKeysByWorkcell().values()) {
|
||||||
assertTrue(Files.isRegularFile(packRoot.resolve("jigsaw-pieces/" + targetPieceKey + ".json")));
|
assertTrue(Files.isRegularFile(packRoot.resolve("jigsaw-pieces/" + targetPieceKey + ".json")));
|
||||||
assertTrue(Files.isRegularFile(packRoot.resolve("objects/" + targetPieceKey + ".iob")));
|
assertTrue(Files.isRegularFile(packRoot.resolve("objects/" + targetPieceKey + ".iob")));
|
||||||
}
|
}
|
||||||
JsonObject pool = readJson(packRoot.resolve("jigsaw-pools/studio/spatial/start.json"));
|
JsonObject pool = readJson(packRoot.resolve("jigsaw-pools/studio/spatial/start.json"));
|
||||||
assertEquals(14, pool.getAsJsonArray("pieces").size());
|
assertEquals(48, pool.getAsJsonArray("pieces").size());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -557,7 +560,7 @@ public class JigsawStudioPersistenceEditorsTest {
|
|||||||
"END").get("displayName").getAsString());
|
"END").get("displayName").getAsString());
|
||||||
|
|
||||||
Path spatialRoot = createSpatialProject("labels-spatial");
|
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);
|
byte[] spatialBefore = Files.readAllBytes(spatialObject);
|
||||||
|
|
||||||
assertTrue(JigsawStudioStructureEditor.updateSpatialWorkcellDisplayName(
|
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 PIECE_POOL_KEY = "studio/identity/pieces";
|
||||||
private static final String RECORDED_AT_FIELD = "recordedAtEpochMilli";
|
private static final String RECORDED_AT_FIELD = "recordedAtEpochMilli";
|
||||||
private static final String SCRIPTED_SESSION_DIGEST =
|
private static final String SCRIPTED_SESSION_DIGEST =
|
||||||
"318d9338a63a44063c85e59a9e74f869473d6e0f15c362536172f7723d960faa";
|
"42b5d3bed8d74b2ef7a010a58302544d0d425c83faba0dc86efb0e41e69ba627";
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
public final TemporaryFolder temporaryFolder = new TemporaryFolder();
|
public final TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||||
|
|||||||
+43
-24
@@ -102,9 +102,15 @@ public class JigsawStudioProjectCreatorTest {
|
|||||||
JsonObject startPool = JsonParser.parseString(Files.readString(
|
JsonObject startPool = JsonParser.parseString(Files.readString(
|
||||||
temporaryDirectory.resolve("jigsaw-pools/settlement/test/start.json"),
|
temporaryDirectory.resolve("jigsaw-pools/settlement/test/start.json"),
|
||||||
StandardCharsets.UTF_8)).getAsJsonObject();
|
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)
|
assertEquals("settlement/test/cross", startPool.getAsJsonArray("pieces").get(0)
|
||||||
.getAsJsonObject().get("piece").getAsString());
|
.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(
|
JsonObject piecePool = JsonParser.parseString(Files.readString(
|
||||||
temporaryDirectory.resolve("jigsaw-pools/settlement/test/pieces.json"),
|
temporaryDirectory.resolve("jigsaw-pools/settlement/test/pieces.json"),
|
||||||
StandardCharsets.UTF_8)).getAsJsonObject();
|
StandardCharsets.UTF_8)).getAsJsonObject();
|
||||||
@@ -229,7 +235,7 @@ public class JigsawStudioProjectCreatorTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void spatialProjectSeedsZeroThroughSixConnectorVariants() throws Exception {
|
public void spatialProjectSeedsEveryYRotationTopologyVariant() throws Exception {
|
||||||
Path temporaryDirectory = temporaryFolder.getRoot().toPath();
|
Path temporaryDirectory = temporaryFolder.getRoot().toPath();
|
||||||
JigsawStudioProjectCreator.Options options = new JigsawStudioProjectCreator.Options(
|
JigsawStudioProjectCreator.Options options = new JigsawStudioProjectCreator.Options(
|
||||||
"stronghold/gallery",
|
"stronghold/gallery",
|
||||||
@@ -246,35 +252,48 @@ public class JigsawStudioProjectCreatorTest {
|
|||||||
JsonObject piecePool = JsonParser.parseString(Files.readString(
|
JsonObject piecePool = JsonParser.parseString(Files.readString(
|
||||||
temporaryDirectory.resolve("jigsaw-pools/stronghold/gallery/pieces.json"),
|
temporaryDirectory.resolve("jigsaw-pools/stronghold/gallery/pieces.json"),
|
||||||
StandardCharsets.UTF_8)).getAsJsonObject();
|
StandardCharsets.UTF_8)).getAsJsonObject();
|
||||||
assertEquals(7, startPool.getAsJsonArray("pieces").size());
|
assertEquals(24, startPool.getAsJsonArray("pieces").size());
|
||||||
assertEquals(7, piecePool.getAsJsonArray("pieces").size());
|
assertEquals("stronghold/gallery/blank", startPool.getAsJsonArray("pieces").get(0)
|
||||||
assertEquals("stronghold/gallery/connectors-1", piecePool.getAsJsonArray("pieces").get(0)
|
|
||||||
.getAsJsonObject().get("piece").getAsString());
|
.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());
|
.get("empty").getAsBoolean());
|
||||||
for (int connectorCount = 0; connectorCount <= 6; connectorCount++) {
|
List<String> horizontalClasses = List.of("blank", "end", "straight", "corner", "tee", "cross");
|
||||||
String pieceName = connectorCount == 0 ? "start" : "connectors-" + connectorCount;
|
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(
|
JsonObject piece = JsonParser.parseString(Files.readString(
|
||||||
temporaryDirectory.resolve("jigsaw-pieces/stronghold/gallery/"
|
temporaryDirectory.resolve("jigsaw-pieces/stronghold/gallery/" + pieceName + ".json"),
|
||||||
+ pieceName + ".json"),
|
|
||||||
StandardCharsets.UTF_8)).getAsJsonObject();
|
StandardCharsets.UTF_8)).getAsJsonObject();
|
||||||
assertEquals(connectorCount, piece.getAsJsonArray("connectors").size());
|
int verticalCount = vertical.isEmpty() ? 0 : vertical.equals("-up-down") ? 2 : 1;
|
||||||
assertEquals(connectorCount + (connectorCount == 1 ? " Connector" : " Connectors"),
|
assertEquals(pieceName, horizontalCounts.get(index) + verticalCount,
|
||||||
piece.get("displayName").getAsString());
|
piece.getAsJsonArray("connectors").size());
|
||||||
assertEquals(16, piece.getAsJsonObject("rules").get("maximumPlacements").getAsInt());
|
assertEquals(4, piece.getAsJsonObject("rules").get("maximumPlacements").getAsInt());
|
||||||
assertEquals(new IrisBlockVector(15, 15, 15), IrisObject.sampleSize(
|
assertEquals(new IrisBlockVector(15, 15, 15), IrisObject.sampleSize(
|
||||||
temporaryDirectory.resolve("objects/stronghold/gallery/"
|
temporaryDirectory.resolve("objects/stronghold/gallery/"
|
||||||
+ pieceName + ".iob").toFile()));
|
+ 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();
|
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());
|
.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());
|
.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());
|
.getAsJsonObject().getAsJsonObject("position").get("y").getAsInt());
|
||||||
|
assertEquals("NORTH_NEGATIVE_Z", cornerShaft.getAsJsonArray("connectors").get(3)
|
||||||
|
.getAsJsonObject().get("top").getAsString());
|
||||||
|
|
||||||
StructureGraphCompilation compilation = StructureResourceBundleGraphCompiler.compile(
|
StructureGraphCompilation compilation = StructureResourceBundleGraphCompiler.compile(
|
||||||
JigsawStudioProjectCreator.bundle(options)).getFirst();
|
JigsawStudioProjectCreator.bundle(options)).getFirst();
|
||||||
@@ -283,7 +302,7 @@ public class JigsawStudioProjectCreatorTest {
|
|||||||
new IrisPosition(0, 0, 0)).assemble(new RNG(1337L));
|
new IrisPosition(0, 0, 0)).assemble(new RNG(1337L));
|
||||||
assertEquals(preview.detail(), StructureAssemblyStatus.COMPLETE, preview.status());
|
assertEquals(preview.detail(), StructureAssemblyStatus.COMPLETE, preview.status());
|
||||||
assertTrue(preview.pieces().size() > 1);
|
assertTrue(preview.pieces().size() > 1);
|
||||||
assertTrue(preview.pieces().size() <= 96);
|
assertTrue(preview.pieces().size() <= 93);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -411,10 +430,10 @@ public class JigsawStudioProjectCreatorTest {
|
|||||||
JsonObject pool = JsonParser.parseString(Files.readString(
|
JsonObject pool = JsonParser.parseString(Files.readString(
|
||||||
temporaryDirectory.resolve("jigsaw-pools/stronghold/test/start.json"),
|
temporaryDirectory.resolve("jigsaw-pools/stronghold/test/start.json"),
|
||||||
StandardCharsets.UTF_8)).getAsJsonObject();
|
StandardCharsets.UTF_8)).getAsJsonObject();
|
||||||
assertEquals(8, pool.getAsJsonArray("pieces").size());
|
assertEquals(25, pool.getAsJsonArray("pieces").size());
|
||||||
assertEquals("stronghold/test/hall", pool.getAsJsonArray("pieces").get(7)
|
assertEquals("stronghold/test/hall", pool.getAsJsonArray("pieces").get(24)
|
||||||
.getAsJsonObject().get("piece").getAsString());
|
.getAsJsonObject().get("piece").getAsString());
|
||||||
assertEquals(3, pool.getAsJsonArray("pieces").get(7)
|
assertEquals(3, pool.getAsJsonArray("pieces").get(24)
|
||||||
.getAsJsonObject().get("weight").getAsInt());
|
.getAsJsonObject().get("weight").getAsInt());
|
||||||
|
|
||||||
StructureWriteResult resized = JigsawStudioStructureEditor.updateCellSize(
|
StructureWriteResult resized = JigsawStudioStructureEditor.updateCellSize(
|
||||||
@@ -435,7 +454,7 @@ public class JigsawStudioProjectCreatorTest {
|
|||||||
IrisBlockVector hallSize = IrisObject.sampleSize(
|
IrisBlockVector hallSize = IrisObject.sampleSize(
|
||||||
temporaryDirectory.resolve("objects/stronghold/test/hall.iob").toFile());
|
temporaryDirectory.resolve("objects/stronghold/test/hall.iob").toFile());
|
||||||
IrisBlockVector starterSize = IrisObject.sampleSize(
|
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(16, 12, 18), hallSize);
|
||||||
assertEquals(new IrisBlockVector(12, 10, 14), starterSize);
|
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() {
|
private static PlayerInteractEvent probe() {
|
||||||
Player player = mock(Player.class);
|
Player player = mock(Player.class);
|
||||||
World world = mock(World.class);
|
World world = mock(World.class);
|
||||||
|
|||||||
+12
-12
@@ -80,20 +80,20 @@ public class JigsawStudioResourceBundleAssemblerTest {
|
|||||||
JigsawStudioResourceBundleAssembler.assemble(
|
JigsawStudioResourceBundleAssembler.assemble(
|
||||||
packRoot,
|
packRoot,
|
||||||
"fort",
|
"fort",
|
||||||
"fort/start",
|
"fort/blank",
|
||||||
emptyObject(),
|
emptyObject(),
|
||||||
List.of(connector),
|
List.of(connector),
|
||||||
false);
|
false);
|
||||||
|
|
||||||
assertEquals(17, assembly.bundle().resources().size());
|
assertEquals(51, assembly.bundle().resources().size());
|
||||||
assertEquals("fort/start", assembly.objectKey());
|
assertEquals("fort/blank", assembly.objectKey());
|
||||||
assertEquals(1, assembly.piece().getConnectors().size());
|
assertEquals(1, assembly.piece().getConnectors().size());
|
||||||
StructureWriteResult result = new StructureTransactionWriter(packRoot)
|
StructureWriteResult result = new StructureTransactionWriter(packRoot)
|
||||||
.write(assembly.bundle(), StructureWriteMode.OVERWRITE);
|
.write(assembly.bundle(), StructureWriteMode.OVERWRITE);
|
||||||
assertTrue(result.successful());
|
assertTrue(result.successful());
|
||||||
assertTrue(result.committed());
|
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(
|
IrisJigsawPiece persisted = new Gson().fromJson(
|
||||||
Files.readString(piecePath, StandardCharsets.UTF_8), IrisJigsawPiece.class);
|
Files.readString(piecePath, StandardCharsets.UTF_8), IrisJigsawPiece.class);
|
||||||
assertEquals(1, persisted.getConnectors().size());
|
assertEquals(1, persisted.getConnectors().size());
|
||||||
@@ -143,7 +143,7 @@ public class JigsawStudioResourceBundleAssemblerTest {
|
|||||||
@Test
|
@Test
|
||||||
public void capturePreservesAbsentDefaultsAndUnknownPieceMetadata() throws Exception {
|
public void capturePreservesAbsentDefaultsAndUnknownPieceMetadata() throws Exception {
|
||||||
Path packRoot = createProject();
|
Path packRoot = createProject();
|
||||||
String pieceResource = "jigsaw-pieces/fort/start.json";
|
String pieceResource = "jigsaw-pieces/fort/blank.json";
|
||||||
Path piecePath = packRoot.resolve(pieceResource);
|
Path piecePath = packRoot.resolve(pieceResource);
|
||||||
JsonObject source = JsonParser.parseString(
|
JsonObject source = JsonParser.parseString(
|
||||||
Files.readString(piecePath, StandardCharsets.UTF_8)).getAsJsonObject();
|
Files.readString(piecePath, StandardCharsets.UTF_8)).getAsJsonObject();
|
||||||
@@ -158,7 +158,7 @@ public class JigsawStudioResourceBundleAssemblerTest {
|
|||||||
JigsawStudioResourceBundleAssembler.assemble(
|
JigsawStudioResourceBundleAssembler.assemble(
|
||||||
packRoot,
|
packRoot,
|
||||||
STRUCTURE_KEY,
|
STRUCTURE_KEY,
|
||||||
"fort/start",
|
"fort/blank",
|
||||||
emptyObject(),
|
emptyObject(),
|
||||||
List.copyOf(sourcePiece.getConnectors()),
|
List.copyOf(sourcePiece.getConnectors()),
|
||||||
false);
|
false);
|
||||||
@@ -173,7 +173,7 @@ public class JigsawStudioResourceBundleAssemblerTest {
|
|||||||
JigsawStudioResourceBundleAssembler.assemble(
|
JigsawStudioResourceBundleAssembler.assemble(
|
||||||
packRoot,
|
packRoot,
|
||||||
STRUCTURE_KEY,
|
STRUCTURE_KEY,
|
||||||
"fort/start",
|
"fort/blank",
|
||||||
emptyObject(),
|
emptyObject(),
|
||||||
List.of(updated),
|
List.of(updated),
|
||||||
false);
|
false);
|
||||||
@@ -202,7 +202,7 @@ public class JigsawStudioResourceBundleAssemblerTest {
|
|||||||
() -> JigsawStudioResourceBundleAssembler.assemble(
|
() -> JigsawStudioResourceBundleAssembler.assemble(
|
||||||
packRoot,
|
packRoot,
|
||||||
STRUCTURE_KEY,
|
STRUCTURE_KEY,
|
||||||
"fort/start",
|
"fort/blank",
|
||||||
emptyObject(),
|
emptyObject(),
|
||||||
List.of(connector()),
|
List.of(connector()),
|
||||||
false));
|
false));
|
||||||
@@ -213,7 +213,7 @@ public class JigsawStudioResourceBundleAssemblerTest {
|
|||||||
@Test
|
@Test
|
||||||
public void refusesManagedDatapackOwnershipWithoutAssemblingAStudioSave() throws Exception {
|
public void refusesManagedDatapackOwnershipWithoutAssemblingAStudioSave() throws Exception {
|
||||||
Path packRoot = createProject();
|
Path packRoot = createProject();
|
||||||
Path objectPath = packRoot.resolve("objects/fort/start.iob");
|
Path objectPath = packRoot.resolve("objects/fort/blank.iob");
|
||||||
byte[] before = Files.readAllBytes(objectPath);
|
byte[] before = Files.readAllBytes(objectPath);
|
||||||
markManagedDatapack(packRoot, STRUCTURE_KEY);
|
markManagedDatapack(packRoot, STRUCTURE_KEY);
|
||||||
|
|
||||||
@@ -221,7 +221,7 @@ public class JigsawStudioResourceBundleAssemblerTest {
|
|||||||
() -> JigsawStudioResourceBundleAssembler.assemble(
|
() -> JigsawStudioResourceBundleAssembler.assemble(
|
||||||
packRoot,
|
packRoot,
|
||||||
STRUCTURE_KEY,
|
STRUCTURE_KEY,
|
||||||
"fort/start",
|
"fort/blank",
|
||||||
emptyObject(),
|
emptyObject(),
|
||||||
List.of(connector()),
|
List.of(connector()),
|
||||||
false));
|
false));
|
||||||
@@ -234,13 +234,13 @@ public class JigsawStudioResourceBundleAssemblerTest {
|
|||||||
@Test
|
@Test
|
||||||
public void writerRejectsAResourceModifiedOutsideTheOwnershipTransaction() throws Exception {
|
public void writerRejectsAResourceModifiedOutsideTheOwnershipTransaction() throws Exception {
|
||||||
Path packRoot = createProject();
|
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");
|
Files.writeString(piecePath, Files.readString(piecePath, StandardCharsets.UTF_8) + "\n");
|
||||||
JigsawStudioResourceBundleAssembler.Assembly assembly =
|
JigsawStudioResourceBundleAssembler.Assembly assembly =
|
||||||
JigsawStudioResourceBundleAssembler.assemble(
|
JigsawStudioResourceBundleAssembler.assemble(
|
||||||
packRoot,
|
packRoot,
|
||||||
STRUCTURE_KEY,
|
STRUCTURE_KEY,
|
||||||
"fort/start",
|
"fort/blank",
|
||||||
emptyObject(),
|
emptyObject(),
|
||||||
List.of(connector().setPool("fort/start")),
|
List.of(connector().setPool("fort/start")),
|
||||||
false);
|
false);
|
||||||
|
|||||||
+8
-4
@@ -181,11 +181,13 @@ public class JigsawStudioServiceCaptureTest {
|
|||||||
when(state.isCustom()).thenReturn(false);
|
when(state.isCustom()).thenReturn(false);
|
||||||
when(state.nativeHandle()).thenReturn(blockData);
|
when(state.nativeHandle()).thenReturn(blockData);
|
||||||
|
|
||||||
JigsawStudioChunkWriter.restoreConnectorChunk(
|
JigsawStudioCapture.LocalPosition position = new JigsawStudioCapture.LocalPosition(1, 2, 3);
|
||||||
|
JigsawStudioChunkWriter.restoreChunkPositions(
|
||||||
world,
|
world,
|
||||||
workcell,
|
workcell,
|
||||||
List.of(renderedConnector),
|
Set.of(position),
|
||||||
Map.of(new JigsawStudioCapture.LocalPosition(1, 2, 3), renderedBlock),
|
Map.of(position, renderedConnector),
|
||||||
|
Map.of(position, renderedBlock),
|
||||||
false);
|
false);
|
||||||
|
|
||||||
verify(world).getBlockAt(11, 22, 33);
|
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, "/iris jigsaw status"));
|
||||||
assertFalse(JigsawStudioProtection.blocksMutatingCommand(ownerId, otherId, "/msg owner hello"));
|
assertFalse(JigsawStudioProtection.blocksMutatingCommand(ownerId, otherId, "/msg owner hello"));
|
||||||
assertTrue(JigsawStudioProtection.blocksMutatingCommand(ownerId, otherId, "/execute run say bypass"));
|
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(
|
assertTrue(JigsawStudioProtection.blocksNonEditableWorkcellMutation(
|
||||||
true, "/fill 0 0 0 1 1 1 stone"));
|
true, "/fill 0 0 0 1 1 1 stone"));
|
||||||
assertTrue(JigsawStudioProtection.blocksNonEditableWorkcellMutation(true, "//paste -a"));
|
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