This commit is contained in:
Brian Neumann-Fopiano
2026-08-10 17:32:44 -04:00
parent 998a5c9f5f
commit dfae45663a
56 changed files with 2194 additions and 191 deletions
@@ -621,7 +621,14 @@ public final class BukkitCommandMessagesExtended {
);
public static final TextKey COMMAND_STRUCTURE_PLACED_PIECES_AT_YOUR_LOCATION = TextKey.of(
"iris.bukkit.commandstructure.placed_pieces_at_your_location",
C.GREEN + "Placed '" + "{structure}" + "' (" + "{value}" + " pieces) at your location."
C.GREEN + "Placed '" + "{structure}" + "' (" + "{value}" + " pieces, "
+ "{value2}" + " block changes) at your location."
);
public static final TextKey COMMAND_STRUCTURE_PLACEMENT_CHANGED_NO_BLOCKS = TextKey.of(
"iris.bukkit.commandstructure.placement_changed_no_blocks",
C.RED + "Structure '" + "{structure}" + "' assembled " + "{value}"
+ " pieces but changed 0 blocks at your location. Check that the selected variants contain "
+ "non-air blocks and that the placement is above the world's minimum height."
);
public static final TextKey COMMAND_STUDIO_OPENING_STUDIO_PACK_SEED = TextKey.of(
"iris.bukkit.commandstudio.opening_studio_pack_seed",
@@ -991,6 +998,7 @@ public final class BukkitCommandMessagesExtended {
COMMAND_STRUCTURE_NO_IRIS_STRUCTURE_THIS_PACK_2,
COMMAND_STRUCTURE_STRUCTURE_ASSEMBLED_0_PIECES,
COMMAND_STRUCTURE_PLACED_PIECES_AT_YOUR_LOCATION,
COMMAND_STRUCTURE_PLACEMENT_CHANGED_NO_BLOCKS,
COMMAND_STUDIO_OPENING_STUDIO_PACK_SEED,
COMMAND_STUDIO_PROVIDE_DIMENSION_PACK_IRIS_STD_IMPORTVANILLA_PACK_DIMENSION,
COMMAND_STUDIO_COULD_NOT_RESOLVE_PACK_DIMENSION,
@@ -182,7 +182,27 @@ public final class InPlaceChunkRegenerator {
world.refreshChunk(chunkX, chunkZ);
}
static void applyBlockDiffs(Chunk chunk, ChunkSnapshot snapshot, ChunkData generated, int minHeight, int maxHeight) {
public static void applyBlockDiffs(
Chunk chunk,
ChunkData generated,
int minHeight,
int maxHeight
) {
applyBlockDiffs(
chunk,
chunk.getChunkSnapshot(false, false, false),
generated,
minHeight,
maxHeight);
}
public static void applyBlockDiffs(
Chunk chunk,
ChunkSnapshot snapshot,
ChunkData generated,
int minHeight,
int maxHeight
) {
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
for (int y = minHeight; y < maxHeight; y++) {
@@ -46,7 +46,7 @@ public final class JigsawStudioGraphMapper {
JigsawStudioVariantCatalog catalog = catalog(data, structure, mode);
IrisPosition configuredCell = structure.getCellSize();
JigsawStudioCellDimensions dimensions = configuredCell == null
? new JigsawStudioCellDimensions(16, 16, 16)
? new JigsawStudioCellDimensions(15, 15, 15)
: new JigsawStudioCellDimensions(
Math.max(1, configuredCell.getX()),
Math.max(1, configuredCell.getY()),
@@ -12,7 +12,7 @@ import java.util.Optional;
public final class JigsawStudioLayout {
public static final int FLOOR_Y = 64;
public static final int PLANAR_COLUMNS = 3;
public static final int PLANAR_GAP = 2;
public static final int PLANAR_GAP = 1;
public static final int MAX_VARIANTS = 512;
public static final String SPATIAL_WORKCELL_ID = "workcell/spatial";
@@ -83,6 +83,16 @@ public final class JigsawStudioSession {
return state.snapshot(workcellId);
}
public synchronized boolean setConnectorsVisible(String workcellId, boolean visible) {
MutableWorkcellState state = requireWorkcellState(workcellId);
if (state.connectorsVisible == visible) {
return false;
}
state.connectorsVisible = visible;
revision++;
return true;
}
public synchronized boolean replaceLayout(JigsawStudioLayout replacement) {
JigsawStudioLayout nextLayout = Objects.requireNonNull(replacement, "Replacement Jigsaw Studio layout");
if (layout.mode() != nextLayout.mode()) {
@@ -107,7 +117,8 @@ public final class JigsawStudioSession {
activeVariantKey,
nextLoadGeneration(),
nextMutationGeneration(),
false));
false,
previous != null && previous.connectorsVisible));
}
layout = nextLayout;
workcells.clear();
@@ -161,7 +172,8 @@ public final class JigsawStudioSession {
targetVariantKey,
nextLoadGeneration(),
nextMutationGeneration(),
false));
false,
previous != null && previous.connectorsVisible));
}
layout = nextLayout;
workcells.clear();
@@ -419,6 +431,7 @@ public final class JigsawStudioSession {
variantKey,
nextLoadGeneration(),
nextMutationGeneration(),
false,
false));
}
}
@@ -520,7 +533,8 @@ public final class JigsawStudioSession {
long mutationGeneration,
boolean dirty,
boolean saveInProgress,
boolean switchInProgress
boolean switchInProgress,
boolean connectorsVisible
) {
}
@@ -635,21 +649,29 @@ public final class JigsawStudioSession {
private long saveGeneration;
private boolean switchInProgress;
private long switchGeneration;
private boolean connectorsVisible;
private MutableWorkcellState(
String activeVariantKey,
long loadGeneration,
long mutationGeneration,
boolean dirty
boolean dirty,
boolean connectorsVisible
) {
this.activeVariantKey = activeVariantKey;
this.loadGeneration = loadGeneration;
this.mutationGeneration = mutationGeneration;
this.dirty = dirty;
this.connectorsVisible = connectorsVisible;
}
private MutableWorkcellState copy() {
return new MutableWorkcellState(activeVariantKey, loadGeneration, mutationGeneration, dirty);
return new MutableWorkcellState(
activeVariantKey,
loadGeneration,
mutationGeneration,
dirty,
connectorsVisible);
}
private WorkcellSnapshot snapshot(String workcellId) {
@@ -660,7 +682,8 @@ public final class JigsawStudioSession {
mutationGeneration,
dirty,
saveInProgress,
switchInProgress);
switchInProgress,
connectorsVisible);
}
}
}
@@ -0,0 +1,513 @@
package art.arcane.iris.core.service;
import art.arcane.iris.core.structure.authoring.StructureBackend;
import art.arcane.iris.core.structure.authoring.StructureCapability;
import art.arcane.iris.core.structure.authoring.StructureHash;
import art.arcane.iris.core.structure.authoring.StructureKey;
import art.arcane.iris.core.structure.authoring.StructureLoss;
import art.arcane.iris.core.structure.authoring.StructureOwnershipManifest;
import art.arcane.iris.core.structure.authoring.StructureResourceBundle;
import art.arcane.iris.core.structure.authoring.StructureSource;
import art.arcane.iris.core.structure.authoring.StructureTransactionWriter;
import art.arcane.iris.core.structure.authoring.StructureWriteOptions;
import art.arcane.iris.core.structure.authoring.StructureWriteResult;
import art.arcane.iris.engine.framework.structure.StructureResourceBundleGraphCompiler;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Base64;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.locks.ReentrantLock;
final class JigsawStudioHistoryStore {
static final int MAX_ITERATIONS = 5;
private static final int SCHEMA_VERSION = 1;
private static final long MAX_HISTORY_BYTES = 512L * 1024L * 1024L;
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
private static final ConcurrentMap<Path, ReentrantLock> LOCKS = new ConcurrentHashMap<>();
private final Path packRoot;
private final StructureKey structureKey;
private final StructureTransactionWriter writer;
private final Path historyPath;
private final ReentrantLock lock;
JigsawStudioHistoryStore(Path packRoot, String structureKey) {
this.packRoot = canonicalPackRoot(packRoot);
this.structureKey = new StructureKey(
"iris",
Objects.requireNonNull(structureKey, "Jigsaw Studio history structure key"));
writer = new StructureTransactionWriter(this.packRoot);
String identityHash = StructureHash.sha256(this.structureKey.value().getBytes(StandardCharsets.UTF_8));
historyPath = this.packRoot.resolve(".iris/jigsaw-history/key-" + identityHash + ".json").normalize();
if (!historyPath.startsWith(this.packRoot)) {
throw new IllegalArgumentException("Jigsaw Studio history path escapes its pack root");
}
lock = LOCKS.computeIfAbsent(historyPath, ignored -> new ReentrantLock());
}
Snapshot snapshotCurrent(String pieceKey) throws IOException {
lock.lock();
try {
return readCurrentSnapshot(pieceKey);
} finally {
lock.unlock();
}
}
int append(Snapshot snapshot) throws IOException {
Objects.requireNonNull(snapshot, "Jigsaw Studio history snapshot");
lock.lock();
try {
HistoryDocument document = readDocument();
if (!document.structure().equals(structureKey.value())) {
throw new IOException("Jigsaw Studio history belongs to " + document.structure());
}
ArrayList<HistoryIteration> iterations = new ArrayList<>(document.iterations());
HistoryIteration iteration = snapshot.iteration();
if (!iterations.isEmpty() && iterations.getLast().sameState(iteration)) {
return iterations.size();
}
iterations.add(iteration);
while (iterations.size() > MAX_ITERATIONS) {
iterations.removeFirst();
}
TreeMap<String, String> blobs = new TreeMap<>(document.blobs());
for (Map.Entry<String, byte[]> resource : snapshot.resources().entrySet()) {
String hash = StructureHash.sha256(resource.getValue());
blobs.putIfAbsent(hash, Base64.getEncoder().encodeToString(resource.getValue()));
}
retainReferencedBlobs(blobs, iterations);
writeDocument(new HistoryDocument(
SCHEMA_VERSION,
structureKey.value(),
List.copyOf(iterations),
Map.copyOf(blobs)));
return iterations.size();
} finally {
lock.unlock();
}
}
UndoResult undoLatest() throws IOException {
lock.lock();
try {
HistoryDocument document = readDocument();
if (document.iterations().isEmpty()) {
return UndoResult.unavailable();
}
HistoryIteration iteration = document.iterations().getLast();
StructureResourceBundle bundle = restoreBundle(iteration, document.blobs());
StructureResourceBundleGraphCompiler.requireViable(bundle);
Path manifestPath = writer.ownershipManifestPath(structureKey);
byte[] currentManifest = readRegularFile(manifestPath, "ownership manifest");
StructureWriteResult result = writer.write(
bundle,
StructureWriteOptions.overwriteExpected(StructureHash.sha256(currentManifest)));
if (!result.successful()) {
return new UndoResult(
false,
true,
document.iterations().size(),
iteration.pieceKey(),
result,
"");
}
ArrayList<HistoryIteration> remaining = new ArrayList<>(document.iterations());
remaining.removeLast();
TreeMap<String, String> blobs = new TreeMap<>(document.blobs());
retainReferencedBlobs(blobs, remaining);
String warning = "";
try {
if (remaining.isEmpty()) {
Files.deleteIfExists(historyPath);
forceDirectory(historyPath.getParent());
} else {
writeDocument(new HistoryDocument(
SCHEMA_VERSION,
structureKey.value(),
List.copyOf(remaining),
Map.copyOf(blobs)));
}
} catch (IOException historyFailure) {
warning = historyFailure.getMessage() == null
? historyFailure.getClass().getSimpleName()
: historyFailure.getMessage();
}
return new UndoResult(
true,
true,
remaining.size(),
iteration.pieceKey(),
result,
warning);
} finally {
lock.unlock();
}
}
int availableIterations() throws IOException {
lock.lock();
try {
return readDocument().iterations().size();
} finally {
lock.unlock();
}
}
void delete() throws IOException {
lock.lock();
try {
if (Files.deleteIfExists(historyPath)) {
forceDirectory(historyPath.getParent());
}
} finally {
lock.unlock();
}
}
Path historyPath() {
return historyPath;
}
private Snapshot readCurrentSnapshot(String pieceKey) throws IOException {
Path manifestPath = writer.ownershipManifestPath(structureKey);
byte[] manifestContent = readRegularFile(manifestPath, "ownership manifest");
StructureOwnershipManifest manifest;
try {
manifest = StructureOwnershipManifest.fromJson(manifestContent);
} catch (RuntimeException exception) {
throw new IOException("Jigsaw Studio history cannot parse the ownership manifest", exception);
}
if (!manifest.structure().equals(structureKey)) {
throw new IOException("Jigsaw Studio ownership manifest belongs to " + manifest.structure());
}
TreeMap<String, byte[]> resources = new TreeMap<>();
for (Map.Entry<String, String> resource : manifest.resourceHashes().entrySet()) {
Path resourcePath = resolveOwnedResource(resource.getKey());
byte[] content = readRegularFile(resourcePath, "owned resource " + resource.getKey());
String actualHash = StructureHash.sha256(content);
if (!resource.getValue().equals(actualHash)) {
throw new IOException("Jigsaw Studio owned resource changed before history capture: "
+ resource.getKey());
}
resources.put(resource.getKey(), content);
}
return new Snapshot(
new HistoryIteration(
System.currentTimeMillis(),
Objects.requireNonNull(pieceKey, "Jigsaw Studio history piece key"),
manifest.source(),
manifest.backend(),
manifest.capabilities(),
manifest.losses(),
manifest.resourceHashes()),
resources);
}
private StructureResourceBundle restoreBundle(
HistoryIteration iteration,
Map<String, String> blobs
) throws IOException {
StructureResourceBundle.Builder builder = StructureResourceBundle.builder(structureKey)
.source(iteration.source())
.backend(iteration.backend())
.capabilities(iteration.capabilities())
.losses(iteration.losses());
for (Map.Entry<String, String> resource : iteration.resourceHashes().entrySet()) {
String encoded = blobs.get(resource.getValue());
if (encoded == null) {
throw new IOException("Jigsaw Studio history is missing resource blob " + resource.getValue());
}
byte[] content;
try {
content = Base64.getDecoder().decode(encoded);
} catch (IllegalArgumentException exception) {
throw new IOException("Jigsaw Studio history contains invalid resource data", exception);
}
if (!resource.getValue().equals(StructureHash.sha256(content))) {
throw new IOException("Jigsaw Studio history resource hash does not match "
+ resource.getKey());
}
builder.resource(resource.getKey(), content);
}
return builder.build();
}
private HistoryDocument readDocument() throws IOException {
if (!Files.exists(historyPath, LinkOption.NOFOLLOW_LINKS)) {
return HistoryDocument.empty(structureKey.value());
}
byte[] content = readRegularFile(historyPath, "history file");
if (content.length > MAX_HISTORY_BYTES) {
throw new IOException("Jigsaw Studio history exceeds " + MAX_HISTORY_BYTES + " bytes");
}
HistoryDocument document;
try {
document = GSON.fromJson(new String(content, StandardCharsets.UTF_8), HistoryDocument.class);
} catch (RuntimeException exception) {
throw new IOException("Jigsaw Studio history is invalid", exception);
}
if (document == null || document.schemaVersion() != SCHEMA_VERSION) {
throw new IOException("Unsupported Jigsaw Studio history schema");
}
HistoryDocument validated;
try {
validated = document.validated();
} catch (RuntimeException exception) {
throw new IOException("Jigsaw Studio history is invalid", exception);
}
if (!validated.structure().equals(structureKey.value())) {
throw new IOException("Jigsaw Studio history belongs to " + validated.structure());
}
return validated;
}
private void writeDocument(HistoryDocument document) throws IOException {
HistoryDocument validated = document.validated();
byte[] content = (GSON.toJson(validated) + "\n").getBytes(StandardCharsets.UTF_8);
if (content.length > MAX_HISTORY_BYTES) {
throw new IOException("Jigsaw Studio history exceeds " + MAX_HISTORY_BYTES + " bytes");
}
Path historyRoot = historyPath.getParent();
Files.createDirectories(historyRoot);
rejectSymbolicPath(historyRoot);
Path temporary = historyRoot.resolve(historyPath.getFileName() + "."
+ UUID.randomUUID() + ".tmp").normalize();
try {
try (FileChannel channel = FileChannel.open(
temporary,
StandardOpenOption.CREATE_NEW,
StandardOpenOption.WRITE)) {
ByteBuffer buffer = ByteBuffer.wrap(content);
while (buffer.hasRemaining()) {
channel.write(buffer);
}
channel.force(true);
}
try {
Files.move(
temporary,
historyPath,
StandardCopyOption.ATOMIC_MOVE,
StandardCopyOption.REPLACE_EXISTING);
} catch (AtomicMoveNotSupportedException exception) {
Files.move(temporary, historyPath, StandardCopyOption.REPLACE_EXISTING);
}
forceDirectory(historyRoot);
} finally {
Files.deleteIfExists(temporary);
}
}
private Path resolveOwnedResource(String relativePath) throws IOException {
StructureResourceBundle.validateRelativePath(relativePath);
Path resource = packRoot.resolve(relativePath).normalize();
if (!resource.startsWith(packRoot)) {
throw new IOException("Jigsaw Studio history resource escapes its pack root: " + relativePath);
}
rejectSymbolicPath(resource.getParent());
return resource;
}
private static byte[] readRegularFile(Path path, String kind) throws IOException {
if (!Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)) {
throw new IOException("Jigsaw Studio " + kind + " is missing or not a regular file: " + path);
}
return Files.readAllBytes(path);
}
private static void retainReferencedBlobs(
Map<String, String> blobs,
List<HistoryIteration> iterations
) {
Set<String> retained = new TreeSet<>();
for (HistoryIteration iteration : iterations) {
retained.addAll(iteration.resourceHashes().values());
}
blobs.keySet().retainAll(retained);
}
private void rejectSymbolicPath(Path path) throws IOException {
Path current = path;
while (current != null && current.startsWith(packRoot)) {
if (Files.isSymbolicLink(current)) {
throw new IOException("Jigsaw Studio history path contains a symbolic link: " + current);
}
if (current.equals(packRoot)) {
return;
}
current = current.getParent();
}
throw new IOException("Jigsaw Studio history path escapes its pack root: " + path);
}
private static Path canonicalPackRoot(Path root) {
Path normalized = Objects.requireNonNull(root, "Jigsaw Studio history pack root")
.toAbsolutePath().normalize();
try {
return normalized.toRealPath();
} catch (IOException exception) {
throw new IllegalArgumentException("Jigsaw Studio history pack root is unavailable: "
+ normalized, exception);
}
}
private static void forceDirectory(Path directory) throws IOException {
try (FileChannel channel = FileChannel.open(directory, StandardOpenOption.READ)) {
channel.force(true);
}
}
record Snapshot(HistoryIteration iteration, Map<String, byte[]> resources) {
Snapshot {
Objects.requireNonNull(iteration, "Jigsaw Studio history iteration");
Objects.requireNonNull(resources, "Jigsaw Studio history resources");
LinkedHashMap<String, byte[]> copies = new LinkedHashMap<>();
for (Map.Entry<String, byte[]> resource : resources.entrySet()) {
copies.put(resource.getKey(), resource.getValue().clone());
}
resources = Map.copyOf(copies);
}
boolean matches(StructureResourceBundle bundle) {
if (!iteration.source().equals(bundle.source())
|| iteration.backend() != bundle.backend()
|| !Set.copyOf(iteration.capabilities()).equals(bundle.capabilities())
|| !iteration.losses().equals(bundle.losses())
|| iteration.resourceHashes().size() != bundle.resources().size()) {
return false;
}
for (Map.Entry<String, StructureResourceBundle.Resource> resource
: bundle.resources().entrySet()) {
if (!resource.getValue().contentHash().equals(
iteration.resourceHashes().get(resource.getKey()))) {
return false;
}
}
return true;
}
}
record UndoResult(
boolean successful,
boolean available,
int remainingIterations,
String pieceKey,
StructureWriteResult writeResult,
String warning
) {
UndoResult {
pieceKey = pieceKey == null ? "" : pieceKey;
warning = warning == null ? "" : warning;
}
static UndoResult unavailable() {
return new UndoResult(false, false, 0, "", null, "");
}
}
private record HistoryDocument(
int schemaVersion,
String structure,
List<HistoryIteration> iterations,
Map<String, String> blobs
) {
private HistoryDocument {
structure = structure == null ? "" : structure;
iterations = iterations == null ? List.of() : List.copyOf(iterations);
blobs = blobs == null ? Map.of() : Map.copyOf(blobs);
}
private static HistoryDocument empty(String structure) {
return new HistoryDocument(SCHEMA_VERSION, structure, List.of(), Map.of());
}
private HistoryDocument validated() throws IOException {
if (schemaVersion != SCHEMA_VERSION || structure.isBlank()) {
throw new IOException("Jigsaw Studio history header is invalid");
}
if (iterations.size() > MAX_ITERATIONS) {
throw new IOException("Jigsaw Studio history contains too many iterations");
}
TreeMap<String, String> validatedBlobs = new TreeMap<>();
for (Map.Entry<String, String> blob : blobs.entrySet()) {
if (!StructureHash.isSha256(blob.getKey()) || blob.getValue() == null) {
throw new IOException("Jigsaw Studio history contains an invalid resource blob");
}
validatedBlobs.put(blob.getKey(), blob.getValue());
}
return new HistoryDocument(
SCHEMA_VERSION,
structure,
List.copyOf(iterations),
Map.copyOf(validatedBlobs));
}
}
private record HistoryIteration(
long recordedAtEpochMilli,
String pieceKey,
StructureSource source,
StructureBackend backend,
List<StructureCapability> capabilities,
List<StructureLoss> losses,
Map<String, String> resourceHashes
) {
private HistoryIteration {
pieceKey = Objects.requireNonNull(pieceKey, "Jigsaw Studio history piece key").trim();
if (pieceKey.isEmpty()) {
throw new IllegalArgumentException("Jigsaw Studio history piece key cannot be empty");
}
Objects.requireNonNull(source, "Jigsaw Studio history source");
Objects.requireNonNull(backend, "Jigsaw Studio history backend");
capabilities = List.copyOf(Objects.requireNonNull(
capabilities,
"Jigsaw Studio history capabilities"));
losses = List.copyOf(Objects.requireNonNull(losses, "Jigsaw Studio history losses"));
TreeMap<String, String> hashes = new TreeMap<>();
for (Map.Entry<String, String> resource : Objects.requireNonNull(
resourceHashes,
"Jigsaw Studio history resource hashes").entrySet()) {
String relativePath = StructureResourceBundle.validateRelativePath(resource.getKey());
if (!StructureHash.isSha256(resource.getValue())) {
throw new IllegalArgumentException("Invalid Jigsaw Studio history resource hash");
}
hashes.put(relativePath, resource.getValue());
}
if (hashes.isEmpty()) {
throw new IllegalArgumentException("Jigsaw Studio history iteration cannot be empty");
}
resourceHashes = Map.copyOf(hashes);
}
private boolean sameState(HistoryIteration other) {
return source.equals(other.source)
&& backend == other.backend
&& pieceKey.equals(other.pieceKey)
&& capabilities.equals(other.capabilities)
&& losses.equals(other.losses)
&& resourceHashes.equals(other.resourceHashes);
}
}
}
@@ -345,7 +345,9 @@ public final class JigsawStudioMenuController {
element.addLore(ChatColor.GRAY + "Loaded: "
+ (active == null ? "None" : safe(active.displayName())));
element.addLore(workcellStatus(workcell));
element.addLore(ChatColor.YELLOW + "Left-click to select");
element.addLore(ChatColor.GRAY + "Connector blocks: "
+ (workcell.connectorsVisible() ? "Visible" : "Hidden"));
element.addLore(ChatColor.YELLOW + "Left-click to select and teleport");
element.addLore(ChatColor.YELLOW + "Right-click for workcell settings");
if (workcell.dirty() && !workcell.saving()) {
element.addLore(ChatColor.GOLD + "Shift-left: Flush Autosave Now");
@@ -564,6 +566,34 @@ public final class JigsawStudioMenuController {
window.setElement(0, 1, spatial);
}
UIElement connectors = element(
"settings-connectors",
workcell.connectorsVisible() ? Material.JIGSAW : Material.STRUCTURE_VOID,
workcell.connectorsVisible()
? ChatColor.GREEN + "Connector Blocks Visible"
: ChatColor.YELLOW + "Connector Blocks Hidden");
connectors.addLore(ChatColor.GRAY + "Hidden connectors retain their metadata and final block state.");
connectors.addLore(ChatColor.YELLOW + "Left-click to "
+ (workcell.connectorsVisible() ? "hide" : "show") + " connector blocks");
connectors.onLeftClick(clicked -> toggleConnectorBlocks(
window.getViewer(),
state.requestId(),
workcell.stableId(),
!workcell.connectorsVisible()));
window.setElement(2, 1, connectors);
UIElement resetConnectors = element(
"settings-reset-connectors",
Material.RECOVERY_COMPASS,
ChatColor.AQUA + "Reset Connector Blocks");
resetConnectors.addLore(ChatColor.GRAY + "Restore every connector in this workcell from disk.");
resetConnectors.addLore(ChatColor.GRAY + "Other edited blocks are left unchanged.");
resetConnectors.onLeftClick(clicked -> resetConnectorBlocks(
window.getViewer(),
state.requestId(),
workcell.stableId()));
window.setElement(4, 1, resetConnectors);
window.setElement(-2, 2, axisElement(
window,
state,
@@ -588,6 +618,17 @@ public final class JigsawStudioMenuController {
window.getViewer(), state.requestId(), workcell.stableId(), 0));
window.setElement(-4, 5, footerBack);
UIElement undo = element(
"settings-undo",
Material.CLOCK,
ChatColor.LIGHT_PURPLE + "Undo Last Autosave");
undo.addLore(ChatColor.GRAY + "Restore the previous owned graph iteration.");
undo.addLore(ChatColor.GRAY + "Up to five autosave iterations are retained on disk.");
undo.onLeftClick(clicked -> undoAutosave(
window.getViewer(),
state.requestId()));
window.setElement(0, 5, undo);
if (workcell.dirty() && !workcell.saving()) {
UIElement saveNow = element(
"save-now",
@@ -1432,9 +1473,43 @@ public final class JigsawStudioMenuController {
if (matchingState(player, requestId, true).isEmpty()) {
return;
}
if (actions.selectWorkcell(player, workcellId)) {
if (actions.teleportToWorkcell(player, workcellId)) {
clearConfirmations(player.getUniqueId());
refreshMain(player, requestId, workcellId, 0);
closeAfterAction(player);
}
}
private void toggleConnectorBlocks(
Player player,
UUID requestId,
String workcellId,
boolean visible
) {
Optional<JigsawStudioMenuState> current = matchingState(player, requestId, true);
if (current.isEmpty() || current.get().workcell(workcellId) == null) {
return;
}
if (actions.setConnectorBlocksVisible(player, workcellId, visible)) {
closeAfterAction(player);
}
}
private void resetConnectorBlocks(Player player, UUID requestId, String workcellId) {
Optional<JigsawStudioMenuState> current = matchingState(player, requestId, true);
if (current.isEmpty() || current.get().workcell(workcellId) == null) {
return;
}
if (actions.resetConnectorBlocks(player, workcellId)) {
closeAfterAction(player);
}
}
private void undoAutosave(Player player, UUID requestId) {
if (matchingState(player, requestId, true).isEmpty()) {
return;
}
if (actions.undoAutosave(player)) {
closeAfterAction(player);
}
}
@@ -2843,6 +2918,14 @@ public final class JigsawStudioMenuController {
boolean selectWorkcell(Player player, String workcellId);
boolean teleportToWorkcell(Player player, String workcellId);
boolean setConnectorBlocksVisible(Player player, String workcellId, boolean visible);
boolean resetConnectorBlocks(Player player, String workcellId);
boolean undoAutosave(Player player);
boolean switchVariant(Player player, String workcellId, String pieceKey, boolean discardDirty);
boolean createVariant(Player player, String workcellId, boolean duplicateActive);
@@ -156,6 +156,7 @@ public record JigsawStudioMenuState(
boolean dirty,
boolean saving,
boolean loading,
boolean connectorsVisible,
List<Variant> variants
) {
public Workcell {
File diff suppressed because it is too large Load Diff
@@ -71,6 +71,7 @@ import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
import java.util.function.Supplier;
@@ -579,7 +580,7 @@ public class StudioSVC implements IrisService {
String dimension,
Consumer<World> onDone
) {
return closeActiveProject().handle((closeResult, closeThrowable) -> {
return closeActiveProjectForReplacement(sender).handle((closeResult, closeThrowable) -> {
if (closeThrowable != null) {
IrisLogging.reportError("Failed while closing an existing studio project before opening \"" + dimension + "\".", closeThrowable);
J.s(() -> sender.sendMessage(IrisLanguage.text(
@@ -662,6 +663,29 @@ public class StudioSVC implements IrisService {
return studioTransitions.submit(this::closeActiveProject);
}
CompletableFuture<StudioOpenCoordinator.StudioCloseResult> closeActiveProjectForReplacement(
VolmitSender sender
) {
IrisProject project = activeProject;
if (project == null) {
return closeActiveProject();
}
JigsawStudioActivation.Request request = JigsawStudioActivation.getRequest(project.getName());
if (request == null || !sender.isPlayer()) {
return closeActiveProject();
}
UUID ownerId = sender.player().getUniqueId();
return JigsawStudioService.get()
.awaitCloseForReplacement(request.requestId(), ownerId)
.thenCompose(ignored -> {
if (activeProject != project) {
return CompletableFuture.failedFuture(new IllegalStateException(
"The active Studio project changed while replacement was waiting to close."));
}
return closeActiveProject();
});
}
private CompletableFuture<StudioOpenCoordinator.StudioCloseResult> closeActiveProject() {
IrisProject project = activeProject;
if (project == null) {
@@ -64,7 +64,7 @@ public class IrisStructure extends IrisRegistrant {
private IrisJigsawBranchFailurePolicy branchFailurePolicy = IrisJigsawBranchFailurePolicy.FAIL_ASSEMBLY;
@Desc("Default Studio cell dimensions. Legacy planar structures use this value for every workcell when planarWorkcells is empty.")
private IrisPosition cellSize = new IrisPosition(16, 16, 16);
private IrisPosition cellSize = new IrisPosition(15, 15, 15);
@Desc("Optional author-facing name for the single spatial Jigsaw Studio workcell. Spatial is shown when this is blank.")
private String spatialWorkcellDisplayName = "";
@@ -35,9 +35,11 @@ import art.arcane.volmlib.util.collection.KList;
import org.bukkit.World;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -184,7 +186,7 @@ public final class JigsawStudioGenerator extends EnginedStudioGenerator {
}
}
void paintChunk(TerrainChunk terrainChunk, int chunkX, int chunkZ) {
public void paintChunk(TerrainChunk terrainChunk, int chunkX, int chunkZ) {
Objects.requireNonNull(terrainChunk, "Jigsaw Studio terrain chunk");
int floorY = Math.max(terrainChunk.getMinHeight(), JigsawStudioLayout.FLOOR_Y);
if (floorY >= terrainChunk.getMaxHeight()) {
@@ -217,7 +219,16 @@ public final class JigsawStudioGenerator extends EnginedStudioGenerator {
continue;
}
paintObject(terrainChunk, workcell, renderedBay, chunkWorldX, chunkWorldZ);
paintConnectors(terrainChunk, workcell, renderedBay, chunkWorldX, chunkWorldZ);
if (session.workcellSnapshot(workcell.stableId()).connectorsVisible()) {
paintConnectors(terrainChunk, workcell, renderedBay, chunkWorldX, chunkWorldZ);
} else {
paintHiddenConnectorFinalStates(
terrainChunk,
workcell,
renderedBay,
chunkWorldX,
chunkWorldZ);
}
}
}
@@ -632,6 +643,34 @@ public final class JigsawStudioGenerator extends EnginedStudioGenerator {
}
}
private void paintHiddenConnectorFinalStates(
TerrainChunk terrainChunk,
JigsawStudioBay workcell,
RenderedBay renderedBay,
int chunkWorldX,
int chunkWorldZ
) {
Set<RenderedPosition> occupied = new HashSet<>(renderedBay.blocks().size());
for (RenderedBlock block : renderedBay.blocks()) {
occupied.add(new RenderedPosition(block.x(), block.y(), block.z()));
}
JigsawStudioBounds bounds = workcell.bounds();
for (RenderedConnector connector : renderedBay.connectors()) {
if (occupied.contains(new RenderedPosition(connector.x(), connector.y(), connector.z()))) {
continue;
}
PlatformBlockState finalState = B.getStateOrNull(connector.connector().getFinalState(), false);
setWorldBlock(
terrainChunk,
bounds.originX() + connector.x(),
bounds.originY() + connector.y(),
bounds.originZ() + connector.z(),
finalState == null ? invalidMarker : finalState,
chunkWorldX,
chunkWorldZ);
}
}
private void paintInvalidBay(
TerrainChunk terrainChunk,
JigsawStudioBay workcell,
@@ -871,4 +910,7 @@ public final class JigsawStudioGenerator extends EnginedStudioGenerator {
record RotatedPosition(int x, int y, int z) {
}
private record RenderedPosition(int x, int y, int z) {
}
}
@@ -0,0 +1,54 @@
package art.arcane.iris.util.common.director.specialhandlers;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.pack.PackDirectoryResolver;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.util.common.director.DirectorParameterHandler;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.director.exceptions.DirectorParsingException;
import java.io.File;
import java.util.LinkedHashSet;
import java.util.Set;
public final class IrisStructureHandler implements DirectorParameterHandler<String> {
@Override
public KList<String> getPossibilities() {
Set<String> keys = new LinkedHashSet<>();
IrisData activeData = data();
if (activeData != null) {
addStructureKeys(keys, activeData);
}
for (File pack : PackDirectoryResolver.listVisiblePackDirectories(
IrisPlatforms.get().dataFolder("packs"))) {
addStructureKeys(keys, IrisData.get(pack));
}
return new KList<>(keys);
}
@Override
public String toString(String value) {
return value == null ? "" : value;
}
@Override
public String parse(String input, boolean force) throws DirectorParsingException {
for (String option : getPossibilities(input)) {
if (option.equalsIgnoreCase(input)) {
return option;
}
}
throw new DirectorParsingException("Unable to find Iris structure \"" + input + "\"");
}
@Override
public boolean supports(Class<?> type) {
return type == String.class;
}
private static void addStructureKeys(Set<String> keys, IrisData data) {
for (String key : data.getStructureLoader().getPossibleKeys()) {
keys.add(key);
}
}
}
+2 -1
View File
@@ -260,7 +260,8 @@
"iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§cDer Pack für Dimension {value} konnte nicht aufgelöst werden",
"iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§cKeine Iris-Struktur '{structure}' in diesem Pack",
"iris.bukkit.commandstructure.structure_assembled_0_pieces": "§cStruktur '{structure}' wurde aus 0 Teilen zusammengesetzt",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§a'{structure}' ({value} Teile) an deiner Position platziert.",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§a'{structure}' ({value} Teile) an deiner Position platziert. ({value2} block changes)",
"iris.bukkit.commandstructure.placement_changed_no_blocks": "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height.",
"iris.bukkit.commandstudio.opening_studio_pack_seed": "§aStudio für den Pack \"{value}\" wird geöffnet (Seed: {seed})",
"iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§cGib ein Dimensions-Pack an: /iris std importvanilla pack=<dimension>",
"iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§cDer Pack für Dimension {value} konnte nicht aufgelöst werden",
+2 -1
View File
@@ -260,7 +260,8 @@
"iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§cNo se pudo resolver el pack de la dimensión {value}",
"iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§cNo existe la estructura de Iris '{structure}' en este pack",
"iris.bukkit.commandstructure.structure_assembled_0_pieces": "§cLa estructura '{structure}' ensambló 0 piezas",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§aSe colocó '{structure}' ({value} piezas) en tu ubicación.",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§aSe colocó '{structure}' ({value} piezas) en tu ubicación. ({value2} block changes)",
"iris.bukkit.commandstructure.placement_changed_no_blocks": "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height.",
"iris.bukkit.commandstudio.opening_studio_pack_seed": "§aAbriendo Studio para el pack \"{value}\" (semilla: {seed})",
"iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§cIndica un pack de dimensión: /iris std importvanilla pack=<dimension>",
"iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§cNo se pudo resolver el pack de la dimensión {value}",
+2 -1
View File
@@ -260,7 +260,8 @@
"iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§cPakkausta ei voitu ratkaista mitan vuoksi {value}",
"iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§cEi iirisrakennetta '{structure}Tässä pakkauksessa",
"iris.bukkit.commandstructure.structure_assembled_0_pieces": "§cRakenne{structure}' koottu 0 kappaletta",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§aPaikka{structure}' ({value} Palaset) sijaintisi.",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§aPaikka{structure}' ({value} Palaset) sijaintisi. ({value2} block changes)",
"iris.bukkit.commandstructure.placement_changed_no_blocks": "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height.",
"iris.bukkit.commandstudio.opening_studio_pack_seed": "§aAvaa studio \"{value}\" pakkaus (siemen: {seed})",
"iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§cAntakaa mittapaketti: /iris std importvanilla pack=<dimension>",
"iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§cPakkausta ei voitu ratkaista mitan vuoksi {value}",
+2 -1
View File
@@ -260,7 +260,8 @@
"iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§cImpossible de résoudre le pack de la dimension {value}",
"iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§cAucune structure Iris '{structure}' dans ce pack",
"iris.bukkit.commandstructure.structure_assembled_0_pieces": "§cLa structure '{structure}' a assemblé 0 pièce",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§aLa structure '{structure}' ({value} pièces) a été placée à votre position.",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§aLa structure '{structure}' ({value} pièces) a été placée à votre position. ({value2} block changes)",
"iris.bukkit.commandstructure.placement_changed_no_blocks": "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height.",
"iris.bukkit.commandstudio.opening_studio_pack_seed": "§aOuverture de Studio pour le pack \"{value}\" (graine : {seed})",
"iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§cIndiquez un pack de dimension : /iris std importvanilla pack=<dimension>",
"iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§cImpossible de résoudre le pack de la dimension {value}",
+2 -1
View File
@@ -260,7 +260,8 @@
"iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§cלא יכול לפתור את החבילה לממד {value}",
"iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§cאין מבנה איריס \"{structure}\"בחבילה הזאת",
"iris.bukkit.commandstructure.structure_assembled_0_pieces": "§cמבנה \"{structure}התאספו 0 חתיכות",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§aמקום \"{structure}' ({value} חתיכות) במיקום שלך.",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§aמקום \"{structure}' ({value} חתיכות) במיקום שלך. ({value2} block changes)",
"iris.bukkit.commandstructure.placement_changed_no_blocks": "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height.",
"iris.bukkit.commandstudio.opening_studio_pack_seed": "§aסטודיו פתיחה ל\"{value}\"חבילה\" (צילום: {seed})",
"iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§cלספק ערכת מימד: /iris std importvanilla pack=<dimension>",
"iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§cלא יכול לפתור את החבילה לממד {value}",
+2 -1
View File
@@ -260,7 +260,8 @@
"iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§cImpossibile risolvere il pack della dimensione {value}",
"iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§cNessuna struttura Iris '{structure}' in questo pack",
"iris.bukkit.commandstructure.structure_assembled_0_pieces": "§cLa struttura '{structure}' è stata assemblata con 0 pezzi",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§a'{structure}' ({value} pezzi) posizionata nella tua posizione.",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§a'{structure}' ({value} pezzi) posizionata nella tua posizione. ({value2} block changes)",
"iris.bukkit.commandstructure.placement_changed_no_blocks": "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height.",
"iris.bukkit.commandstudio.opening_studio_pack_seed": "§aApertura di Studio per il pack \"{value}\" (seed: {seed})",
"iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§cSpecifica un Pack di dimensione: /iris std importvanilla pack=<dimension>",
"iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§cImpossibile risolvere il pack della dimensione {value}",
+2 -1
View File
@@ -260,7 +260,8 @@
"iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§cディメンション {value} のパックを解決できませんでした",
"iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§cこのパックには Iris 構造物 '{structure}' がありません",
"iris.bukkit.commandstructure.structure_assembled_0_pieces": "§c構造物 '{structure}' は 0 ピースで組み立てられました",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§a現在位置に '{structure}'{value} ピース)を配置しました。",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§a現在位置に '{structure}'{value} ピース)を配置しました。 ({value2} block changes)",
"iris.bukkit.commandstructure.placement_changed_no_blocks": "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height.",
"iris.bukkit.commandstudio.opening_studio_pack_seed": "§aパック \"{value}\" のスタジオを開いています(シード: {seed})",
"iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§cディメンションパックを指定してください: /iris std importvanilla pack=<dimension>",
"iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§cディメンション {value} のパックを解決できませんでした",
+2 -1
View File
@@ -260,7 +260,8 @@
"iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§c차원을 위한 팩을 해결할 수 없습니다 {value}",
"iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§c아이리스 구조 없음 '{structure}이 팩에서",
"iris.bukkit.commandstructure.structure_assembled_0_pieces": "§c구조물 '{structure}' 조립 결과: 조각 0개",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§a장소 '{structure}' ({value} 당신의 위치에 조각).",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§a장소 '{structure}' ({value} 당신의 위치에 조각). ({value2} block changes)",
"iris.bukkit.commandstructure.placement_changed_no_blocks": "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height.",
"iris.bukkit.commandstudio.opening_studio_pack_seed": "§a\"를 위한 오프닝 스튜디오{value}\"팩 (seed: {seed})",
"iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§c차원 팩을 제공: /iris std importvanilla pack=<dimension>",
"iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§c차원을 위한 팩을 해결할 수 없습니다 {value}",
+2 -1
View File
@@ -260,7 +260,8 @@
"iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§cNepavyko išspręsti pakuotės dimensijai {value}",
"iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§cNėra rainelės struktūros \"{structure}\"šioje pakuotėje",
"iris.bukkit.commandstructure.structure_assembled_0_pieces": "§cStruktūra \"{structure}'surinkti 0 vienetai",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§aPateikta \"{structure}' ({value} vienetų).",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§aPateikta \"{structure}' ({value} vienetų). ({value2} block changes)",
"iris.bukkit.commandstructure.placement_changed_no_blocks": "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height.",
"iris.bukkit.commandstudio.opening_studio_pack_seed": "§aAtidarymo studija \"{value}\"pakuotė (sėkla: {seed})",
"iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§cPateikite matmenų paketą: /iris std importvanilla pack=<dimension>",
"iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§cNepavyko išspręsti pakuotės dimensijai {value}",
+2 -1
View File
@@ -260,7 +260,8 @@
"iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§cKon het pakket voor dimensie niet oplossen {value}",
"iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§cGeen irisstructuur '{structure}' in deze verpakking",
"iris.bukkit.commandstructure.structure_assembled_0_pieces": "§cStructuur{structure}' gemonteerd 0 stuks",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§aGeplaatst '{structure}' ({value} stukken) op uw locatie.",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§aGeplaatst '{structure}' ({value} stukken) op uw locatie. ({value2} block changes)",
"iris.bukkit.commandstructure.placement_changed_no_blocks": "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height.",
"iris.bukkit.commandstudio.opening_studio_pack_seed": "§aopenen studio voor de \"{value}\" verpakking (zaad: {seed})",
"iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§cGeef een maatpakket: /iris std importvanilla pack=<dimension>",
"iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§cKon het pakket voor dimensie niet oplossen {value}",
+2 -1
View File
@@ -260,7 +260,8 @@
"iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§cNie można rozwiązać pakietu dla wymiaru {value}",
"iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§cBrak struktury tęczówki \"{structure}'w tym opakowaniu",
"iris.bukkit.commandstructure.structure_assembled_0_pieces": "§cStruktura \"{structure}\"zmontowane 0 kawałki",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§aUmieszczone \"{structure}' ({value} sztuk) w miejscu.",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§aUmieszczone \"{structure}' ({value} sztuk) w miejscu. ({value2} block changes)",
"iris.bukkit.commandstructure.placement_changed_no_blocks": "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height.",
"iris.bukkit.commandstudio.opening_studio_pack_seed": "§aStudio otwarcia dla \"{value}\"opakowanie (nasiona: {seed})",
"iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§cNależy podać zestaw wymiarów: /iris std importvanilla pack=<dimension>",
"iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§cNie można rozwiązać pakietu dla wymiaru {value}",
+2 -1
View File
@@ -260,7 +260,8 @@
"iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§cNão foi possível resolver o pacote para a dimensão {value}",
"iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§cSem estrutura íris '{structure}' nesta pack",
"iris.bukkit.commandstructure.structure_assembled_0_pieces": "§cEstrutura{structure}' montados 0 peças",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§aColocado '{structure}' ({value} peças) na sua localização.",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§aColocado '{structure}' ({value} peças) na sua localização. ({value2} block changes)",
"iris.bukkit.commandstructure.placement_changed_no_blocks": "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height.",
"iris.bukkit.commandstudio.opening_studio_pack_seed": "§aEstúdio de abertura para o \"{value}\" pack (sementes: {seed})",
"iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§cFornecer um pacote de dimensões: /iris std importvanilla pack=<dimension>",
"iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§cNão foi possível resolver o pacote para a dimensão {value}",
+2 -1
View File
@@ -260,7 +260,8 @@
"iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§cНе удалось решить пакет для измерения {value}",
"iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§cНет структуры радужной оболочки{structure}В этой пачке",
"iris.bukkit.commandstructure.structure_assembled_0_pieces": "§cСтруктура{structure}собранный 0 части",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§aПомещение '{structure}' ({value} куски) в вашем месте.",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§aПомещение '{structure}' ({value} куски) в вашем месте. ({value2} block changes)",
"iris.bukkit.commandstructure.placement_changed_no_blocks": "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height.",
"iris.bukkit.commandstudio.opening_studio_pack_seed": "§aОткрытие студии для\"{value}\"пак (семя):{seed})",
"iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§cПредоставьте размерный пакет: /iris std importvanilla pack=<dimension>",
"iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§cНе удалось решить пакет для измерения {value}",
+2 -1
View File
@@ -260,7 +260,8 @@
"iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§cpaketi boyut için çözemez {value}",
"iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§cyok iris structure \"{structure}“Bu pakette",
"iris.bukkit.commandstructure.structure_assembled_0_pieces": "§cYapı \"{structure}\"Bir araya geldi\" 0 parçalar parça parçaları",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§ayerleştirildi {structure}' ({value} parçalar) konumunuzda.",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§ayerleştirildi {structure}' ({value} parçalar) konumunuzda. ({value2} block changes)",
"iris.bukkit.commandstructure.placement_changed_no_blocks": "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height.",
"iris.bukkit.commandstudio.opening_studio_pack_seed": "§aAçılış stüdyosu \"{value}\" paket (seed: {seed})",
"iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§cBir boyut paketi sağlayın: /iris std importvanilla pack=<dimension>",
"iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§cpaketi boyut için çözemez {value}",
+2 -1
View File
@@ -260,7 +260,8 @@
"iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§cKhông thể giải quyết gói cho kích thước {value}",
"iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§cKhông có cấu trúc Iris{structure}'Trong gói này",
"iris.bukkit.commandstructure.structure_assembled_0_pieces": "§cCấu trúc{structure}Tập hợp 0 mảnh",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§aĐã đặt '{structure}' ({value} Các mảnh) tại vị trí.",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§aĐã đặt '{structure}' ({value} Các mảnh) tại vị trí. ({value2} block changes)",
"iris.bukkit.commandstructure.placement_changed_no_blocks": "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height.",
"iris.bukkit.commandstudio.opening_studio_pack_seed": "§aMở studio cho \"{value}\" Gói (dòng dõi: {seed})",
"iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§cCung cấp một gói chiều: /iris std importvanilla pack=<dimension>",
"iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§cKhông thể giải quyết gói cho kích thước {value}",
+2 -1
View File
@@ -260,7 +260,8 @@
"iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§c无法解析大小包 {value}",
"iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§c无Iris结构 '{structure}\"在这个包里\"",
"iris.bukkit.commandstructure.structure_assembled_0_pieces": "§c结构 '{structure}组装 0 块",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§a已放置 '{structure}' ({value}在您的位置。",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§a已放置 '{structure}' ({value}在您的位置。 ({value2} block changes)",
"iris.bukkit.commandstructure.placement_changed_no_blocks": "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height.",
"iris.bukkit.commandstudio.opening_studio_pack_seed": "§a工作室 \"{value}\" 包(种子: {seed})",
"iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§c提供一个维度包: /iris std importvanilla pack=<dimension>",
"iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§c无法解析大小包 {value}",
+2 -1
View File
@@ -260,7 +260,8 @@
"iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§c無法解析大小包 {value}",
"iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§c無Iris結構 '{structure}\"在這個包裡\"",
"iris.bukkit.commandstructure.structure_assembled_0_pieces": "§c結構 '{structure}組裝 0 塊",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§a已放置 '{structure}' ({value}在您的位置。",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§a已放置 '{structure}' ({value}在您的位置。 ({value2} block changes)",
"iris.bukkit.commandstructure.placement_changed_no_blocks": "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height.",
"iris.bukkit.commandstudio.opening_studio_pack_seed": "§a工作室 \"{value}\" 包(種子: {seed})",
"iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§c提供一個維度包: /iris std importvanilla pack=<dimension>",
"iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§c無法解析大小包 {value}",
@@ -44,7 +44,7 @@ public class JigsawStudioLayoutTest {
}
@Test
public void planarWorkcellsUseDirectTwoBlockSpacing() {
public void planarWorkcellsUseDirectOneBlockSpacing() {
JigsawStudioLayout layout = JigsawStudioLayout.create(
JigsawStudioMode.PLANAR_JIGSAW,
new JigsawStudioCellDimensions(16, 8, 16),
@@ -53,12 +53,12 @@ public class JigsawStudioLayoutTest {
JigsawStudioBay end = layout.get("workcell/end");
JigsawStudioBay corner = layout.get("workcell/corner");
assertEquals(18, end.bounds().originX() - blank.bounds().originX());
assertEquals(18, corner.bounds().originZ() - blank.bounds().originZ());
assertEquals(2, end.bounds().originX() - blank.bounds().maxX() - 1);
assertEquals(2, corner.bounds().originZ() - blank.bounds().maxZ() - 1);
assertEquals(17, end.bounds().originX() - blank.bounds().originX());
assertEquals(17, corner.bounds().originZ() - blank.bounds().originZ());
assertEquals(1, end.bounds().originX() - blank.bounds().maxX() - 1);
assertEquals(1, corner.bounds().originZ() - blank.bounds().maxZ() - 1);
assertEquals(3, layout.columns());
assertEquals(2, layout.gap());
assertEquals(1, layout.gap());
}
@Test
@@ -87,9 +87,9 @@ public class JigsawStudioLayoutTest {
JigsawStudioBay corner = layout.get("workcell/corner");
JigsawStudioBay tee = layout.get("workcell/tee");
assertEquals(14, end.bounds().originX() - blank.bounds().originX());
assertEquals(36, straight.bounds().originX() - blank.bounds().originX());
assertEquals(13, corner.bounds().originZ() - blank.bounds().originZ());
assertEquals(13, end.bounds().originX() - blank.bounds().originX());
assertEquals(34, straight.bounds().originX() - blank.bounds().originX());
assertEquals(12, corner.bounds().originZ() - blank.bounds().originZ());
assertEquals(new JigsawStudioCellDimensions(7, 8, 15), tee.bounds().dimensions());
assertFalse(tee.enabled());
assertTrue(blank.enabled());
@@ -181,14 +181,19 @@ public class JigsawStudioSessionTest {
"workcell/end", east.pieceKey(), false).token().orElseThrow();
assertTrue(session.completeVariantSwitch(switchToken));
long selectedLoad = session.workcellSnapshot("workcell/end").loadGeneration();
assertFalse(session.workcellSnapshot("workcell/end").connectorsVisible());
assertTrue(session.setConnectorsVisible("workcell/end", true));
assertFalse(session.setConnectorsVisible("workcell/end", true));
assertTrue(session.replaceLayout(planarLayout(north, east)));
assertSame(east, session.activeVariant("workcell/end").orElseThrow());
assertEquals(selectedLoad, session.workcellSnapshot("workcell/end").loadGeneration());
assertTrue(session.workcellSnapshot("workcell/end").connectorsVisible());
assertTrue(session.replaceLayout(planarLayout(north)));
assertSame(north, session.activeVariant("workcell/end").orElseThrow());
assertTrue(session.workcellSnapshot("workcell/end").loadGeneration() > selectedLoad);
assertTrue(session.workcellSnapshot("workcell/end").connectorsVisible());
}
@Test
@@ -0,0 +1,141 @@
package art.arcane.iris.core.service;
import art.arcane.iris.core.structure.authoring.StructureBackend;
import art.arcane.iris.core.structure.authoring.StructureCapability;
import art.arcane.iris.core.structure.authoring.StructureHash;
import art.arcane.iris.core.structure.authoring.StructureKey;
import art.arcane.iris.core.structure.authoring.StructureOwnershipManifest;
import art.arcane.iris.core.structure.authoring.StructureResourceBundle;
import art.arcane.iris.core.structure.authoring.StructureSource;
import art.arcane.iris.core.structure.authoring.StructureTransactionWriter;
import art.arcane.iris.core.structure.authoring.StructureWriteOptions;
import art.arcane.iris.core.structure.authoring.StructureWriteResult;
import art.arcane.iris.engine.object.IrisObject;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.ByteArrayOutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import static org.junit.Assert.assertArrayEquals;
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 JigsawStudioHistoryStoreTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void retainsFiveDeduplicatedIterationsAndRestoresThemThroughTheWriter() throws Exception {
Path root = temporaryFolder.newFolder("history").toPath();
StructureTransactionWriter writer = new StructureTransactionWriter(root);
JigsawStudioHistoryStore history = new JigsawStudioHistoryStore(root, "qa/history");
assertTrue(writer.write(bundle(0), StructureWriteOptions.addOnly()).successful());
for (int version = 1; version <= 7; version++) {
JigsawStudioHistoryStore.Snapshot previous = history.snapshotCurrent("qa/history/piece");
assertEquals(Math.min(version, JigsawStudioHistoryStore.MAX_ITERATIONS), history.append(previous));
StructureWriteResult write = writer.write(
bundle(version),
StructureWriteOptions.overwriteExpected(manifestHash(writer)));
assertTrue(write.successful());
}
assertEquals(JigsawStudioHistoryStore.MAX_ITERATIONS, history.availableIterations());
assertTrue(Files.isRegularFile(history.historyPath()));
try (Stream<Path> historyFiles = Files.list(history.historyPath().getParent())) {
assertEquals(1L, historyFiles.filter(Files::isRegularFile).count());
}
for (int expectedVersion = 6; expectedVersion >= 2; expectedVersion--) {
JigsawStudioHistoryStore.UndoResult undo = history.undoLatest();
assertTrue(undo.available());
assertTrue(undo.successful());
assertEquals("qa/history/piece", undo.pieceKey());
assertEquals(expectedVersion - 2, undo.remainingIterations());
assertArrayEquals(
content(expectedVersion),
Files.readAllBytes(root.resolve("objects/qa/history/object.iob")));
assertOwnedResourcesMatchManifest(root, writer);
}
assertFalse(Files.exists(history.historyPath()));
assertFalse(history.undoLatest().available());
}
@Test
public void identicalSnapshotsDoNotConsumeAnotherIteration() throws Exception {
Path root = temporaryFolder.newFolder("dedup").toPath();
StructureTransactionWriter writer = new StructureTransactionWriter(root);
JigsawStudioHistoryStore history = new JigsawStudioHistoryStore(root, "qa/history");
assertTrue(writer.write(bundle(0), StructureWriteOptions.addOnly()).successful());
JigsawStudioHistoryStore.Snapshot snapshot = history.snapshotCurrent("qa/history/piece");
assertEquals(1, history.append(snapshot));
assertEquals(1, history.append(snapshot));
assertEquals(1, history.availableIterations());
}
@Test
public void refusesToSnapshotAnOwnedResourceThatChangedOutsideTheWriter() throws Exception {
Path root = temporaryFolder.newFolder("modified").toPath();
StructureTransactionWriter writer = new StructureTransactionWriter(root);
JigsawStudioHistoryStore history = new JigsawStudioHistoryStore(root, "qa/history");
assertTrue(writer.write(bundle(0), StructureWriteOptions.addOnly()).successful());
Files.writeString(root.resolve("objects/qa/history/object.iob"), "external-change");
assertThrows(Exception.class, () -> history.snapshotCurrent("qa/history/piece"));
assertFalse(Files.exists(history.historyPath()));
}
private static StructureResourceBundle bundle(int version) throws Exception {
StructureKey key = new StructureKey("iris", "qa/history");
return StructureResourceBundle.builder(key)
.source(StructureSource.of(StructureSource.Kind.IRIS, key))
.backend(StructureBackend.IRIS_ASSEMBLY)
.capability(StructureCapability.BLOCKS)
.textResource("structures/qa/history.json", "{\"startPool\":\"qa/history/start\"}")
.textResource("jigsaw-pools/qa/history/start.json",
"{\"pieces\":[{\"piece\":\"qa/history/piece\"}]}")
.textResource("jigsaw-pieces/qa/history/piece.json",
"{\"object\":\"qa/history/object\",\"connectors\":[]}")
.resource("objects/qa/history/object.iob", content(version))
.build();
}
private static byte[] content(int version) throws Exception {
IrisObject object = new IrisObject(version + 1, 1, 1);
try (ByteArrayOutputStream output = new ByteArrayOutputStream()) {
object.write(output);
return output.toByteArray();
}
}
private static String manifestHash(StructureTransactionWriter writer) throws Exception {
Path manifestPath = writer.ownershipManifestPath(new StructureKey("iris", "qa/history"));
return StructureHash.sha256(Files.readAllBytes(manifestPath));
}
private static void assertOwnedResourcesMatchManifest(
Path root,
StructureTransactionWriter writer
) throws Exception {
Path manifestPath = writer.ownershipManifestPath(new StructureKey("iris", "qa/history"));
StructureOwnershipManifest manifest = StructureOwnershipManifest.fromJson(
Files.readAllBytes(manifestPath));
for (Map.Entry<String, String> resource : manifest.resourceHashes().entrySet()) {
assertEquals(
resource.getValue(),
StructureHash.sha256(Files.readAllBytes(root.resolve(resource.getKey()))));
}
assertEquals(
List.of(StructureCapability.BLOCKS),
manifest.capabilities());
}
}
@@ -27,9 +27,11 @@ import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -113,6 +115,55 @@ public class JigsawStudioLifecycleTest {
assertEquals(JigsawStudioService.SaveStart.CLOSING, service.tryBeginSave(request.requestId()));
}
@Test
public void ownerReplacementWaitsForAutosaveThenClaimsClose() {
assertTrue(JigsawStudioActivation.tryBeginOpen(OWNER));
JigsawStudioActivation.Request request = activateOwnedStudio();
JigsawStudioActivation.finishOpen(OWNER);
JigsawStudioSession session = JigsawStudioActivation.getSession(request.requestId());
JigsawStudioService service = new JigsawStudioService();
assertEquals(
JigsawStudioSession.DirtyStatus.MARKED,
session.markWorkcellDirty(JigsawStudioLayout.SPATIAL_WORKCELL_ID).status());
AtomicReference<Runnable> retry = new AtomicReference<>();
try (MockedStatic<J> scheduling = mockStatic(J.class)) {
scheduling.when(() -> J.s(any(Runnable.class), eq(5))).thenAnswer(invocation -> {
retry.set(invocation.getArgument(0));
return null;
});
CompletableFuture<Void> readiness = service.awaitCloseForReplacement(
request.requestId(), OWNER);
assertFalse(readiness.isDone());
assertTrue(retry.get() != null);
JigsawStudioSession.SaveStart save = session.beginSave(
JigsawStudioLayout.SPATIAL_WORKCELL_ID);
assertEquals(JigsawStudioSession.SaveStatus.STARTED, save.status());
assertTrue(session.markWorkcellSaved(save.identity().orElseThrow()));
retry.get().run();
readiness.join();
assertNull(service.closeProtectionFailure(request.requestId()));
}
}
@Test
public void nonOwnerReplacementFailsWithoutWaiting() {
assertTrue(JigsawStudioActivation.tryBeginOpen(OWNER));
JigsawStudioActivation.Request request = activateOwnedStudio();
JigsawStudioActivation.finishOpen(OWNER);
JigsawStudioService service = new JigsawStudioService();
try (MockedStatic<J> scheduling = mockStatic(J.class)) {
CompletableFuture<Void> readiness = service.awaitCloseForReplacement(
request.requestId(), OTHER_OWNER);
assertTrue(readiness.isCompletedExceptionally());
scheduling.verifyNoInteractions();
}
}
@Test
public void lateJigsawGuiMutationKeepsCloseBehindTheFinalSnapshotAndAutosaveBarriers()
throws ReflectiveOperationException {
@@ -75,6 +75,7 @@ public class JigsawStudioMenuControllerTest {
false,
false,
false,
false,
List.of(active));
assertEquals(ChatColor.GREEN + "Autosaved", JigsawStudioMenuController.workcellStatus(fresh));
@@ -123,6 +124,7 @@ public class JigsawStudioMenuControllerTest {
true,
false,
false,
true,
List.of(active));
JigsawStudioMenuState state = state(evaluation, corner);
themes.add("late-theme");
@@ -161,6 +163,7 @@ public class JigsawStudioMenuControllerTest {
false,
false,
false,
false,
List.of(active)));
assertThrows(IllegalArgumentException.class, () -> new JigsawStudioMenuState(
WORLD_ID,
@@ -519,6 +522,7 @@ public class JigsawStudioMenuControllerTest {
true,
false,
false,
false,
variants);
}
@@ -5,6 +5,7 @@ import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioBounds;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioActivation;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioBay;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioBayKind;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioCellDimensions;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioCompatibilityTarget;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioLayout;
@@ -15,6 +16,8 @@ import art.arcane.iris.core.runtime.jigsaw.JigsawStudioSession;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioVariant;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioVariantCatalog;
import art.arcane.iris.core.runtime.jigsaw.JigsawPlanarTopology;
import art.arcane.iris.core.runtime.jigsaw.JigsawPlanarArchetype;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioWorkcellSpec;
import art.arcane.iris.core.structure.authoring.StructureWriteResult;
import art.arcane.iris.engine.object.IrisDirection;
import art.arcane.iris.engine.object.IrisJigsawConnector;
@@ -40,6 +43,7 @@ import art.arcane.volmlib.util.collection.KMap;
import org.bukkit.Chunk;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.block.data.BlockData;
import org.bukkit.block.data.Directional;
@@ -103,9 +107,79 @@ import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class JigsawStudioServiceCaptureTest {
@Test
public void hiddenConnectorResetRestoresOnlyItsSavedOrdinaryBlock() throws Exception {
World world = mock(World.class);
Block target = mock(Block.class);
BlockData blockData = mock(BlockData.class);
PlatformBlockState state = mock(PlatformBlockState.class);
JigsawStudioCellDimensions dimensions = new JigsawStudioCellDimensions(15, 15, 15);
JigsawStudioBay workcell = new JigsawStudioBay(
"spatial",
JigsawStudioBayKind.SPATIAL_WORKCELL,
Optional.empty(),
"",
new JigsawStudioBounds(10, 20, 30, dimensions));
IrisJigsawConnector connector = connectorAt(1, 2, 3)
.setFinalState("minecraft:stone");
JigsawStudioGenerator.RenderedConnector renderedConnector =
new JigsawStudioGenerator.RenderedConnector(
1,
2,
3,
connector,
"north_up");
JigsawStudioGenerator.RenderedBlock renderedBlock =
new JigsawStudioGenerator.RenderedBlock(1, 2, 3, state, null);
when(world.getBlockAt(11, 22, 33)).thenReturn(target);
when(state.isCustom()).thenReturn(false);
when(state.nativeHandle()).thenReturn(blockData);
JigsawStudioService.restoreConnectorChunk(
world,
workcell,
List.of(renderedConnector),
Map.of(new JigsawStudioService.LocalPosition(1, 2, 3), renderedBlock),
false);
verify(world).getBlockAt(11, 22, 33);
verify(target).setBlockData(blockData, false);
}
@Test
public void liveRelayoutDetectsMovedBoundsAndIncludesCageChunks() {
JigsawStudioCellDimensions originalDimensions = new JigsawStudioCellDimensions(16, 8, 16);
JigsawStudioLayout original = JigsawStudioLayout.create(
JigsawStudioMode.PLANAR_JIGSAW,
originalDimensions,
JigsawStudioVariantCatalog.empty());
List<JigsawStudioWorkcellSpec> expandedSpecs = new ArrayList<>();
for (JigsawPlanarArchetype archetype : JigsawPlanarArchetype.values()) {
expandedSpecs.add(new JigsawStudioWorkcellSpec(
archetype,
"",
archetype == JigsawPlanarArchetype.BLANK
? new JigsawStudioCellDimensions(33, 12, 17)
: originalDimensions,
true));
}
JigsawStudioLayout expanded = JigsawStudioLayout.createPlanar(
originalDimensions,
expandedSpecs,
JigsawStudioVariantCatalog.empty());
assertFalse(JigsawStudioService.layoutGeometryChanged(original, original));
assertTrue(JigsawStudioService.layoutGeometryChanged(original, expanded));
Set<Long> chunks = JigsawStudioService.relayoutChunks(original, expanded);
assertTrue(chunks.contains(0L));
assertTrue(chunks.contains(((long) 4 << 32)));
}
@Test
public void mappedGraphOwnershipControlsNewVariantsEvenWhenTheCatalogIsEmpty() {
JigsawStudioCellDimensions dimensions = new JigsawStudioCellDimensions(16, 16, 16);
@@ -720,6 +794,60 @@ public class JigsawStudioServiceCaptureTest {
}
}
@Test
public void hiddenConnectorCapturesExactBlockStateTilePayloadAndMetadata() throws Throwable {
JigsawStudioBounds bounds = new JigsawStudioBounds(
0,
64,
0,
new JigsawStudioCellDimensions(1, 1, 1));
IrisJigsawConnector connector = connector()
.setChannel("gate/owned")
.setSelectionPriority(-7)
.setPlacementPriority(11);
IrisJigsawPiece piece = new IrisJigsawPiece().setConnectors(new KList<>());
piece.getConnectors().add(connector);
BlockData chestData = directionalBlockData(Material.CHEST, BlockFace.EAST);
PlatformBlockState chestState = BukkitBlockState.of(chestData);
IrisObject sourceObject = new IrisObject(1, 1, 1);
sourceObject.setUnsigned(0, 0, 0, chestState);
KMap<String, Object> properties = new KMap<>();
properties.put("CustomName", "Hidden Connector Chest");
properties.put("Lock", "iris:hidden");
TileData tileData = new TileData("minecraft:chest", properties);
Block block = mock(Block.class);
when(block.getBlockData()).thenReturn(chestData);
World world = mock(World.class);
when(world.getBlockAt(0, 64, 0)).thenReturn(block);
JigsawStudioService.ChunkCaptureArea area = JigsawStudioService.chunkIntersections(bounds).getFirst();
JigsawStudioService.ChunkSnapshot snapshot;
try (MockedStatic<TileData> tiles = mockStatic(TileData.class)) {
tiles.when(() -> TileData.getTileState(block, false)).thenReturn(tileData);
snapshot = JigsawStudioService.captureChunkIntersection(
world,
bounds,
piece,
sourceObject,
area,
0,
false);
}
JigsawStudioService.Capture capture = JigsawStudioService.aggregateSnapshots(
bounds,
List.of(area),
List.of(snapshot));
IrisObject restored = readCapturedObject(capture.objectContent(), chestState);
IrisJigsawConnector captured = capture.connectors().getFirst();
assertEquals(chestData.getAsString(), captured.getFinalState());
assertEquals("gate/owned", captured.getChannel());
assertEquals(-7, captured.getSelectionPriority());
assertEquals(11, captured.getPlacementPriority());
assertEquals(tileData, restored.getStates().get(restored.getSigned(0, 0, 0)));
assertTrue(capture.hasBlockEntities());
}
@Test
public void noOpTeeAndCrossCapturePreservesCreatorOrderForSeededAssembly() throws IOException {
JigsawStudioCellDimensions dimensions = new JigsawStudioCellDimensions(16, 16, 16);
@@ -11,10 +11,12 @@ import art.arcane.iris.core.runtime.jigsaw.JigsawStudioMode;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioVariantCatalog;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.util.common.plugin.VolmitPlugin;
import art.arcane.iris.util.common.plugin.VolmitSender;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.MockedStatic;
import org.bukkit.entity.Player;
import java.lang.reflect.Field;
import java.util.UUID;
@@ -92,6 +94,26 @@ public class StudioSVCJigsawProtectionTest {
verify(project).close();
}
@Test
public void ownerCanReplaceJigsawStudioThroughOrdinaryStudioOpen() throws ReflectiveOperationException {
activateOwnedStudio();
IrisProject project = mock(IrisProject.class);
when(project.getName()).thenReturn("overworld");
StudioOpenCoordinator.StudioCloseResult closeResult = successfulClose();
when(project.close()).thenReturn(CompletableFuture.completedFuture(closeResult));
Player player = mock(Player.class);
when(player.getUniqueId()).thenReturn(OWNER);
VolmitSender sender = mock(VolmitSender.class);
when(sender.isPlayer()).thenReturn(true);
when(sender.player()).thenReturn(player);
StudioSVC studio = new StudioSVC();
setActiveProject(studio, project);
assertEquals(closeResult, studio.closeActiveProjectForReplacement(sender).join());
assertNull(studio.getActiveProject());
verify(project).close();
}
private static JigsawStudioActivation.Request activateOwnedStudio() {
assertTrue(JigsawStudioActivation.tryBeginOpen(OWNER));
JigsawStudioCellDimensions dimensions = new JigsawStudioCellDimensions(16, 16, 16);
@@ -18,7 +18,7 @@ public class IrisJigsawModelMetadataTest {
assertEquals(IrisJigsawCompatibility.IRIS_EXTENDED, structure.resolvedCompatibility());
assertEquals(IrisJigsawBranchFailurePolicy.FAIL_ASSEMBLY,
structure.resolvedBranchFailurePolicy());
assertEquals(new IrisPosition(16, 16, 16), structure.getCellSize());
assertEquals(new IrisPosition(15, 15, 15), structure.getCellSize());
assertEquals("", connector.getChannel());
assertEquals("minecraft:air", connector.getFinalState());
assertEquals(0, connector.getSelectionPriority());
@@ -50,7 +50,7 @@ public class IrisJigsawModelMetadataTest {
assertEquals(IrisJigsawCompatibility.IRIS_EXTENDED, structure.resolvedCompatibility());
assertEquals(IrisJigsawBranchFailurePolicy.FAIL_ASSEMBLY,
structure.resolvedBranchFailurePolicy());
assertEquals(new IrisPosition(16, 16, 16), structure.getCellSize());
assertEquals(new IrisPosition(15, 15, 15), structure.getCellSize());
assertEquals("", connector.getChannel());
assertEquals("minecraft:air", connector.getFinalState());
assertEquals(0, connector.getSelectionPriority());
@@ -28,11 +28,13 @@ import art.arcane.iris.engine.object.JigsawJoint;
import art.arcane.iris.engine.object.TileData;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.data.B;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
import org.bukkit.block.data.BlockData;
import org.bukkit.generator.ChunkGenerator.ChunkData;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.util.HashMap;
import java.util.List;
@@ -52,9 +54,70 @@ import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
public class JigsawStudioGeneratorTest {
@Test
@SuppressWarnings("unchecked")
public void connectorBlocksAreHiddenByDefaultAndCanBeShownPerWorkcell() {
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);
IrisJigsawConnector connector = new IrisJigsawConnector()
.setPosition(new IrisPosition(1, 1, 1))
.setDirection(IrisDirection.NORTH_NEGATIVE_Z)
.setTop(IrisDirection.UP_POSITIVE_Y)
.setPool("test/start")
.setName("door")
.setTargetName("door")
.setJoint(JigsawJoint.ALIGNED)
.setFinalState("minecraft:stone");
IrisJigsawPiece piece = new IrisJigsawPiece()
.setObject("test/room")
.setConnectors(new KList<>());
piece.getConnectors().add(connector);
PlatformBlockState stone = mock(PlatformBlockState.class);
IrisObject object = new IrisObject(3, 3, 3);
object.setUnsigned(1, 1, 1, stone);
when(pieceLoader.load("test/room", false)).thenReturn(piece);
when(objectLoader.load("test/room", false)).thenReturn(object);
JigsawStudioVariant variant = new JigsawStudioVariant(
"test/room",
"test/room",
"",
Optional.of(new JigsawStudioCellDimensions(3, 3, 3)),
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(3, 3, 3),
new JigsawStudioVariantCatalog(List.of(variant)));
JigsawStudioBay workcell = fixture.layout().bays().getFirst();
int worldX = workcell.bounds().originX() + 1;
int worldY = workcell.bounds().originY() + 1;
int worldZ = workcell.bounds().originZ() + 1;
assertFalse(fixture.generator().getSession().workcellSnapshot(
workcell.stableId()).connectorsVisible());
assertSame(stone, stateAt(fixture.generator(), worldX, worldY, worldZ));
PlatformBlockState marker = mock(PlatformBlockState.class);
fixture.generator().getSession().setConnectorsVisible(workcell.stableId(), true);
try (MockedStatic<B> blocks = mockStatic(B.class)) {
blocks.when(() -> B.getState("minecraft:jigsaw[orientation=north_up]")).thenReturn(marker);
assertSame(marker, stateAt(fixture.generator(), worldX, worldY, worldZ));
}
}
@Test
public void serviceRegistrationIsPublishedAfterTheRegistrationFinishes() throws Exception {
GeneratorFixture fixture = fixture(