mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-08-27 04:37:47 +00:00
Fixes
This commit is contained in:
+9
-1
@@ -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 = "";
|
||||
|
||||
+44
-2
@@ -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) {
|
||||
}
|
||||
}
|
||||
|
||||
+54
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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} のパックを解決できませんでした",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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());
|
||||
|
||||
+63
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user