Split jigsaw command tree

- Move each jigsaw subgroup executor into its own top-level command class
- Move the adoption plan store, handlers and helpers into CommandJigsawAdopt
- Move the vanilla export lease and reporting helpers into CommandJigsawExports
- Move the studio open lifecycle into CommandJigsawOpen
- Point the jigsaw contract test at the relocated command types
- Exclude the new jigsaw command sources from the generation revision scope
This commit is contained in:
Brian Neumann-Fopiano
2026-09-08 06:20:54 -04:00
parent 85e12caf9b
commit a4e22006e0
13 changed files with 1634 additions and 1473 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,424 @@
package art.arcane.iris.core.commands;
import art.arcane.iris.Iris;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.structure.authoring.StructureKey;
import art.arcane.iris.core.structure.authoring.StructureOwnershipManifest;
import art.arcane.iris.core.structure.authoring.StructureTransactionWriter;
import art.arcane.iris.core.structure.conversion.IrisStructureAdoptionDiagnostic;
import art.arcane.iris.core.structure.conversion.IrisStructureAdoptionInputKind;
import art.arcane.iris.core.structure.conversion.IrisStructureAdoptionPlan;
import art.arcane.iris.core.structure.conversion.IrisStructureAdoptionRequest;
import art.arcane.iris.core.structure.conversion.IrisStructureAdoptionResult;
import art.arcane.iris.core.structure.conversion.IrisStructureAdoptionService;
import art.arcane.iris.core.structure.conversion.IrisStructureAdoptionStrategy;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.iris.util.common.format.C;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.director.DirectorOrigin;
import art.arcane.volmlib.util.director.DirectorParameterHandler;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import art.arcane.volmlib.util.director.exceptions.DirectorParsingException;
import org.bukkit.entity.Player;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
@Director(name = "adopt", description = "Inspect and apply safe ownership adoption plans",
origin = DirectorOrigin.PLAYER)
public class CommandJigsawAdopt implements DirectorExecutor {
private static final Map<UUID, AdoptionPlanBinding> ADOPTION_PLANS = new ConcurrentHashMap<>();
private static final Map<Path, IrisStructureAdoptionService> ADOPTION_SERVICES = new ConcurrentHashMap<>();
@Director(description = "Inspect an Iris graph for safe Studio ownership adoption", sync = true,
origin = DirectorOrigin.PLAYER)
public void inspect(
@Param(description = "Pack dimension") IrisDimension dimension,
@Param(description = "Existing Iris structure key") String source,
@Param(description = "New owned Iris key, or auto", defaultValue = "auto") String target,
@Param(description = "auto, in-place, or clone", defaultValue = "auto",
customHandler = JigsawAdoptionStrategyHandler.class) String strategy
) {
VolmitSender commandSender = sender();
IrisData data = CommandJigsaw.requireData(dimension, commandSender);
if (data == null) {
return;
}
String sourceKey;
Optional<StructureKey> targetKey;
IrisStructureAdoptionStrategy adoptionStrategy;
Path root;
try {
sourceKey = parseAdoptionSource(source);
targetKey = parseAdoptionTarget(target);
adoptionStrategy = parseAdoptionStrategy(strategy);
root = canonicalAdoptionRoot(data.getDataFolder().toPath());
} catch (IOException | IllegalArgumentException exception) {
CommandJigsaw.sendError(commandSender, "Could not inspect adoption source: " + exception.getMessage());
return;
}
Player targetPlayer = player();
UUID ownerId = targetPlayer.getUniqueId();
commandSender.sendMessage(C.GRAY + "Inspecting Jigsaw adoption source '" + sourceKey
+ "' asynchronously...");
try {
J.a(() -> runAdoptionInspect(
targetPlayer,
commandSender,
ownerId,
dimension,
data,
root,
sourceKey,
targetKey,
adoptionStrategy));
} catch (RuntimeException exception) {
Iris.reportError("Failed to schedule Jigsaw adoption inspection for '"
+ sourceKey + "'.", exception);
CommandJigsaw.sendError(commandSender, "Jigsaw adoption inspection could not be scheduled: "
+ exception.getMessage());
}
}
@Director(description = "Apply one inspected adoption plan without overwriting", sync = true,
origin = DirectorOrigin.PLAYER)
public void apply(
@Param(description = "Plan UUID returned by adopt inspect",
customHandler = JigsawAdoptionPlanHandler.class) String planId
) {
VolmitSender commandSender = sender();
UUID parsedPlanId;
try {
parsedPlanId = UUID.fromString(planId);
} catch (IllegalArgumentException exception) {
CommandJigsaw.sendError(commandSender, "Adoption plan ID must be a UUID.");
return;
}
if (CommandJigsaw.studioIsActiveOrOpening()) {
CommandJigsaw.sendError(commandSender,
"Close the active or opening Jigsaw Studio before applying an adoption plan.");
return;
}
pruneAdoptionPlanBindings();
AdoptionPlanBinding binding = ADOPTION_PLANS.get(parsedPlanId);
UUID playerId = player().getUniqueId();
if (binding == null || !binding.ownerId().equals(playerId)) {
CommandJigsaw.sendError(commandSender, "No active adoption plan '" + parsedPlanId
+ "' belongs to your player session. Run adopt inspect again.");
return;
}
if (binding.service().plan(parsedPlanId).isEmpty()) {
ADOPTION_PLANS.remove(parsedPlanId, binding);
CommandJigsaw.sendError(commandSender, "Adoption plan '" + parsedPlanId
+ "' expired or was already consumed. Run adopt inspect again.");
return;
}
if (!ADOPTION_PLANS.remove(parsedPlanId, binding)) {
CommandJigsaw.sendError(commandSender, "Adoption plan '" + parsedPlanId + "' is already being applied.");
return;
}
Player targetPlayer = player();
commandSender.sendMessage(C.GRAY + "Applying adoption plan '" + parsedPlanId
+ "' asynchronously without overwrite...");
try {
J.a(() -> runAdoptionApply(targetPlayer, commandSender, parsedPlanId, binding));
} catch (RuntimeException exception) {
ADOPTION_PLANS.putIfAbsent(parsedPlanId, binding);
Iris.reportError("Failed to schedule Jigsaw adoption plan '" + parsedPlanId + "'.", exception);
CommandJigsaw.sendError(commandSender, "Jigsaw adoption could not be scheduled: " + exception.getMessage());
}
}
private static void pruneAdoptionPlanBindings() {
for (Map.Entry<UUID, AdoptionPlanBinding> entry : ADOPTION_PLANS.entrySet()) {
AdoptionPlanBinding binding = entry.getValue();
if (binding.service().plan(entry.getKey()).isEmpty()) {
ADOPTION_PLANS.remove(entry.getKey(), binding);
}
}
}
private static void runAdoptionApply(
Player targetPlayer,
VolmitSender commandSender,
UUID planId,
AdoptionPlanBinding binding
) {
if (CommandJigsaw.studioIsActiveOrOpening()) {
ADOPTION_PLANS.putIfAbsent(planId, binding);
J.runEntity(targetPlayer, () -> CommandJigsaw.sendError(commandSender,
"Close the active or opening Jigsaw Studio before applying an adoption plan."));
return;
}
IrisStructureAdoptionResult result;
Throwable failure = null;
try {
result = binding.service().apply(binding.plan());
} catch (Throwable throwable) {
result = null;
failure = throwable;
Iris.reportError("Failed to apply Jigsaw adoption plan '" + planId + "'.", throwable);
}
IrisStructureAdoptionResult completedResult = result;
Throwable completedFailure = failure;
J.runEntity(targetPlayer, () -> finishAdoptionApply(
targetPlayer,
commandSender,
binding,
completedResult,
completedFailure));
}
private static void runAdoptionInspect(
Player targetPlayer,
VolmitSender commandSender,
UUID ownerId,
IrisDimension dimension,
IrisData data,
Path root,
String sourceKey,
Optional<StructureKey> targetKey,
IrisStructureAdoptionStrategy strategy
) {
IrisStructureAdoptionService service = null;
IrisStructureAdoptionPlan plan = null;
Throwable failure = null;
try {
IrisStructureAdoptionInputKind inputKind = adoptionInputKind(root, sourceKey);
service = ADOPTION_SERVICES.computeIfAbsent(root, IrisStructureAdoptionService::new);
plan = service.inspect(new IrisStructureAdoptionRequest(
sourceKey,
targetKey,
strategy,
inputKind));
} catch (Throwable throwable) {
failure = throwable;
Iris.reportError("Failed to inspect Jigsaw adoption source '" + sourceKey + "'.", throwable);
}
IrisStructureAdoptionService completedService = service;
IrisStructureAdoptionPlan completedPlan = plan;
Throwable completedFailure = failure;
J.runEntity(targetPlayer, () -> finishAdoptionInspect(
commandSender,
ownerId,
dimension,
data,
completedService,
completedPlan,
completedFailure));
}
private static void finishAdoptionInspect(
VolmitSender commandSender,
UUID ownerId,
IrisDimension dimension,
IrisData data,
IrisStructureAdoptionService service,
IrisStructureAdoptionPlan plan,
Throwable failure
) {
if (failure != null || service == null || plan == null) {
CommandJigsaw.sendError(commandSender, "Jigsaw adoption inspection failed: "
+ (failure == null ? "no result" : failure.getMessage()));
return;
}
pruneAdoptionPlanBindings();
if (service.plan(plan.planId()).isPresent()) {
ADOPTION_PLANS.put(plan.planId(), new AdoptionPlanBinding(
ownerId, dimension, data, service, plan));
}
reportAdoptionPlan(commandSender, plan);
}
private static void finishAdoptionApply(
Player targetPlayer,
VolmitSender commandSender,
AdoptionPlanBinding binding,
IrisStructureAdoptionResult result,
Throwable failure
) {
if (failure != null || result == null) {
CommandJigsaw.sendError(commandSender, "Jigsaw adoption failed: "
+ (failure == null ? "no result" : failure.getMessage()));
return;
}
reportAdoptionResult(commandSender, result);
if (!result.successful()) {
return;
}
StructureKey target = result.receipt().orElseThrow().targetStructure();
binding.data().invalidateStructureResources();
CommandJigsawOpen.openProject(targetPlayer, commandSender, binding.dimension(), target.path(), CommandJigsaw.DEFAULT_STUDIO_SEED);
}
private static void reportAdoptionPlan(VolmitSender commandSender, IrisStructureAdoptionPlan plan) {
C statusColor = plan.canApply() ? C.GREEN : C.RED;
commandSender.sendMessage(statusColor + "Adoption plan " + plan.planId() + " -> "
+ plan.disposition() + " for " + plan.targetStructure().value() + ".");
commandSender.sendMessage(C.GRAY + "Resources=" + plan.resourceCount()
+ ", bytes=" + plan.totalSourceBytes()
+ ", errors=" + plan.errorCount()
+ ", warnings=" + plan.warningCount() + ".");
for (IrisStructureAdoptionDiagnostic diagnostic : plan.diagnostics()) {
commandSender.sendMessage(adoptionColor(diagnostic).toString() + diagnostic.summary());
}
if (plan.canApply()) {
commandSender.sendMessage(C.AQUA + "Apply with /iris jigsaw adopt apply " + plan.planId());
}
}
private static void reportAdoptionResult(VolmitSender commandSender, IrisStructureAdoptionResult result) {
commandSender.sendMessage((result.successful() ? C.GREEN : C.RED)
+ "Adoption plan " + result.planId() + ": " + result.status() + ".");
for (IrisStructureAdoptionDiagnostic diagnostic : result.diagnostics()) {
commandSender.sendMessage(adoptionColor(diagnostic).toString() + diagnostic.summary());
}
}
private static C adoptionColor(IrisStructureAdoptionDiagnostic diagnostic) {
return switch (diagnostic.severity()) {
case ERROR -> C.RED;
case WARNING -> C.YELLOW;
case INFO -> C.GRAY;
};
}
static String parseAdoptionSource(String value) {
String normalized = Objects.requireNonNull(value, "Adoption source").trim()
.toLowerCase(Locale.ROOT);
StructureKey source = StructureKey.parse(normalized, "iris");
if (!"iris".equals(source.namespace())) {
throw new IllegalArgumentException("Adoption sources must use the iris namespace.");
}
return source.path();
}
static Optional<StructureKey> parseAdoptionTarget(String value) {
String normalized = Objects.requireNonNull(value, "Adoption target").trim()
.toLowerCase(Locale.ROOT);
if (normalized.isEmpty() || "auto".equals(normalized)) {
return Optional.empty();
}
StructureKey target = StructureKey.parse(normalized, "iris");
if (!"iris".equals(target.namespace())) {
throw new IllegalArgumentException("Adoption targets must use the iris namespace.");
}
return Optional.of(target);
}
private static IrisStructureAdoptionStrategy parseAdoptionStrategy(String value) {
return switch (Objects.requireNonNull(value, "Adoption strategy").trim().toLowerCase(Locale.ROOT)) {
case "auto" -> IrisStructureAdoptionStrategy.AUTO;
case "in-place", "in_place", "inplace", "claim" -> IrisStructureAdoptionStrategy.IN_PLACE;
case "clone", "copy" -> IrisStructureAdoptionStrategy.CLONE;
default -> throw new IllegalArgumentException("Adoption strategy must be auto, in-place, or clone.");
};
}
static IrisStructureAdoptionInputKind adoptionInputKind(Path packRoot, String source) throws IOException {
Path root = canonicalAdoptionRoot(packRoot);
StructureKey sourceKey = new StructureKey("iris", parseAdoptionSource(source));
Path manifestPath = new StructureTransactionWriter(root).ownershipManifestPath(sourceKey);
if (!Files.exists(manifestPath, LinkOption.NOFOLLOW_LINKS)) {
return IrisStructureAdoptionInputKind.UNOWNED_IRIS;
}
if (!Files.isRegularFile(manifestPath, LinkOption.NOFOLLOW_LINKS)) {
throw new IOException("Ownership manifest is not a regular file: " + manifestPath);
}
StructureOwnershipManifest manifest;
try {
manifest = StructureOwnershipManifest.fromJson(Files.readAllBytes(manifestPath));
} catch (RuntimeException exception) {
throw new IOException("Invalid ownership manifest at " + manifestPath, exception);
}
if (!manifest.structure().equals(sourceKey)) {
throw new IOException("Ownership manifest at " + manifestPath
+ " belongs to " + manifest.structure().value());
}
return manifest.provenance().origin() == StructureOwnershipManifest.Origin.MANAGED_DATAPACK
? IrisStructureAdoptionInputKind.MANAGED_DATAPACK
: IrisStructureAdoptionInputKind.UNOWNED_IRIS;
}
private static Path canonicalAdoptionRoot(Path packRoot) throws IOException {
Path normalized = Objects.requireNonNull(packRoot, "Adoption pack root").toAbsolutePath().normalize();
return Files.exists(normalized, LinkOption.NOFOLLOW_LINKS) ? normalized.toRealPath() : normalized;
}
private record AdoptionPlanBinding(
UUID ownerId,
IrisDimension dimension,
IrisData data,
IrisStructureAdoptionService service,
IrisStructureAdoptionPlan plan
) {
}
public static final class JigsawAdoptionStrategyHandler implements DirectorParameterHandler<String> {
@Override
public KList<String> getPossibilities() {
return new KList<>("auto", "in-place", "clone");
}
@Override
public String toString(String value) {
return value == null ? "" : value;
}
@Override
public String parse(String input, boolean force) throws DirectorParsingException {
try {
return switch (parseAdoptionStrategy(input)) {
case AUTO -> "auto";
case IN_PLACE -> "in-place";
case CLONE -> "clone";
};
} catch (IllegalArgumentException exception) {
throw new DirectorParsingException(exception.getMessage());
}
}
@Override
public boolean supports(Class<?> type) {
return type == String.class;
}
}
public static final class JigsawAdoptionPlanHandler implements DirectorParameterHandler<String> {
@Override
public KList<String> getPossibilities() {
return new KList<>();
}
@Override
public String toString(String value) {
return value == null ? "" : value;
}
@Override
public String parse(String input, boolean force) throws DirectorParsingException {
try {
return UUID.fromString(input).toString();
} catch (IllegalArgumentException exception) {
throw new DirectorParsingException("Adoption plan ID must be a UUID.");
}
}
@Override
public boolean supports(Class<?> type) {
return type == String.class;
}
}
}
@@ -0,0 +1,84 @@
package art.arcane.iris.core.commands;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioBay;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioCellDimensions;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioGraphEditor;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioVariant;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.engine.object.IrisPosition;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.volmlib.util.director.DirectorOrigin;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import org.bukkit.block.Block;
import org.bukkit.block.data.type.Jigsaw;
@Director(name = "connector", description = "Inspect and configure targeted jigsaw connectors",
origin = DirectorOrigin.PLAYER)
public class CommandJigsawConnector implements DirectorExecutor {
@Director(description = "Set the Iris-only channel on the targeted jigsaw marker", sync = true,
origin = DirectorOrigin.PLAYER)
public void channel(
@Param(description = "Exact channel, or none to clear") String channel
) {
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
if (context == null) {
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
return;
}
Block target = player().getTargetBlockExact(8);
if (target == null || !(target.getBlockData() instanceof Jigsaw)) {
CommandJigsaw.sendError(this, "Look directly at a jigsaw marker within eight blocks.");
return;
}
JigsawStudioBay bay = context.session().layout().findAt(
target.getX(), target.getY(), target.getZ());
JigsawStudioVariant activeVariant = bay == null
? null
: context.session().activeVariant(bay.stableId()).orElse(null);
if (activeVariant == null) {
CommandJigsaw.sendError(this, "The targeted marker must be inside a workcell with an active variant.");
return;
}
if (!activeVariant.owned()) {
CommandJigsaw.sendError(this, "The active variant is read-only. Adopt it into this project before editing it.");
return;
}
IrisObject object = context.request().source().getObjectLoader().load(activeVariant.objectKey());
if (object == null) {
CommandJigsaw.sendError(this, "The active variant object '" + activeVariant.objectKey() + "' is missing.");
return;
}
IrisPosition canonicalPosition = new IrisPosition(
target.getX() - bay.bounds().originX(),
target.getY() - bay.bounds().originY(),
target.getZ() - bay.bounds().originZ());
IrisPosition position;
try {
position = activeVariant.canonicalToSourcePosition(
canonicalPosition,
new JigsawStudioCellDimensions(object.getW(), object.getH(), object.getD()));
} catch (IllegalArgumentException exception) {
CommandJigsaw.sendError(this, "Connector channel update failed: " + exception.getMessage());
return;
}
String normalized = channel == null || channel.isBlank() || "none".equalsIgnoreCase(channel)
? "none"
: channel.trim();
CommandJigsaw.runGraphMutation(player(), context, () -> {
JigsawStudioGraphEditor.updateConnectorChannel(
context.request().source().getDataFolder().toPath(),
context.request().structureKey(),
activeVariant.pieceKey(),
position,
channel);
return CommandJigsaw.mappedMutationResult(
context,
"",
"",
"Persisted connector channel '" + normalized + "' on piece '"
+ activeVariant.pieceKey() + "' at " + position.getX() + "," + position.getY()
+ "," + position.getZ() + ".");
});
}
}
@@ -0,0 +1,159 @@
package art.arcane.iris.core.commands;
import art.arcane.iris.Iris;
import art.arcane.iris.core.service.JigsawStudioService;
import art.arcane.iris.core.structure.export.VanillaJigsawDatapackExporter;
import art.arcane.iris.core.structure.export.VanillaJigsawExportDiagnostic;
import art.arcane.iris.core.structure.export.VanillaJigsawExportFormat;
import art.arcane.iris.core.structure.export.VanillaJigsawExportRequest;
import art.arcane.iris.core.structure.export.VanillaJigsawExportResult;
import art.arcane.iris.util.common.format.C;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import org.bukkit.entity.Player;
import java.nio.file.Path;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Pattern;
final class CommandJigsawExports {
private static final Pattern EXPORT_OUTPUT_NAME = Pattern.compile("[a-zA-Z0-9_-][a-zA-Z0-9._-]{0,127}");
private static final Set<UUID> EXPORTING_PLAYERS = ConcurrentHashMap.newKeySet();
private static final Set<Path> EXPORT_DESTINATIONS = ConcurrentHashMap.newKeySet();
private CommandJigsawExports() {
}
static String exportStartError(JigsawStudioService.ExportStart exportStart) {
Objects.requireNonNull(exportStart, "Jigsaw Studio export start status");
return switch (exportStart) {
case NOT_ACTIVE -> "The active Jigsaw Studio is no longer available.";
case NOT_OWNER -> "Only the Jigsaw Studio owner can export this project.";
case DIRTY -> "Wait for the pending autosave or discard the edits before exporting the on-disk graph.";
case CLOSING -> "The active Jigsaw Studio is closing and cannot be exported.";
case SAVE_IN_PROGRESS -> "Wait for the current Jigsaw Studio save to finish before exporting.";
case OPERATION_IN_PROGRESS -> "Wait for the current Jigsaw Studio operation to finish before exporting.";
case IN_PROGRESS -> "A Jigsaw Studio export is already in progress.";
case STARTED -> throw new IllegalArgumentException("STARTED is not an export failure");
};
}
static boolean beginExport(UUID playerId, Path destination) {
Objects.requireNonNull(playerId);
Path normalizedDestination = Objects.requireNonNull(destination).toAbsolutePath().normalize();
if (!EXPORTING_PLAYERS.add(playerId)) {
return false;
}
if (EXPORT_DESTINATIONS.add(normalizedDestination)) {
return true;
}
EXPORTING_PLAYERS.remove(playerId);
return false;
}
static void finishExport(UUID playerId, Path destination) {
EXPORTING_PLAYERS.remove(Objects.requireNonNull(playerId));
EXPORT_DESTINATIONS.remove(Objects.requireNonNull(destination).toAbsolutePath().normalize());
}
static void runExport(ExportOperation operation) {
VanillaJigsawExportResult result = null;
Throwable failure = null;
try {
result = new VanillaJigsawDatapackExporter().export(operation.request());
} catch (Throwable throwable) {
failure = throwable;
Iris.reportError("Vanilla Jigsaw export failed for '" + operation.structureKey() + "'.", throwable);
} finally {
operation.lease().release();
}
VanillaJigsawExportResult completedResult = result;
Throwable completedFailure = failure;
Runnable response = () -> reportExport(operation, completedResult, completedFailure);
J.runEntity(operation.player(), response);
}
private static void reportExport(
ExportOperation operation,
VanillaJigsawExportResult result,
Throwable failure
) {
if (failure != null || result == null) {
operation.sender().sendMessage(C.RED + "Background vanilla export for '"
+ operation.structureKey() + "' failed: "
+ (failure == null ? "no result" : failure.getMessage()));
return;
}
for (VanillaJigsawExportDiagnostic diagnostic : result.diagnostics()) {
C color = diagnostic.isBlocking() ? C.RED : C.YELLOW;
operation.sender().sendMessage(color.toString() + diagnostic.code() + ": " + diagnostic.message());
}
CommandJigsaw.ActiveContext current = CommandJigsaw.active(operation.player());
boolean sameSession = current != null
&& current.request().requestId().equals(operation.requestId());
String contextLabel = sameSession ? "Vanilla export" : "Background vanilla export for '"
+ operation.structureKey() + "'";
operation.sender().sendMessage((result.isSuccess() ? C.GREEN : C.RED) + contextLabel + " "
+ result.status() + ": " + result.output());
}
static VanillaJigsawExportFormat parseFormat(String value) {
return switch (value.trim().toLowerCase(Locale.ROOT)) {
case "zip" -> VanillaJigsawExportFormat.ZIP;
case "directory", "dir", "folder" -> VanillaJigsawExportFormat.DIRECTORY;
default -> throw new IllegalArgumentException("Export format must be directory or zip.");
};
}
static Path resolveExportDestination(
Path exportRoot,
String output,
VanillaJigsawExportFormat format
) {
Path root = Objects.requireNonNull(exportRoot, "Jigsaw export root").toAbsolutePath().normalize();
String requested = Objects.requireNonNull(output, "Jigsaw export output name");
String name = requested.trim();
if (!name.equals(requested) || !EXPORT_OUTPUT_NAME.matcher(name).matches()) {
throw new IllegalArgumentException(
"Export output must be one file name using letters, numbers, '.', '_', or '-'.");
}
String fileName = format == VanillaJigsawExportFormat.ZIP && !name.endsWith(".zip")
? name + ".zip" : name;
Path destination = root.resolve(fileName).normalize();
if (destination.equals(root) || !root.equals(destination.getParent())) {
throw new IllegalArgumentException("Export output must be one artifact inside the Studio exports folder.");
}
return destination;
}
record ExportOperation(
Player player,
VolmitSender sender,
UUID requestId,
String structureKey,
VanillaJigsawExportRequest request,
ExportLease lease
) {
}
static final class ExportLease {
private final Runnable release;
private final AtomicBoolean released;
ExportLease(Runnable release) {
this.release = Objects.requireNonNull(release, "Jigsaw export lease release action");
this.released = new AtomicBoolean();
}
void release() {
if (released.compareAndSet(false, true)) {
release.run();
}
}
}
}
@@ -0,0 +1,251 @@
package art.arcane.iris.core.commands;
import art.arcane.iris.Iris;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.pack.BrokenPackException;
import art.arcane.iris.core.runtime.WorldRuntimeControlService;
import art.arcane.iris.core.runtime.StudioOpenCoordinator;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioActivation;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioBay;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioGraphMapper;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioLayout;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioSession;
import art.arcane.iris.core.service.JigsawStudioService;
import art.arcane.iris.core.service.StudioSVC;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisStructure;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.util.common.format.C;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Player;
import java.util.UUID;
import java.util.concurrent.CompletionException;
final class CommandJigsawOpen {
private CommandJigsawOpen() {
}
static void openProject(
Player targetPlayer,
VolmitSender commandSender,
IrisDimension dimension,
String structure,
long seed
) {
UUID playerId = targetPlayer.getUniqueId();
UUID ownerId = JigsawStudioActivation.activeOwnerId();
if (ownerId != null && !ownerId.equals(playerId)) {
CommandJigsaw.sendError(commandSender, "Jigsaw Studio is already owned by another player session.");
return;
}
UUID openingOwner = JigsawStudioActivation.openingOwnerId();
if (openingOwner != null) {
CommandJigsaw.sendError(commandSender, openingOwner.equals(playerId)
? "Your Jigsaw Studio is already opening."
: "Jigsaw Studio is already opening for another player.");
return;
}
CommandJigsaw.ActiveContext previous = CommandJigsaw.active(targetPlayer);
if (ownerId != null && previous == null) {
CommandJigsaw.sendError(commandSender, "Your active Jigsaw Studio binding is unavailable. Close it before opening another project.");
return;
}
CommandJigsaw.ActiveContext dirty = CommandJigsaw.findDirtyContext();
if (dirty != null) {
CommandJigsaw.sendError(commandSender,
"The current Jigsaw Studio is waiting for autosave. Wait for it to finish or close with discard=true.");
return;
}
IrisData data = CommandJigsaw.requireData(dimension, commandSender);
if (data == null) {
return;
}
IrisStructure irisStructure = data.load(IrisStructure.class, structure, false);
if (irisStructure == null) {
CommandJigsaw.sendError(commandSender, "No Iris jigsaw structure '" + structure + "' exists in this pack.");
return;
}
JigsawStudioLayout layout;
try {
layout = JigsawStudioGraphMapper.map(data, irisStructure);
} catch (RuntimeException exception) {
Iris.reportError("Failed to map Jigsaw Studio graph '" + structure + "'.", exception);
CommandJigsaw.sendError(commandSender, "Could not map this jigsaw graph: " + exception.getMessage());
return;
}
String packKey = dimension.getLoadKey();
if (!JigsawStudioActivation.tryBeginOpen(playerId)) {
CommandJigsaw.sendError(commandSender,
"Jigsaw Studio began opening for another player. Try again after it finishes.");
return;
}
JigsawStudioService studioService = JigsawStudioService.get();
UUID closingRequestId = previous == null ? null : previous.request().requestId();
if (closingRequestId != null) {
JigsawStudioService.CloseStart closeStart = studioService.tryBeginClose(
closingRequestId,
playerId,
false);
if (closeStart != JigsawStudioService.CloseStart.STARTED) {
JigsawStudioActivation.finishOpen(playerId);
CommandJigsaw.sendError(commandSender, closeStart == JigsawStudioService.CloseStart.SAVE_IN_PROGRESS
? "The current Jigsaw Studio is saving. Wait for it to finish before opening another project."
: closeStart == JigsawStudioService.CloseStart.OPERATION_IN_PROGRESS
? "The current Jigsaw Studio is loading a variant. Wait for it to finish before opening another project."
: closeStart == JigsawStudioService.CloseStart.DIRTY
? "The current Jigsaw Studio is waiting for autosave. Wait for it to finish before opening another project."
: "The current Jigsaw Studio could not be reserved for replacement.");
return;
}
}
JigsawStudioActivation.StagedActivation staged;
try {
staged = JigsawStudioActivation.stage(
packKey,
structure,
layout.mode(),
CommandJigsaw.compatibilityOf(irisStructure),
layout.cellDimensions(),
data,
layout,
playerId,
closingRequestId);
} catch (RuntimeException exception) {
JigsawStudioActivation.finishOpen(playerId);
studioService.cancelClose(closingRequestId);
Iris.reportError("Failed to stage Jigsaw Studio for '" + structure + "'.", exception);
CommandJigsaw.sendError(commandSender, "Jigsaw Studio staging failed: " + exception.getMessage());
return;
}
commandSender.sendMessage(C.GREEN + "Opening transient Jigsaw Studio for '" + structure + "' with "
+ layout.bays().size() + " workcells.");
try {
Iris.service(StudioSVC.class).openTracked(
commandSender,
seed,
packKey,
CommandJigsaw.STUDIO_OPEN_KIND,
() -> beginStagedOpen(staged),
world -> finishOpen(targetPlayer, world, staged, studioService))
.whenComplete((result, throwable) -> J.s(() -> finishOpenAttempt(
commandSender,
playerId,
structure,
closingRequestId,
studioService,
staged,
result,
throwable)));
} catch (Throwable exception) {
JigsawStudioActivation.finishOpen(playerId);
studioService.cancelClose(closingRequestId);
JigsawStudioActivation.rollback(staged);
Iris.reportError("Failed to open Jigsaw Studio for '" + structure + "'.", exception);
CommandJigsaw.sendError(commandSender, "Jigsaw Studio open failed: " + exception.getMessage());
}
}
private static void beginStagedOpen(JigsawStudioActivation.StagedActivation staged) {
if (!JigsawStudioActivation.beginStagedGeneration(staged)) {
throw new IllegalStateException("The staged Jigsaw Studio activation is no longer current");
}
}
private static void finishOpenAttempt(
VolmitSender commandSender,
UUID playerId,
String structureKey,
UUID closingRequestId,
JigsawStudioService studioService,
JigsawStudioActivation.StagedActivation staged,
StudioOpenCoordinator.StudioOpenResult result,
Throwable throwable
) {
JigsawStudioActivation.finishOpen(playerId);
if (throwable == null && result != null) {
studioService.cancelClose(closingRequestId);
return;
}
studioService.cancelClose(closingRequestId);
JigsawStudioActivation.rollback(staged);
Throwable failure = unwrapCompletionFailure(throwable == null
? new IllegalStateException("Studio open completed without a result")
: throwable);
if (isExpectedOpenDenial(failure)) {
Iris.warn("Jigsaw Studio open for '%s' was denied: %s",
structureKey, failure.getMessage());
return;
}
Iris.reportError("Failed to open Jigsaw Studio for '" + structureKey + "'.", failure);
CommandJigsaw.sendError(commandSender, "Jigsaw Studio open failed: " + failure.getMessage());
}
private static void finishOpen(
Player targetPlayer,
World world,
JigsawStudioActivation.StagedActivation staged,
JigsawStudioService studioService
) {
if (world == null) {
throw new IllegalStateException("Jigsaw Studio opened without a world");
}
JigsawStudioActivation.Request request = staged.request();
JigsawStudioSession session = staged.session();
try {
WorldRuntimeControlService.get().applyObjectStudioWorldRules(world);
} catch (Throwable exception) {
Iris.reportError("Failed to apply Jigsaw Studio world rules for '" + world.getName() + "'.", exception);
}
JigsawStudioBay destination = initialBay(session.layout());
Location target = destination == null
? new Location(world, 0.5D, 66D, 0.5D)
: new Location(
world,
destination.bounds().originX() + destination.bounds().dimensions().width() / 2.0D,
destination.bounds().maxY() + 2.0D,
destination.bounds().originZ() + destination.bounds().dimensions().depth() / 2.0D);
if (destination != null) {
session.selectBay(destination.stableId());
}
if (!JigsawStudioActivation.commit(staged)) {
throw new IllegalStateException("The staged Jigsaw Studio activation could not be committed");
}
studioService.activationCommitted(world, request.requestId());
CommandJigsaw.PLAYER_PACKS.put(targetPlayer.getUniqueId(), new CommandJigsaw.SessionBinding(
request.packKey(), request.requestId()));
try {
J.runEntity(targetPlayer, () -> BukkitPlatform.teleportAsync(targetPlayer, target)
.thenRun(() -> J.runEntity(targetPlayer, () -> targetPlayer.setGameMode(GameMode.CREATIVE))));
} catch (RuntimeException exception) {
Iris.reportError("Failed to move the player to the committed Jigsaw Studio workcell.", exception);
}
}
private static JigsawStudioBay initialBay(JigsawStudioLayout layout) {
for (JigsawStudioBay bay : layout.bays()) {
if (layout.defaultVariant(bay).isPresent()) {
return bay;
}
}
return layout.bays().isEmpty() ? null : layout.bays().getFirst();
}
private static Throwable unwrapCompletionFailure(Throwable failure) {
Throwable current = failure;
while (current instanceof CompletionException && current.getCause() != null) {
current = current.getCause();
}
return current;
}
static boolean isExpectedOpenDenial(Throwable failure) {
return unwrapCompletionFailure(failure) instanceof BrokenPackException;
}
}
@@ -0,0 +1,238 @@
package art.arcane.iris.core.commands;
import art.arcane.iris.core.runtime.jigsaw.JigsawPlanarTopology;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioBay;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioCellDimensions;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioCompatibilityTarget;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioGraphEditor;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioLayout;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioMode;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioPoolEditor;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioVariant;
import art.arcane.iris.core.service.JigsawStudioService;
import art.arcane.iris.engine.object.IrisJigsawPiece;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.engine.object.IrisPosition;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.volmlib.util.director.DirectorOrigin;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import org.bukkit.Location;
import java.io.IOException;
@Director(name = "piece", description = "Manage contextual Studio pieces", origin = DirectorOrigin.PLAYER)
public class CommandJigsawPiece implements DirectorExecutor {
@Director(description = "Create an owned piece from the selected Studio topology", sync = true,
origin = DirectorOrigin.PLAYER)
public void create(
@Param(description = "Owned pool resource key") String poolKey,
@Param(description = "New owned piece and object key") String pieceKey,
@Param(description = "Positive selection weight", defaultValue = "1") int weight
) {
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
if (context == null) {
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
return;
}
JigsawPlanarTopology topology = null;
JigsawStudioBay targetWorkcell = context.session().layout().get(JigsawStudioLayout.SPATIAL_WORKCELL_ID);
if (context.session().layout().mode() == JigsawStudioMode.PLANAR_JIGSAW) {
Location location = player().getLocation();
JigsawStudioBay contextual = context.session().layout().findAt(
location.getBlockX(), location.getBlockY(), location.getBlockZ());
if (contextual == null) {
contextual = CommandJigsaw.selectedBay(context);
} else {
context.session().selectBay(contextual.stableId());
}
if (contextual == null || contextual.topology().isEmpty()) {
CommandJigsaw.sendError(this, "Stand in or select a planar topology workcell before creating a variant.");
return;
}
topology = contextual.topology().get();
targetWorkcell = contextual;
}
if (targetWorkcell == null) {
CommandJigsaw.sendError(this, "The selected Jigsaw Studio workcell is no longer available.");
return;
}
JigsawPlanarTopology selectedTopology = topology;
String activationWorkcellId = targetWorkcell.stableId();
JigsawStudioCellDimensions dimensions = targetWorkcell.capacity();
String topologyText = selectedTopology == null ? "blank spatial" : selectedTopology.name();
CommandJigsaw.runGraphMutation(player(), context, () -> {
JigsawStudioGraphEditor.createPiece(
context.request().source().getDataFolder().toPath(),
context.request().structureKey(),
poolKey,
pieceKey,
weight,
dimensions,
selectedTopology);
return CommandJigsaw.mappedMutationResult(
context,
activationWorkcellId,
pieceKey,
"Created owned " + topologyText + " piece '" + pieceKey
+ "' and added it to pool '" + poolKey + "' atomically.");
});
}
@Director(description = "Add an existing jigsaw piece to an owned pool transactionally", sync = true,
origin = DirectorOrigin.PLAYER)
public void add(
@Param(description = "Owned pool resource key") String poolKey,
@Param(description = "Existing jigsaw piece key") String pieceKey,
@Param(description = "Positive selection weight", defaultValue = "1") int weight
) {
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
if (context == null) {
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
return;
}
IrisJigsawPiece piece = context.request().source().getJigsawPieceLoader().load(pieceKey);
if (piece == null) {
CommandJigsaw.sendError(this, "No jigsaw piece '" + pieceKey + "' exists in this pack.");
return;
}
if (piece.getObject() == null || piece.getObject().isBlank()) {
CommandJigsaw.sendError(this, "Jigsaw piece '" + pieceKey + "' does not declare an object.");
return;
}
IrisObject object = context.request().source().getObjectLoader().load(piece.getObject());
if (object == null) {
CommandJigsaw.sendError(this, "Jigsaw piece '" + pieceKey + "' has no loadable object.");
return;
}
CommandJigsaw.PieceWorkcellResolution resolution;
try {
resolution = CommandJigsaw.resolvePieceWorkcell(context.session().layout(), piece, object);
} catch (IllegalArgumentException exception) {
CommandJigsaw.sendError(this, "Jigsaw piece '" + pieceKey + "' cannot be assigned to a Studio workcell: "
+ exception.getMessage());
return;
}
if (!resolution.fits()) {
IrisPosition required = resolution.requiredDimensions();
JigsawStudioCellDimensions capacity = resolution.workcell().capacity();
CommandJigsaw.sendError(this, "Jigsaw piece '" + pieceKey + "' requires " + required.getX() + "x"
+ required.getY() + "x" + required.getZ() + " in " + resolution.workcell().displayName()
+ ", but that workcell capacity is " + capacity.width() + "x" + capacity.height()
+ "x" + capacity.depth() + ".");
return;
}
String objectKey = piece.getObject();
CommandJigsaw.runGraphMutation(player(), context, () -> {
if (!JigsawStudioGraphEditor.ownsPiece(
context.request().source().getDataFolder().toPath(),
context.request().structureKey(),
pieceKey,
objectKey)) {
throw new IOException("Piece add accepts only piece/object resources already owned by this project; "
+ "use piece create for a new owned piece");
}
JigsawStudioPoolEditor.PoolUpdate update = JigsawStudioPoolEditor.addPiece(
context.request().source().getDataFolder().toPath(),
context.request().structureKey(),
poolKey,
pieceKey,
weight);
if (!update.changed()) {
throw new IOException("The owned pool is missing or already references '" + pieceKey + "'");
}
JigsawStudioLayout layout = CommandJigsaw.loadPersistedLayout(context);
String activationWorkcellId = CommandJigsaw.workcellIdForVariant(layout, pieceKey);
return new JigsawStudioService.CommandGraphMutationResult(
layout,
activationWorkcellId,
pieceKey,
"Added '" + pieceKey + "' to pool '" + poolKey + "' in one graph transaction.");
});
}
@Director(description = "Remove the selected piece from an owned pool transactionally", sync = true,
origin = DirectorOrigin.PLAYER)
public void remove(
@Param(description = "Owned pool resource key") String poolKey
) {
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
if (context == null) {
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
return;
}
JigsawStudioVariant selected = CommandJigsaw.selectedVariant(context);
if (selected == null) {
CommandJigsaw.sendError(this, "Select a workcell with an active variant before removing its pool entry.");
return;
}
String pieceKey = selected.pieceKey();
CommandJigsaw.runGraphMutation(player(), context, () -> {
JigsawStudioPoolEditor.PoolUpdate update = JigsawStudioPoolEditor.removePiece(
context.request().source().getDataFolder().toPath(),
context.request().structureKey(),
poolKey,
pieceKey);
if (!update.changed()) {
throw new IOException("The owned pool does not reference selected piece '" + pieceKey + "'");
}
return CommandJigsaw.mappedMutationResult(
context,
"",
"",
"Removed '" + pieceKey + "' from pool '" + poolKey + "' in one graph transaction.");
});
}
@Director(description = "Allow or forbid cardinal rotation of the selected piece", sync = true,
origin = DirectorOrigin.PLAYER)
public void rotatable(
@Param(description = "Whether the selected piece may rotate") boolean rotatable
) {
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
if (context == null) {
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
return;
}
JigsawStudioVariant selected = CommandJigsaw.selectedVariant(context);
if (selected == null) {
CommandJigsaw.sendError(this, "Select a workcell with an active variant before changing rotation.");
return;
}
if (!rotatable
&& context.request().compatibilityTarget()
== JigsawStudioCompatibilityTarget.VANILLA_PORTABLE) {
CommandJigsaw.sendError(this, "Vanilla-portable pieces must remain rotatable.");
return;
}
String pieceKey = selected.pieceKey();
CommandJigsaw.runGraphMutation(player(), context, () -> {
JigsawStudioGraphEditor.updateRotatable(
context.request().source().getDataFolder().toPath(),
context.request().structureKey(),
pieceKey,
rotatable);
return CommandJigsaw.mappedMutationResult(
context,
"",
"",
"Persisted piece '" + pieceKey + "' rotatable=" + rotatable + ".");
});
}
@Director(description = "Resize the selected piece object exactly to workcell capacity", sync = true,
origin = DirectorOrigin.PLAYER)
public void expand() {
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
if (context == null) {
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
return;
}
JigsawStudioBay workcell = CommandJigsaw.selectedBay(context);
if (workcell == null) {
CommandJigsaw.sendError(this, "Select a workcell before expanding its active variant.");
return;
}
JigsawStudioService.get().expandVariantToCell(player(), workcell.stableId());
}
}
@@ -0,0 +1,38 @@
package art.arcane.iris.core.commands;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioGraphEditor;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.volmlib.util.director.DirectorOrigin;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
@Director(name = "pool", description = "Manage owned jigsaw pools", origin = DirectorOrigin.PLAYER)
public class CommandJigsawPool implements DirectorExecutor {
@Director(description = "Create an empty owned jigsaw pool", sync = true,
origin = DirectorOrigin.PLAYER)
public void create(
@Param(description = "New owned pool resource key") String poolKey,
@Param(description = "Owned fallback pool key, or none", defaultValue = "none")
String fallbackPoolKey
) {
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
if (context == null) {
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
return;
}
String fallback = "none".equalsIgnoreCase(fallbackPoolKey) ? "" : fallbackPoolKey;
CommandJigsaw.runGraphMutation(player(), context, () -> {
JigsawStudioGraphEditor.createPool(
context.request().source().getDataFolder().toPath(),
context.request().structureKey(),
poolKey,
fallback);
return CommandJigsaw.mappedMutationResult(
context,
"",
"",
"Created owned pool '" + poolKey + "' with fallback '"
+ (fallback.isBlank() ? "none" : fallback) + "'.");
});
}
}
@@ -0,0 +1,26 @@
package art.arcane.iris.core.commands;
import art.arcane.iris.core.service.JigsawStudioService;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.volmlib.util.director.DirectorOrigin;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
@Director(name = "preview", description = "Assemble a read-only jigsaw preview",
origin = DirectorOrigin.PLAYER)
public class CommandJigsawPreview implements DirectorExecutor {
@Director(name = "goto", aliases = "teleport", description = "Teleport to the live seed-1337 preview",
sync = true, origin = DirectorOrigin.PLAYER)
public void gotoPreview() {
JigsawStudioService.get().goToPreview(player());
}
@Director(description = "Assemble a deterministic preview at your location", sync = true,
origin = DirectorOrigin.PLAYER)
public void assemble(
@Param(description = "Assembly seed", defaultValue = "1337") long seed
) {
JigsawStudioService.get().previewStudio(player(), seed);
}
}
@@ -0,0 +1,74 @@
package art.arcane.iris.core.commands;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioCompatibilityTarget;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioPoolEditor;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioStructureEditor;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.volmlib.util.director.DirectorOrigin;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
@Director(name = "rules", aliases = "rule", description = "Manage structure-wide jigsaw rules",
origin = DirectorOrigin.PLAYER)
public class CommandJigsawRules implements DirectorExecutor {
@Director(description = "Set maximum expansion depth and horizontal radius", sync = true,
origin = DirectorOrigin.PLAYER)
public void limits(
@Param(description = "Maximum expansion depth") int maxDepth,
@Param(description = "Maximum radius in chunks") int maxSizeChunks
) {
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
if (context == null) {
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
return;
}
if (context.request().compatibilityTarget() == JigsawStudioCompatibilityTarget.VANILLA_PORTABLE
&& (maxDepth > 20 || maxSizeChunks > 8)) {
CommandJigsaw.sendError(this, "Vanilla-portable rules require maxDepth <= 20 and maxSizeChunks <= 8.");
return;
}
CommandJigsaw.runGraphMutation(player(), context, () -> {
JigsawStudioStructureEditor.updateLimits(
context.request().source().getDataFolder().toPath(),
context.request().structureKey(),
maxDepth,
maxSizeChunks);
return CommandJigsaw.mappedMutationResult(
context,
"",
"",
"Persisted jigsaw limits: maxDepth=" + maxDepth
+ ", maxSizeChunks=" + maxSizeChunks + ".");
});
}
@Director(description = "Set or clear one owned pool's direct fallback", sync = true,
origin = DirectorOrigin.PLAYER)
public void fallback(
@Param(description = "Owned pool resource key") String poolKey,
@Param(description = "Owned fallback pool key, or none") String fallbackPoolKey
) {
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
if (context == null) {
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
return;
}
String fallback = "none".equalsIgnoreCase(fallbackPoolKey) ? "" : fallbackPoolKey;
CommandJigsaw.runGraphMutation(player(), context, () -> {
JigsawStudioPoolEditor.PoolUpdate update = JigsawStudioPoolEditor.updateFallback(
context.request().source().getDataFolder().toPath(),
context.request().structureKey(),
poolKey,
fallback);
if (!update.changed()) {
return CommandJigsaw.mappedMutationResult(context, "", "", "Pool fallback is unchanged.");
}
return CommandJigsaw.mappedMutationResult(
context,
"",
"",
"Persisted pool '" + poolKey + "' fallback as '"
+ (fallback.isBlank() ? "none" : fallback) + "'.");
});
}
}
@@ -0,0 +1,169 @@
package art.arcane.iris.core.commands;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioBay;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioCellDimensions;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioPoolEditor;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioVariant;
import art.arcane.iris.core.service.JigsawStudioService;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.volmlib.util.director.DirectorOrigin;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import java.io.IOException;
@Director(name = "variant", description = "Manage selected jigsaw variants", origin = DirectorOrigin.PLAYER)
public class CommandJigsawVariant implements DirectorExecutor {
@Director(description = "Set the selected piece weight in a pool", sync = true,
origin = DirectorOrigin.PLAYER)
public void weight(
@Param(description = "Pool resource key") String poolKey,
@Param(description = "Positive selection weight") int weight
) {
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
if (context == null) {
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
return;
}
JigsawStudioVariant selected = CommandJigsaw.selectedVariant(context);
if (selected == null) {
CommandJigsaw.sendError(this, "Select a workcell with an active variant before changing its weight.");
return;
}
String pieceKey = selected.pieceKey();
CommandJigsaw.runGraphMutation(player(), context, () -> {
JigsawStudioPoolEditor.WeightUpdate update = JigsawStudioPoolEditor.updateWeight(
context.request().source().getDataFolder().toPath(),
context.request().structureKey(),
poolKey,
pieceKey,
weight);
if (!update.changed()) {
throw new IOException("Pool '" + poolKey + "' does not reference selected piece '"
+ pieceKey + "'");
}
return CommandJigsaw.mappedMutationResult(
context,
"",
"",
"Updated " + update.changedEntries() + " pool entry weight atomically in "
+ update.poolPath() + ".");
});
}
@Director(description = "Resize only the active variant object within workcell capacity", sync = true,
origin = DirectorOrigin.PLAYER)
public void resize(
@Param(description = "Variant width") int width,
@Param(description = "Variant height") int height,
@Param(description = "Variant depth") int depth
) {
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
if (context == null) {
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
return;
}
JigsawStudioBay selectedWorkcell = CommandJigsaw.selectedBay(context);
JigsawStudioVariant selectedVariant = CommandJigsaw.selectedVariant(context);
if (selectedWorkcell == null || selectedVariant == null) {
CommandJigsaw.sendError(this, "Select a workcell with an active variant before resizing it.");
return;
}
JigsawStudioCellDimensions dimensions;
try {
dimensions = new JigsawStudioCellDimensions(width, height, depth);
} catch (IllegalArgumentException exception) {
CommandJigsaw.sendError(this, exception.getMessage());
return;
}
JigsawStudioService.get().resizeVariant(
player(),
selectedWorkcell.stableId(),
selectedVariant.pieceKey(),
dimensions);
}
@Director(description = "Set the active variant's author-facing label", sync = true,
origin = DirectorOrigin.PLAYER)
public void label(
@Param(description = "Display label; quote labels containing spaces") String displayName
) {
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
if (context == null) {
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
return;
}
JigsawStudioBay selectedWorkcell = CommandJigsaw.selectedBay(context);
JigsawStudioVariant selectedVariant = CommandJigsaw.selectedVariant(context);
if (selectedWorkcell == null || selectedVariant == null) {
CommandJigsaw.sendError(this, "Select a workcell with an active variant before renaming it.");
return;
}
JigsawStudioService.get().updateVariantDisplayName(
player(),
selectedWorkcell.stableId(),
selectedVariant.pieceKey(),
displayName);
}
@Director(name = "label-reset", aliases = "reset-label",
description = "Reset the active variant to its resource-key label", sync = true,
origin = DirectorOrigin.PLAYER)
public void labelReset() {
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
if (context == null) {
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
return;
}
JigsawStudioBay selectedWorkcell = CommandJigsaw.selectedBay(context);
JigsawStudioVariant selectedVariant = CommandJigsaw.selectedVariant(context);
if (selectedWorkcell == null || selectedVariant == null) {
CommandJigsaw.sendError(this, "Select a workcell with an active variant before resetting its label.");
return;
}
JigsawStudioService.get().updateVariantDisplayName(
player(),
selectedWorkcell.stableId(),
selectedVariant.pieceKey(),
"");
}
@Director(description = "Duplicate the active variant in only this workcell", sync = true,
origin = DirectorOrigin.PLAYER)
public void duplicate() {
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
if (context == null) {
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
return;
}
JigsawStudioBay selected = CommandJigsaw.selectedBay(context);
if (selected == null || CommandJigsaw.selectedVariant(context) == null) {
CommandJigsaw.sendError(this, "Select a workcell with an active variant before duplicating it.");
return;
}
JigsawStudioService.get().createVariant(player(), selected.stableId(), true);
}
@Director(name = "duplicate-family", aliases = "family",
description = "Duplicate every enabled workcell's active variant as one coherent family",
sync = true, origin = DirectorOrigin.PLAYER)
public void duplicateFamily(
@Param(description = "Family key or next", defaultValue = "next") String themeKey
) {
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
if (context == null) {
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
return;
}
String selectedTheme = themeKey;
if ("next".equalsIgnoreCase(themeKey)) {
try {
selectedTheme = CommandJigsaw.nextThemeKey(context);
} catch (IOException | ArithmeticException exception) {
CommandJigsaw.sendError(this, exception.getMessage());
return;
}
}
JigsawStudioService.get().duplicateActiveFamily(player(), selectedTheme);
}
}
@@ -0,0 +1,81 @@
package art.arcane.iris.core.commands;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioBay;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioCellDimensions;
import art.arcane.iris.core.service.JigsawStudioService;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.volmlib.util.director.DirectorOrigin;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
@Director(name = "workcell", aliases = "cell", description = "Manage the selected Studio workcell",
origin = DirectorOrigin.PLAYER)
public class CommandJigsawWorkcell implements DirectorExecutor {
@Director(description = "Set capacity without resizing any variant object", sync = true,
origin = DirectorOrigin.PLAYER)
public void capacity(
@Param(description = "Capacity width") int width,
@Param(description = "Capacity height") int height,
@Param(description = "Capacity depth") int depth
) {
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
if (context == null) {
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
return;
}
JigsawStudioBay selected = CommandJigsaw.selectedBay(context);
if (selected == null) {
CommandJigsaw.sendError(this, "Select a workcell before changing its capacity.");
return;
}
JigsawStudioCellDimensions dimensions;
try {
dimensions = new JigsawStudioCellDimensions(width, height, depth);
} catch (IllegalArgumentException exception) {
CommandJigsaw.sendError(this, exception.getMessage());
return;
}
JigsawStudioService.get().updateWorkcellDimensions(
player(),
selected.stableId(),
dimensions);
}
@Director(description = "Set the selected workcell's author-facing label", sync = true,
origin = DirectorOrigin.PLAYER)
public void label(
@Param(description = "Display label; quote labels containing spaces") String displayName
) {
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
if (context == null) {
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
return;
}
JigsawStudioBay selected = CommandJigsaw.selectedBay(context);
if (selected == null) {
CommandJigsaw.sendError(this, "Select a workcell before renaming it.");
return;
}
JigsawStudioService.get().updateWorkcellDisplayName(
player(),
selected.stableId(),
displayName);
}
@Director(name = "label-reset", aliases = "reset-label",
description = "Reset the selected workcell to its canonical solver label", sync = true,
origin = DirectorOrigin.PLAYER)
public void labelReset() {
CommandJigsaw.ActiveContext context = CommandJigsaw.active(player());
if (context == null) {
CommandJigsaw.sendError(this, "Open a Jigsaw Studio project first.");
return;
}
JigsawStudioBay selected = CommandJigsaw.selectedBay(context);
if (selected == null) {
CommandJigsaw.sendError(this, "Select a workcell before resetting its label.");
return;
}
JigsawStudioService.get().updateWorkcellDisplayName(player(), selected.stableId(), "");
}
}
@@ -108,10 +108,10 @@ public class CommandJigsawContractTest {
BrokenPackException failure = new BrokenPackException(
"overworld", List.of("Biome 'broken' has no resolvable regions."));
assertTrue(CommandJigsaw.isExpectedOpenDenial(failure));
assertTrue(CommandJigsaw.isExpectedOpenDenial(
assertTrue(CommandJigsawOpen.isExpectedOpenDenial(failure));
assertTrue(CommandJigsawOpen.isExpectedOpenDenial(
new CompletionException(failure)));
assertFalse(CommandJigsaw.isExpectedOpenDenial(
assertFalse(CommandJigsawOpen.isExpectedOpenDenial(
new IllegalStateException("Unexpected Studio failure")));
}
@@ -138,20 +138,20 @@ public class CommandJigsawContractTest {
@Test
public void adoptionCommandsExposeTwoStepPlanContract() throws Exception {
Method inspect = CommandJigsaw.CommandJigsawAdopt.class.getDeclaredMethod(
Method inspect = CommandJigsawAdopt.class.getDeclaredMethod(
"inspect", IrisDimension.class, String.class, String.class, String.class);
Method apply = CommandJigsaw.CommandJigsawAdopt.class.getDeclaredMethod("apply", String.class);
Method apply = CommandJigsawAdopt.class.getDeclaredMethod("apply", String.class);
Parameter[] inspectParameters = inspect.getParameters();
assertNotNull(inspect.getAnnotation(Director.class));
assertNotNull(apply.getAnnotation(Director.class));
assertParameter(inspectParameters[2], "auto", null);
assertParameter(inspectParameters[3], "auto", CommandJigsaw.JigsawAdoptionStrategyHandler.class);
assertEquals(CommandJigsaw.JigsawAdoptionPlanHandler.class,
assertParameter(inspectParameters[3], "auto", CommandJigsawAdopt.JigsawAdoptionStrategyHandler.class);
assertEquals(CommandJigsawAdopt.JigsawAdoptionPlanHandler.class,
apply.getParameters()[0].getAnnotation(Param.class).customHandler());
CommandJigsaw.JigsawAdoptionStrategyHandler strategyHandler =
new CommandJigsaw.JigsawAdoptionStrategyHandler();
CommandJigsawAdopt.JigsawAdoptionStrategyHandler strategyHandler =
new CommandJigsawAdopt.JigsawAdoptionStrategyHandler();
assertEquals(List.of("auto", "in-place", "clone"), strategyHandler.getPossibilities());
assertEquals("in-place", strategyHandler.parse("claim", false));
assertEquals("clone", strategyHandler.parse("copy", false));
@@ -163,14 +163,14 @@ public class CommandJigsawContractTest {
Path root = temporaryFolder.newFolder("adoption-provenance").toPath();
assertEquals(IrisStructureAdoptionInputKind.UNOWNED_IRIS,
CommandJigsaw.adoptionInputKind(root, "unowned"));
CommandJigsawAdopt.adoptionInputKind(root, "unowned"));
writeManifest(root, "datapack-created", StructureSource.Kind.DATAPACK,
StructureOwnershipManifest.Provenance.created());
assertEquals(IrisStructureAdoptionInputKind.UNOWNED_IRIS,
CommandJigsaw.adoptionInputKind(root, "datapack-created"));
CommandJigsawAdopt.adoptionInputKind(root, "datapack-created"));
writeManifest(root, "managed-provenance", StructureSource.Kind.IRIS, managedProvenance());
assertEquals(IrisStructureAdoptionInputKind.MANAGED_DATAPACK,
CommandJigsaw.adoptionInputKind(root, "managed-provenance"));
CommandJigsawAdopt.adoptionInputKind(root, "managed-provenance"));
}
@Test
@@ -275,70 +275,70 @@ public class CommandJigsawContractTest {
@Test
public void exposesNestedPieceVariantAndPreviewCommands() throws Exception {
assertNotNull(CommandJigsaw.CommandJigsawPool.class
assertNotNull(CommandJigsawPool.class
.getDeclaredMethod("create", String.class, String.class)
.getAnnotation(Director.class));
assertNotNull(CommandJigsaw.CommandJigsawConnector.class
assertNotNull(CommandJigsawConnector.class
.getDeclaredMethod("channel", String.class)
.getAnnotation(Director.class));
assertNotNull(CommandJigsaw.CommandJigsawPiece.class
assertNotNull(CommandJigsawPiece.class
.getDeclaredMethod("create", String.class, String.class, int.class)
.getAnnotation(Director.class));
assertNotNull(CommandJigsaw.CommandJigsawPiece.class
assertNotNull(CommandJigsawPiece.class
.getDeclaredMethod("add", String.class, String.class, int.class)
.getAnnotation(Director.class));
assertNotNull(CommandJigsaw.CommandJigsawPiece.class
assertNotNull(CommandJigsawPiece.class
.getDeclaredMethod("remove", String.class)
.getAnnotation(Director.class));
assertNotNull(CommandJigsaw.CommandJigsawPiece.class
assertNotNull(CommandJigsawPiece.class
.getDeclaredMethod("rotatable", boolean.class)
.getAnnotation(Director.class));
assertNotNull(CommandJigsaw.CommandJigsawPiece.class
assertNotNull(CommandJigsawPiece.class
.getDeclaredMethod("expand")
.getAnnotation(Director.class));
assertEquals("Resize the selected piece object exactly to workcell capacity",
CommandJigsaw.CommandJigsawPiece.class
CommandJigsawPiece.class
.getDeclaredMethod("expand")
.getAnnotation(Director.class)
.description());
assertNotNull(CommandJigsaw.CommandJigsawVariant.class
assertNotNull(CommandJigsawVariant.class
.getDeclaredMethod("weight", String.class, int.class)
.getAnnotation(Director.class));
assertNotNull(CommandJigsaw.CommandJigsawVariant.class
assertNotNull(CommandJigsawVariant.class
.getDeclaredMethod("resize", int.class, int.class, int.class)
.getAnnotation(Director.class));
assertNotNull(CommandJigsaw.CommandJigsawVariant.class
assertNotNull(CommandJigsawVariant.class
.getDeclaredMethod("label", String.class)
.getAnnotation(Director.class));
assertNotNull(CommandJigsaw.CommandJigsawVariant.class
assertNotNull(CommandJigsawVariant.class
.getDeclaredMethod("labelReset")
.getAnnotation(Director.class));
assertNotNull(CommandJigsaw.CommandJigsawVariant.class
assertNotNull(CommandJigsawVariant.class
.getDeclaredMethod("duplicate")
.getAnnotation(Director.class));
Method duplicateFamily = CommandJigsaw.CommandJigsawVariant.class
Method duplicateFamily = CommandJigsawVariant.class
.getDeclaredMethod("duplicateFamily", String.class);
assertNotNull(duplicateFamily.getAnnotation(Director.class));
assertParameter(duplicateFamily.getParameters()[0], "next", null);
assertNotNull(CommandJigsaw.CommandJigsawWorkcell.class
assertNotNull(CommandJigsawWorkcell.class
.getDeclaredMethod("capacity", int.class, int.class, int.class)
.getAnnotation(Director.class));
assertNotNull(CommandJigsaw.CommandJigsawWorkcell.class
assertNotNull(CommandJigsawWorkcell.class
.getDeclaredMethod("label", String.class)
.getAnnotation(Director.class));
assertNotNull(CommandJigsaw.CommandJigsawWorkcell.class
assertNotNull(CommandJigsawWorkcell.class
.getDeclaredMethod("labelReset")
.getAnnotation(Director.class));
assertNotNull(CommandJigsaw.CommandJigsawRules.class
assertNotNull(CommandJigsawRules.class
.getDeclaredMethod("limits", int.class, int.class)
.getAnnotation(Director.class));
assertNotNull(CommandJigsaw.CommandJigsawRules.class
assertNotNull(CommandJigsawRules.class
.getDeclaredMethod("fallback", String.class, String.class)
.getAnnotation(Director.class));
assertNotNull(CommandJigsaw.CommandJigsawPreview.class
assertNotNull(CommandJigsawPreview.class
.getDeclaredMethod("assemble", long.class)
.getAnnotation(Director.class));
assertNotNull(CommandJigsaw.CommandJigsawPreview.class
assertNotNull(CommandJigsawPreview.class
.getDeclaredMethod("gotoPreview")
.getAnnotation(Director.class));
}
@@ -393,14 +393,14 @@ public class CommandJigsawContractTest {
public void everyExecutableCommandRejectsNonPlayerOrigins() {
Class<?>[] commandTypes = {
CommandJigsaw.class,
CommandJigsaw.CommandJigsawConnector.class,
CommandJigsaw.CommandJigsawPool.class,
CommandJigsaw.CommandJigsawPiece.class,
CommandJigsaw.CommandJigsawVariant.class,
CommandJigsaw.CommandJigsawWorkcell.class,
CommandJigsaw.CommandJigsawRules.class,
CommandJigsaw.CommandJigsawPreview.class,
CommandJigsaw.CommandJigsawAdopt.class
CommandJigsawConnector.class,
CommandJigsawPool.class,
CommandJigsawPiece.class,
CommandJigsawVariant.class,
CommandJigsawWorkcell.class,
CommandJigsawRules.class,
CommandJigsawPreview.class,
CommandJigsawAdopt.class
};
for (Class<?> commandType : commandTypes) {
@@ -417,17 +417,17 @@ public class CommandJigsawContractTest {
public void exportOutputIsOneSafeChildArtifact() {
Path root = Path.of("build", "jigsaw-exports").toAbsolutePath().normalize();
assertEquals(root.resolve("village.zip"), CommandJigsaw.resolveExportDestination(
assertEquals(root.resolve("village.zip"), CommandJigsawExports.resolveExportDestination(
root, "village", VanillaJigsawExportFormat.ZIP));
assertEquals(root.resolve("village"), CommandJigsaw.resolveExportDestination(
assertEquals(root.resolve("village"), CommandJigsawExports.resolveExportDestination(
root, "village", VanillaJigsawExportFormat.DIRECTORY));
assertThrows(IllegalArgumentException.class, () -> CommandJigsaw.resolveExportDestination(
assertThrows(IllegalArgumentException.class, () -> CommandJigsawExports.resolveExportDestination(
root, "", VanillaJigsawExportFormat.DIRECTORY));
assertThrows(IllegalArgumentException.class, () -> CommandJigsaw.resolveExportDestination(
assertThrows(IllegalArgumentException.class, () -> CommandJigsawExports.resolveExportDestination(
root, ".", VanillaJigsawExportFormat.DIRECTORY));
assertThrows(IllegalArgumentException.class, () -> CommandJigsaw.resolveExportDestination(
assertThrows(IllegalArgumentException.class, () -> CommandJigsawExports.resolveExportDestination(
root, "../all-exports", VanillaJigsawExportFormat.DIRECTORY));
assertThrows(IllegalArgumentException.class, () -> CommandJigsaw.resolveExportDestination(
assertThrows(IllegalArgumentException.class, () -> CommandJigsawExports.resolveExportDestination(
root, "nested/export", VanillaJigsawExportFormat.DIRECTORY));
}
@@ -438,42 +438,42 @@ public class CommandJigsawContractTest {
Path firstDestination = Path.of("build", "jigsaw-exports", "first.zip");
Path secondDestination = Path.of("build", "jigsaw-exports", "second.zip");
assertEquals(true, CommandJigsaw.beginExport(firstPlayer, firstDestination));
assertEquals(true, CommandJigsawExports.beginExport(firstPlayer, firstDestination));
try {
assertEquals(false, CommandJigsaw.beginExport(firstPlayer, secondDestination));
assertEquals(false, CommandJigsaw.beginExport(secondPlayer, firstDestination));
assertEquals(false, CommandJigsawExports.beginExport(firstPlayer, secondDestination));
assertEquals(false, CommandJigsawExports.beginExport(secondPlayer, firstDestination));
} finally {
CommandJigsaw.finishExport(firstPlayer, firstDestination);
CommandJigsawExports.finishExport(firstPlayer, firstDestination);
}
assertEquals(true, CommandJigsaw.beginExport(secondPlayer, firstDestination));
CommandJigsaw.finishExport(secondPlayer, firstDestination);
assertEquals(true, CommandJigsawExports.beginExport(secondPlayer, firstDestination));
CommandJigsawExports.finishExport(secondPlayer, firstDestination);
}
@Test
public void exportStartFailuresHavePreciseOperatorMessages() {
assertEquals("The active Jigsaw Studio is no longer available.",
CommandJigsaw.exportStartError(JigsawStudioService.ExportStart.NOT_ACTIVE));
CommandJigsawExports.exportStartError(JigsawStudioService.ExportStart.NOT_ACTIVE));
assertEquals("Only the Jigsaw Studio owner can export this project.",
CommandJigsaw.exportStartError(JigsawStudioService.ExportStart.NOT_OWNER));
CommandJigsawExports.exportStartError(JigsawStudioService.ExportStart.NOT_OWNER));
assertEquals("Wait for the pending autosave or discard the edits before exporting the on-disk graph.",
CommandJigsaw.exportStartError(JigsawStudioService.ExportStart.DIRTY));
CommandJigsawExports.exportStartError(JigsawStudioService.ExportStart.DIRTY));
assertEquals("The active Jigsaw Studio is closing and cannot be exported.",
CommandJigsaw.exportStartError(JigsawStudioService.ExportStart.CLOSING));
CommandJigsawExports.exportStartError(JigsawStudioService.ExportStart.CLOSING));
assertEquals("Wait for the current Jigsaw Studio save to finish before exporting.",
CommandJigsaw.exportStartError(JigsawStudioService.ExportStart.SAVE_IN_PROGRESS));
CommandJigsawExports.exportStartError(JigsawStudioService.ExportStart.SAVE_IN_PROGRESS));
assertEquals("Wait for the current Jigsaw Studio operation to finish before exporting.",
CommandJigsaw.exportStartError(JigsawStudioService.ExportStart.OPERATION_IN_PROGRESS));
CommandJigsawExports.exportStartError(JigsawStudioService.ExportStart.OPERATION_IN_PROGRESS));
assertEquals("A Jigsaw Studio export is already in progress.",
CommandJigsaw.exportStartError(JigsawStudioService.ExportStart.IN_PROGRESS));
CommandJigsawExports.exportStartError(JigsawStudioService.ExportStart.IN_PROGRESS));
assertThrows(IllegalArgumentException.class,
() -> CommandJigsaw.exportStartError(JigsawStudioService.ExportStart.STARTED));
() -> CommandJigsawExports.exportStartError(JigsawStudioService.ExportStart.STARTED));
}
@Test
public void exportLeaseReleaseActionRunsExactlyOnce() {
AtomicInteger releases = new AtomicInteger();
CommandJigsaw.ExportLease lease = new CommandJigsaw.ExportLease(releases::incrementAndGet);
CommandJigsawExports.ExportLease lease = new CommandJigsawExports.ExportLease(releases::incrementAndGet);
lease.release();
lease.release();
@@ -35,6 +35,16 @@ public final class GenerationRevisionScope {
"adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandPregen.java",
"adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandIris.java",
"adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandJigsaw.java",
"adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandJigsawAdopt.java",
"adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandJigsawConnector.java",
"adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandJigsawExports.java",
"adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandJigsawOpen.java",
"adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandJigsawPiece.java",
"adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandJigsawPool.java",
"adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandJigsawPreview.java",
"adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandJigsawRules.java",
"adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandJigsawVariant.java",
"adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandJigsawWorkcell.java",
"adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandStudio.java",
"adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandObject.java",
"adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/PaperCommandRegistrar.java",