This commit is contained in:
Brian Neumann-Fopiano committed 2026-08-10 17:32:44 -04:00
1 parent 998a5c9f5f
commit dfae45663a
56 files changed
+2194 -191

No files matched your search

@@ -621,7 +621,14 @@ public final class BukkitCommandMessagesExtended {
);
public static final TextKey COMMAND_STRUCTURE_PLACED_PIECES_AT_YOUR_LOCATION = TextKey.of(
"iris.bukkit.commandstructure.placed_pieces_at_your_location",
C.GREEN + "Placed '" + "{structure}" + "' (" + "{value}" + " pieces) at your location."
C.GREEN + "Placed '" + "{structure}" + "' (" + "{value}" + " pieces, "
+ "{value2}" + " block changes) at your location."
);
public static final TextKey COMMAND_STRUCTURE_PLACEMENT_CHANGED_NO_BLOCKS = TextKey.of(
"iris.bukkit.commandstructure.placement_changed_no_blocks",
C.RED + "Structure '" + "{structure}" + "' assembled " + "{value}"
+ " pieces but changed 0 blocks at your location. Check that the selected variants contain "
+ "non-air blocks and that the placement is above the world's minimum height."
);
public static final TextKey COMMAND_STUDIO_OPENING_STUDIO_PACK_SEED = TextKey.of(
"iris.bukkit.commandstudio.opening_studio_pack_seed",
@@ -991,6 +998,7 @@ public final class BukkitCommandMessagesExtended {
COMMAND_STRUCTURE_NO_IRIS_STRUCTURE_THIS_PACK_2,
COMMAND_STRUCTURE_STRUCTURE_ASSEMBLED_0_PIECES,
COMMAND_STRUCTURE_PLACED_PIECES_AT_YOUR_LOCATION,
COMMAND_STRUCTURE_PLACEMENT_CHANGED_NO_BLOCKS,
COMMAND_STUDIO_OPENING_STUDIO_PACK_SEED,
COMMAND_STUDIO_PROVIDE_DIMENSION_PACK_IRIS_STD_IMPORTVANILLA_PACK_DIMENSION,
COMMAND_STUDIO_COULD_NOT_RESOLVE_PACK_DIMENSION,
@@ -182,7 +182,27 @@ public final class InPlaceChunkRegenerator {
world.refreshChunk(chunkX, chunkZ);
}
static void applyBlockDiffs(Chunk chunk, ChunkSnapshot snapshot, ChunkData generated, int minHeight, int maxHeight) {
public static void applyBlockDiffs(
Chunk chunk,
ChunkData generated,
int minHeight,
int maxHeight
) {
applyBlockDiffs(
chunk,
chunk.getChunkSnapshot(false, false, false),
generated,
minHeight,
maxHeight);
}
public static void applyBlockDiffs(
Chunk chunk,
ChunkSnapshot snapshot,
ChunkData generated,
int minHeight,
int maxHeight
) {
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
for (int y = minHeight; y < maxHeight; y++) {
@@ -46,7 +46,7 @@ public final class JigsawStudioGraphMapper {
JigsawStudioVariantCatalog catalog = catalog(data, structure, mode);
IrisPosition configuredCell = structure.getCellSize();
JigsawStudioCellDimensions dimensions = configuredCell == null
? new JigsawStudioCellDimensions(16, 16, 16)
? new JigsawStudioCellDimensions(15, 15, 15)
: new JigsawStudioCellDimensions(
Math.max(1, configuredCell.getX()),
Math.max(1, configuredCell.getY()),
@@ -12,7 +12,7 @@ import java.util.Optional;
public final class JigsawStudioLayout {
public static final int FLOOR_Y = 64;
public static final int PLANAR_COLUMNS = 3;
public static final int PLANAR_GAP = 2;
public static final int PLANAR_GAP = 1;
public static final int MAX_VARIANTS = 512;
public static final String SPATIAL_WORKCELL_ID = "workcell/spatial";
@@ -83,6 +83,16 @@ public final class JigsawStudioSession {
return state.snapshot(workcellId);
}
public synchronized boolean setConnectorsVisible(String workcellId, boolean visible) {
MutableWorkcellState state = requireWorkcellState(workcellId);
if (state.connectorsVisible == visible) {
return false;
}
state.connectorsVisible = visible;
revision++;
return true;
}
public synchronized boolean replaceLayout(JigsawStudioLayout replacement) {
JigsawStudioLayout nextLayout = Objects.requireNonNull(replacement, "Replacement Jigsaw Studio layout");
if (layout.mode() != nextLayout.mode()) {
@@ -107,7 +117,8 @@ public final class JigsawStudioSession {
activeVariantKey,
nextLoadGeneration(),
nextMutationGeneration(),
false));
false,
previous != null && previous.connectorsVisible));
}
layout = nextLayout;
workcells.clear();
@@ -161,7 +172,8 @@ public final class JigsawStudioSession {
targetVariantKey,
nextLoadGeneration(),
nextMutationGeneration(),
false));
false,
previous != null && previous.connectorsVisible));
}
layout = nextLayout;
workcells.clear();
@@ -419,6 +431,7 @@ public final class JigsawStudioSession {
variantKey,
nextLoadGeneration(),
nextMutationGeneration(),
false,
false));
}
}
@@ -520,7 +533,8 @@ public final class JigsawStudioSession {
long mutationGeneration,
boolean dirty,
boolean saveInProgress,
boolean switchInProgress
boolean switchInProgress,
boolean connectorsVisible
) {
}
@@ -635,21 +649,29 @@ public final class JigsawStudioSession {
private long saveGeneration;
private boolean switchInProgress;
private long switchGeneration;
private boolean connectorsVisible;
private MutableWorkcellState(
String activeVariantKey,
long loadGeneration,
long mutationGeneration,
boolean dirty
boolean dirty,
boolean connectorsVisible
) {
this.activeVariantKey = activeVariantKey;
this.loadGeneration = loadGeneration;
this.mutationGeneration = mutationGeneration;
this.dirty = dirty;
this.connectorsVisible = connectorsVisible;
}
private MutableWorkcellState copy() {
return new MutableWorkcellState(activeVariantKey, loadGeneration, mutationGeneration, dirty);
return new MutableWorkcellState(
activeVariantKey,
loadGeneration,
mutationGeneration,
dirty,
connectorsVisible);
}
private WorkcellSnapshot snapshot(String workcellId) {
@@ -660,7 +682,8 @@ public final class JigsawStudioSession {
mutationGeneration,
dirty,
saveInProgress,
switchInProgress);
switchInProgress,
connectorsVisible);
}
}
}
@@ -0,0 +1,513 @@
package art.arcane.iris.core.service;
import art.arcane.iris.core.structure.authoring.StructureBackend;
import art.arcane.iris.core.structure.authoring.StructureCapability;
import art.arcane.iris.core.structure.authoring.StructureHash;
import art.arcane.iris.core.structure.authoring.StructureKey;
import art.arcane.iris.core.structure.authoring.StructureLoss;
import art.arcane.iris.core.structure.authoring.StructureOwnershipManifest;
import art.arcane.iris.core.structure.authoring.StructureResourceBundle;
import art.arcane.iris.core.structure.authoring.StructureSource;
import art.arcane.iris.core.structure.authoring.StructureTransactionWriter;
import art.arcane.iris.core.structure.authoring.StructureWriteOptions;
import art.arcane.iris.core.structure.authoring.StructureWriteResult;
import art.arcane.iris.engine.framework.structure.StructureResourceBundleGraphCompiler;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Base64;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.locks.ReentrantLock;
final class JigsawStudioHistoryStore {
static final int MAX_ITERATIONS = 5;
private static final int SCHEMA_VERSION = 1;
private static final long MAX_HISTORY_BYTES = 512L * 1024L * 1024L;
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
private static final ConcurrentMap<Path, ReentrantLock> LOCKS = new ConcurrentHashMap<>();
private final Path packRoot;
private final StructureKey structureKey;
private final StructureTransactionWriter writer;
private final Path historyPath;
private final ReentrantLock lock;
JigsawStudioHistoryStore(Path packRoot, String structureKey) {
this.packRoot = canonicalPackRoot(packRoot);
this.structureKey = new StructureKey(
"iris",
Objects.requireNonNull(structureKey, "Jigsaw Studio history structure key"));
writer = new StructureTransactionWriter(this.packRoot);
String identityHash = StructureHash.sha256(this.structureKey.value().getBytes(StandardCharsets.UTF_8));
historyPath = this.packRoot.resolve(".iris/jigsaw-history/key-" + identityHash + ".json").normalize();
if (!historyPath.startsWith(this.packRoot)) {
throw new IllegalArgumentException("Jigsaw Studio history path escapes its pack root");
}
lock = LOCKS.computeIfAbsent(historyPath, ignored -> new ReentrantLock());
}
Snapshot snapshotCurrent(String pieceKey) throws IOException {
lock.lock();
try {
return readCurrentSnapshot(pieceKey);
} finally {
lock.unlock();
}
}
int append(Snapshot snapshot) throws IOException {
Objects.requireNonNull(snapshot, "Jigsaw Studio history snapshot");
lock.lock();
try {
HistoryDocument document = readDocument();
if (!document.structure().equals(structureKey.value())) {
throw new IOException("Jigsaw Studio history belongs to " + document.structure());
}
ArrayList<HistoryIteration> iterations = new ArrayList<>(document.iterations());
HistoryIteration iteration = snapshot.iteration();
if (!iterations.isEmpty() && iterations.getLast().sameState(iteration)) {
return iterations.size();
}
iterations.add(iteration);
while (iterations.size() > MAX_ITERATIONS) {
iterations.removeFirst();
}
TreeMap<String, String> blobs = new TreeMap<>(document.blobs());
for (Map.Entry<String, byte[]> resource : snapshot.resources().entrySet()) {
String hash = StructureHash.sha256(resource.getValue());
blobs.putIfAbsent(hash, Base64.getEncoder().encodeToString(resource.getValue()));
}
retainReferencedBlobs(blobs, iterations);
writeDocument(new HistoryDocument(
SCHEMA_VERSION,
structureKey.value(),
List.copyOf(iterations),
Map.copyOf(blobs)));
return iterations.size();
} finally {
lock.unlock();
}
}
UndoResult undoLatest() throws IOException {
lock.lock();
try {
HistoryDocument document = readDocument();
if (document.iterations().isEmpty()) {
return UndoResult.unavailable();
}
HistoryIteration iteration = document.iterations().getLast();
StructureResourceBundle bundle = restoreBundle(iteration, document.blobs());
StructureResourceBundleGraphCompiler.requireViable(bundle);
Path manifestPath = writer.ownershipManifestPath(structureKey);
byte[] currentManifest = readRegularFile(manifestPath, "ownership manifest");
StructureWriteResult result = writer.write(
bundle,
StructureWriteOptions.overwriteExpected(StructureHash.sha256(currentManifest)));
if (!result.successful()) {
return new UndoResult(
false,
true,
document.iterations().size(),
iteration.pieceKey(),
result,
"");
}
ArrayList<HistoryIteration> remaining = new ArrayList<>(document.iterations());
remaining.removeLast();
TreeMap<String, String> blobs = new TreeMap<>(document.blobs());
retainReferencedBlobs(blobs, remaining);
String warning = "";
try {
if (remaining.isEmpty()) {
Files.deleteIfExists(historyPath);
forceDirectory(historyPath.getParent());
} else {
writeDocument(new HistoryDocument(
SCHEMA_VERSION,
structureKey.value(),
List.copyOf(remaining),
Map.copyOf(blobs)));
}
} catch (IOException historyFailure) {
warning = historyFailure.getMessage() == null
? historyFailure.getClass().getSimpleName()
: historyFailure.getMessage();
}
return new UndoResult(
true,
true,
remaining.size(),
iteration.pieceKey(),
result,
warning);
} finally {
lock.unlock();
}
}
int availableIterations() throws IOException {
lock.lock();
try {
return readDocument().iterations().size();
} finally {
lock.unlock();
}
}
void delete() throws IOException {
lock.lock();
try {
if (Files.deleteIfExists(historyPath)) {
forceDirectory(historyPath.getParent());
}
} finally {
lock.unlock();
}
}
Path historyPath() {
return historyPath;
}
private Snapshot readCurrentSnapshot(String pieceKey) throws IOException {
Path manifestPath = writer.ownershipManifestPath(structureKey);
byte[] manifestContent = readRegularFile(manifestPath, "ownership manifest");
StructureOwnershipManifest manifest;
try {
manifest = StructureOwnershipManifest.fromJson(manifestContent);
} catch (RuntimeException exception) {
throw new IOException("Jigsaw Studio history cannot parse the ownership manifest", exception);
}
if (!manifest.structure().equals(structureKey)) {
throw new IOException("Jigsaw Studio ownership manifest belongs to " + manifest.structure());
}
TreeMap<String, byte[]> resources = new TreeMap<>();
for (Map.Entry<String, String> resource : manifest.resourceHashes().entrySet()) {
Path resourcePath = resolveOwnedResource(resource.getKey());
byte[] content = readRegularFile(resourcePath, "owned resource " + resource.getKey());
String actualHash = StructureHash.sha256(content);
if (!resource.getValue().equals(actualHash)) {
throw new IOException("Jigsaw Studio owned resource changed before history capture: "
+ resource.getKey());
}
resources.put(resource.getKey(), content);
}
return new Snapshot(
new HistoryIteration(
System.currentTimeMillis(),
Objects.requireNonNull(pieceKey, "Jigsaw Studio history piece key"),
manifest.source(),
manifest.backend(),
manifest.capabilities(),
manifest.losses(),
manifest.resourceHashes()),
resources);
}
private StructureResourceBundle restoreBundle(
HistoryIteration iteration,
Map<String, String> blobs
) throws IOException {
StructureResourceBundle.Builder builder = StructureResourceBundle.builder(structureKey)
.source(iteration.source())
.backend(iteration.backend())
.capabilities(iteration.capabilities())
.losses(iteration.losses());
for (Map.Entry<String, String> resource : iteration.resourceHashes().entrySet()) {
String encoded = blobs.get(resource.getValue());
if (encoded == null) {
throw new IOException("Jigsaw Studio history is missing resource blob " + resource.getValue());
}
byte[] content;
try {
content = Base64.getDecoder().decode(encoded);
} catch (IllegalArgumentException exception) {
throw new IOException("Jigsaw Studio history contains invalid resource data", exception);
}
if (!resource.getValue().equals(StructureHash.sha256(content))) {
throw new IOException("Jigsaw Studio history resource hash does not match "
+ resource.getKey());
}
builder.resource(resource.getKey(), content);
}
return builder.build();
}
private HistoryDocument readDocument() throws IOException {
if (!Files.exists(historyPath, LinkOption.NOFOLLOW_LINKS)) {
return HistoryDocument.empty(structureKey.value());
}
byte[] content = readRegularFile(historyPath, "history file");
if (content.length > MAX_HISTORY_BYTES) {
throw new IOException("Jigsaw Studio history exceeds " + MAX_HISTORY_BYTES + " bytes");
}
HistoryDocument document;
try {
document = GSON.fromJson(new String(content, StandardCharsets.UTF_8), HistoryDocument.class);
} catch (RuntimeException exception) {
throw new IOException("Jigsaw Studio history is invalid", exception);
}
if (document == null || document.schemaVersion() != SCHEMA_VERSION) {
throw new IOException("Unsupported Jigsaw Studio history schema");
}
HistoryDocument validated;
try {
validated = document.validated();
} catch (RuntimeException exception) {
throw new IOException("Jigsaw Studio history is invalid", exception);
}
if (!validated.structure().equals(structureKey.value())) {
throw new IOException("Jigsaw Studio history belongs to " + validated.structure());
}
return validated;
}
private void writeDocument(HistoryDocument document) throws IOException {
HistoryDocument validated = document.validated();
byte[] content = (GSON.toJson(validated) + "\n").getBytes(StandardCharsets.UTF_8);
if (content.length > MAX_HISTORY_BYTES) {
throw new IOException("Jigsaw Studio history exceeds " + MAX_HISTORY_BYTES + " bytes");
}
Path historyRoot = historyPath.getParent();
Files.createDirectories(historyRoot);
rejectSymbolicPath(historyRoot);
Path temporary = historyRoot.resolve(historyPath.getFileName() + "."
+ UUID.randomUUID() + ".tmp").normalize();
try {
try (FileChannel channel = FileChannel.open(
temporary,
StandardOpenOption.CREATE_NEW,
StandardOpenOption.WRITE)) {
ByteBuffer buffer = ByteBuffer.wrap(content);
while (buffer.hasRemaining()) {
channel.write(buffer);
}
channel.force(true);
}
try {
Files.move(
temporary,
historyPath,
StandardCopyOption.ATOMIC_MOVE,
StandardCopyOption.REPLACE_EXISTING);
} catch (AtomicMoveNotSupportedException exception) {
Files.move(temporary, historyPath, StandardCopyOption.REPLACE_EXISTING);
}
forceDirectory(historyRoot);
} finally {
Files.deleteIfExists(temporary);
}
}
private Path resolveOwnedResource(String relativePath) throws IOException {
StructureResourceBundle.validateRelativePath(relativePath);
Path resource = packRoot.resolve(relativePath).normalize();
if (!resource.startsWith(packRoot)) {
throw new IOException("Jigsaw Studio history resource escapes its pack root: " + relativePath);
}
rejectSymbolicPath(resource.getParent());
return resource;
}
private static byte[] readRegularFile(Path path, String kind) throws IOException {
if (!Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)) {
throw new IOException("Jigsaw Studio " + kind + " is missing or not a regular file: " + path);
}
return Files.readAllBytes(path);
}
private static void retainReferencedBlobs(
Map<String, String> blobs,
List<HistoryIteration> iterations
) {
Set<String> retained = new TreeSet<>();
for (HistoryIteration iteration : iterations) {
retained.addAll(iteration.resourceHashes().values());
}
blobs.keySet().retainAll(retained);
}
private void rejectSymbolicPath(Path path) throws IOException {
Path current = path;
while (current != null && current.startsWith(packRoot)) {
if (Files.isSymbolicLink(current)) {
throw new IOException("Jigsaw Studio history path contains a symbolic link: " + current);
}
if (current.equals(packRoot)) {
return;
}
current = current.getParent();
}
throw new IOException("Jigsaw Studio history path escapes its pack root: " + path);
}
private static Path canonicalPackRoot(Path root) {
Path normalized = Objects.requireNonNull(root, "Jigsaw Studio history pack root")
.toAbsolutePath().normalize();
try {
return normalized.toRealPath();
} catch (IOException exception) {
throw new IllegalArgumentException("Jigsaw Studio history pack root is unavailable: "
+ normalized, exception);
}
}
private static void forceDirectory(Path directory) throws IOException {
try (FileChannel channel = FileChannel.open(directory, StandardOpenOption.READ)) {
channel.force(true);
}
}
record Snapshot(HistoryIteration iteration, Map<String, byte[]> resources) {
Snapshot {
Objects.requireNonNull(iteration, "Jigsaw Studio history iteration");
Objects.requireNonNull(resources, "Jigsaw Studio history resources");
LinkedHashMap<String, byte[]> copies = new LinkedHashMap<>();
for (Map.Entry<String, byte[]> resource : resources.entrySet()) {
copies.put(resource.getKey(), resource.getValue().clone());
}
resources = Map.copyOf(copies);
}
boolean matches(StructureResourceBundle bundle) {
if (!iteration.source().equals(bundle.source())
|| iteration.backend() != bundle.backend()
|| !Set.copyOf(iteration.capabilities()).equals(bundle.capabilities())
|| !iteration.losses().equals(bundle.losses())
|| iteration.resourceHashes().size() != bundle.resources().size()) {
return false;
}
for (Map.Entry<String, StructureResourceBundle.Resource> resource
: bundle.resources().entrySet()) {
if (!resource.getValue().contentHash().equals(
iteration.resourceHashes().get(resource.getKey()))) {
return false;
}
}
return true;
}
}
record UndoResult(
boolean successful,
boolean available,
int remainingIterations,
String pieceKey,
StructureWriteResult writeResult,
String warning
) {
UndoResult {
pieceKey = pieceKey == null ? "" : pieceKey;
warning = warning == null ? "" : warning;
}
static UndoResult unavailable() {
return new UndoResult(false, false, 0, "", null, "");
}
}
private record HistoryDocument(
int schemaVersion,
String structure,
List<HistoryIteration> iterations,
Map<String, String> blobs
) {
private HistoryDocument {
structure = structure == null ? "" : structure;
iterations = iterations == null ? List.of() : List.copyOf(iterations);
blobs = blobs == null ? Map.of() : Map.copyOf(blobs);
}
private static HistoryDocument empty(String structure) {
return new HistoryDocument(SCHEMA_VERSION, structure, List.of(), Map.of());
}
private HistoryDocument validated() throws IOException {
if (schemaVersion != SCHEMA_VERSION || structure.isBlank()) {
throw new IOException("Jigsaw Studio history header is invalid");
}
if (iterations.size() > MAX_ITERATIONS) {
throw new IOException("Jigsaw Studio history contains too many iterations");
}
TreeMap<String, String> validatedBlobs = new TreeMap<>();
for (Map.Entry<String, String> blob : blobs.entrySet()) {
if (!StructureHash.isSha256(blob.getKey()) || blob.getValue() == null) {
throw new IOException("Jigsaw Studio history contains an invalid resource blob");
}
validatedBlobs.put(blob.getKey(), blob.getValue());
}
return new HistoryDocument(
SCHEMA_VERSION,
structure,
List.copyOf(iterations),
Map.copyOf(validatedBlobs));
}
}
private record HistoryIteration(
long recordedAtEpochMilli,
String pieceKey,
StructureSource source,
StructureBackend backend,
List<StructureCapability> capabilities,
List<StructureLoss> losses,
Map<String, String> resourceHashes
) {
private HistoryIteration {
pieceKey = Objects.requireNonNull(pieceKey, "Jigsaw Studio history piece key").trim();
if (pieceKey.isEmpty()) {
throw new IllegalArgumentException("Jigsaw Studio history piece key cannot be empty");
}
Objects.requireNonNull(source, "Jigsaw Studio history source");
Objects.requireNonNull(backend, "Jigsaw Studio history backend");
capabilities = List.copyOf(Objects.requireNonNull(
capabilities,
"Jigsaw Studio history capabilities"));
losses = List.copyOf(Objects.requireNonNull(losses, "Jigsaw Studio history losses"));
TreeMap<String, String> hashes = new TreeMap<>();
for (Map.Entry<String, String> resource : Objects.requireNonNull(
resourceHashes,
"Jigsaw Studio history resource hashes").entrySet()) {
String relativePath = StructureResourceBundle.validateRelativePath(resource.getKey());
if (!StructureHash.isSha256(resource.getValue())) {
throw new IllegalArgumentException("Invalid Jigsaw Studio history resource hash");
}
hashes.put(relativePath, resource.getValue());
}
if (hashes.isEmpty()) {
throw new IllegalArgumentException("Jigsaw Studio history iteration cannot be empty");
}
resourceHashes = Map.copyOf(hashes);
}
private boolean sameState(HistoryIteration other) {
return source.equals(other.source)
&& backend == other.backend
&& pieceKey.equals(other.pieceKey)
&& capabilities.equals(other.capabilities)
&& losses.equals(other.losses)
&& resourceHashes.equals(other.resourceHashes);
}
}
}
@@ -345,7 +345,9 @@ public final class JigsawStudioMenuController {
element.addLore(ChatColor.GRAY + "Loaded: "
+ (active == null ? "None" : safe(active.displayName())));
element.addLore(workcellStatus(workcell));
element.addLore(ChatColor.YELLOW + "Left-click to select");
element.addLore(ChatColor.GRAY + "Connector blocks: "
+ (workcell.connectorsVisible() ? "Visible" : "Hidden"));
element.addLore(ChatColor.YELLOW + "Left-click to select and teleport");
element.addLore(ChatColor.YELLOW + "Right-click for workcell settings");
if (workcell.dirty() && !workcell.saving()) {
element.addLore(ChatColor.GOLD + "Shift-left: Flush Autosave Now");
@@ -564,6 +566,34 @@ public final class JigsawStudioMenuController {
window.setElement(0, 1, spatial);
}
UIElement connectors = element(
"settings-connectors",
workcell.connectorsVisible() ? Material.JIGSAW : Material.STRUCTURE_VOID,
workcell.connectorsVisible()
? ChatColor.GREEN + "Connector Blocks Visible"
: ChatColor.YELLOW + "Connector Blocks Hidden");
connectors.addLore(ChatColor.GRAY + "Hidden connectors retain their metadata and final block state.");
connectors.addLore(ChatColor.YELLOW + "Left-click to "
+ (workcell.connectorsVisible() ? "hide" : "show") + " connector blocks");
connectors.onLeftClick(clicked -> toggleConnectorBlocks(
window.getViewer(),
state.requestId(),
workcell.stableId(),
!workcell.connectorsVisible()));
window.setElement(2, 1, connectors);
UIElement resetConnectors = element(
"settings-reset-connectors",
Material.RECOVERY_COMPASS,
ChatColor.AQUA + "Reset Connector Blocks");
resetConnectors.addLore(ChatColor.GRAY + "Restore every connector in this workcell from disk.");
resetConnectors.addLore(ChatColor.GRAY + "Other edited blocks are left unchanged.");
resetConnectors.onLeftClick(clicked -> resetConnectorBlocks(
window.getViewer(),
state.requestId(),
workcell.stableId()));
window.setElement(4, 1, resetConnectors);
window.setElement(-2, 2, axisElement(
window,
state,
@@ -588,6 +618,17 @@ public final class JigsawStudioMenuController {
window.getViewer(), state.requestId(), workcell.stableId(), 0));
window.setElement(-4, 5, footerBack);
UIElement undo = element(
"settings-undo",
Material.CLOCK,
ChatColor.LIGHT_PURPLE + "Undo Last Autosave");
undo.addLore(ChatColor.GRAY + "Restore the previous owned graph iteration.");
undo.addLore(ChatColor.GRAY + "Up to five autosave iterations are retained on disk.");
undo.onLeftClick(clicked -> undoAutosave(
window.getViewer(),
state.requestId()));
window.setElement(0, 5, undo);
if (workcell.dirty() && !workcell.saving()) {
UIElement saveNow = element(
"save-now",
@@ -1432,9 +1473,43 @@ public final class JigsawStudioMenuController {
if (matchingState(player, requestId, true).isEmpty()) {
return;
}
if (actions.selectWorkcell(player, workcellId)) {
if (actions.teleportToWorkcell(player, workcellId)) {
clearConfirmations(player.getUniqueId());
refreshMain(player, requestId, workcellId, 0);
closeAfterAction(player);
}
}
private void toggleConnectorBlocks(
Player player,
UUID requestId,
String workcellId,
boolean visible
) {
Optional<JigsawStudioMenuState> current = matchingState(player, requestId, true);
if (current.isEmpty() || current.get().workcell(workcellId) == null) {
return;
}
if (actions.setConnectorBlocksVisible(player, workcellId, visible)) {
closeAfterAction(player);
}
}
private void resetConnectorBlocks(Player player, UUID requestId, String workcellId) {
Optional<JigsawStudioMenuState> current = matchingState(player, requestId, true);
if (current.isEmpty() || current.get().workcell(workcellId) == null) {
return;
}
if (actions.resetConnectorBlocks(player, workcellId)) {
closeAfterAction(player);
}
}
private void undoAutosave(Player player, UUID requestId) {
if (matchingState(player, requestId, true).isEmpty()) {
return;
}
if (actions.undoAutosave(player)) {
closeAfterAction(player);
}
}
@@ -2843,6 +2918,14 @@ public final class JigsawStudioMenuController {
boolean selectWorkcell(Player player, String workcellId);
boolean teleportToWorkcell(Player player, String workcellId);
boolean setConnectorBlocksVisible(Player player, String workcellId, boolean visible);
boolean resetConnectorBlocks(Player player, String workcellId);
boolean undoAutosave(Player player);
boolean switchVariant(Player player, String workcellId, String pieceKey, boolean discardDirty);
boolean createVariant(Player player, String workcellId, boolean duplicateActive);
@@ -156,6 +156,7 @@ public record JigsawStudioMenuState(
boolean dirty,
boolean saving,
boolean loading,
boolean connectorsVisible,
List<Variant> variants
) {
public Workcell {
File diff suppressed because it is too large. Load diff
@@ -71,6 +71,7 @@ import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
import java.util.function.Supplier;
@@ -579,7 +580,7 @@ public class StudioSVC implements IrisService {
String dimension,
Consumer<World> onDone
) {
return closeActiveProject().handle((closeResult, closeThrowable) -> {
return closeActiveProjectForReplacement(sender).handle((closeResult, closeThrowable) -> {
if (closeThrowable != null) {
IrisLogging.reportError("Failed while closing an existing studio project before opening \"" + dimension + "\".", closeThrowable);
J.s(() -> sender.sendMessage(IrisLanguage.text(
@@ -662,6 +663,29 @@ public class StudioSVC implements IrisService {
return studioTransitions.submit(this::closeActiveProject);
}
CompletableFuture<StudioOpenCoordinator.StudioCloseResult> closeActiveProjectForReplacement(
VolmitSender sender
) {
IrisProject project = activeProject;
if (project == null) {
return closeActiveProject();
}
JigsawStudioActivation.Request request = JigsawStudioActivation.getRequest(project.getName());
if (request == null || !sender.isPlayer()) {
return closeActiveProject();
}
UUID ownerId = sender.player().getUniqueId();
return JigsawStudioService.get()
.awaitCloseForReplacement(request.requestId(), ownerId)
.thenCompose(ignored -> {
if (activeProject != project) {
return CompletableFuture.failedFuture(new IllegalStateException(
"The active Studio project changed while replacement was waiting to close."));
}
return closeActiveProject();
});
}
private CompletableFuture<StudioOpenCoordinator.StudioCloseResult> closeActiveProject() {
IrisProject project = activeProject;
if (project == null) {
@@ -64,7 +64,7 @@ public class IrisStructure extends IrisRegistrant {
private IrisJigsawBranchFailurePolicy branchFailurePolicy = IrisJigsawBranchFailurePolicy.FAIL_ASSEMBLY;
@Desc("Default Studio cell dimensions. Legacy planar structures use this value for every workcell when planarWorkcells is empty.")
private IrisPosition cellSize = new IrisPosition(16, 16, 16);
private IrisPosition cellSize = new IrisPosition(15, 15, 15);
@Desc("Optional author-facing name for the single spatial Jigsaw Studio workcell. Spatial is shown when this is blank.")
private String spatialWorkcellDisplayName = "";
@@ -35,9 +35,11 @@ import art.arcane.volmlib.util.collection.KList;
import org.bukkit.World;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -184,7 +186,7 @@ public final class JigsawStudioGenerator extends EnginedStudioGenerator {
}
}
void paintChunk(TerrainChunk terrainChunk, int chunkX, int chunkZ) {
public void paintChunk(TerrainChunk terrainChunk, int chunkX, int chunkZ) {
Objects.requireNonNull(terrainChunk, "Jigsaw Studio terrain chunk");
int floorY = Math.max(terrainChunk.getMinHeight(), JigsawStudioLayout.FLOOR_Y);
if (floorY >= terrainChunk.getMaxHeight()) {
@@ -217,7 +219,16 @@ public final class JigsawStudioGenerator extends EnginedStudioGenerator {
continue;
}
paintObject(terrainChunk, workcell, renderedBay, chunkWorldX, chunkWorldZ);
paintConnectors(terrainChunk, workcell, renderedBay, chunkWorldX, chunkWorldZ);
if (session.workcellSnapshot(workcell.stableId()).connectorsVisible()) {
paintConnectors(terrainChunk, workcell, renderedBay, chunkWorldX, chunkWorldZ);
} else {
paintHiddenConnectorFinalStates(
terrainChunk,
workcell,
renderedBay,
chunkWorldX,
chunkWorldZ);
}
}
}
@@ -632,6 +643,34 @@ public final class JigsawStudioGenerator extends EnginedStudioGenerator {
}
}
private void paintHiddenConnectorFinalStates(
TerrainChunk terrainChunk,
JigsawStudioBay workcell,
RenderedBay renderedBay,
int chunkWorldX,
int chunkWorldZ
) {
Set<RenderedPosition> occupied = new HashSet<>(renderedBay.blocks().size());
for (RenderedBlock block : renderedBay.blocks()) {
occupied.add(new RenderedPosition(block.x(), block.y(), block.z()));
}
JigsawStudioBounds bounds = workcell.bounds();
for (RenderedConnector connector : renderedBay.connectors()) {
if (occupied.contains(new RenderedPosition(connector.x(), connector.y(), connector.z()))) {
continue;
}
PlatformBlockState finalState = B.getStateOrNull(connector.connector().getFinalState(), false);
setWorldBlock(
terrainChunk,
bounds.originX() + connector.x(),
bounds.originY() + connector.y(),
bounds.originZ() + connector.z(),
finalState == null ? invalidMarker : finalState,
chunkWorldX,
chunkWorldZ);
}
}
private void paintInvalidBay(
TerrainChunk terrainChunk,
JigsawStudioBay workcell,
@@ -871,4 +910,7 @@ public final class JigsawStudioGenerator extends EnginedStudioGenerator {
record RotatedPosition(int x, int y, int z) {
}
private record RenderedPosition(int x, int y, int z) {
}
}
@@ -0,0 +1,54 @@
package art.arcane.iris.util.common.director.specialhandlers;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.pack.PackDirectoryResolver;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.util.common.director.DirectorParameterHandler;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.director.exceptions.DirectorParsingException;
import java.io.File;
import java.util.LinkedHashSet;
import java.util.Set;
public final class IrisStructureHandler implements DirectorParameterHandler<String> {
@Override
public KList<String> getPossibilities() {
Set<String> keys = new LinkedHashSet<>();
IrisData activeData = data();
if (activeData != null) {
addStructureKeys(keys, activeData);
}
for (File pack : PackDirectoryResolver.listVisiblePackDirectories(
IrisPlatforms.get().dataFolder("packs"))) {
addStructureKeys(keys, IrisData.get(pack));
}
return new KList<>(keys);
}
@Override
public String toString(String value) {
return value == null ? "" : value;
}
@Override
public String parse(String input, boolean force) throws DirectorParsingException {
for (String option : getPossibilities(input)) {
if (option.equalsIgnoreCase(input)) {
return option;
}
}
throw new DirectorParsingException("Unable to find Iris structure \"" + input + "\"");
}
@Override
public boolean supports(Class<?> type) {
return type == String.class;
}
private static void addStructureKeys(Set<String> keys, IrisData data) {
for (String key : data.getStructureLoader().getPossibleKeys()) {
keys.add(key);
}
}
}
+2 -1
View File
@@ -260,7 +260,8 @@
"iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§cDer Pack für Dimension {value} konnte nicht aufgelöst werden",
"iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§cKeine Iris-Struktur '{structure}' in diesem Pack",
"iris.bukkit.commandstructure.structure_assembled_0_pieces": "§cStruktur '{structure}' wurde aus 0 Teilen zusammengesetzt",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§a'{structure}' ({value} Teile) an deiner Position platziert.",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§a'{structure}' ({value} Teile) an deiner Position platziert. ({value2} block changes)",
"iris.bukkit.commandstructure.placement_changed_no_blocks": "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height.",
"iris.bukkit.commandstudio.opening_studio_pack_seed": "§aStudio für den Pack \"{value}\" wird geöffnet (Seed: {seed})",
"iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§cGib ein Dimensions-Pack an: /iris std importvanilla pack=<dimension>",
"iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§cDer Pack für Dimension {value} konnte nicht aufgelöst werden",
+2 -1
View File
@@ -260,7 +260,8 @@
"iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§cNo se pudo resolver el pack de la dimensión {value}",
"iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§cNo existe la estructura de Iris '{structure}' en este pack",
"iris.bukkit.commandstructure.structure_assembled_0_pieces": "§cLa estructura '{structure}' ensambló 0 piezas",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§aSe colocó '{structure}' ({value} piezas) en tu ubicación.",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§aSe colocó '{structure}' ({value} piezas) en tu ubicación. ({value2} block changes)",
"iris.bukkit.commandstructure.placement_changed_no_blocks": "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height.",
"iris.bukkit.commandstudio.opening_studio_pack_seed": "§aAbriendo Studio para el pack \"{value}\" (semilla: {seed})",
"iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§cIndica un pack de dimensión: /iris std importvanilla pack=<dimension>",
"iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§cNo se pudo resolver el pack de la dimensión {value}",
+2 -1
View File
@@ -260,7 +260,8 @@
"iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§cPakkausta ei voitu ratkaista mitan vuoksi {value}",
"iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§cEi iirisrakennetta '{structure}Tässä pakkauksessa",
"iris.bukkit.commandstructure.structure_assembled_0_pieces": "§cRakenne{structure}' koottu 0 kappaletta",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§aPaikka{structure}' ({value} Palaset) sijaintisi.",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§aPaikka{structure}' ({value} Palaset) sijaintisi. ({value2} block changes)",
"iris.bukkit.commandstructure.placement_changed_no_blocks": "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height.",
"iris.bukkit.commandstudio.opening_studio_pack_seed": "§aAvaa studio \"{value}\" pakkaus (siemen: {seed})",
"iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§cAntakaa mittapaketti: /iris std importvanilla pack=<dimension>",
"iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§cPakkausta ei voitu ratkaista mitan vuoksi {value}",
+2 -1
View File
@@ -260,7 +260,8 @@
"iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§cImpossible de résoudre le pack de la dimension {value}",
"iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§cAucune structure Iris '{structure}' dans ce pack",
"iris.bukkit.commandstructure.structure_assembled_0_pieces": "§cLa structure '{structure}' a assemblé 0 pièce",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§aLa structure '{structure}' ({value} pièces) a été placée à votre position.",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§aLa structure '{structure}' ({value} pièces) a été placée à votre position. ({value2} block changes)",
"iris.bukkit.commandstructure.placement_changed_no_blocks": "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height.",
"iris.bukkit.commandstudio.opening_studio_pack_seed": "§aOuverture de Studio pour le pack \"{value}\" (graine : {seed})",
"iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§cIndiquez un pack de dimension : /iris std importvanilla pack=<dimension>",
"iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§cImpossible de résoudre le pack de la dimension {value}",
+2 -1
View File
@@ -260,7 +260,8 @@
"iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§cלא יכול לפתור את החבילה לממד {value}",
"iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§cאין מבנה איריס \"{structure}\"בחבילה הזאת",
"iris.bukkit.commandstructure.structure_assembled_0_pieces": "§cמבנה \"{structure}התאספו 0 חתיכות",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§aמקום \"{structure}' ({value} חתיכות) במיקום שלך.",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§aמקום \"{structure}' ({value} חתיכות) במיקום שלך. ({value2} block changes)",
"iris.bukkit.commandstructure.placement_changed_no_blocks": "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height.",
"iris.bukkit.commandstudio.opening_studio_pack_seed": "§aסטודיו פתיחה ל\"{value}\"חבילה\" (צילום: {seed})",
"iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§cלספק ערכת מימד: /iris std importvanilla pack=<dimension>",
"iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§cלא יכול לפתור את החבילה לממד {value}",
+2 -1
View File
@@ -260,7 +260,8 @@
"iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§cImpossibile risolvere il pack della dimensione {value}",
"iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§cNessuna struttura Iris '{structure}' in questo pack",
"iris.bukkit.commandstructure.structure_assembled_0_pieces": "§cLa struttura '{structure}' è stata assemblata con 0 pezzi",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§a'{structure}' ({value} pezzi) posizionata nella tua posizione.",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§a'{structure}' ({value} pezzi) posizionata nella tua posizione. ({value2} block changes)",
"iris.bukkit.commandstructure.placement_changed_no_blocks": "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height.",
"iris.bukkit.commandstudio.opening_studio_pack_seed": "§aApertura di Studio per il pack \"{value}\" (seed: {seed})",
"iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§cSpecifica un Pack di dimensione: /iris std importvanilla pack=<dimension>",
"iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§cImpossibile risolvere il pack della dimensione {value}",
+2 -1
View File
@@ -260,7 +260,8 @@
"iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§cディメンション {value} のパックを解決できませんでした",
"iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§cこのパックには Iris 構造物 '{structure}' がありません",
"iris.bukkit.commandstructure.structure_assembled_0_pieces": "§c構造物 '{structure}' は 0 ピースで組み立てられました",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§a現在位置に '{structure}'({value} ピース)を配置しました。",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§a現在位置に '{structure}'({value} ピース)を配置しました。 ({value2} block changes)",
"iris.bukkit.commandstructure.placement_changed_no_blocks": "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height.",
"iris.bukkit.commandstudio.opening_studio_pack_seed": "§aパック \"{value}\" のスタジオを開いています(シード: {seed})",
"iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§cディメンションパックを指定してください: /iris std importvanilla pack=<dimension>",
"iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§cディメンション {value} のパックを解決できませんでした",
+2 -1
View File
@@ -260,7 +260,8 @@
"iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§c차원을 위한 팩을 해결할 수 없습니다 {value}",
"iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§c아이리스 구조 없음 '{structure}이 팩에서",
"iris.bukkit.commandstructure.structure_assembled_0_pieces": "§c구조물 '{structure}' 조립 결과: 조각 0개",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§a장소 '{structure}' ({value} 당신의 위치에 조각).",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§a장소 '{structure}' ({value} 당신의 위치에 조각). ({value2} block changes)",
"iris.bukkit.commandstructure.placement_changed_no_blocks": "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height.",
"iris.bukkit.commandstudio.opening_studio_pack_seed": "§a\"를 위한 오프닝 스튜디오{value}\"팩 (seed: {seed})",
"iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§c차원 팩을 제공: /iris std importvanilla pack=<dimension>",
"iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§c차원을 위한 팩을 해결할 수 없습니다 {value}",
+2 -1
View File
@@ -260,7 +260,8 @@
"iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§cNepavyko išspręsti pakuotės dimensijai {value}",
"iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§cNėra rainelės struktūros \"{structure}\"šioje pakuotėje",
"iris.bukkit.commandstructure.structure_assembled_0_pieces": "§cStruktūra \"{structure}'surinkti 0 vienetai",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§aPateikta \"{structure}' ({value} vienetų).",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§aPateikta \"{structure}' ({value} vienetų). ({value2} block changes)",
"iris.bukkit.commandstructure.placement_changed_no_blocks": "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height.",
"iris.bukkit.commandstudio.opening_studio_pack_seed": "§aAtidarymo studija \"{value}\"pakuotė (sėkla: {seed})",
"iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§cPateikite matmenų paketą: /iris std importvanilla pack=<dimension>",
"iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§cNepavyko išspręsti pakuotės dimensijai {value}",
+2 -1
View File
@@ -260,7 +260,8 @@
"iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§cKon het pakket voor dimensie niet oplossen {value}",
"iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§cGeen irisstructuur '{structure}' in deze verpakking",
"iris.bukkit.commandstructure.structure_assembled_0_pieces": "§cStructuur{structure}' gemonteerd 0 stuks",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§aGeplaatst '{structure}' ({value} stukken) op uw locatie.",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§aGeplaatst '{structure}' ({value} stukken) op uw locatie. ({value2} block changes)",
"iris.bukkit.commandstructure.placement_changed_no_blocks": "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height.",
"iris.bukkit.commandstudio.opening_studio_pack_seed": "§aopenen studio voor de \"{value}\" verpakking (zaad: {seed})",
"iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§cGeef een maatpakket: /iris std importvanilla pack=<dimension>",
"iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§cKon het pakket voor dimensie niet oplossen {value}",
+2 -1
View File
@@ -260,7 +260,8 @@
"iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§cNie można rozwiązać pakietu dla wymiaru {value}",
"iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§cBrak struktury tęczówki \"{structure}'w tym opakowaniu",
"iris.bukkit.commandstructure.structure_assembled_0_pieces": "§cStruktura \"{structure}\"zmontowane 0 kawałki",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§aUmieszczone \"{structure}' ({value} sztuk) w miejscu.",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§aUmieszczone \"{structure}' ({value} sztuk) w miejscu. ({value2} block changes)",
"iris.bukkit.commandstructure.placement_changed_no_blocks": "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height.",
"iris.bukkit.commandstudio.opening_studio_pack_seed": "§aStudio otwarcia dla \"{value}\"opakowanie (nasiona: {seed})",
"iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§cNależy podać zestaw wymiarów: /iris std importvanilla pack=<dimension>",
"iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§cNie można rozwiązać pakietu dla wymiaru {value}",
+2 -1
View File
@@ -260,7 +260,8 @@
"iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§cNão foi possível resolver o pacote para a dimensão {value}",
"iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§cSem estrutura íris '{structure}' nesta pack",
"iris.bukkit.commandstructure.structure_assembled_0_pieces": "§cEstrutura{structure}' montados 0 peças",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§aColocado '{structure}' ({value} peças) na sua localização.",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§aColocado '{structure}' ({value} peças) na sua localização. ({value2} block changes)",
"iris.bukkit.commandstructure.placement_changed_no_blocks": "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height.",
"iris.bukkit.commandstudio.opening_studio_pack_seed": "§aEstúdio de abertura para o \"{value}\" pack (sementes: {seed})",
"iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§cFornecer um pacote de dimensões: /iris std importvanilla pack=<dimension>",
"iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§cNão foi possível resolver o pacote para a dimensão {value}",
+2 -1
View File
@@ -260,7 +260,8 @@
"iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§cНе удалось решить пакет для измерения {value}",
"iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§cНет структуры радужной оболочки{structure}В этой пачке",
"iris.bukkit.commandstructure.structure_assembled_0_pieces": "§cСтруктура{structure}собранный 0 части",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§aПомещение '{structure}' ({value} куски) в вашем месте.",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§aПомещение '{structure}' ({value} куски) в вашем месте. ({value2} block changes)",
"iris.bukkit.commandstructure.placement_changed_no_blocks": "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height.",
"iris.bukkit.commandstudio.opening_studio_pack_seed": "§aОткрытие студии для\"{value}\"пак (семя):{seed})",
"iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§cПредоставьте размерный пакет: /iris std importvanilla pack=<dimension>",
"iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§cНе удалось решить пакет для измерения {value}",
+2 -1
View File
@@ -260,7 +260,8 @@
"iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§cpaketi boyut için çözemez {value}",
"iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§cyok iris structure \"{structure}“Bu pakette",
"iris.bukkit.commandstructure.structure_assembled_0_pieces": "§cYapı \"{structure}\"Bir araya geldi\" 0 parçalar parça parçaları",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§ayerleştirildi ‘{structure}' ({value} parçalar) konumunuzda.",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§ayerleştirildi ‘{structure}' ({value} parçalar) konumunuzda. ({value2} block changes)",
"iris.bukkit.commandstructure.placement_changed_no_blocks": "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height.",
"iris.bukkit.commandstudio.opening_studio_pack_seed": "§aAçılış stüdyosu \"{value}\" paket (seed: {seed})",
"iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§cBir boyut paketi sağlayın: /iris std importvanilla pack=<dimension>",
"iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§cpaketi boyut için çözemez {value}",
+2 -1
View File
@@ -260,7 +260,8 @@
"iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§cKhông thể giải quyết gói cho kích thước {value}",
"iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§cKhông có cấu trúc Iris{structure}'Trong gói này",
"iris.bukkit.commandstructure.structure_assembled_0_pieces": "§cCấu trúc{structure}Tập hợp 0 mảnh",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§aĐã đặt '{structure}' ({value} Các mảnh) tại vị trí.",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§aĐã đặt '{structure}' ({value} Các mảnh) tại vị trí. ({value2} block changes)",
"iris.bukkit.commandstructure.placement_changed_no_blocks": "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height.",
"iris.bukkit.commandstudio.opening_studio_pack_seed": "§aMở studio cho \"{value}\" Gói (dòng dõi: {seed})",
"iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§cCung cấp một gói chiều: /iris std importvanilla pack=<dimension>",
"iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§cKhông thể giải quyết gói cho kích thước {value}",
+2 -1
View File
@@ -260,7 +260,8 @@
"iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§c无法解析大小包 {value}",
"iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§c无Iris结构 '{structure}\"在这个包里\"",
"iris.bukkit.commandstructure.structure_assembled_0_pieces": "§c结构 '{structure}组装 0 块",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§a已放置 '{structure}' ({value}在您的位置。",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§a已放置 '{structure}' ({value}在您的位置。 ({value2} block changes)",
"iris.bukkit.commandstructure.placement_changed_no_blocks": "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height.",
"iris.bukkit.commandstudio.opening_studio_pack_seed": "§a工作室 \"{value}\" 包(种子: {seed})",
"iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§c提供一个维度包: /iris std importvanilla pack=<dimension>",
"iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§c无法解析大小包 {value}",
+2 -1
View File
@@ -260,7 +260,8 @@
"iris.bukkit.commandstructure.could_not_resolve_pack_dimension_5": "§c無法解析大小包 {value}",
"iris.bukkit.commandstructure.no_iris_structure_this_pack_2": "§c無Iris結構 '{structure}\"在這個包裡\"",
"iris.bukkit.commandstructure.structure_assembled_0_pieces": "§c結構 '{structure}組裝 0 塊",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§a已放置 '{structure}' ({value}在您的位置。",
"iris.bukkit.commandstructure.placed_pieces_at_your_location": "§a已放置 '{structure}' ({value}在您的位置。 ({value2} block changes)",
"iris.bukkit.commandstructure.placement_changed_no_blocks": "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height.",
"iris.bukkit.commandstudio.opening_studio_pack_seed": "§a工作室 \"{value}\" 包(種子: {seed})",
"iris.bukkit.commandstudio.provide_dimension_pack_iris_std_importvanilla_pack_dimension": "§c提供一個維度包: /iris std importvanilla pack=<dimension>",
"iris.bukkit.commandstudio.could_not_resolve_pack_dimension": "§c無法解析大小包 {value}",