mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 22:10:18 +00:00
Split jigsaw studio service
- Move world registration and shutdown quiescing into JigsawStudioRegistry - Move save, export and close arbitration into JigsawStudioSaveLifecycle - Move autosave scheduling, drains and undo into JigsawStudioAutosaveScheduler - Move variant creation, duplication and deletion into JigsawStudioVariantEditor - Move variant metadata and resize edits into JigsawStudioVariantProperties - Move workcell capacity, labels and connector blocks into JigsawStudioWorkcellEditor - Move graph mutation and live relayout into JigsawStudioGraphMutations - Move dynamic graph evaluation into JigsawStudioEvaluator - Move menu state, studio tools and deferred duplication into JigsawStudioToolbelt - Move the studio world event handlers into JigsawStudioProtectionListener - Move ownership and mutation guards into JigsawStudioProtection - Move variant materialization and hydration into JigsawStudioMaterializer - Move dirty marking into JigsawStudioDirtyTracker - Move workcell capture scheduling into JigsawStudioCaptureScheduler - Move chunk capture and connector aggregation into JigsawStudioCapture - Move materialized and rendered chunk writing into JigsawStudioChunkWriter - Move jigsaw marker NBT watching into JigsawStudioTileWatcher - Move particle preview rendering into JigsawStudioVisualization - Move player workcell context into JigsawStudioPlayerContext - Point the studio service tests at the relocated collaborators - Record the split studio sources in the Bukkit purity allowlist
This commit is contained in:
1 parent
8b9d1fb2fb
commit
2451c89cee
26 files changed
+9617
-8574
No files matched your search
@@ -62,11 +62,37 @@ art/arcane/iris/core/service/BoardSVC.java
|
|||||||
art/arcane/iris/core/service/EntityRiseSVC.java
|
art/arcane/iris/core/service/EntityRiseSVC.java
|
||||||
art/arcane/iris/core/service/ExternalDataSVC.java
|
art/arcane/iris/core/service/ExternalDataSVC.java
|
||||||
art/arcane/iris/core/service/GlobalCacheSVC.java
|
art/arcane/iris/core/service/GlobalCacheSVC.java
|
||||||
|
art/arcane/iris/core/service/JigsawStudioAutosaveScheduler.java
|
||||||
|
art/arcane/iris/core/service/JigsawStudioCapture.java
|
||||||
|
art/arcane/iris/core/service/JigsawStudioCaptureScheduler.java
|
||||||
|
art/arcane/iris/core/service/JigsawStudioChunkWriter.java
|
||||||
|
art/arcane/iris/core/service/JigsawStudioDetailsMenu.java
|
||||||
|
art/arcane/iris/core/service/JigsawStudioDirtyTracker.java
|
||||||
|
art/arcane/iris/core/service/JigsawStudioGraphMutations.java
|
||||||
|
art/arcane/iris/core/service/JigsawStudioMainMenu.java
|
||||||
art/arcane/iris/core/service/JigsawStudioMarkerParser.java
|
art/arcane/iris/core/service/JigsawStudioMarkerParser.java
|
||||||
|
art/arcane/iris/core/service/JigsawStudioMaterializer.java
|
||||||
|
art/arcane/iris/core/service/JigsawStudioMenuActions.java
|
||||||
art/arcane/iris/core/service/JigsawStudioMenuController.java
|
art/arcane/iris/core/service/JigsawStudioMenuController.java
|
||||||
|
art/arcane/iris/core/service/JigsawStudioMenuFormat.java
|
||||||
|
art/arcane/iris/core/service/JigsawStudioPlayerContext.java
|
||||||
art/arcane/iris/core/service/JigsawStudioPreviewRenderer.java
|
art/arcane/iris/core/service/JigsawStudioPreviewRenderer.java
|
||||||
|
art/arcane/iris/core/service/JigsawStudioProtection.java
|
||||||
|
art/arcane/iris/core/service/JigsawStudioProtectionListener.java
|
||||||
|
art/arcane/iris/core/service/JigsawStudioRegistry.java
|
||||||
|
art/arcane/iris/core/service/JigsawStudioSaveLifecycle.java
|
||||||
art/arcane/iris/core/service/JigsawStudioService.java
|
art/arcane/iris/core/service/JigsawStudioService.java
|
||||||
|
art/arcane/iris/core/service/JigsawStudioStructureMenu.java
|
||||||
|
art/arcane/iris/core/service/JigsawStudioTileWatcher.java
|
||||||
art/arcane/iris/core/service/JigsawStudioToolCodec.java
|
art/arcane/iris/core/service/JigsawStudioToolCodec.java
|
||||||
|
art/arcane/iris/core/service/JigsawStudioToolbelt.java
|
||||||
|
art/arcane/iris/core/service/JigsawStudioToolboxMenu.java
|
||||||
|
art/arcane/iris/core/service/JigsawStudioVariantEditor.java
|
||||||
|
art/arcane/iris/core/service/JigsawStudioVariantMenu.java
|
||||||
|
art/arcane/iris/core/service/JigsawStudioVariantProperties.java
|
||||||
|
art/arcane/iris/core/service/JigsawStudioVisualization.java
|
||||||
|
art/arcane/iris/core/service/JigsawStudioWorkcellEditor.java
|
||||||
|
art/arcane/iris/core/service/JigsawStudioWorkcellMenu.java
|
||||||
art/arcane/iris/core/service/MultiverseSVC.java
|
art/arcane/iris/core/service/MultiverseSVC.java
|
||||||
art/arcane/iris/core/service/ObjectSVC.java
|
art/arcane/iris/core/service/ObjectSVC.java
|
||||||
art/arcane/iris/core/service/ObjectStudioSaveService.java
|
art/arcane/iris/core/service/ObjectStudioSaveService.java
|
||||||
|
|||||||
@@ -0,0 +1,670 @@
|
|||||||
|
package art.arcane.iris.core.service;
|
||||||
|
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioActivation;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioBay;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioBounds;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioLayout;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioSession;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioVariant;
|
||||||
|
import art.arcane.iris.core.service.JigsawStudioCapture.ChunkCaptureArea;
|
||||||
|
import art.arcane.iris.core.service.JigsawStudioCapture.ChunkSnapshot;
|
||||||
|
import art.arcane.iris.core.service.JigsawStudioCaptureScheduler.CaptureCoordinator;
|
||||||
|
import art.arcane.iris.core.service.JigsawStudioCaptureScheduler.CaptureTarget;
|
||||||
|
import art.arcane.iris.core.service.JigsawStudioCaptureScheduler.CaptureWork;
|
||||||
|
import art.arcane.iris.core.service.JigsawStudioMaterializer.VariantReloadRequest;
|
||||||
|
import art.arcane.iris.core.service.JigsawStudioSaveLifecycle.BayReadiness;
|
||||||
|
import art.arcane.iris.core.service.JigsawStudioSaveLifecycle.SaveAttempt;
|
||||||
|
import art.arcane.iris.core.service.JigsawStudioSaveLifecycle.SaveStart;
|
||||||
|
import art.arcane.iris.core.service.JigsawStudioService.ActiveStudio;
|
||||||
|
import art.arcane.iris.core.service.JigsawStudioService.CommandGraphMutationResult;
|
||||||
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
|
import art.arcane.iris.util.common.scheduling.J;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioCapture.captureChunkIntersection;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioCapture.chunkIntersections;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioCaptureScheduler.resolveCaptureTarget;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioGraphMutations.loadMappedLayout;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioProtection.authorizeOwner;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioService.AUTOSAVE_RETRY_TICKS;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioService.failureMessage;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioService.message;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioService.writeFailure;
|
||||||
|
|
||||||
|
final class JigsawStudioAutosaveScheduler {
|
||||||
|
static final int AUTOSAVE_DEBOUNCE_TICKS = 40;
|
||||||
|
|
||||||
|
private static final List<Integer> AUTOSAVE_PERSISTENT_RETRY_DELAYS =
|
||||||
|
List.of(40, 80, 160, 320, 600);
|
||||||
|
|
||||||
|
private final JigsawStudioService service;
|
||||||
|
final Map<AutosaveKey, AutosaveTicket> autosaves = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
JigsawStudioAutosaveScheduler(JigsawStudioService service) {
|
||||||
|
this.service = Objects.requireNonNull(service, "Jigsaw Studio service");
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean undoAutosave(Player player) {
|
||||||
|
if (player == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!J.isOwnedByCurrentRegion(player)) {
|
||||||
|
return J.runEntity(player, () -> undoAutosave(player));
|
||||||
|
}
|
||||||
|
ActiveStudio studio = service.studios.get(player.getWorld().getUID());
|
||||||
|
if (studio == null || !authorizeOwner(player, studio)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioSession session = studio.generator().getSession();
|
||||||
|
String reservationFailure = service.graphMutations.beginGraphMutation(studio, session);
|
||||||
|
if (!reservationFailure.isEmpty()) {
|
||||||
|
message(player, reservationFailure);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Map<String, VariantReloadRequest> activeReloads = new HashMap<>();
|
||||||
|
for (JigsawStudioBay workcell : session.layout().bays()) {
|
||||||
|
session.activeVariant(workcell.stableId()).ifPresent(variant -> activeReloads.put(
|
||||||
|
variant.pieceKey(),
|
||||||
|
new VariantReloadRequest(
|
||||||
|
workcell.stableId(),
|
||||||
|
studio.generator().renderBay(workcell))));
|
||||||
|
}
|
||||||
|
JigsawStudioActivation.Request request = studio.generator().getRequest();
|
||||||
|
message(player, "Restoring the previous Jigsaw Studio autosave iteration...");
|
||||||
|
return service.graphMutations.scheduleGraphMutation(
|
||||||
|
player,
|
||||||
|
studio,
|
||||||
|
() -> {
|
||||||
|
Path packRoot = request.source().getDataFolder().toPath();
|
||||||
|
JigsawStudioHistoryStore.UndoResult result = new JigsawStudioHistoryStore(
|
||||||
|
packRoot,
|
||||||
|
request.structureKey()).undoLatest();
|
||||||
|
if (!result.available()) {
|
||||||
|
return new CommandGraphMutationResult(
|
||||||
|
session.layout(),
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"No earlier autosave iteration is available.");
|
||||||
|
}
|
||||||
|
if (!result.successful()) {
|
||||||
|
throw new IOException("Jigsaw Studio undo failed: "
|
||||||
|
+ writeFailure(result.writeResult()));
|
||||||
|
}
|
||||||
|
request.source().invalidateStructureResources();
|
||||||
|
JigsawStudioLayout restoredLayout = loadMappedLayout(studio);
|
||||||
|
VariantReloadRequest reload = activeReloads.get(result.pieceKey());
|
||||||
|
Optional<VariantReloadRequest> activeReload = reload == null
|
||||||
|
|| restoredLayout.get(reload.workcellId()) == null
|
||||||
|
? Optional.empty()
|
||||||
|
: Optional.of(reload);
|
||||||
|
String warning = result.warning().isEmpty()
|
||||||
|
? ""
|
||||||
|
: " History cleanup warning: " + result.warning();
|
||||||
|
return new CommandGraphMutationResult(
|
||||||
|
restoredLayout,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
Map.of(),
|
||||||
|
activeReload,
|
||||||
|
"Restored the previous autosave iteration. "
|
||||||
|
+ result.remainingIterations() + " earlier iteration(s) remain."
|
||||||
|
+ warning);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean flushAutosave(Player player, String workcellId) {
|
||||||
|
if (player == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!J.isOwnedByCurrentRegion(player)) {
|
||||||
|
return J.runEntity(player, () -> flushAutosave(player, workcellId));
|
||||||
|
}
|
||||||
|
ActiveStudio studio = service.studios.get(player.getWorld().getUID());
|
||||||
|
if (studio == null || !authorizeOwner(player, studio)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioSession session = studio.generator().getSession();
|
||||||
|
String targetId = workcellId == null || workcellId.isBlank()
|
||||||
|
? session.selectedBayId().orElse("")
|
||||||
|
: workcellId;
|
||||||
|
JigsawStudioBay bay = session.layout().get(targetId);
|
||||||
|
if (bay == null) {
|
||||||
|
message(player, "Select a Jigsaw Studio workcell before saving now.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
UUID requestId = studio.generator().getRequest().requestId();
|
||||||
|
service.tileWatcher.finalizeJigsawTileWatches(requestId);
|
||||||
|
if (service.tileWatcher.hasJigsawTileWatch(requestId)) {
|
||||||
|
message(player, "Wait for the open vanilla jigsaw-block editor to finish its final snapshot.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
SaveAttempt attempt = service.saveLifecycle.startSave(studio, studio.world(), bay, player, true);
|
||||||
|
if (attempt == SaveAttempt.STARTED) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (attempt == SaveAttempt.PERSISTENT_FAILURE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
AutosaveTicket ticket = autosaves.get(new AutosaveKey(requestId, bay.stableId()));
|
||||||
|
if (ticket != null) {
|
||||||
|
expediteAutosave(
|
||||||
|
ticket,
|
||||||
|
attempt == SaveAttempt.RETRY
|
||||||
|
? AUTOSAVE_RETRY_TICKS
|
||||||
|
: AUTOSAVE_DEBOUNCE_TICKS);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void scheduleAutosave(
|
||||||
|
ActiveStudio studio,
|
||||||
|
JigsawStudioBay bay,
|
||||||
|
JigsawStudioSession.DirtyIdentity identity,
|
||||||
|
int delayTicks
|
||||||
|
) {
|
||||||
|
UUID requestId = studio.generator().getRequest().requestId();
|
||||||
|
AutosaveKey key = new AutosaveKey(requestId, bay.stableId());
|
||||||
|
AutosaveTicket ticket = new AutosaveTicket(
|
||||||
|
key,
|
||||||
|
studio,
|
||||||
|
identity,
|
||||||
|
new AtomicBoolean(false),
|
||||||
|
new AtomicBoolean(false));
|
||||||
|
autosaves.put(key, ticket);
|
||||||
|
scheduleAutosave(ticket, delayTicks);
|
||||||
|
}
|
||||||
|
|
||||||
|
void scheduleAutosave(AutosaveTicket ticket, int delayTicks) {
|
||||||
|
if (autosaves.get(ticket.key()) != ticket
|
||||||
|
|| !ticket.scheduled().compareAndSet(false, true)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
JigsawStudioBay bay = resolveAutosaveBay(ticket);
|
||||||
|
if (bay == null) {
|
||||||
|
ticket.scheduled().set(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
JigsawStudioBounds bounds = bay.bounds();
|
||||||
|
boolean scheduled = J.runRegion(
|
||||||
|
ticket.studio().world(),
|
||||||
|
bounds.originX() >> 4,
|
||||||
|
bounds.originZ() >> 4,
|
||||||
|
() -> runAutosave(ticket),
|
||||||
|
delayTicks);
|
||||||
|
if (!scheduled) {
|
||||||
|
ticket.scheduled().set(false);
|
||||||
|
if (ticket.scheduleFailureLogged().compareAndSet(false, true)) {
|
||||||
|
IrisLogging.warn("Jigsaw Studio autosave could not schedule workcell %s for request %s; Iris will keep retrying",
|
||||||
|
ticket.key().workcellId(), ticket.key().requestId());
|
||||||
|
}
|
||||||
|
scheduleAutosaveReconciliation(ticket);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private JigsawStudioBay resolveAutosaveBay(AutosaveTicket ticket) {
|
||||||
|
if (autosaves.get(ticket.key()) != ticket) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
JigsawStudioBay bay = resolveCurrentAutosaveBay(
|
||||||
|
ticket.studio().generator().getSession(),
|
||||||
|
ticket.key().workcellId());
|
||||||
|
if (bay != null) {
|
||||||
|
return bay;
|
||||||
|
}
|
||||||
|
if (autosaves.remove(ticket.key(), ticket)) {
|
||||||
|
reportAutosaveFailure(
|
||||||
|
ticket,
|
||||||
|
new AutosavePersistentFailure(
|
||||||
|
"workcell is absent from the current Studio layout; pending ticket retired",
|
||||||
|
null));
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static JigsawStudioBay resolveCurrentAutosaveBay(
|
||||||
|
JigsawStudioSession session,
|
||||||
|
String workcellId
|
||||||
|
) {
|
||||||
|
JigsawStudioSession activeSession = Objects.requireNonNull(
|
||||||
|
session,
|
||||||
|
"Jigsaw Studio autosave session");
|
||||||
|
return activeSession.layout().get(Objects.requireNonNull(
|
||||||
|
workcellId,
|
||||||
|
"Jigsaw Studio autosave workcell ID"));
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean retainCurrentAutosaveFailure(
|
||||||
|
ActiveStudio studio,
|
||||||
|
String workcellId,
|
||||||
|
AutosavePersistentFailure failure
|
||||||
|
) {
|
||||||
|
AutosaveKey key = new AutosaveKey(
|
||||||
|
studio.generator().getRequest().requestId(),
|
||||||
|
workcellId);
|
||||||
|
AutosaveTicket ticket = autosaves.get(key);
|
||||||
|
return ticket != null && retainPersistentAutosaveFailure(ticket, failure);
|
||||||
|
}
|
||||||
|
|
||||||
|
AutosaveFailureState autosaveFailureState(
|
||||||
|
ActiveStudio studio,
|
||||||
|
JigsawStudioSession.SaveIdentity saveIdentity
|
||||||
|
) {
|
||||||
|
AutosaveKey key = new AutosaveKey(
|
||||||
|
studio.generator().getRequest().requestId(),
|
||||||
|
saveIdentity.workcellId());
|
||||||
|
AutosaveTicket ticket = autosaves.get(key);
|
||||||
|
return ticket != null && matchesSaveIdentity(ticket.identity(), saveIdentity)
|
||||||
|
? ticket.failureState()
|
||||||
|
: new AutosaveFailureState();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean retainPersistentAutosaveFailure(
|
||||||
|
AutosaveTicket ticket,
|
||||||
|
AutosavePersistentFailure failure
|
||||||
|
) {
|
||||||
|
if (autosaves.get(ticket.key()) != ticket
|
||||||
|
|| !service.isCurrentRequest(ticket.studio(), ticket.key().requestId())
|
||||||
|
|| !ticket.studio().generator().getSession().isDirtyCurrent(ticket.identity())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
AutosaveFailureDecision decision = reportAutosaveFailure(ticket, failure);
|
||||||
|
expediteAutosave(ticket, decision.retryTicks());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AutosaveFailureDecision reportAutosaveFailure(
|
||||||
|
AutosaveTicket ticket,
|
||||||
|
AutosavePersistentFailure failure
|
||||||
|
) {
|
||||||
|
JigsawStudioActivation.Request request = ticket.studio().generator().getRequest();
|
||||||
|
return recordPersistentAutosaveFailure(
|
||||||
|
ticket.failureState(),
|
||||||
|
ticket.key().requestId(),
|
||||||
|
request.structureKey(),
|
||||||
|
ticket.key().workcellId(),
|
||||||
|
ticket.identity().variantKey(),
|
||||||
|
failure.detail(),
|
||||||
|
failure.cause());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void reportPersistentSaveFailure(
|
||||||
|
ActiveStudio studio,
|
||||||
|
JigsawStudioSession.SaveIdentity saveIdentity,
|
||||||
|
AutosaveFailureState failureState,
|
||||||
|
AutosavePersistentFailure failure
|
||||||
|
) {
|
||||||
|
JigsawStudioActivation.Request request = studio.generator().getRequest();
|
||||||
|
recordPersistentAutosaveFailure(
|
||||||
|
failureState,
|
||||||
|
request.requestId(),
|
||||||
|
request.structureKey(),
|
||||||
|
saveIdentity.workcellId(),
|
||||||
|
saveIdentity.variantKey(),
|
||||||
|
failure.detail(),
|
||||||
|
failure.cause());
|
||||||
|
}
|
||||||
|
|
||||||
|
static AutosaveFailureDecision recordPersistentAutosaveFailure(
|
||||||
|
AutosaveFailureState state,
|
||||||
|
UUID requestId,
|
||||||
|
String structureKey,
|
||||||
|
String workcellId,
|
||||||
|
String pieceKey,
|
||||||
|
String detail,
|
||||||
|
Throwable cause
|
||||||
|
) {
|
||||||
|
AutosaveFailureState activeState = Objects.requireNonNull(
|
||||||
|
state,
|
||||||
|
"Jigsaw Studio autosave failure state");
|
||||||
|
AutosaveFailureDecision decision = activeState.recordPersistentFailure();
|
||||||
|
if (!decision.logFailure()) {
|
||||||
|
return decision;
|
||||||
|
}
|
||||||
|
String context = autosaveFailureContext(
|
||||||
|
requestId,
|
||||||
|
structureKey,
|
||||||
|
workcellId,
|
||||||
|
pieceKey,
|
||||||
|
detail);
|
||||||
|
if (cause == null) {
|
||||||
|
IrisLogging.warn("%s", context);
|
||||||
|
} else {
|
||||||
|
IrisLogging.reportError(context, cause);
|
||||||
|
}
|
||||||
|
return decision;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String autosaveFailureContext(
|
||||||
|
UUID requestId,
|
||||||
|
String structureKey,
|
||||||
|
String workcellId,
|
||||||
|
String pieceKey,
|
||||||
|
String detail
|
||||||
|
) {
|
||||||
|
return "Jigsaw Studio save failure: request="
|
||||||
|
+ Objects.requireNonNull(requestId, "Jigsaw Studio autosave request ID")
|
||||||
|
+ " structure="
|
||||||
|
+ Objects.requireNonNull(structureKey, "Jigsaw Studio autosave structure key")
|
||||||
|
+ " workcell="
|
||||||
|
+ Objects.requireNonNull(workcellId, "Jigsaw Studio autosave workcell ID")
|
||||||
|
+ " piece="
|
||||||
|
+ Objects.requireNonNull(pieceKey, "Jigsaw Studio autosave piece key")
|
||||||
|
+ " failure="
|
||||||
|
+ Objects.requireNonNull(detail, "Jigsaw Studio autosave failure detail");
|
||||||
|
}
|
||||||
|
|
||||||
|
static int persistentAutosaveRetryTicks(int failureCount) {
|
||||||
|
int index = Math.max(0, Math.min(
|
||||||
|
failureCount - 1,
|
||||||
|
AUTOSAVE_PERSISTENT_RETRY_DELAYS.size() - 1));
|
||||||
|
return AUTOSAVE_PERSISTENT_RETRY_DELAYS.get(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void scheduleAutosaveReconciliation(AutosaveTicket ticket) {
|
||||||
|
try {
|
||||||
|
J.s(() -> {
|
||||||
|
if (autosaves.get(ticket.key()) == ticket
|
||||||
|
&& !ticket.scheduled().get()) {
|
||||||
|
scheduleAutosave(ticket, AUTOSAVE_RETRY_TICKS);
|
||||||
|
}
|
||||||
|
}, AUTOSAVE_RETRY_TICKS);
|
||||||
|
} catch (Throwable exception) {
|
||||||
|
IrisLogging.reportError(exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void runAutosave(AutosaveTicket ticket) {
|
||||||
|
if (autosaves.get(ticket.key()) != ticket) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ticket.scheduled().set(false);
|
||||||
|
ActiveStudio studio = ticket.studio();
|
||||||
|
JigsawStudioSession session = studio.generator().getSession();
|
||||||
|
if (!service.isCurrentRequest(studio, ticket.key().requestId())) {
|
||||||
|
autosaves.remove(ticket.key(), ticket);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
JigsawStudioBay bay = resolveAutosaveBay(ticket);
|
||||||
|
if (bay == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!session.isDirtyCurrent(ticket.identity())) {
|
||||||
|
autosaves.remove(ticket.key(), ticket);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SaveAttempt attempt = service.saveLifecycle.startSave(studio, studio.world(), bay, null, false);
|
||||||
|
if (attempt == SaveAttempt.STARTED || attempt == SaveAttempt.PERSISTENT_FAILURE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
scheduleAutosave(
|
||||||
|
ticket,
|
||||||
|
attempt == SaveAttempt.RETRY
|
||||||
|
? AUTOSAVE_RETRY_TICKS
|
||||||
|
: AUTOSAVE_DEBOUNCE_TICKS);
|
||||||
|
}
|
||||||
|
|
||||||
|
void completeAutosaveAttempt(
|
||||||
|
ActiveStudio studio,
|
||||||
|
JigsawStudioSession.SaveIdentity saveIdentity,
|
||||||
|
AutosaveFailureState failureState,
|
||||||
|
AutosavePersistentFailure persistentFailure
|
||||||
|
) {
|
||||||
|
AutosaveKey key = new AutosaveKey(
|
||||||
|
studio.generator().getRequest().requestId(),
|
||||||
|
saveIdentity.workcellId());
|
||||||
|
AutosaveTicket ticket = autosaves.get(key);
|
||||||
|
if (ticket == null || !matchesSaveIdentity(ticket.identity(), saveIdentity)) {
|
||||||
|
if (persistentFailure != null) {
|
||||||
|
reportPersistentSaveFailure(studio, saveIdentity, failureState, persistentFailure);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (resolveAutosaveBay(ticket) == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!studio.generator().getSession().isDirtyCurrent(ticket.identity())) {
|
||||||
|
autosaves.remove(key, ticket);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (persistentFailure != null) {
|
||||||
|
retainPersistentAutosaveFailure(ticket, persistentFailure);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
scheduleAutosave(ticket, AUTOSAVE_RETRY_TICKS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean matchesSaveIdentity(
|
||||||
|
JigsawStudioSession.DirtyIdentity dirtyIdentity,
|
||||||
|
JigsawStudioSession.SaveIdentity saveIdentity
|
||||||
|
) {
|
||||||
|
return dirtyIdentity.sessionId().equals(saveIdentity.sessionId())
|
||||||
|
&& dirtyIdentity.workcellId().equals(saveIdentity.workcellId())
|
||||||
|
&& dirtyIdentity.variantKey().equals(saveIdentity.variantKey())
|
||||||
|
&& dirtyIdentity.loadGeneration() == saveIdentity.loadGeneration()
|
||||||
|
&& dirtyIdentity.mutationGeneration() == saveIdentity.mutationGeneration();
|
||||||
|
}
|
||||||
|
|
||||||
|
void drainAutosavesBeforeDisable() {
|
||||||
|
for (ActiveStudio studio : List.copyOf(service.studios.values())) {
|
||||||
|
try {
|
||||||
|
drainAutosavesBeforeRemoval(studio);
|
||||||
|
} catch (Throwable exception) {
|
||||||
|
IrisLogging.reportError(
|
||||||
|
"Failed to drain Jigsaw Studio autosaves in world "
|
||||||
|
+ studio.worldId() + " during shutdown.",
|
||||||
|
exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!autosaves.isEmpty()) {
|
||||||
|
IrisLogging.warn("Jigsaw Studio disabled with %d autosave operation(s) still pending after the final drain attempt",
|
||||||
|
autosaves.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void drainAutosavesBeforeRemoval(ActiveStudio studio) {
|
||||||
|
if (studio == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
UUID requestId = studio.generator().getRequest().requestId();
|
||||||
|
if (service.saveLifecycle.discardingRequest(requestId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (J.isFolia() || !J.isPrimaryThread()) {
|
||||||
|
expediteAutosaves(requestId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (AutosaveTicket ticket : List.copyOf(autosaves.values())) {
|
||||||
|
if (requestId.equals(ticket.key().requestId())) {
|
||||||
|
drainAutosaveSynchronously(ticket);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean drainAutosaveSynchronously(AutosaveTicket ticket) {
|
||||||
|
if (autosaves.get(ticket.key()) != ticket) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
ActiveStudio studio = ticket.studio();
|
||||||
|
JigsawStudioSession session = studio.generator().getSession();
|
||||||
|
if (!service.isCurrentRequest(studio, ticket.key().requestId())) {
|
||||||
|
autosaves.remove(ticket.key(), ticket);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
JigsawStudioBay bay = resolveAutosaveBay(ticket);
|
||||||
|
if (bay == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!session.isDirtyCurrent(ticket.identity())) {
|
||||||
|
autosaves.remove(ticket.key(), ticket);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
JigsawStudioVariant activeVariant = session.activeVariant(bay.stableId()).orElse(null);
|
||||||
|
if (activeVariant == null || !activeVariant.owned()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
BayReadiness readiness = studio.population(bay).readiness();
|
||||||
|
if (!readiness.ready()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
CaptureTarget captureTarget;
|
||||||
|
try {
|
||||||
|
captureTarget = resolveCaptureTarget(studio, bay, activeVariant);
|
||||||
|
} catch (IOException exception) {
|
||||||
|
retainPersistentAutosaveFailure(
|
||||||
|
ticket,
|
||||||
|
new AutosavePersistentFailure(
|
||||||
|
"Jigsaw Studio cannot capture this workcell: " + failureMessage(exception),
|
||||||
|
exception));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioSession.SaveStart sessionSave = session.beginSave(bay.stableId());
|
||||||
|
if (sessionSave.status() != JigsawStudioSession.SaveStatus.STARTED) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioSession.SaveIdentity saveIdentity = sessionSave.identity().orElseThrow();
|
||||||
|
if (service.saveLifecycle.tryBeginSave(ticket.key().requestId()) != SaveStart.STARTED) {
|
||||||
|
session.abortSave(saveIdentity);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
List<ChunkCaptureArea> areas = chunkIntersections(captureTarget.bounds());
|
||||||
|
CaptureWork work = new CaptureWork(
|
||||||
|
studio,
|
||||||
|
studio.world(),
|
||||||
|
bay,
|
||||||
|
captureTarget,
|
||||||
|
null,
|
||||||
|
ticket.key().requestId(),
|
||||||
|
saveIdentity,
|
||||||
|
ticket.failureState());
|
||||||
|
CaptureCoordinator coordinator =
|
||||||
|
service.captureScheduler.coordinator(work, areas);
|
||||||
|
List<ChunkSnapshot> snapshots = new ArrayList<>(areas.size());
|
||||||
|
try {
|
||||||
|
for (ChunkCaptureArea area : areas) {
|
||||||
|
if (!studio.world().isChunkLoaded(area.chunkX(), area.chunkZ())) {
|
||||||
|
throw new IOException("Workcell chunk " + area.chunkX() + "," + area.chunkZ()
|
||||||
|
+ " is not loaded during the final autosave drain");
|
||||||
|
}
|
||||||
|
snapshots.add(captureChunkIntersection(
|
||||||
|
studio.world(),
|
||||||
|
captureTarget.bounds(),
|
||||||
|
captureTarget.piece(),
|
||||||
|
captureTarget.object(),
|
||||||
|
area,
|
||||||
|
captureTarget.displayRotationQuarterTurns(),
|
||||||
|
captureTarget.connectorsVisible()));
|
||||||
|
}
|
||||||
|
service.captureScheduler.assembleAndPersist(coordinator, snapshots);
|
||||||
|
} catch (Throwable exception) {
|
||||||
|
coordinator.fail("Jigsaw Studio final autosave drain failed: "
|
||||||
|
+ failureMessage(exception), exception);
|
||||||
|
}
|
||||||
|
return !session.isDirtyCurrent(ticket.identity());
|
||||||
|
}
|
||||||
|
|
||||||
|
void expediteAutosaves(UUID requestId) {
|
||||||
|
if (requestId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (AutosaveTicket current : List.copyOf(autosaves.values())) {
|
||||||
|
if (!requestId.equals(current.key().requestId())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
expediteAutosave(current, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void expediteAutosave(AutosaveTicket current, int delayTicks) {
|
||||||
|
AutosaveTicket expedited = new AutosaveTicket(
|
||||||
|
current.key(),
|
||||||
|
current.studio(),
|
||||||
|
current.identity(),
|
||||||
|
new AtomicBoolean(false),
|
||||||
|
current.scheduleFailureLogged(),
|
||||||
|
current.failureState());
|
||||||
|
if (autosaves.replace(current.key(), current, expedited)) {
|
||||||
|
scheduleAutosave(expedited, delayTicks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearAutosaves(UUID requestId) {
|
||||||
|
if (requestId != null) {
|
||||||
|
autosaves.keySet().removeIf(key -> key.requestId().equals(requestId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record AutosaveKey(UUID requestId, String workcellId) {
|
||||||
|
AutosaveKey {
|
||||||
|
Objects.requireNonNull(requestId, "Jigsaw Studio autosave request ID");
|
||||||
|
Objects.requireNonNull(workcellId, "Jigsaw Studio autosave workcell ID");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static final class AutosaveFailureState {
|
||||||
|
private final AtomicInteger persistentFailures = new AtomicInteger();
|
||||||
|
private final AtomicBoolean failureLogged = new AtomicBoolean(false);
|
||||||
|
|
||||||
|
AutosaveFailureDecision recordPersistentFailure() {
|
||||||
|
int failureCount = persistentFailures.incrementAndGet();
|
||||||
|
return new AutosaveFailureDecision(
|
||||||
|
persistentAutosaveRetryTicks(failureCount),
|
||||||
|
failureLogged.compareAndSet(false, true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record AutosaveFailureDecision(int retryTicks, boolean logFailure) {
|
||||||
|
}
|
||||||
|
|
||||||
|
record AutosavePersistentFailure(String detail, Throwable cause) {
|
||||||
|
AutosavePersistentFailure {
|
||||||
|
Objects.requireNonNull(detail, "Jigsaw Studio persistent autosave failure detail");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record AutosaveTicket(
|
||||||
|
AutosaveKey key,
|
||||||
|
ActiveStudio studio,
|
||||||
|
JigsawStudioSession.DirtyIdentity identity,
|
||||||
|
AtomicBoolean scheduled,
|
||||||
|
AtomicBoolean scheduleFailureLogged,
|
||||||
|
AutosaveFailureState failureState
|
||||||
|
) {
|
||||||
|
private AutosaveTicket(
|
||||||
|
AutosaveKey key,
|
||||||
|
ActiveStudio studio,
|
||||||
|
JigsawStudioSession.DirtyIdentity identity,
|
||||||
|
AtomicBoolean scheduled,
|
||||||
|
AtomicBoolean scheduleFailureLogged
|
||||||
|
) {
|
||||||
|
this(
|
||||||
|
key,
|
||||||
|
studio,
|
||||||
|
identity,
|
||||||
|
scheduled,
|
||||||
|
scheduleFailureLogged,
|
||||||
|
new AutosaveFailureState());
|
||||||
|
}
|
||||||
|
|
||||||
|
private AutosaveTicket {
|
||||||
|
Objects.requireNonNull(key, "Jigsaw Studio autosave key");
|
||||||
|
Objects.requireNonNull(studio, "Jigsaw Studio autosave studio");
|
||||||
|
Objects.requireNonNull(identity, "Jigsaw Studio autosave dirty identity");
|
||||||
|
Objects.requireNonNull(scheduled, "Jigsaw Studio autosave schedule state");
|
||||||
|
Objects.requireNonNull(scheduleFailureLogged, "Jigsaw Studio autosave schedule warning state");
|
||||||
|
Objects.requireNonNull(failureState, "Jigsaw Studio autosave failure state");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,697 @@
|
|||||||
|
package art.arcane.iris.core.service;
|
||||||
|
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawPlanarArchetype;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawPlanarDirection;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawPlanarTopology;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioBay;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioBounds;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioCellDimensions;
|
||||||
|
import art.arcane.iris.engine.object.IrisDirection;
|
||||||
|
import art.arcane.iris.engine.object.IrisJigsawConnector;
|
||||||
|
import art.arcane.iris.engine.object.IrisJigsawPiece;
|
||||||
|
import art.arcane.iris.engine.object.IrisObject;
|
||||||
|
import art.arcane.iris.engine.object.IrisObjectRotation;
|
||||||
|
import art.arcane.iris.engine.object.IrisPosition;
|
||||||
|
import art.arcane.iris.engine.object.JigsawJoint;
|
||||||
|
import art.arcane.iris.engine.object.TileData;
|
||||||
|
import art.arcane.iris.platform.bukkit.BukkitBlockState;
|
||||||
|
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||||
|
import art.arcane.iris.spi.PlatformBlockState;
|
||||||
|
import art.arcane.iris.util.common.data.B;
|
||||||
|
import art.arcane.volmlib.util.collection.KMap;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.block.Block;
|
||||||
|
import org.bukkit.block.data.BlockData;
|
||||||
|
import org.bukkit.block.data.type.Jigsaw;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioChunkWriter.restoreCapturedMetadataForDisplay;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioService.chunkKey;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioService.failureMessage;
|
||||||
|
|
||||||
|
final class JigsawStudioCapture {
|
||||||
|
private static final Comparator<IrisJigsawConnector> NEW_CONNECTOR_SOURCE_POSITION_ORDER = Comparator
|
||||||
|
.comparingInt((IrisJigsawConnector connector) -> connector.getPosition().getX())
|
||||||
|
.thenComparingInt(connector -> connector.getPosition().getY())
|
||||||
|
.thenComparingInt(connector -> connector.getPosition().getZ());
|
||||||
|
|
||||||
|
private JigsawStudioCapture() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<ChunkCaptureArea> chunkIntersections(JigsawStudioBounds bounds) {
|
||||||
|
JigsawStudioBounds captureBounds = Objects.requireNonNull(bounds, "Jigsaw Studio capture bounds");
|
||||||
|
List<ChunkCaptureArea> areas = new ArrayList<>();
|
||||||
|
int minimumChunkX = captureBounds.originX() >> 4;
|
||||||
|
int maximumChunkX = captureBounds.maxX() >> 4;
|
||||||
|
int minimumChunkZ = captureBounds.originZ() >> 4;
|
||||||
|
int maximumChunkZ = captureBounds.maxZ() >> 4;
|
||||||
|
for (int chunkX = minimumChunkX; chunkX <= maximumChunkX; chunkX++) {
|
||||||
|
int chunkOriginX = chunkX << 4;
|
||||||
|
int minimumX = Math.max(0, chunkOriginX - captureBounds.originX());
|
||||||
|
int maximumX = Math.min(
|
||||||
|
captureBounds.dimensions().width(),
|
||||||
|
chunkOriginX + 16 - captureBounds.originX());
|
||||||
|
for (int chunkZ = minimumChunkZ; chunkZ <= maximumChunkZ; chunkZ++) {
|
||||||
|
int chunkOriginZ = chunkZ << 4;
|
||||||
|
int minimumZ = Math.max(0, chunkOriginZ - captureBounds.originZ());
|
||||||
|
int maximumZ = Math.min(
|
||||||
|
captureBounds.dimensions().depth(),
|
||||||
|
chunkOriginZ + 16 - captureBounds.originZ());
|
||||||
|
areas.add(new ChunkCaptureArea(
|
||||||
|
chunkX,
|
||||||
|
chunkZ,
|
||||||
|
minimumX,
|
||||||
|
maximumX,
|
||||||
|
minimumZ,
|
||||||
|
maximumZ));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return List.copyOf(areas);
|
||||||
|
}
|
||||||
|
|
||||||
|
static ChunkSnapshot captureChunkIntersection(
|
||||||
|
World world,
|
||||||
|
JigsawStudioBounds bounds,
|
||||||
|
IrisJigsawPiece sourcePiece,
|
||||||
|
IrisObject sourceObject,
|
||||||
|
ChunkCaptureArea area
|
||||||
|
) throws IOException {
|
||||||
|
return captureChunkIntersection(world, bounds, sourcePiece, sourceObject, area, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static ChunkSnapshot captureChunkIntersection(
|
||||||
|
World world,
|
||||||
|
JigsawStudioBounds bounds,
|
||||||
|
IrisJigsawPiece sourcePiece,
|
||||||
|
IrisObject sourceObject,
|
||||||
|
ChunkCaptureArea area,
|
||||||
|
int displayRotationQuarterTurns
|
||||||
|
) throws IOException {
|
||||||
|
return captureChunkIntersection(
|
||||||
|
world,
|
||||||
|
bounds,
|
||||||
|
sourcePiece,
|
||||||
|
sourceObject,
|
||||||
|
area,
|
||||||
|
displayRotationQuarterTurns,
|
||||||
|
true);
|
||||||
|
}
|
||||||
|
|
||||||
|
static ChunkSnapshot captureChunkIntersection(
|
||||||
|
World world,
|
||||||
|
JigsawStudioBounds bounds,
|
||||||
|
IrisJigsawPiece sourcePiece,
|
||||||
|
IrisObject sourceObject,
|
||||||
|
ChunkCaptureArea area,
|
||||||
|
int displayRotationQuarterTurns,
|
||||||
|
boolean connectorsVisible
|
||||||
|
) throws IOException {
|
||||||
|
World captureWorld = Objects.requireNonNull(world, "Jigsaw Studio capture world");
|
||||||
|
JigsawStudioBounds captureBounds = Objects.requireNonNull(bounds, "Jigsaw Studio capture bounds");
|
||||||
|
IrisObject captureSource = Objects.requireNonNull(sourceObject, "Jigsaw Studio source object");
|
||||||
|
ChunkCaptureArea captureArea = Objects.requireNonNull(area, "Jigsaw Studio chunk capture area");
|
||||||
|
int quarterTurns = Math.floorMod(displayRotationQuarterTurns, 4);
|
||||||
|
int sourceWidth = (quarterTurns & 1) == 0
|
||||||
|
? captureBounds.dimensions().width()
|
||||||
|
: captureBounds.dimensions().depth();
|
||||||
|
int sourceDepth = (quarterTurns & 1) == 0
|
||||||
|
? captureBounds.dimensions().depth()
|
||||||
|
: captureBounds.dimensions().width();
|
||||||
|
List<CapturedBlock> blocks = new ArrayList<>();
|
||||||
|
List<CapturedConnector> connectors = new ArrayList<>();
|
||||||
|
Map<LocalPosition, IrisJigsawConnector> hiddenConnectors = connectorsVisible
|
||||||
|
? Map.of()
|
||||||
|
: displayedSourceConnectors(sourcePiece, captureBounds.dimensions(), quarterTurns);
|
||||||
|
for (int x = captureArea.minimumX(); x < captureArea.maximumX(); x++) {
|
||||||
|
for (int y = 0; y < bounds.dimensions().height(); y++) {
|
||||||
|
for (int z = captureArea.minimumZ(); z < captureArea.maximumZ(); z++) {
|
||||||
|
Block block = captureWorld.getBlockAt(
|
||||||
|
captureBounds.originX() + x,
|
||||||
|
captureBounds.originY() + y,
|
||||||
|
captureBounds.originZ() + z);
|
||||||
|
BlockData blockData = block.getBlockData();
|
||||||
|
IrisJigsawConnector hiddenConnector = hiddenConnectors.get(new LocalPosition(x, y, z));
|
||||||
|
if (hiddenConnector != null) {
|
||||||
|
hiddenConnector.setFinalState(blockData.getAsString());
|
||||||
|
connectors.add(CapturedConnector.from(hiddenConnector));
|
||||||
|
}
|
||||||
|
if (hiddenConnector == null && blockData instanceof Jigsaw jigsaw) {
|
||||||
|
KMap<String, Object> nbt = BukkitPlatform.serializeTile(block.getLocation());
|
||||||
|
if (nbt == null) {
|
||||||
|
throw new IOException("Cannot read jigsaw marker NBT at "
|
||||||
|
+ block.getX() + "," + block.getY() + "," + block.getZ()
|
||||||
|
+ ". The active NMS binding must support tile serialization.");
|
||||||
|
}
|
||||||
|
IrisJigsawConnector connector;
|
||||||
|
try {
|
||||||
|
connector = JigsawStudioMarkerParser.parse(nbt, jigsaw.getOrientation(), x, y, z);
|
||||||
|
} catch (IllegalArgumentException exception) {
|
||||||
|
throw new IOException("Invalid jigsaw marker at "
|
||||||
|
+ block.getX() + "," + block.getY() + "," + block.getZ()
|
||||||
|
+ ": " + failureMessage(exception), exception);
|
||||||
|
}
|
||||||
|
restoreCapturedMetadataForDisplay(
|
||||||
|
connector,
|
||||||
|
sourcePiece,
|
||||||
|
captureBounds.dimensions(),
|
||||||
|
quarterTurns);
|
||||||
|
BlockData finalData;
|
||||||
|
try {
|
||||||
|
finalData = Bukkit.createBlockData(connector.getFinalState());
|
||||||
|
} catch (IllegalArgumentException exception) {
|
||||||
|
throw new IOException("Invalid final_state '" + connector.getFinalState()
|
||||||
|
+ "' at " + block.getX() + "," + block.getY() + "," + block.getZ(), exception);
|
||||||
|
}
|
||||||
|
connector.setFinalState(finalData.getAsString());
|
||||||
|
connectors.add(CapturedConnector.from(connector));
|
||||||
|
if (finalData.getMaterial() != Material.STRUCTURE_VOID) {
|
||||||
|
blocks.add(new CapturedBlock(
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
z,
|
||||||
|
BukkitBlockState.of(finalData),
|
||||||
|
null));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (isAir(blockData.getMaterial())) {
|
||||||
|
LocalPosition sourcePosition = inversePosition(
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
z,
|
||||||
|
sourceWidth,
|
||||||
|
sourceDepth,
|
||||||
|
quarterTurns);
|
||||||
|
PlatformBlockState retainedAir = retainedSourceAir(
|
||||||
|
captureSource,
|
||||||
|
sourcePosition.x(),
|
||||||
|
sourcePosition.y(),
|
||||||
|
sourcePosition.z(),
|
||||||
|
blockData);
|
||||||
|
if (retainedAir != null) {
|
||||||
|
blocks.add(new CapturedBlock(x, y, z, retainedAir, null));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (blockData.getMaterial() == Material.STRUCTURE_VOID) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
blocks.add(new CapturedBlock(
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
z,
|
||||||
|
BukkitBlockState.of(blockData),
|
||||||
|
TileData.getTileState(block, false)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new ChunkSnapshot(captureArea, blocks, connectors);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<LocalPosition, IrisJigsawConnector> displayedSourceConnectors(
|
||||||
|
IrisJigsawPiece sourcePiece,
|
||||||
|
JigsawStudioCellDimensions displayDimensions,
|
||||||
|
int displayRotationQuarterTurns
|
||||||
|
) throws IOException {
|
||||||
|
IrisJigsawPiece source = Objects.requireNonNull(sourcePiece, "Jigsaw Studio source piece");
|
||||||
|
if (source.getConnectors() == null) {
|
||||||
|
throw new IOException("Jigsaw Studio source piece has no connector list");
|
||||||
|
}
|
||||||
|
int quarterTurns = Math.floorMod(displayRotationQuarterTurns, 4);
|
||||||
|
int sourceWidth = (quarterTurns & 1) == 0
|
||||||
|
? displayDimensions.width()
|
||||||
|
: displayDimensions.depth();
|
||||||
|
int sourceDepth = (quarterTurns & 1) == 0
|
||||||
|
? displayDimensions.depth()
|
||||||
|
: displayDimensions.width();
|
||||||
|
IrisObjectRotation rotation = IrisObjectRotation.of(0, -90.0D * quarterTurns, 0);
|
||||||
|
Map<LocalPosition, IrisJigsawConnector> displayed = new HashMap<>(source.getConnectors().size());
|
||||||
|
for (IrisJigsawConnector sourceConnector : source.getConnectors()) {
|
||||||
|
LocalPosition sourcePosition = connectorPosition(sourceConnector, "source piece");
|
||||||
|
LocalPosition position = forwardPosition(
|
||||||
|
sourcePosition.x(),
|
||||||
|
sourcePosition.y(),
|
||||||
|
sourcePosition.z(),
|
||||||
|
sourceWidth,
|
||||||
|
sourceDepth,
|
||||||
|
quarterTurns);
|
||||||
|
IrisJigsawConnector connector = CapturedConnector.from(sourceConnector).toConnector()
|
||||||
|
.setPosition(new IrisPosition(position.x(), position.y(), position.z()))
|
||||||
|
.setDirection(rotation.rotate(sourceConnector.getDirection()))
|
||||||
|
.setTop(rotation.rotate(sourceConnector.getTop()));
|
||||||
|
if (displayed.put(position, connector) != null) {
|
||||||
|
throw new IOException("Jigsaw Studio source piece has duplicate displayed connector position "
|
||||||
|
+ connectorLocation(position));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Map.copyOf(displayed);
|
||||||
|
}
|
||||||
|
|
||||||
|
static PlatformBlockState retainedSourceAir(
|
||||||
|
IrisObject sourceObject,
|
||||||
|
int x,
|
||||||
|
int y,
|
||||||
|
int z,
|
||||||
|
BlockData current
|
||||||
|
) {
|
||||||
|
IrisObject source = Objects.requireNonNull(sourceObject, "Jigsaw Studio source object");
|
||||||
|
BlockData currentState = Objects.requireNonNull(current, "Jigsaw Studio current block state");
|
||||||
|
if (!isAir(currentState.getMaterial())
|
||||||
|
|| x < 0
|
||||||
|
|| y < 0
|
||||||
|
|| z < 0
|
||||||
|
|| x >= source.getW()
|
||||||
|
|| y >= source.getH()
|
||||||
|
|| z >= source.getD()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
PlatformBlockState original = source.getBlocks().get(source.getSigned(x, y, z));
|
||||||
|
return original != null && original.isAir() ? original : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isAir(Material material) {
|
||||||
|
return material == Material.AIR || material == Material.CAVE_AIR || material == Material.VOID_AIR;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Capture aggregateSnapshots(
|
||||||
|
JigsawStudioBounds bounds,
|
||||||
|
List<ChunkCaptureArea> expectedAreas,
|
||||||
|
List<ChunkSnapshot> snapshots
|
||||||
|
) throws IOException {
|
||||||
|
return aggregateSnapshots(bounds, expectedAreas, snapshots, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Capture aggregateSnapshots(
|
||||||
|
JigsawStudioBounds bounds,
|
||||||
|
List<ChunkCaptureArea> expectedAreas,
|
||||||
|
List<ChunkSnapshot> snapshots,
|
||||||
|
int displayRotationQuarterTurns
|
||||||
|
) throws IOException {
|
||||||
|
JigsawStudioBounds captureBounds = Objects.requireNonNull(bounds, "Jigsaw Studio capture bounds");
|
||||||
|
int quarterTurns = Math.floorMod(displayRotationQuarterTurns, 4);
|
||||||
|
List<ChunkCaptureArea> requiredAreas = List.copyOf(expectedAreas);
|
||||||
|
List<ChunkSnapshot> capturedSnapshots = List.copyOf(snapshots);
|
||||||
|
Map<Long, ChunkCaptureArea> requiredByChunk = new HashMap<>();
|
||||||
|
for (ChunkCaptureArea area : requiredAreas) {
|
||||||
|
if (requiredByChunk.put(chunkKey(area.chunkX(), area.chunkZ()), area) != null) {
|
||||||
|
throw new IOException("Duplicate required bay chunk " + area.chunkX() + "," + area.chunkZ());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (capturedSnapshots.size() != requiredByChunk.size()) {
|
||||||
|
throw new IOException("Expected " + requiredByChunk.size() + " bay chunk snapshot(s), received "
|
||||||
|
+ capturedSnapshots.size());
|
||||||
|
}
|
||||||
|
Map<Long, ChunkSnapshot> capturedByChunk = new HashMap<>();
|
||||||
|
for (ChunkSnapshot snapshot : capturedSnapshots) {
|
||||||
|
ChunkCaptureArea area = snapshot.area();
|
||||||
|
long key = chunkKey(area.chunkX(), area.chunkZ());
|
||||||
|
ChunkCaptureArea required = requiredByChunk.get(key);
|
||||||
|
if (!area.equals(required)) {
|
||||||
|
throw new IOException("Unexpected bay chunk intersection for "
|
||||||
|
+ area.chunkX() + "," + area.chunkZ());
|
||||||
|
}
|
||||||
|
if (capturedByChunk.put(key, snapshot) != null) {
|
||||||
|
throw new IOException("Duplicate bay chunk snapshot " + area.chunkX() + "," + area.chunkZ());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!capturedByChunk.keySet().containsAll(requiredByChunk.keySet())) {
|
||||||
|
throw new IOException("One or more required bay chunk snapshots are missing");
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ChunkSnapshot> ordered = new ArrayList<>(capturedSnapshots);
|
||||||
|
ordered.sort(Comparator
|
||||||
|
.comparingInt((ChunkSnapshot snapshot) -> snapshot.area().chunkX())
|
||||||
|
.thenComparingInt(snapshot -> snapshot.area().chunkZ()));
|
||||||
|
int sourceWidth = (quarterTurns & 1) == 0
|
||||||
|
? captureBounds.dimensions().width()
|
||||||
|
: captureBounds.dimensions().depth();
|
||||||
|
int sourceDepth = (quarterTurns & 1) == 0
|
||||||
|
? captureBounds.dimensions().depth()
|
||||||
|
: captureBounds.dimensions().width();
|
||||||
|
IrisObject object = new IrisObject(
|
||||||
|
sourceWidth,
|
||||||
|
captureBounds.dimensions().height(),
|
||||||
|
sourceDepth);
|
||||||
|
IrisObjectRotation inverseRotation = IrisObjectRotation.of(0, 90.0D * quarterTurns, 0);
|
||||||
|
List<IrisJigsawConnector> connectors = new ArrayList<>();
|
||||||
|
Set<LocalPosition> capturedBlocks = new HashSet<>();
|
||||||
|
Set<LocalPosition> capturedConnectors = new HashSet<>();
|
||||||
|
boolean hasBlockEntities = false;
|
||||||
|
for (ChunkSnapshot snapshot : ordered) {
|
||||||
|
ChunkCaptureArea area = snapshot.area();
|
||||||
|
for (CapturedBlock block : snapshot.blocks()) {
|
||||||
|
if (!area.contains(block.x(), block.z())
|
||||||
|
|| block.y() < 0
|
||||||
|
|| block.y() >= captureBounds.dimensions().height()) {
|
||||||
|
throw new IOException("Captured block falls outside bay chunk "
|
||||||
|
+ area.chunkX() + "," + area.chunkZ());
|
||||||
|
}
|
||||||
|
LocalPosition position = inversePosition(
|
||||||
|
block.x(), block.y(), block.z(), sourceWidth, sourceDepth, quarterTurns);
|
||||||
|
if (!capturedBlocks.add(position)) {
|
||||||
|
throw new IOException("Duplicate captured block at "
|
||||||
|
+ block.x() + "," + block.y() + "," + block.z());
|
||||||
|
}
|
||||||
|
PlatformBlockState sourceState = quarterTurns == 0
|
||||||
|
? block.state()
|
||||||
|
: inverseRotation.rotate(block.state(), 0, 0, 0);
|
||||||
|
if (sourceState == null) {
|
||||||
|
throw new IOException("Captured block state cannot be inverse-rotated at "
|
||||||
|
+ block.x() + "," + block.y() + "," + block.z());
|
||||||
|
}
|
||||||
|
object.setUnsigned(position.x(), position.y(), position.z(), sourceState);
|
||||||
|
TileData tileData = block.tileData();
|
||||||
|
if (tileData != null) {
|
||||||
|
object.setUnsignedTile(position.x(), position.y(), position.z(), tileData.clone());
|
||||||
|
hasBlockEntities = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (CapturedConnector capturedConnector : snapshot.connectors()) {
|
||||||
|
if (!area.contains(capturedConnector.x(), capturedConnector.z())
|
||||||
|
|| capturedConnector.y() < 0
|
||||||
|
|| capturedConnector.y() >= captureBounds.dimensions().height()) {
|
||||||
|
throw new IOException("Captured connector falls outside bay chunk "
|
||||||
|
+ area.chunkX() + "," + area.chunkZ());
|
||||||
|
}
|
||||||
|
LocalPosition position = inversePosition(
|
||||||
|
capturedConnector.x(),
|
||||||
|
capturedConnector.y(),
|
||||||
|
capturedConnector.z(),
|
||||||
|
sourceWidth,
|
||||||
|
sourceDepth,
|
||||||
|
quarterTurns);
|
||||||
|
if (!capturedConnectors.add(position)) {
|
||||||
|
throw new IOException("Duplicate captured connector at "
|
||||||
|
+ capturedConnector.x() + "," + capturedConnector.y() + ","
|
||||||
|
+ capturedConnector.z());
|
||||||
|
}
|
||||||
|
IrisJigsawConnector connector = capturedConnector.toConnector();
|
||||||
|
if (quarterTurns != 0) {
|
||||||
|
connector.setPosition(new IrisPosition(position.x(), position.y(), position.z()));
|
||||||
|
connector.setDirection(inverseRotation.rotate(connector.getDirection()));
|
||||||
|
connector.setTop(inverseRotation.rotate(connector.getTop()));
|
||||||
|
PlatformBlockState finalState = B.getStateOrNull(connector.getFinalState(), false);
|
||||||
|
if (finalState == null) {
|
||||||
|
throw new IOException("Captured connector final state cannot be parsed at "
|
||||||
|
+ capturedConnector.x() + "," + capturedConnector.y() + ","
|
||||||
|
+ capturedConnector.z());
|
||||||
|
}
|
||||||
|
PlatformBlockState sourceFinalState = inverseRotation.rotate(finalState, 0, 0, 0);
|
||||||
|
if (sourceFinalState == null) {
|
||||||
|
throw new IOException("Captured connector final state cannot be inverse-rotated at "
|
||||||
|
+ capturedConnector.x() + "," + capturedConnector.y() + ","
|
||||||
|
+ capturedConnector.z());
|
||||||
|
}
|
||||||
|
connector.setFinalState(sourceFinalState.key());
|
||||||
|
}
|
||||||
|
connectors.add(connector);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try (ByteArrayOutputStream output = new ByteArrayOutputStream()) {
|
||||||
|
object.write(output);
|
||||||
|
return new Capture(output.toByteArray(), connectors, hasBlockEntities);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<IrisJigsawConnector> preserveCapturedConnectorOrder(
|
||||||
|
IrisJigsawPiece sourcePiece,
|
||||||
|
List<IrisJigsawConnector> capturedConnectors
|
||||||
|
) throws IOException {
|
||||||
|
IrisJigsawPiece source = Objects.requireNonNull(sourcePiece, "Jigsaw Studio source piece");
|
||||||
|
List<IrisJigsawConnector> sourceConnectors = source.getConnectors();
|
||||||
|
if (sourceConnectors == null) {
|
||||||
|
throw new IOException("Jigsaw Studio source piece has no connector list");
|
||||||
|
}
|
||||||
|
List<IrisJigsawConnector> captured = new ArrayList<>(Objects.requireNonNull(
|
||||||
|
capturedConnectors,
|
||||||
|
"Jigsaw Studio captured connectors"));
|
||||||
|
Map<LocalPosition, IrisJigsawConnector> capturedByPosition = new HashMap<>(captured.size());
|
||||||
|
for (IrisJigsawConnector connector : captured) {
|
||||||
|
LocalPosition position = connectorPosition(connector, "captured");
|
||||||
|
if (capturedByPosition.put(position, connector) != null) {
|
||||||
|
throw new IOException("Jigsaw Studio capture has duplicate connector position "
|
||||||
|
+ connectorLocation(position));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<IrisJigsawConnector> ordered = new ArrayList<>(captured.size());
|
||||||
|
Set<LocalPosition> sourcePositions = new HashSet<>(sourceConnectors.size());
|
||||||
|
for (IrisJigsawConnector connector : sourceConnectors) {
|
||||||
|
LocalPosition position = connectorPosition(connector, "source piece");
|
||||||
|
if (!sourcePositions.add(position)) {
|
||||||
|
throw new IOException("Jigsaw Studio source piece has duplicate connector position "
|
||||||
|
+ connectorLocation(position));
|
||||||
|
}
|
||||||
|
IrisJigsawConnector matched = capturedByPosition.remove(position);
|
||||||
|
if (matched != null) {
|
||||||
|
ordered.add(matched);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<IrisJigsawConnector> appended = new ArrayList<>(capturedByPosition.values());
|
||||||
|
appended.sort(NEW_CONNECTOR_SOURCE_POSITION_ORDER);
|
||||||
|
ordered.addAll(appended);
|
||||||
|
return List.copyOf(ordered);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LocalPosition connectorPosition(IrisJigsawConnector connector, String owner) throws IOException {
|
||||||
|
if (connector == null || connector.getPosition() == null) {
|
||||||
|
throw new IOException("Jigsaw Studio " + owner + " contains a connector without a position");
|
||||||
|
}
|
||||||
|
IrisPosition position = connector.getPosition();
|
||||||
|
return new LocalPosition(position.getX(), position.getY(), position.getZ());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String connectorLocation(LocalPosition position) {
|
||||||
|
return position.x() + "," + position.y() + "," + position.z();
|
||||||
|
}
|
||||||
|
|
||||||
|
static LocalPosition inversePosition(
|
||||||
|
int x,
|
||||||
|
int y,
|
||||||
|
int z,
|
||||||
|
int sourceWidth,
|
||||||
|
int sourceDepth,
|
||||||
|
int displayRotationQuarterTurns
|
||||||
|
) {
|
||||||
|
return switch (Math.floorMod(displayRotationQuarterTurns, 4)) {
|
||||||
|
case 0 -> new LocalPosition(x, y, z);
|
||||||
|
case 1 -> new LocalPosition(z, y, sourceDepth - 1 - x);
|
||||||
|
case 2 -> new LocalPosition(sourceWidth - 1 - x, y, sourceDepth - 1 - z);
|
||||||
|
case 3 -> new LocalPosition(sourceWidth - 1 - z, y, x);
|
||||||
|
default -> throw new IllegalStateException("Unreachable Jigsaw Studio inverse rotation");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LocalPosition forwardPosition(
|
||||||
|
int x,
|
||||||
|
int y,
|
||||||
|
int z,
|
||||||
|
int sourceWidth,
|
||||||
|
int sourceDepth,
|
||||||
|
int displayRotationQuarterTurns
|
||||||
|
) {
|
||||||
|
return switch (Math.floorMod(displayRotationQuarterTurns, 4)) {
|
||||||
|
case 0 -> new LocalPosition(x, y, z);
|
||||||
|
case 1 -> new LocalPosition(sourceDepth - 1 - z, y, x);
|
||||||
|
case 2 -> new LocalPosition(sourceWidth - 1 - x, y, sourceDepth - 1 - z);
|
||||||
|
case 3 -> new LocalPosition(z, y, sourceWidth - 1 - x);
|
||||||
|
default -> throw new IllegalStateException("Unreachable Jigsaw Studio display rotation");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static void requireWorkcellTopology(
|
||||||
|
JigsawStudioBay workcell,
|
||||||
|
List<IrisJigsawConnector> connectors,
|
||||||
|
int displayRotationQuarterTurns
|
||||||
|
) throws IOException {
|
||||||
|
JigsawPlanarArchetype expected = workcell.archetype().orElse(null);
|
||||||
|
if (expected == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int mask = 0;
|
||||||
|
for (IrisJigsawConnector connector : connectors) {
|
||||||
|
mask |= switch (connector.getDirection()) {
|
||||||
|
case NORTH_NEGATIVE_Z -> JigsawPlanarDirection.NORTH.bit();
|
||||||
|
case EAST_POSITIVE_X -> JigsawPlanarDirection.EAST.bit();
|
||||||
|
case SOUTH_POSITIVE_Z -> JigsawPlanarDirection.SOUTH.bit();
|
||||||
|
case WEST_NEGATIVE_X -> JigsawPlanarDirection.WEST.bit();
|
||||||
|
case UP_POSITIVE_Y, DOWN_NEGATIVE_Y -> throw new WorkcellTopologyException(
|
||||||
|
"Planar workcell '" + workcell.stableId()
|
||||||
|
+ "' cannot save a vertical connector. Remove it or use Reset Connector Blocks.");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
JigsawPlanarTopology sourceTopology = JigsawPlanarTopology.fromMask(mask);
|
||||||
|
JigsawPlanarTopology displayedTopology = sourceTopology.rotateClockwise(displayRotationQuarterTurns);
|
||||||
|
if (displayedTopology != expected.canonicalTopology()) {
|
||||||
|
throw new WorkcellTopologyException("Workcell '" + workcell.stableId() + "' requires "
|
||||||
|
+ topologyDescription(expected.canonicalTopology())
|
||||||
|
+ ", but the edited markers form "
|
||||||
|
+ topologyDescription(displayedTopology)
|
||||||
|
+ ". Use Reset Connector Blocks to restore the saved topology, or edit the markers to match the floor glyph.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String topologyDescription(JigsawPlanarTopology topology) {
|
||||||
|
int connectorCount = topology.directions().size();
|
||||||
|
return topology.name().toLowerCase(Locale.ROOT).replace('_', ' ')
|
||||||
|
+ " (" + connectorCount + " horizontal connector"
|
||||||
|
+ (connectorCount == 1 ? "" : "s") + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
static void storeConnectorFinalState(
|
||||||
|
IrisObject object,
|
||||||
|
int x,
|
||||||
|
int y,
|
||||||
|
int z,
|
||||||
|
BlockData finalData
|
||||||
|
) {
|
||||||
|
Objects.requireNonNull(object, "Jigsaw Studio captured object");
|
||||||
|
BlockData activeFinalData = Objects.requireNonNull(finalData, "Jigsaw connector final state");
|
||||||
|
if (activeFinalData.getMaterial() != Material.STRUCTURE_VOID) {
|
||||||
|
object.setUnsigned(x, y, z, BukkitBlockState.of(activeFinalData));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record ChunkCaptureArea(
|
||||||
|
int chunkX,
|
||||||
|
int chunkZ,
|
||||||
|
int minimumX,
|
||||||
|
int maximumX,
|
||||||
|
int minimumZ,
|
||||||
|
int maximumZ
|
||||||
|
) {
|
||||||
|
ChunkCaptureArea {
|
||||||
|
if (minimumX < 0 || minimumZ < 0 || maximumX <= minimumX || maximumZ <= minimumZ) {
|
||||||
|
throw new IllegalArgumentException("Jigsaw Studio chunk capture intersection is invalid");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean contains(int x, int z) {
|
||||||
|
return x >= minimumX && x < maximumX && z >= minimumZ && z < maximumZ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record CapturedBlock(
|
||||||
|
int x,
|
||||||
|
int y,
|
||||||
|
int z,
|
||||||
|
PlatformBlockState state,
|
||||||
|
TileData tileData
|
||||||
|
) {
|
||||||
|
CapturedBlock {
|
||||||
|
Objects.requireNonNull(state, "Jigsaw Studio captured block state");
|
||||||
|
tileData = tileData == null ? null : tileData.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
public TileData tileData() {
|
||||||
|
return tileData == null ? null : tileData.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record CapturedConnector(
|
||||||
|
int x,
|
||||||
|
int y,
|
||||||
|
int z,
|
||||||
|
IrisDirection direction,
|
||||||
|
IrisDirection top,
|
||||||
|
String pool,
|
||||||
|
String name,
|
||||||
|
String targetName,
|
||||||
|
String channel,
|
||||||
|
JigsawJoint joint,
|
||||||
|
String finalState,
|
||||||
|
int selectionPriority,
|
||||||
|
int placementPriority
|
||||||
|
) {
|
||||||
|
CapturedConnector {
|
||||||
|
Objects.requireNonNull(direction, "Jigsaw Studio captured connector direction");
|
||||||
|
Objects.requireNonNull(top, "Jigsaw Studio captured connector top");
|
||||||
|
Objects.requireNonNull(pool, "Jigsaw Studio captured connector pool");
|
||||||
|
Objects.requireNonNull(name, "Jigsaw Studio captured connector name");
|
||||||
|
Objects.requireNonNull(targetName, "Jigsaw Studio captured connector target");
|
||||||
|
channel = channel == null ? "" : channel;
|
||||||
|
Objects.requireNonNull(joint, "Jigsaw Studio captured connector joint");
|
||||||
|
Objects.requireNonNull(finalState, "Jigsaw Studio captured connector final state");
|
||||||
|
}
|
||||||
|
|
||||||
|
static CapturedConnector from(IrisJigsawConnector connector) {
|
||||||
|
IrisJigsawConnector source = Objects.requireNonNull(connector, "Jigsaw Studio captured connector");
|
||||||
|
IrisPosition position = source.getPosition();
|
||||||
|
return new CapturedConnector(
|
||||||
|
position.getX(),
|
||||||
|
position.getY(),
|
||||||
|
position.getZ(),
|
||||||
|
source.getDirection(),
|
||||||
|
source.getTop(),
|
||||||
|
source.getPool(),
|
||||||
|
source.getName(),
|
||||||
|
source.getTargetName(),
|
||||||
|
source.getChannel(),
|
||||||
|
source.getJoint(),
|
||||||
|
source.getFinalState(),
|
||||||
|
source.getSelectionPriority(),
|
||||||
|
source.getPlacementPriority());
|
||||||
|
}
|
||||||
|
|
||||||
|
IrisJigsawConnector toConnector() {
|
||||||
|
return new IrisJigsawConnector()
|
||||||
|
.setPosition(new IrisPosition(x, y, z))
|
||||||
|
.setDirection(direction)
|
||||||
|
.setTop(top)
|
||||||
|
.setPool(pool)
|
||||||
|
.setName(name)
|
||||||
|
.setTargetName(targetName)
|
||||||
|
.setChannel(channel)
|
||||||
|
.setJoint(joint)
|
||||||
|
.setFinalState(finalState)
|
||||||
|
.setSelectionPriority(selectionPriority)
|
||||||
|
.setPlacementPriority(placementPriority);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record ChunkSnapshot(
|
||||||
|
ChunkCaptureArea area,
|
||||||
|
List<CapturedBlock> blocks,
|
||||||
|
List<CapturedConnector> connectors
|
||||||
|
) {
|
||||||
|
ChunkSnapshot {
|
||||||
|
Objects.requireNonNull(area, "Jigsaw Studio captured chunk area");
|
||||||
|
blocks = List.copyOf(blocks);
|
||||||
|
connectors = List.copyOf(connectors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record LocalPosition(int x, int y, int z) {
|
||||||
|
}
|
||||||
|
|
||||||
|
record Capture(byte[] objectContent, List<IrisJigsawConnector> connectors, boolean hasBlockEntities) {
|
||||||
|
Capture {
|
||||||
|
objectContent = Objects.requireNonNull(objectContent, "Jigsaw Studio object content").clone();
|
||||||
|
connectors = List.copyOf(connectors);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] objectContent() {
|
||||||
|
return objectContent.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static final class WorkcellTopologyException extends IOException {
|
||||||
|
WorkcellTopologyException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,493 @@
|
|||||||
|
package art.arcane.iris.core.service;
|
||||||
|
|
||||||
|
import art.arcane.iris.core.loader.IrisData;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioActivation;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioBay;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioBounds;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioCellDimensions;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioLayout;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioSession;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioVariant;
|
||||||
|
import art.arcane.iris.core.service.JigsawStudioAutosaveScheduler.AutosaveFailureState;
|
||||||
|
import art.arcane.iris.core.service.JigsawStudioAutosaveScheduler.AutosavePersistentFailure;
|
||||||
|
import art.arcane.iris.core.service.JigsawStudioCapture.Capture;
|
||||||
|
import art.arcane.iris.core.service.JigsawStudioCapture.ChunkCaptureArea;
|
||||||
|
import art.arcane.iris.core.service.JigsawStudioCapture.ChunkSnapshot;
|
||||||
|
import art.arcane.iris.core.service.JigsawStudioCapture.WorkcellTopologyException;
|
||||||
|
import art.arcane.iris.core.service.JigsawStudioService.ActiveStudio;
|
||||||
|
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 art.arcane.iris.engine.object.IrisJigsawConnector;
|
||||||
|
import art.arcane.iris.engine.object.IrisJigsawPiece;
|
||||||
|
import art.arcane.iris.engine.object.IrisObject;
|
||||||
|
import art.arcane.iris.engine.platform.studio.generators.JigsawStudioGenerator;
|
||||||
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
|
import art.arcane.iris.util.common.scheduling.J;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
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.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioCapture.aggregateSnapshots;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioCapture.captureChunkIntersection;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioCapture.chunkIntersections;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioCapture.preserveCapturedConnectorOrder;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioCapture.requireWorkcellTopology;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioService.chunkKey;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioService.failureMessage;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioService.message;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioService.playSaveSound;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioService.writeFailure;
|
||||||
|
|
||||||
|
final class JigsawStudioCaptureScheduler {
|
||||||
|
private final JigsawStudioService service;
|
||||||
|
|
||||||
|
JigsawStudioCaptureScheduler(JigsawStudioService service) {
|
||||||
|
this.service = Objects.requireNonNull(service, "Jigsaw Studio service");
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean scheduleCapture(
|
||||||
|
ActiveStudio studio,
|
||||||
|
World world,
|
||||||
|
JigsawStudioBay bay,
|
||||||
|
CaptureTarget captureTarget,
|
||||||
|
Player player,
|
||||||
|
UUID requestId,
|
||||||
|
JigsawStudioSession.SaveIdentity saveIdentity,
|
||||||
|
AutosaveFailureState autosaveFailureState
|
||||||
|
) {
|
||||||
|
List<ChunkCaptureArea> areas = chunkIntersections(captureTarget.bounds());
|
||||||
|
CaptureWork work = new CaptureWork(
|
||||||
|
studio,
|
||||||
|
world,
|
||||||
|
bay,
|
||||||
|
captureTarget,
|
||||||
|
player,
|
||||||
|
requestId,
|
||||||
|
saveIdentity,
|
||||||
|
autosaveFailureState);
|
||||||
|
CaptureCoordinator coordinator = new CaptureCoordinator(
|
||||||
|
work,
|
||||||
|
areas);
|
||||||
|
for (ChunkCaptureArea area : areas) {
|
||||||
|
if (coordinator.stopped()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
boolean scheduled = J.runRegion(
|
||||||
|
world,
|
||||||
|
area.chunkX(),
|
||||||
|
area.chunkZ(),
|
||||||
|
() -> captureChunk(coordinator, area));
|
||||||
|
if (!scheduled) {
|
||||||
|
coordinator.fail("Iris could not schedule bay chunk "
|
||||||
|
+ area.chunkX() + "," + area.chunkZ() + " on its owning region.", null);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return !coordinator.stopped();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void captureChunk(CaptureCoordinator coordinator, ChunkCaptureArea area) {
|
||||||
|
if (coordinator.stopped()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ActiveStudio studio = coordinator.studio();
|
||||||
|
World world = coordinator.world();
|
||||||
|
if (!service.saveLifecycle.isCurrentSave(studio, coordinator.requestId(), coordinator.saveIdentity())) {
|
||||||
|
coordinator.fail("Jigsaw Studio changed before every bay chunk could be captured; save cancelled.", null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!world.isChunkLoaded(area.chunkX(), area.chunkZ())) {
|
||||||
|
coordinator.fail("Bay chunk " + area.chunkX() + "," + area.chunkZ()
|
||||||
|
+ " is not loaded. Visit the whole bay and try again.", null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
ChunkSnapshot snapshot = captureChunkIntersection(
|
||||||
|
world,
|
||||||
|
coordinator.captureTarget().bounds(),
|
||||||
|
coordinator.captureTarget().piece(),
|
||||||
|
coordinator.captureTarget().object(),
|
||||||
|
area,
|
||||||
|
coordinator.captureTarget().displayRotationQuarterTurns(),
|
||||||
|
coordinator.captureTarget().connectorsVisible());
|
||||||
|
if (!service.saveLifecycle.isCurrentSave(studio, coordinator.requestId(), coordinator.saveIdentity())) {
|
||||||
|
coordinator.fail("Jigsaw Studio changed while bay chunks were being captured; save cancelled.", null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
coordinator.accept(snapshot);
|
||||||
|
} catch (Throwable exception) {
|
||||||
|
coordinator.fail("Jigsaw Studio capture failed in chunk "
|
||||||
|
+ area.chunkX() + "," + area.chunkZ() + ": " + failureMessage(exception), exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void assembleAndPersist(CaptureCoordinator coordinator, List<ChunkSnapshot> snapshots) {
|
||||||
|
if (coordinator.failed()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ActiveStudio studio = coordinator.studio();
|
||||||
|
if (!service.saveLifecycle.isCurrentSave(studio, coordinator.requestId(), coordinator.saveIdentity())) {
|
||||||
|
coordinator.fail("Jigsaw Studio changed before captured chunks could be assembled; save cancelled.", null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Capture capture = aggregateSnapshots(
|
||||||
|
coordinator.captureTarget().bounds(),
|
||||||
|
coordinator.areas(),
|
||||||
|
snapshots,
|
||||||
|
coordinator.captureTarget().displayRotationQuarterTurns());
|
||||||
|
List<IrisJigsawConnector> connectors = preserveCapturedConnectorOrder(
|
||||||
|
coordinator.captureTarget().piece(),
|
||||||
|
capture.connectors());
|
||||||
|
requireWorkcellTopology(
|
||||||
|
coordinator.bay(),
|
||||||
|
connectors,
|
||||||
|
coordinator.captureTarget().displayRotationQuarterTurns());
|
||||||
|
if (!service.saveLifecycle.isCurrentSave(studio, coordinator.requestId(), coordinator.saveIdentity())) {
|
||||||
|
coordinator.fail("Jigsaw Studio changed before the captured bay could be written; save cancelled.", null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
message(coordinator.player(), "Captured " + connectors.size()
|
||||||
|
+ " connector(s) from " + snapshots.size()
|
||||||
|
+ " chunk(s); validating and writing the owned structure graph...");
|
||||||
|
persistCapture(coordinator, capture, connectors);
|
||||||
|
} catch (WorkcellTopologyException exception) {
|
||||||
|
coordinator.failPersistent(
|
||||||
|
"Jigsaw Studio cannot autosave this planar workcell: "
|
||||||
|
+ failureMessage(exception),
|
||||||
|
null);
|
||||||
|
} catch (Throwable exception) {
|
||||||
|
coordinator.failPersistent(
|
||||||
|
"Jigsaw Studio capture assembly failed: " + failureMessage(exception),
|
||||||
|
exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void persistCapture(
|
||||||
|
CaptureCoordinator coordinator,
|
||||||
|
Capture capture,
|
||||||
|
List<IrisJigsawConnector> connectors
|
||||||
|
) {
|
||||||
|
ActiveStudio studio = coordinator.studio();
|
||||||
|
JigsawStudioBay bay = coordinator.bay();
|
||||||
|
Player player = coordinator.player();
|
||||||
|
try {
|
||||||
|
if (!service.saveLifecycle.isCurrentSave(studio, coordinator.requestId(), coordinator.saveIdentity())) {
|
||||||
|
message(player, "Jigsaw Studio changed before the captured bay could be written; save cancelled.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
JigsawStudioActivation.Request request = studio.generator().getRequest();
|
||||||
|
IrisData source = request.source();
|
||||||
|
Path packRoot = source.getDataFolder().toPath();
|
||||||
|
JigsawStudioResourceBundleAssembler.Assembly assembly =
|
||||||
|
JigsawStudioResourceBundleAssembler.assemble(
|
||||||
|
packRoot,
|
||||||
|
request.structureKey(),
|
||||||
|
coordinator.saveIdentity().variantKey(),
|
||||||
|
capture.objectContent(),
|
||||||
|
connectors,
|
||||||
|
capture.hasBlockEntities());
|
||||||
|
StructureResourceBundleGraphCompiler.requireViable(assembly.bundle());
|
||||||
|
if (!service.saveLifecycle.isCurrentSave(studio, coordinator.requestId(), coordinator.saveIdentity())) {
|
||||||
|
message(player, "Jigsaw Studio changed during validation; save cancelled before writing.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
JigsawStudioHistoryStore historyStore = new JigsawStudioHistoryStore(
|
||||||
|
packRoot,
|
||||||
|
request.structureKey());
|
||||||
|
JigsawStudioHistoryStore.Snapshot previous = historyStore.snapshotCurrent(
|
||||||
|
coordinator.saveIdentity().variantKey());
|
||||||
|
if (!previous.matches(assembly.bundle())) {
|
||||||
|
historyStore.append(previous);
|
||||||
|
}
|
||||||
|
StructureWriteResult result;
|
||||||
|
synchronized (service.saveLifecycleLock) {
|
||||||
|
if (!service.saveLifecycle.isCurrentSave(studio, coordinator.requestId(), coordinator.saveIdentity())) {
|
||||||
|
message(player, "Jigsaw Studio changed before the validated save entered the writer; save cancelled.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
result = new StructureTransactionWriter(packRoot)
|
||||||
|
.write(assembly.bundle(), StructureWriteOptions.overwriteExpected(
|
||||||
|
assembly.expectedManifestHash()));
|
||||||
|
}
|
||||||
|
if (!result.successful()) {
|
||||||
|
String failure = writeFailure(result);
|
||||||
|
coordinator.recordPersistentFailure(failure, result.failure().orElse(null));
|
||||||
|
message(player, failure);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
source.invalidateStructureResources();
|
||||||
|
studio.generator().invalidateRender(bay.stableId(), coordinator.saveIdentity().variantKey());
|
||||||
|
JigsawStudioSession session = studio.generator().getSession();
|
||||||
|
boolean unchanged = session.markWorkcellSaved(coordinator.saveIdentity());
|
||||||
|
try {
|
||||||
|
service.reloadSessionLayout(studio);
|
||||||
|
} catch (IOException | RuntimeException refreshFailure) {
|
||||||
|
IrisLogging.reportError(refreshFailure);
|
||||||
|
message(player, "The graph saved, but Studio could not refresh its variant catalog. "
|
||||||
|
+ "Close and reopen this project before continuing: "
|
||||||
|
+ failureMessage(refreshFailure));
|
||||||
|
}
|
||||||
|
String cleanup = result.status() == StructureWriteResult.Status.COMMITTED_CLEANUP_REQUIRED
|
||||||
|
? " The graph committed, but transaction cleanup requires operator attention in the console."
|
||||||
|
: "";
|
||||||
|
String mutationNotice = unchanged ? "" : " Newer edits remain unsaved.";
|
||||||
|
message(player, "Saved piece '" + coordinator.saveIdentity().variantKey() + "' and object '"
|
||||||
|
+ assembly.objectKey() + "' atomically." + mutationNotice + cleanup);
|
||||||
|
playSaveSound(player);
|
||||||
|
if (unchanged) {
|
||||||
|
service.evaluator.scheduleEvaluation(studio);
|
||||||
|
}
|
||||||
|
IrisLogging.debug("Jigsaw Studio saved: structure=%s piece=%s object=%s connectors=%d status=%s",
|
||||||
|
request.structureKey(), coordinator.saveIdentity().variantKey(), assembly.objectKey(),
|
||||||
|
connectors.size(), result.status());
|
||||||
|
} catch (Throwable exception) {
|
||||||
|
String failure = "Jigsaw Studio save failed: " + failureMessage(exception);
|
||||||
|
coordinator.recordPersistentFailure(failure, exception);
|
||||||
|
message(player, failure);
|
||||||
|
} finally {
|
||||||
|
coordinator.complete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Set<Long> requiredChunks(
|
||||||
|
JigsawStudioBay bay,
|
||||||
|
JigsawStudioGenerator.RenderedBay rendered
|
||||||
|
) {
|
||||||
|
Set<Long> chunks = new HashSet<>();
|
||||||
|
if (!rendered.valid()) {
|
||||||
|
return chunks;
|
||||||
|
}
|
||||||
|
JigsawStudioBounds bounds = bay.bounds();
|
||||||
|
int minimumChunkX = bounds.originX() >> 4;
|
||||||
|
int maximumChunkX = bounds.maxX() >> 4;
|
||||||
|
int minimumChunkZ = bounds.originZ() >> 4;
|
||||||
|
int maximumChunkZ = bounds.maxZ() >> 4;
|
||||||
|
for (int chunkX = minimumChunkX; chunkX <= maximumChunkX; chunkX++) {
|
||||||
|
for (int chunkZ = minimumChunkZ; chunkZ <= maximumChunkZ; chunkZ++) {
|
||||||
|
chunks.add(chunkKey(chunkX, chunkZ));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Set.copyOf(chunks);
|
||||||
|
}
|
||||||
|
|
||||||
|
static CaptureTarget resolveCaptureTarget(
|
||||||
|
ActiveStudio studio,
|
||||||
|
JigsawStudioBay bay,
|
||||||
|
JigsawStudioVariant variant
|
||||||
|
)
|
||||||
|
throws IOException {
|
||||||
|
IrisData source = studio.generator().getRequest().source();
|
||||||
|
JigsawStudioLayout currentLayout = studio.generator().getSession().layout();
|
||||||
|
JigsawStudioBay currentBay = currentLayout.get(bay.stableId());
|
||||||
|
if (currentBay == null) {
|
||||||
|
throw new IOException("workcell '" + bay.stableId() + "' is absent from the current Studio layout");
|
||||||
|
}
|
||||||
|
JigsawStudioVariant activeVariant = Objects.requireNonNull(variant, "Jigsaw Studio capture variant");
|
||||||
|
if (!currentLayout.accepts(currentBay, activeVariant)) {
|
||||||
|
throw new IOException("variant '" + activeVariant.pieceKey()
|
||||||
|
+ "' does not belong to workcell '" + currentBay.stableId() + "'");
|
||||||
|
}
|
||||||
|
IrisJigsawPiece piece = source.getJigsawPieceLoader().load(activeVariant.pieceKey(), false);
|
||||||
|
if (piece == null || piece.getObject() == null || piece.getObject().isBlank()) {
|
||||||
|
throw new IOException("piece '" + activeVariant.pieceKey() + "' is missing or has no object");
|
||||||
|
}
|
||||||
|
IrisObject object = source.getObjectLoader().load(piece.getObject(), false);
|
||||||
|
if (object == null || object.getW() < 1 || object.getH() < 1 || object.getD() < 1) {
|
||||||
|
throw new IOException("object '" + piece.getObject() + "' is missing or has invalid dimensions");
|
||||||
|
}
|
||||||
|
JigsawStudioBounds bayBounds = currentBay.bounds();
|
||||||
|
JigsawStudioCellDimensions sourceDimensions = new JigsawStudioCellDimensions(
|
||||||
|
object.getW(), object.getH(), object.getD());
|
||||||
|
JigsawStudioCellDimensions canonicalDimensions = activeVariant.canonicalDimensions(sourceDimensions);
|
||||||
|
if (canonicalDimensions.width() > bayBounds.dimensions().width()
|
||||||
|
|| canonicalDimensions.height() > bayBounds.dimensions().height()
|
||||||
|
|| canonicalDimensions.depth() > bayBounds.dimensions().depth()) {
|
||||||
|
throw new IOException("object '" + piece.getObject() + "' is "
|
||||||
|
+ canonicalDimensions.width() + "x" + canonicalDimensions.height() + "x"
|
||||||
|
+ canonicalDimensions.depth() + " in its displayed orientation"
|
||||||
|
+ " but bay '" + currentBay.stableId() + "' is only "
|
||||||
|
+ bayBounds.dimensions().width() + "x" + bayBounds.dimensions().height() + "x"
|
||||||
|
+ bayBounds.dimensions().depth());
|
||||||
|
}
|
||||||
|
return new CaptureTarget(
|
||||||
|
new JigsawStudioBounds(
|
||||||
|
bayBounds.originX(),
|
||||||
|
bayBounds.originY(),
|
||||||
|
bayBounds.originZ(),
|
||||||
|
canonicalDimensions),
|
||||||
|
piece,
|
||||||
|
object,
|
||||||
|
activeVariant.sourceToCanonicalQuarterTurns(),
|
||||||
|
studio.generator().getSession().workcellSnapshot(currentBay.stableId()).connectorsVisible());
|
||||||
|
}
|
||||||
|
|
||||||
|
CaptureCoordinator coordinator(CaptureWork work, List<ChunkCaptureArea> areas) {
|
||||||
|
return new CaptureCoordinator(work, areas);
|
||||||
|
}
|
||||||
|
|
||||||
|
final class CaptureCoordinator {
|
||||||
|
private final CaptureWork work;
|
||||||
|
private final List<ChunkCaptureArea> areas;
|
||||||
|
private final Map<Long, ChunkSnapshot> snapshots = new ConcurrentHashMap<>();
|
||||||
|
private final AtomicInteger remaining;
|
||||||
|
private final AtomicBoolean failed = new AtomicBoolean(false);
|
||||||
|
private final AtomicBoolean assemblyStarted = new AtomicBoolean(false);
|
||||||
|
private final AtomicBoolean operationFinished = new AtomicBoolean(false);
|
||||||
|
private volatile AutosavePersistentFailure persistentFailure;
|
||||||
|
|
||||||
|
CaptureCoordinator(CaptureWork work, List<ChunkCaptureArea> areas) {
|
||||||
|
this.work = Objects.requireNonNull(work, "Jigsaw Studio capture work");
|
||||||
|
this.areas = List.copyOf(areas);
|
||||||
|
this.remaining = new AtomicInteger(this.areas.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
private ActiveStudio studio() {
|
||||||
|
return work.studio();
|
||||||
|
}
|
||||||
|
|
||||||
|
private World world() {
|
||||||
|
return work.world();
|
||||||
|
}
|
||||||
|
|
||||||
|
private JigsawStudioBay bay() {
|
||||||
|
return work.bay();
|
||||||
|
}
|
||||||
|
|
||||||
|
private CaptureTarget captureTarget() {
|
||||||
|
return work.captureTarget();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Player player() {
|
||||||
|
return work.player();
|
||||||
|
}
|
||||||
|
|
||||||
|
private UUID requestId() {
|
||||||
|
return work.requestId();
|
||||||
|
}
|
||||||
|
|
||||||
|
private JigsawStudioSession.SaveIdentity saveIdentity() {
|
||||||
|
return work.saveIdentity();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ChunkCaptureArea> areas() {
|
||||||
|
return areas;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean stopped() {
|
||||||
|
return failed.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean failed() {
|
||||||
|
return failed.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void accept(ChunkSnapshot snapshot) {
|
||||||
|
if (failed.get() || assemblyStarted.get()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ChunkCaptureArea area = snapshot.area();
|
||||||
|
long key = chunkKey(area.chunkX(), area.chunkZ());
|
||||||
|
if (snapshots.putIfAbsent(key, snapshot) != null) {
|
||||||
|
fail("Jigsaw Studio received duplicate snapshot for bay chunk "
|
||||||
|
+ area.chunkX() + "," + area.chunkZ() + ".", null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int incomplete = remaining.decrementAndGet();
|
||||||
|
if (incomplete < 0) {
|
||||||
|
fail("Jigsaw Studio received more bay chunk snapshots than expected.", null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (incomplete == 0 && assemblyStarted.compareAndSet(false, true)) {
|
||||||
|
List<ChunkSnapshot> completed = List.copyOf(snapshots.values());
|
||||||
|
J.a(() -> assembleAndPersist(this, completed));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void fail(String failure, Throwable exception) {
|
||||||
|
if (!failed.compareAndSet(false, true)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (exception != null) {
|
||||||
|
IrisLogging.reportError(exception);
|
||||||
|
}
|
||||||
|
complete();
|
||||||
|
message(player(), failure);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void failPersistent(String failure, Throwable exception) {
|
||||||
|
if (!failed.compareAndSet(false, true)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
recordPersistentFailure(failure, exception);
|
||||||
|
complete();
|
||||||
|
message(player(), failure);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void recordPersistentFailure(String failure, Throwable exception) {
|
||||||
|
persistentFailure = new AutosavePersistentFailure(failure, exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void complete() {
|
||||||
|
if (operationFinished.compareAndSet(false, true)) {
|
||||||
|
studio().generator().getSession().abortSave(saveIdentity());
|
||||||
|
service.saveLifecycle.finishSave(requestId());
|
||||||
|
service.autosaveScheduler.completeAutosaveAttempt(
|
||||||
|
studio(),
|
||||||
|
saveIdentity(),
|
||||||
|
work.autosaveFailureState(),
|
||||||
|
persistentFailure);
|
||||||
|
service.playerContext.refreshWorkcellContext(studio().worldId(), saveIdentity().workcellId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record CaptureWork(
|
||||||
|
ActiveStudio studio,
|
||||||
|
World world,
|
||||||
|
JigsawStudioBay bay,
|
||||||
|
CaptureTarget captureTarget,
|
||||||
|
Player player,
|
||||||
|
UUID requestId,
|
||||||
|
JigsawStudioSession.SaveIdentity saveIdentity,
|
||||||
|
AutosaveFailureState autosaveFailureState
|
||||||
|
) {
|
||||||
|
CaptureWork {
|
||||||
|
Objects.requireNonNull(studio, "Jigsaw Studio capture studio");
|
||||||
|
Objects.requireNonNull(world, "Jigsaw Studio capture world");
|
||||||
|
Objects.requireNonNull(bay, "Jigsaw Studio capture bay");
|
||||||
|
Objects.requireNonNull(captureTarget, "Jigsaw Studio capture target");
|
||||||
|
Objects.requireNonNull(requestId, "Jigsaw Studio capture request ID");
|
||||||
|
Objects.requireNonNull(saveIdentity, "Jigsaw Studio capture save identity");
|
||||||
|
Objects.requireNonNull(autosaveFailureState, "Jigsaw Studio capture autosave failure state");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record CaptureTarget(
|
||||||
|
JigsawStudioBounds bounds,
|
||||||
|
IrisJigsawPiece piece,
|
||||||
|
IrisObject object,
|
||||||
|
int displayRotationQuarterTurns,
|
||||||
|
boolean connectorsVisible
|
||||||
|
) {
|
||||||
|
CaptureTarget {
|
||||||
|
Objects.requireNonNull(bounds, "Jigsaw Studio capture bounds");
|
||||||
|
Objects.requireNonNull(piece, "Jigsaw Studio capture piece");
|
||||||
|
Objects.requireNonNull(object, "Jigsaw Studio capture object");
|
||||||
|
displayRotationQuarterTurns = Math.floorMod(displayRotationQuarterTurns, 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,624 @@
|
|||||||
|
package art.arcane.iris.core.service;
|
||||||
|
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioBay;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioBounds;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioCellDimensions;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioMarkerKeyCodec;
|
||||||
|
import art.arcane.iris.core.service.JigsawStudioCapture.CapturedConnector;
|
||||||
|
import art.arcane.iris.core.service.JigsawStudioCapture.ChunkCaptureArea;
|
||||||
|
import art.arcane.iris.core.service.JigsawStudioCapture.LocalPosition;
|
||||||
|
import art.arcane.iris.core.service.JigsawStudioSaveLifecycle.BayReadiness;
|
||||||
|
import art.arcane.iris.core.service.JigsawStudioService.ActiveStudio;
|
||||||
|
import art.arcane.iris.engine.object.IrisJigsawConnector;
|
||||||
|
import art.arcane.iris.engine.object.IrisJigsawPiece;
|
||||||
|
import art.arcane.iris.engine.object.IrisObjectRotation;
|
||||||
|
import art.arcane.iris.engine.object.IrisPosition;
|
||||||
|
import art.arcane.iris.engine.object.TileData;
|
||||||
|
import art.arcane.iris.engine.platform.studio.generators.JigsawStudioGenerator;
|
||||||
|
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||||
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
|
import art.arcane.iris.spi.PlatformBlockState;
|
||||||
|
import art.arcane.volmlib.util.collection.KMap;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.block.Block;
|
||||||
|
import org.bukkit.block.data.BlockData;
|
||||||
|
import org.bukkit.block.data.type.Jigsaw;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioCapture.inversePosition;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioService.chunkKey;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioService.failureMessage;
|
||||||
|
|
||||||
|
final class JigsawStudioChunkWriter {
|
||||||
|
private JigsawStudioChunkWriter() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static void restoreConnectorChunk(
|
||||||
|
World world,
|
||||||
|
JigsawStudioBay workcell,
|
||||||
|
List<JigsawStudioGenerator.RenderedConnector> connectors,
|
||||||
|
Map<LocalPosition, JigsawStudioGenerator.RenderedBlock> renderedBlocks,
|
||||||
|
boolean connectorsVisible
|
||||||
|
) throws IOException {
|
||||||
|
JigsawStudioBounds bounds = workcell.bounds();
|
||||||
|
for (JigsawStudioGenerator.RenderedConnector connector : connectors) {
|
||||||
|
LocalPosition position = new LocalPosition(connector.x(), connector.y(), connector.z());
|
||||||
|
Block target = world.getBlockAt(
|
||||||
|
bounds.originX() + connector.x(),
|
||||||
|
bounds.originY() + connector.y(),
|
||||||
|
bounds.originZ() + connector.z());
|
||||||
|
if (connectorsVisible) {
|
||||||
|
BlockData marker;
|
||||||
|
try {
|
||||||
|
marker = Bukkit.createBlockData(
|
||||||
|
"minecraft:jigsaw[orientation=" + connector.orientation() + "]");
|
||||||
|
} catch (IllegalArgumentException exception) {
|
||||||
|
throw new IOException("Invalid saved connector orientation '"
|
||||||
|
+ connector.orientation() + "'", exception);
|
||||||
|
}
|
||||||
|
target.setBlockData(marker, false);
|
||||||
|
BukkitPlatform.deserializeTile(markerNbt(connector.connector()), target.getLocation());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
JigsawStudioGenerator.RenderedBlock renderedBlock = renderedBlocks.get(position);
|
||||||
|
BlockData restored;
|
||||||
|
if (renderedBlock != null) {
|
||||||
|
if (renderedBlock.state().isCustom()
|
||||||
|
|| !(renderedBlock.state().nativeHandle() instanceof BlockData blockData)) {
|
||||||
|
throw new IOException("Saved connector block '" + renderedBlock.state().key()
|
||||||
|
+ "' cannot be restored directly in Bukkit Studio");
|
||||||
|
}
|
||||||
|
restored = blockData;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
restored = Bukkit.createBlockData(connector.connector().getFinalState());
|
||||||
|
} catch (IllegalArgumentException exception) {
|
||||||
|
throw new IOException("Invalid saved connector final state '"
|
||||||
|
+ connector.connector().getFinalState() + "'", exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
target.setBlockData(restored, false);
|
||||||
|
if (renderedBlock != null && renderedBlock.tileData() != null) {
|
||||||
|
TileData tileData = renderedBlock.tileData();
|
||||||
|
if (!tileData.isApplicable(target.getBlockData()) || !tileData.toBukkitTry(target)) {
|
||||||
|
throw new IOException("Saved connector tile data could not be restored at "
|
||||||
|
+ target.getX() + "," + target.getY() + "," + target.getZ());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static String validateMaterialization(JigsawStudioGenerator.RenderedBay rendered) {
|
||||||
|
if (!rendered.valid()) {
|
||||||
|
return rendered.failure();
|
||||||
|
}
|
||||||
|
for (JigsawStudioGenerator.RenderedBlock block : rendered.blocks()) {
|
||||||
|
PlatformBlockState state = block.state();
|
||||||
|
if (state.isCustom()) {
|
||||||
|
return "custom block '" + state.key()
|
||||||
|
+ "' requires provider-owned placement and cannot be swapped live in Studio";
|
||||||
|
}
|
||||||
|
if (!(state.nativeHandle() instanceof BlockData)) {
|
||||||
|
return "block '" + state.key() + "' has no Bukkit block-data representation";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (JigsawStudioGenerator.RenderedConnector connector : rendered.connectors()) {
|
||||||
|
try {
|
||||||
|
BlockData data = Bukkit.createBlockData(
|
||||||
|
"minecraft:jigsaw[orientation=" + connector.orientation() + "]");
|
||||||
|
if (!(data instanceof Jigsaw)) {
|
||||||
|
return "connector orientation '" + connector.orientation()
|
||||||
|
+ "' did not resolve to a jigsaw marker";
|
||||||
|
}
|
||||||
|
} catch (IllegalArgumentException exception) {
|
||||||
|
return "connector orientation '" + connector.orientation() + "' is invalid";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
static void writeMaterializedChunk(
|
||||||
|
World world,
|
||||||
|
JigsawStudioBay workcell,
|
||||||
|
JigsawStudioGenerator.RenderedBay rendered,
|
||||||
|
ChunkCaptureArea area,
|
||||||
|
boolean connectorsVisible
|
||||||
|
) throws IOException {
|
||||||
|
JigsawStudioBounds bounds = workcell.bounds();
|
||||||
|
Map<LocalPosition, BlockData> expected = materializedBlockData(rendered, connectorsVisible);
|
||||||
|
BlockData air = Material.AIR.createBlockData();
|
||||||
|
for (int x = area.minimumX(); x < area.maximumX(); x++) {
|
||||||
|
for (int y = 0; y < bounds.dimensions().height(); y++) {
|
||||||
|
for (int z = area.minimumZ(); z < area.maximumZ(); z++) {
|
||||||
|
Block target = world.getBlockAt(
|
||||||
|
bounds.originX() + x,
|
||||||
|
bounds.originY() + y,
|
||||||
|
bounds.originZ() + z);
|
||||||
|
BlockData blockData = expected.getOrDefault(new LocalPosition(x, y, z), air);
|
||||||
|
target.setBlockData(blockData, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
applyRenderedBayChunk(world, workcell, rendered, area.chunkX(), area.chunkZ(), connectorsVisible);
|
||||||
|
verifyMaterializedChunk(world, workcell, rendered, area, expected, connectorsVisible);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<LocalPosition, BlockData> materializedBlockData(
|
||||||
|
JigsawStudioGenerator.RenderedBay rendered,
|
||||||
|
boolean connectorsVisible
|
||||||
|
) throws IOException {
|
||||||
|
Map<LocalPosition, BlockData> expected = new HashMap<>();
|
||||||
|
for (JigsawStudioGenerator.RenderedBlock block : rendered.blocks()) {
|
||||||
|
if (block.state().isCustom() || !(block.state().nativeHandle() instanceof BlockData blockData)) {
|
||||||
|
throw new IOException("Rendered block '" + block.state().key()
|
||||||
|
+ "' cannot be placed directly in Bukkit Studio");
|
||||||
|
}
|
||||||
|
expected.put(new LocalPosition(block.x(), block.y(), block.z()), blockData);
|
||||||
|
}
|
||||||
|
for (JigsawStudioGenerator.RenderedConnector connector : rendered.connectors()) {
|
||||||
|
if (!connectorsVisible && expected.containsKey(
|
||||||
|
new LocalPosition(connector.x(), connector.y(), connector.z()))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
BlockData marker;
|
||||||
|
try {
|
||||||
|
marker = Bukkit.createBlockData(connectorsVisible
|
||||||
|
? "minecraft:jigsaw[orientation=" + connector.orientation() + "]"
|
||||||
|
: connector.connector().getFinalState());
|
||||||
|
} catch (IllegalArgumentException exception) {
|
||||||
|
throw new IOException("Invalid rendered connector orientation '"
|
||||||
|
+ connector.orientation() + "'", exception);
|
||||||
|
}
|
||||||
|
expected.put(new LocalPosition(connector.x(), connector.y(), connector.z()), marker);
|
||||||
|
}
|
||||||
|
return expected;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void verifyMaterializedChunk(
|
||||||
|
World world,
|
||||||
|
JigsawStudioBay workcell,
|
||||||
|
JigsawStudioGenerator.RenderedBay rendered,
|
||||||
|
ChunkCaptureArea area,
|
||||||
|
Map<LocalPosition, BlockData> expected,
|
||||||
|
boolean connectorsVisible
|
||||||
|
) throws IOException {
|
||||||
|
JigsawStudioBounds bounds = workcell.bounds();
|
||||||
|
BlockData air = Material.AIR.createBlockData();
|
||||||
|
for (int x = area.minimumX(); x < area.maximumX(); x++) {
|
||||||
|
for (int y = 0; y < bounds.dimensions().height(); y++) {
|
||||||
|
for (int z = area.minimumZ(); z < area.maximumZ(); z++) {
|
||||||
|
Block target = world.getBlockAt(
|
||||||
|
bounds.originX() + x,
|
||||||
|
bounds.originY() + y,
|
||||||
|
bounds.originZ() + z);
|
||||||
|
BlockData expectedData = expected.getOrDefault(new LocalPosition(x, y, z), air);
|
||||||
|
if (!target.getBlockData().equals(expectedData)) {
|
||||||
|
throw new IOException("Workcell block did not materialize at "
|
||||||
|
+ target.getX() + "," + target.getY() + "," + target.getZ());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
verifyRenderedBayChunk(world, workcell, rendered, area.chunkX(), area.chunkZ(), connectorsVisible);
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean hydrateChunk(
|
||||||
|
ActiveStudio studio,
|
||||||
|
World world,
|
||||||
|
int chunkX,
|
||||||
|
int chunkZ
|
||||||
|
) {
|
||||||
|
long key = chunkKey(chunkX, chunkZ);
|
||||||
|
boolean verificationRequired = false;
|
||||||
|
for (JigsawStudioBay bay : studio.generator().getLayout().bays()) {
|
||||||
|
BayPopulation population = studio.population(bay);
|
||||||
|
if (!population.needsApplication(key) && !population.needsVerification(key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
JigsawStudioGenerator.RenderedBay rendered = studio.generator().renderBay(bay);
|
||||||
|
if (!rendered.valid()) {
|
||||||
|
population.fail(rendered.failure());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (population.needsApplication(key)) {
|
||||||
|
applyRenderedBayChunk(
|
||||||
|
world,
|
||||||
|
bay,
|
||||||
|
rendered,
|
||||||
|
chunkX,
|
||||||
|
chunkZ,
|
||||||
|
studio.generator().getSession().workcellSnapshot(bay.stableId()).connectorsVisible());
|
||||||
|
population.markApplied(key);
|
||||||
|
verificationRequired = true;
|
||||||
|
} else {
|
||||||
|
verifyRenderedBayChunk(
|
||||||
|
world,
|
||||||
|
bay,
|
||||||
|
rendered,
|
||||||
|
chunkX,
|
||||||
|
chunkZ,
|
||||||
|
studio.generator().getSession().workcellSnapshot(bay.stableId()).connectorsVisible());
|
||||||
|
population.markHydrated(key);
|
||||||
|
}
|
||||||
|
} catch (Throwable exception) {
|
||||||
|
if (population.fail("chunk " + chunkX + "," + chunkZ
|
||||||
|
+ " could not hydrate: " + failureMessage(exception))) {
|
||||||
|
IrisLogging.reportError(exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return verificationRequired;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void applyRenderedBayChunk(
|
||||||
|
World world,
|
||||||
|
JigsawStudioBay bay,
|
||||||
|
JigsawStudioGenerator.RenderedBay rendered,
|
||||||
|
int chunkX,
|
||||||
|
int chunkZ,
|
||||||
|
boolean connectorsVisible
|
||||||
|
) throws IOException {
|
||||||
|
JigsawStudioBounds bounds = bay.bounds();
|
||||||
|
Set<LocalPosition> connectorPositions = new HashSet<>(rendered.connectors().size());
|
||||||
|
if (connectorsVisible) {
|
||||||
|
for (JigsawStudioGenerator.RenderedConnector connector : rendered.connectors()) {
|
||||||
|
connectorPositions.add(new LocalPosition(connector.x(), connector.y(), connector.z()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (JigsawStudioGenerator.RenderedBlock block : rendered.blocks()) {
|
||||||
|
TileData tileData = block.tileData();
|
||||||
|
if (tileData == null
|
||||||
|
|| connectorPositions.contains(new LocalPosition(block.x(), block.y(), block.z()))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int worldX = bounds.originX() + block.x();
|
||||||
|
int worldZ = bounds.originZ() + block.z();
|
||||||
|
if ((worldX >> 4) != chunkX || (worldZ >> 4) != chunkZ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Block target = world.getBlockAt(worldX, bounds.originY() + block.y(), worldZ);
|
||||||
|
if (!tileData.isApplicable(target.getBlockData())) {
|
||||||
|
throw new IOException("source tile state does not match rendered block at "
|
||||||
|
+ target.getX() + "," + target.getY() + "," + target.getZ());
|
||||||
|
}
|
||||||
|
if (!tileData.toBukkitTry(target)) {
|
||||||
|
throw new IOException("source tile state could not hydrate at "
|
||||||
|
+ target.getX() + "," + target.getY() + "," + target.getZ());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!connectorsVisible) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (JigsawStudioGenerator.RenderedConnector renderedConnector : rendered.connectors()) {
|
||||||
|
int worldX = bounds.originX() + renderedConnector.x();
|
||||||
|
int worldZ = bounds.originZ() + renderedConnector.z();
|
||||||
|
if ((worldX >> 4) != chunkX || (worldZ >> 4) != chunkZ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Block target = world.getBlockAt(
|
||||||
|
worldX,
|
||||||
|
bounds.originY() + renderedConnector.y(),
|
||||||
|
worldZ);
|
||||||
|
BlockData blockData = target.getBlockData();
|
||||||
|
if (!(blockData instanceof Jigsaw jigsaw)) {
|
||||||
|
throw new IOException("expected a jigsaw marker at "
|
||||||
|
+ target.getX() + "," + target.getY() + "," + target.getZ());
|
||||||
|
}
|
||||||
|
String actualOrientation = jigsaw.getOrientation().name().toLowerCase(Locale.ROOT);
|
||||||
|
if (!actualOrientation.equals(renderedConnector.orientation())) {
|
||||||
|
throw new IOException("jigsaw marker orientation at "
|
||||||
|
+ target.getX() + "," + target.getY() + "," + target.getZ()
|
||||||
|
+ " is " + actualOrientation + " instead of " + renderedConnector.orientation());
|
||||||
|
}
|
||||||
|
KMap<String, Object> expected = markerNbt(renderedConnector.connector());
|
||||||
|
BukkitPlatform.deserializeTile(expected, target.getLocation());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void verifyRenderedBayChunk(
|
||||||
|
World world,
|
||||||
|
JigsawStudioBay bay,
|
||||||
|
JigsawStudioGenerator.RenderedBay rendered,
|
||||||
|
int chunkX,
|
||||||
|
int chunkZ,
|
||||||
|
boolean connectorsVisible
|
||||||
|
) throws IOException {
|
||||||
|
JigsawStudioBounds bounds = bay.bounds();
|
||||||
|
Set<LocalPosition> connectorPositions = new HashSet<>(rendered.connectors().size());
|
||||||
|
if (connectorsVisible) {
|
||||||
|
for (JigsawStudioGenerator.RenderedConnector connector : rendered.connectors()) {
|
||||||
|
connectorPositions.add(new LocalPosition(connector.x(), connector.y(), connector.z()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (JigsawStudioGenerator.RenderedBlock block : rendered.blocks()) {
|
||||||
|
TileData tileData = block.tileData();
|
||||||
|
if (tileData == null
|
||||||
|
|| connectorPositions.contains(new LocalPosition(block.x(), block.y(), block.z()))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int worldX = bounds.originX() + block.x();
|
||||||
|
int worldZ = bounds.originZ() + block.z();
|
||||||
|
if ((worldX >> 4) != chunkX || (worldZ >> 4) != chunkZ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Block target = world.getBlockAt(worldX, bounds.originY() + block.y(), worldZ);
|
||||||
|
if (!tileData.isApplicable(target.getBlockData())) {
|
||||||
|
throw new IOException("hydrated tile state no longer matches its block at "
|
||||||
|
+ target.getX() + "," + target.getY() + "," + target.getZ());
|
||||||
|
}
|
||||||
|
KMap<String, Object> hydrated = BukkitPlatform.serializeTile(target.getLocation());
|
||||||
|
if (hydrated == null
|
||||||
|
|| tileData.getProperties() != null
|
||||||
|
&& !nbtContains(tileData.getProperties(), hydrated)) {
|
||||||
|
throw new IOException("the active NMS binding did not preserve source tile NBT at "
|
||||||
|
+ target.getX() + "," + target.getY() + "," + target.getZ());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!connectorsVisible) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (JigsawStudioGenerator.RenderedConnector renderedConnector : rendered.connectors()) {
|
||||||
|
int worldX = bounds.originX() + renderedConnector.x();
|
||||||
|
int worldZ = bounds.originZ() + renderedConnector.z();
|
||||||
|
if ((worldX >> 4) != chunkX || (worldZ >> 4) != chunkZ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Block target = world.getBlockAt(
|
||||||
|
worldX,
|
||||||
|
bounds.originY() + renderedConnector.y(),
|
||||||
|
worldZ);
|
||||||
|
BlockData blockData = target.getBlockData();
|
||||||
|
if (!(blockData instanceof Jigsaw jigsaw)
|
||||||
|
|| !jigsaw.getOrientation().name().toLowerCase(Locale.ROOT)
|
||||||
|
.equals(renderedConnector.orientation())) {
|
||||||
|
throw new IOException("jigsaw marker changed before hydration completed at "
|
||||||
|
+ target.getX() + "," + target.getY() + "," + target.getZ());
|
||||||
|
}
|
||||||
|
KMap<String, Object> expected = markerNbt(renderedConnector.connector());
|
||||||
|
KMap<String, Object> hydrated = BukkitPlatform.serializeTile(target.getLocation());
|
||||||
|
if (!markerNbtMatches(expected, hydrated)) {
|
||||||
|
throw new IOException("the active NMS binding did not preserve jigsaw marker NBT at "
|
||||||
|
+ target.getX() + "," + target.getY() + "," + target.getZ());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static KMap<String, Object> markerNbt(IrisJigsawConnector connector) {
|
||||||
|
IrisJigsawConnector activeConnector = Objects.requireNonNull(connector, "Jigsaw Studio connector");
|
||||||
|
KMap<String, Object> nbt = new KMap<>();
|
||||||
|
nbt.put("name", studioIdentifier(activeConnector.getName()));
|
||||||
|
nbt.put("target", studioIdentifier(activeConnector.getTargetName()));
|
||||||
|
nbt.put("pool", JigsawStudioMarkerKeyCodec.encodePool(activeConnector.getPool()));
|
||||||
|
nbt.put("final_state", activeConnector.getFinalState());
|
||||||
|
nbt.put("joint", activeConnector.getJoint().name().toLowerCase(Locale.ROOT));
|
||||||
|
nbt.put("selection_priority", activeConnector.getSelectionPriority());
|
||||||
|
nbt.put("placement_priority", activeConnector.getPlacementPriority());
|
||||||
|
return nbt;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean markerNbtMatches(
|
||||||
|
Map<String, Object> expected,
|
||||||
|
Map<String, Object> hydrated
|
||||||
|
) {
|
||||||
|
if (hydrated == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (Map.Entry<String, Object> entry : expected.entrySet()) {
|
||||||
|
Object actual = hydrated.get(entry.getKey());
|
||||||
|
if (entry.getValue() instanceof Number expectedNumber) {
|
||||||
|
if (!(actual instanceof Number actualNumber)
|
||||||
|
|| expectedNumber.longValue() != actualNumber.longValue()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if (!Objects.equals(entry.getValue(), actual)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean nbtContains(Object expected, Object actual) {
|
||||||
|
if (expected instanceof Number expectedNumber) {
|
||||||
|
if (!(actual instanceof Number actualNumber)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (isIntegral(expectedNumber) && isIntegral(actualNumber)) {
|
||||||
|
return expectedNumber.longValue() == actualNumber.longValue();
|
||||||
|
}
|
||||||
|
return Double.compare(expectedNumber.doubleValue(), actualNumber.doubleValue()) == 0;
|
||||||
|
}
|
||||||
|
if (expected instanceof Map<?, ?> expectedMap) {
|
||||||
|
if (!(actual instanceof Map<?, ?> actualMap)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (Map.Entry<?, ?> entry : expectedMap.entrySet()) {
|
||||||
|
if (!actualMap.containsKey(entry.getKey())
|
||||||
|
|| !nbtContains(entry.getValue(), actualMap.get(entry.getKey()))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (expected instanceof List<?> expectedList) {
|
||||||
|
if (!(actual instanceof List<?> actualList) || expectedList.size() != actualList.size()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (int index = 0; index < expectedList.size(); index++) {
|
||||||
|
if (!nbtContains(expectedList.get(index), actualList.get(index))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return Objects.equals(expected, actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isIntegral(Number number) {
|
||||||
|
return number instanceof Byte
|
||||||
|
|| number instanceof Short
|
||||||
|
|| number instanceof Integer
|
||||||
|
|| number instanceof Long;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void restoreCapturedMetadata(
|
||||||
|
IrisJigsawConnector captured,
|
||||||
|
IrisJigsawPiece sourcePiece
|
||||||
|
) {
|
||||||
|
IrisJigsawConnector activeCaptured = Objects.requireNonNull(captured, "Captured jigsaw connector");
|
||||||
|
IrisJigsawConnector original = connectorAt(sourcePiece, activeCaptured.getPosition());
|
||||||
|
if (original == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
activeCaptured.setName(restoreIdentifier(activeCaptured.getName(), original.getName()));
|
||||||
|
activeCaptured.setTargetName(restoreIdentifier(
|
||||||
|
activeCaptured.getTargetName(),
|
||||||
|
original.getTargetName()));
|
||||||
|
activeCaptured.setChannel(original.getChannel() == null ? "" : original.getChannel());
|
||||||
|
}
|
||||||
|
|
||||||
|
static void restoreCapturedMetadataForDisplay(
|
||||||
|
IrisJigsawConnector captured,
|
||||||
|
IrisJigsawPiece sourcePiece,
|
||||||
|
JigsawStudioCellDimensions displayDimensions,
|
||||||
|
int displayRotationQuarterTurns
|
||||||
|
) {
|
||||||
|
if (Math.floorMod(displayRotationQuarterTurns, 4) == 0) {
|
||||||
|
restoreCapturedMetadata(captured, sourcePiece);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int quarterTurns = Math.floorMod(displayRotationQuarterTurns, 4);
|
||||||
|
int sourceWidth = (quarterTurns & 1) == 0
|
||||||
|
? displayDimensions.width()
|
||||||
|
: displayDimensions.depth();
|
||||||
|
int sourceDepth = (quarterTurns & 1) == 0
|
||||||
|
? displayDimensions.depth()
|
||||||
|
: displayDimensions.width();
|
||||||
|
LocalPosition position = inversePosition(
|
||||||
|
captured.getPosition().getX(),
|
||||||
|
captured.getPosition().getY(),
|
||||||
|
captured.getPosition().getZ(),
|
||||||
|
sourceWidth,
|
||||||
|
sourceDepth,
|
||||||
|
quarterTurns);
|
||||||
|
IrisObjectRotation inverseRotation = IrisObjectRotation.of(0, 90.0D * quarterTurns, 0);
|
||||||
|
IrisJigsawConnector sourceOriented = CapturedConnector.from(captured).toConnector()
|
||||||
|
.setPosition(new IrisPosition(position.x(), position.y(), position.z()))
|
||||||
|
.setDirection(inverseRotation.rotate(captured.getDirection()))
|
||||||
|
.setTop(inverseRotation.rotate(captured.getTop()));
|
||||||
|
restoreCapturedMetadata(sourceOriented, sourcePiece);
|
||||||
|
captured.setName(sourceOriented.getName());
|
||||||
|
captured.setTargetName(sourceOriented.getTargetName());
|
||||||
|
captured.setChannel(sourceOriented.getChannel());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IrisJigsawConnector connectorAt(IrisJigsawPiece piece, IrisPosition position) {
|
||||||
|
if (piece == null || piece.getConnectors() == null || position == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (IrisJigsawConnector connector : piece.getConnectors()) {
|
||||||
|
if (connector != null && position.equals(connector.getPosition())) {
|
||||||
|
return connector;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String restoreIdentifier(String captured, String original) {
|
||||||
|
if (original != null && studioIdentifier(original).equals(captured)) {
|
||||||
|
return original;
|
||||||
|
}
|
||||||
|
return captured;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String studioIdentifier(String value) {
|
||||||
|
String normalized = Objects.requireNonNull(value, "Jigsaw Studio marker identifier").trim();
|
||||||
|
return normalized.indexOf(':') < 0 ? "iris:" + normalized : normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
static final class BayPopulation {
|
||||||
|
private final Set<Long> requiredChunks;
|
||||||
|
private final Set<Long> generatedChunks = ConcurrentHashMap.newKeySet();
|
||||||
|
private final Set<Long> appliedChunks = ConcurrentHashMap.newKeySet();
|
||||||
|
private final Set<Long> hydratedChunks = ConcurrentHashMap.newKeySet();
|
||||||
|
private volatile String failure;
|
||||||
|
|
||||||
|
BayPopulation(Set<Long> requiredChunks, String failure) {
|
||||||
|
this.requiredChunks = Set.copyOf(requiredChunks);
|
||||||
|
this.failure = failure == null ? "" : failure;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean markGenerated(long chunk) {
|
||||||
|
if (!requiredChunks.contains(chunk)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
generatedChunks.add(chunk);
|
||||||
|
return !hydratedChunks.contains(chunk) && failure.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean needsApplication(long chunk) {
|
||||||
|
return failure.isEmpty()
|
||||||
|
&& requiredChunks.contains(chunk)
|
||||||
|
&& generatedChunks.contains(chunk)
|
||||||
|
&& !appliedChunks.contains(chunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean needsVerification(long chunk) {
|
||||||
|
return failure.isEmpty()
|
||||||
|
&& requiredChunks.contains(chunk)
|
||||||
|
&& generatedChunks.contains(chunk)
|
||||||
|
&& appliedChunks.contains(chunk)
|
||||||
|
&& !hydratedChunks.contains(chunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
void markApplied(long chunk) {
|
||||||
|
if (requiredChunks.contains(chunk)) {
|
||||||
|
appliedChunks.add(chunk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void markHydrated(long chunk) {
|
||||||
|
if (requiredChunks.contains(chunk)) {
|
||||||
|
hydratedChunks.add(chunk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void markFullyReady() {
|
||||||
|
generatedChunks.addAll(requiredChunks);
|
||||||
|
appliedChunks.addAll(requiredChunks);
|
||||||
|
hydratedChunks.addAll(requiredChunks);
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized boolean fail(String reason) {
|
||||||
|
if (!failure.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
failure = reason == null || reason.isBlank() ? "unknown hydration failure" : reason;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
BayReadiness readiness() {
|
||||||
|
boolean ready = failure.isEmpty()
|
||||||
|
&& !requiredChunks.isEmpty()
|
||||||
|
&& generatedChunks.containsAll(requiredChunks)
|
||||||
|
&& hydratedChunks.containsAll(requiredChunks);
|
||||||
|
return new BayReadiness(
|
||||||
|
ready,
|
||||||
|
failure,
|
||||||
|
requiredChunks.size(),
|
||||||
|
generatedChunks.size(),
|
||||||
|
hydratedChunks.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
package art.arcane.iris.core.service;
|
||||||
|
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioBay;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioSession;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioVariant;
|
||||||
|
import art.arcane.iris.core.service.JigsawStudioService.ActiveStudio;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.block.Block;
|
||||||
|
import org.bukkit.inventory.Inventory;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioAutosaveScheduler.AUTOSAVE_DEBOUNCE_TICKS;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioProtection.inventoryBlocks;
|
||||||
|
|
||||||
|
final class JigsawStudioDirtyTracker {
|
||||||
|
private final JigsawStudioService service;
|
||||||
|
|
||||||
|
JigsawStudioDirtyTracker(JigsawStudioService service) {
|
||||||
|
this.service = Objects.requireNonNull(service, "Jigsaw Studio service");
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean markDirty(World world, int worldX, int worldY, int worldZ) {
|
||||||
|
if (world == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ActiveStudio studio = service.studios.get(world.getUID());
|
||||||
|
if (studio == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (service.saveLifecycle.reopenRequiredRequests.contains(studio.generator().getRequest().requestId())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioSession session = studio.generator().getSession();
|
||||||
|
JigsawStudioBay bay = session.layout().findAt(worldX, worldY, worldZ);
|
||||||
|
if (bay == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioVariant activeVariant = session.activeVariant(bay.stableId()).orElse(null);
|
||||||
|
if (activeVariant == null || !activeVariant.owned()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioSession.DirtyMark dirtyMark = session.markWorkcellDirty(bay.stableId());
|
||||||
|
if (dirtyMark.status() != JigsawStudioSession.DirtyStatus.MARKED) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
service.autosaveScheduler.scheduleAutosave(
|
||||||
|
studio,
|
||||||
|
bay,
|
||||||
|
dirtyMark.identity().orElseThrow(),
|
||||||
|
AUTOSAVE_DEBOUNCE_TICKS);
|
||||||
|
service.markEvaluationStale(studio);
|
||||||
|
if (dirtyMark.newlyDirty()) {
|
||||||
|
service.playerContext.refreshWorkcellContext(studio.worldId(), bay.stableId());
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int markAllDirty(World world) {
|
||||||
|
if (world == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
ActiveStudio studio = service.studios.get(world.getUID());
|
||||||
|
return studio == null ? 0 : markAllDirty(studio);
|
||||||
|
}
|
||||||
|
|
||||||
|
void markDirty(Block block) {
|
||||||
|
if (block != null) {
|
||||||
|
markDirty(block.getWorld(), block.getX(), block.getY(), block.getZ());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void markDirty(List<Block> blocks) {
|
||||||
|
for (Block block : blocks) {
|
||||||
|
markDirty(block);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void markDirty(Inventory inventory) {
|
||||||
|
for (Block block : inventoryBlocks(inventory)) {
|
||||||
|
markDirty(block);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void markAllDirty() {
|
||||||
|
for (ActiveStudio studio : service.studios.values()) {
|
||||||
|
markAllDirty(studio);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int markAllDirty(ActiveStudio studio) {
|
||||||
|
int changed = 0;
|
||||||
|
JigsawStudioSession session = studio.generator().getSession();
|
||||||
|
for (JigsawStudioBay bay : session.layout().bays()) {
|
||||||
|
JigsawStudioVariant activeVariant = session.activeVariant(bay.stableId()).orElse(null);
|
||||||
|
if (activeVariant == null || !activeVariant.owned()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
JigsawStudioSession.DirtyMark dirtyMark = session.markWorkcellDirty(bay.stableId());
|
||||||
|
if (dirtyMark.status() == JigsawStudioSession.DirtyStatus.MARKED) {
|
||||||
|
service.autosaveScheduler.scheduleAutosave(
|
||||||
|
studio,
|
||||||
|
bay,
|
||||||
|
dirtyMark.identity().orElseThrow(),
|
||||||
|
AUTOSAVE_DEBOUNCE_TICKS);
|
||||||
|
changed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (changed > 0) {
|
||||||
|
service.markEvaluationStale(studio);
|
||||||
|
for (JigsawStudioBay bay : session.layout().bays()) {
|
||||||
|
service.playerContext.refreshWorkcellContext(studio.worldId(), bay.stableId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,308 @@
|
|||||||
|
package art.arcane.iris.core.service;
|
||||||
|
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioActivation;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioLayout;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioMode;
|
||||||
|
import art.arcane.iris.core.service.JigsawStudioService.ActiveStudio;
|
||||||
|
import art.arcane.iris.engine.framework.PlacedStructurePiece;
|
||||||
|
import art.arcane.iris.engine.framework.StructureAssembler;
|
||||||
|
import art.arcane.iris.engine.framework.structure.StructureAssemblyResult;
|
||||||
|
import art.arcane.iris.engine.framework.structure.StructureGraphCompilation;
|
||||||
|
import art.arcane.iris.engine.framework.structure.StructureGraphCompiler;
|
||||||
|
import art.arcane.iris.engine.framework.structure.StructureGraphDiagnostic;
|
||||||
|
import art.arcane.iris.engine.framework.structure.StructureGraphResolver;
|
||||||
|
import art.arcane.iris.engine.object.IrisPosition;
|
||||||
|
import art.arcane.iris.engine.object.IrisStructure;
|
||||||
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
|
import art.arcane.iris.util.common.scheduling.J;
|
||||||
|
import art.arcane.volmlib.util.math.RNG;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioService.failureMessage;
|
||||||
|
|
||||||
|
final class JigsawStudioEvaluator {
|
||||||
|
private static final long PREVIEW_SEED = 1337L;
|
||||||
|
private static final int SPATIAL_PREVIEW_BASE_Y = JigsawStudioLayout.FLOOR_Y + 48;
|
||||||
|
|
||||||
|
private final JigsawStudioService service;
|
||||||
|
final Map<UUID, JigsawStudioGraphEvaluation> evaluations = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
JigsawStudioEvaluator(JigsawStudioService service) {
|
||||||
|
this.service = Objects.requireNonNull(service, "Jigsaw Studio service");
|
||||||
|
}
|
||||||
|
|
||||||
|
void scheduleInitialEvaluation(ActiveStudio studio) {
|
||||||
|
if (studio == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
JigsawStudioActivation.Request request = studio.generator().getRequest();
|
||||||
|
UUID requestId = request.requestId();
|
||||||
|
if (!claimInitialEvaluation(
|
||||||
|
studio.evaluationGeneration(),
|
||||||
|
service.isCurrentRequest(studio, requestId))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
publishEvaluation(studio, requestId, 1L);
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean claimInitialEvaluation(AtomicLong generation, boolean currentRequest) {
|
||||||
|
return currentRequest
|
||||||
|
&& Objects.requireNonNull(generation, "Jigsaw Studio evaluation generation")
|
||||||
|
.compareAndSet(0L, 1L);
|
||||||
|
}
|
||||||
|
|
||||||
|
void scheduleEvaluation(ActiveStudio studio) {
|
||||||
|
if (studio == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
JigsawStudioActivation.Request request = studio.generator().getRequest();
|
||||||
|
UUID requestId = request.requestId();
|
||||||
|
if (!service.isCurrentRequest(studio, requestId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
publishEvaluation(studio, requestId, studio.evaluationGeneration().incrementAndGet());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void publishEvaluation(ActiveStudio studio, UUID requestId, long generation) {
|
||||||
|
JigsawStudioGraphEvaluation previous = evaluations.get(requestId);
|
||||||
|
JigsawStudioPreviewRenderer.PreviewBounds previousBounds = previous == null
|
||||||
|
? JigsawStudioPreviewRenderer.PreviewBounds.empty()
|
||||||
|
: previous.previewBounds();
|
||||||
|
evaluations.put(requestId, new JigsawStudioGraphEvaluation(
|
||||||
|
requestId,
|
||||||
|
generation,
|
||||||
|
PREVIEW_SEED,
|
||||||
|
JigsawStudioEvaluationState.PENDING,
|
||||||
|
previous == null ? "" : previous.selectedTheme(),
|
||||||
|
previous == null ? 0 : previous.pieceCount(),
|
||||||
|
"Compiling the committed graph and assembling seed 1337",
|
||||||
|
previousBounds));
|
||||||
|
service.playerContext.refreshAllWorkcellContexts(studio);
|
||||||
|
J.a(() -> evaluateGraph(studio, requestId, generation));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void evaluateGraph(ActiveStudio studio, UUID requestId, long generation) {
|
||||||
|
EvaluationComputation computation;
|
||||||
|
try {
|
||||||
|
JigsawStudioActivation.Request request = studio.generator().getRequest();
|
||||||
|
IrisStructure structure = request.source().load(
|
||||||
|
IrisStructure.class,
|
||||||
|
request.structureKey(),
|
||||||
|
false);
|
||||||
|
if (structure == null) {
|
||||||
|
throw new IOException("The active structure resource no longer exists");
|
||||||
|
}
|
||||||
|
StructureGraphCompilation compilation = StructureGraphCompiler.compile(
|
||||||
|
structure,
|
||||||
|
StructureGraphResolver.forData(request.source()));
|
||||||
|
StructureGraphDiagnostic firstError = firstDiagnostic(
|
||||||
|
compilation,
|
||||||
|
StructureGraphDiagnostic.Severity.ERROR);
|
||||||
|
if (firstError != null) {
|
||||||
|
computation = invalidEvaluation(
|
||||||
|
requestId,
|
||||||
|
generation,
|
||||||
|
firstError.code() + ": " + firstError.message());
|
||||||
|
} else {
|
||||||
|
IrisPosition origin = previewOrigin(studio.generator().getLayout(), structure);
|
||||||
|
StructureAssembler assembler = StructureAssembler.forCompilation(compilation, origin);
|
||||||
|
StructureAssemblyResult assembly = assembler.assemble(new RNG(PREVIEW_SEED));
|
||||||
|
computation = evaluationForAssembly(
|
||||||
|
requestId,
|
||||||
|
generation,
|
||||||
|
compilation,
|
||||||
|
assembly,
|
||||||
|
studio.generator().getLayout().mode());
|
||||||
|
}
|
||||||
|
} catch (Throwable exception) {
|
||||||
|
IrisLogging.reportError(exception);
|
||||||
|
computation = invalidEvaluation(requestId, generation, failureMessage(exception));
|
||||||
|
}
|
||||||
|
completeEvaluation(studio, computation);
|
||||||
|
}
|
||||||
|
|
||||||
|
private EvaluationComputation evaluationForAssembly(
|
||||||
|
UUID requestId,
|
||||||
|
long generation,
|
||||||
|
StructureGraphCompilation compilation,
|
||||||
|
StructureAssemblyResult assembly,
|
||||||
|
JigsawStudioMode mode
|
||||||
|
) throws IOException {
|
||||||
|
if (assembly.status().isFailure()) {
|
||||||
|
return invalidEvaluation(
|
||||||
|
requestId,
|
||||||
|
generation,
|
||||||
|
assembly.status().name().toLowerCase(Locale.ROOT) + ": " + assembly.detail());
|
||||||
|
}
|
||||||
|
if (!assembly.hasOutput()) {
|
||||||
|
return new EvaluationComputation(
|
||||||
|
new JigsawStudioGraphEvaluation(
|
||||||
|
requestId,
|
||||||
|
generation,
|
||||||
|
PREVIEW_SEED,
|
||||||
|
JigsawStudioEvaluationState.WARNING,
|
||||||
|
assembly.selectedTheme(),
|
||||||
|
0,
|
||||||
|
assembly.detail().isEmpty()
|
||||||
|
? "Seed 1337 intentionally generated no structure"
|
||||||
|
: assembly.detail(),
|
||||||
|
JigsawStudioPreviewRenderer.PreviewBounds.empty()),
|
||||||
|
JigsawStudioPreviewRenderer.PreviewPlan.empty());
|
||||||
|
}
|
||||||
|
List<PlacedStructurePiece> aligned = alignPreviewPieces(assembly.pieces(), mode);
|
||||||
|
JigsawStudioPreviewRenderer.PreviewPlan plan = JigsawStudioPreviewRenderer.plan(aligned);
|
||||||
|
StructureGraphDiagnostic firstWarning = firstDiagnostic(
|
||||||
|
compilation,
|
||||||
|
StructureGraphDiagnostic.Severity.WARNING);
|
||||||
|
JigsawStudioEvaluationState state = firstWarning == null
|
||||||
|
? JigsawStudioEvaluationState.VALID
|
||||||
|
: JigsawStudioEvaluationState.WARNING;
|
||||||
|
String detail = firstWarning == null
|
||||||
|
? "Seed 1337 assembled " + aligned.size() + " piece(s)"
|
||||||
|
: firstWarning.code() + ": " + firstWarning.message();
|
||||||
|
return new EvaluationComputation(
|
||||||
|
new JigsawStudioGraphEvaluation(
|
||||||
|
requestId,
|
||||||
|
generation,
|
||||||
|
PREVIEW_SEED,
|
||||||
|
state,
|
||||||
|
assembly.selectedTheme(),
|
||||||
|
aligned.size(),
|
||||||
|
detail,
|
||||||
|
plan.bounds()),
|
||||||
|
plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void completeEvaluation(ActiveStudio studio, EvaluationComputation computation) {
|
||||||
|
JigsawStudioGraphEvaluation evaluated = computation.evaluation();
|
||||||
|
UUID requestId = evaluated.requestId();
|
||||||
|
if (!isCurrentEvaluation(studio, requestId, evaluated.generation())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
service.previewRenderer.render(
|
||||||
|
studio.world(),
|
||||||
|
requestId,
|
||||||
|
evaluated.generation(),
|
||||||
|
computation.plan(),
|
||||||
|
result -> {
|
||||||
|
if (!isCurrentEvaluation(studio, requestId, evaluated.generation())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
JigsawStudioGraphEvaluation completed = result.successful()
|
||||||
|
? evaluated
|
||||||
|
: new JigsawStudioGraphEvaluation(
|
||||||
|
requestId,
|
||||||
|
evaluated.generation(),
|
||||||
|
PREVIEW_SEED,
|
||||||
|
JigsawStudioEvaluationState.WARNING,
|
||||||
|
evaluated.selectedTheme(),
|
||||||
|
evaluated.pieceCount(),
|
||||||
|
evaluated.detail() + "; " + result.failure(),
|
||||||
|
evaluated.previewBounds());
|
||||||
|
evaluations.put(requestId, completed);
|
||||||
|
service.playerContext.refreshAllWorkcellContexts(studio);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isCurrentEvaluation(ActiveStudio studio, UUID requestId, long generation) {
|
||||||
|
if (!service.isCurrentRequest(studio, requestId)
|
||||||
|
|| studio.evaluationGeneration().get() != generation) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioGraphEvaluation current = evaluations.get(requestId);
|
||||||
|
return current != null && current.generation() == generation;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static EvaluationComputation invalidEvaluation(
|
||||||
|
UUID requestId,
|
||||||
|
long generation,
|
||||||
|
String detail
|
||||||
|
) {
|
||||||
|
return new EvaluationComputation(
|
||||||
|
new JigsawStudioGraphEvaluation(
|
||||||
|
requestId,
|
||||||
|
generation,
|
||||||
|
PREVIEW_SEED,
|
||||||
|
JigsawStudioEvaluationState.INVALID,
|
||||||
|
"",
|
||||||
|
0,
|
||||||
|
detail,
|
||||||
|
JigsawStudioPreviewRenderer.PreviewBounds.empty()),
|
||||||
|
JigsawStudioPreviewRenderer.PreviewPlan.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static StructureGraphDiagnostic firstDiagnostic(
|
||||||
|
StructureGraphCompilation compilation,
|
||||||
|
StructureGraphDiagnostic.Severity severity
|
||||||
|
) {
|
||||||
|
for (StructureGraphDiagnostic diagnostic : compilation.getDiagnostics()) {
|
||||||
|
if (diagnostic.severity() == severity) {
|
||||||
|
return diagnostic;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IrisPosition previewOrigin(JigsawStudioLayout layout, IrisStructure structure) {
|
||||||
|
int radius = Math.max(1, structure.getMaxSizeChunks()) * 16;
|
||||||
|
return new IrisPosition(
|
||||||
|
-radius - 32,
|
||||||
|
0,
|
||||||
|
Math.max(16, layout.extentZ() / 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<PlacedStructurePiece> alignPreviewPieces(
|
||||||
|
List<PlacedStructurePiece> pieces,
|
||||||
|
JigsawStudioMode mode
|
||||||
|
) {
|
||||||
|
int minimumY = Integer.MAX_VALUE;
|
||||||
|
for (PlacedStructurePiece piece : pieces) {
|
||||||
|
minimumY = Math.min(minimumY, piece.getMinY());
|
||||||
|
}
|
||||||
|
int baseY = mode == JigsawStudioMode.SPATIAL_JIGSAW
|
||||||
|
? SPATIAL_PREVIEW_BASE_Y
|
||||||
|
: JigsawStudioLayout.FLOOR_Y + 1;
|
||||||
|
int shiftY = baseY - minimumY;
|
||||||
|
List<PlacedStructurePiece> aligned = new ArrayList<>(pieces.size());
|
||||||
|
for (PlacedStructurePiece piece : pieces) {
|
||||||
|
aligned.add(new PlacedStructurePiece(
|
||||||
|
piece.getPiece(),
|
||||||
|
piece.getObject(),
|
||||||
|
piece.getX(),
|
||||||
|
piece.getY() + shiftY,
|
||||||
|
piece.getZ(),
|
||||||
|
piece.getRotation(),
|
||||||
|
piece.getMinX(),
|
||||||
|
piece.getMinY() + shiftY,
|
||||||
|
piece.getMinZ(),
|
||||||
|
piece.getMaxX(),
|
||||||
|
piece.getMaxY() + shiftY,
|
||||||
|
piece.getMaxZ()));
|
||||||
|
}
|
||||||
|
return List.copyOf(aligned);
|
||||||
|
}
|
||||||
|
|
||||||
|
static IrisStructure loadStudioStructure(ActiveStudio studio) {
|
||||||
|
JigsawStudioActivation.Request request = studio.generator().getRequest();
|
||||||
|
return request.source().load(IrisStructure.class, request.structureKey(), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private record EvaluationComputation(
|
||||||
|
JigsawStudioGraphEvaluation evaluation,
|
||||||
|
JigsawStudioPreviewRenderer.PreviewPlan plan
|
||||||
|
) {
|
||||||
|
private EvaluationComputation {
|
||||||
|
Objects.requireNonNull(evaluation, "Jigsaw Studio graph evaluation");
|
||||||
|
Objects.requireNonNull(plan, "Jigsaw Studio evaluation preview plan");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,417 @@
|
|||||||
|
package art.arcane.iris.core.service;
|
||||||
|
|
||||||
|
import art.arcane.iris.core.loader.IrisData;
|
||||||
|
import art.arcane.iris.core.nms.INMS;
|
||||||
|
import art.arcane.iris.core.runtime.InPlaceChunkRegenerator;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioActivation;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioBay;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioBounds;
|
||||||
|
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.ActiveStudio;
|
||||||
|
import art.arcane.iris.core.service.JigsawStudioService.CommandGraphMutation;
|
||||||
|
import art.arcane.iris.core.service.JigsawStudioService.CommandGraphMutationResult;
|
||||||
|
import art.arcane.iris.engine.data.chunk.TerrainChunk;
|
||||||
|
import art.arcane.iris.engine.object.IrisStructure;
|
||||||
|
import art.arcane.iris.engine.platform.studio.generators.JigsawStudioGenerator;
|
||||||
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
|
import art.arcane.iris.util.common.scheduling.J;
|
||||||
|
import org.bukkit.Chunk;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioChunkWriter.applyRenderedBayChunk;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioChunkWriter.verifyRenderedBayChunk;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioProtection.authorizeOwner;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioService.failureMessage;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioService.message;
|
||||||
|
|
||||||
|
final class JigsawStudioGraphMutations {
|
||||||
|
private final JigsawStudioService service;
|
||||||
|
final Set<UUID> graphMutationsInProgress = new HashSet<>();
|
||||||
|
|
||||||
|
JigsawStudioGraphMutations(JigsawStudioService service) {
|
||||||
|
this.service = Objects.requireNonNull(service, "Jigsaw Studio service");
|
||||||
|
}
|
||||||
|
|
||||||
|
String beginGraphMutation(
|
||||||
|
ActiveStudio studio,
|
||||||
|
JigsawStudioSession session
|
||||||
|
) {
|
||||||
|
UUID requestId = studio.generator().getRequest().requestId();
|
||||||
|
service.tileWatcher.finalizeJigsawTileWatches(requestId);
|
||||||
|
synchronized (service.saveLifecycleLock) {
|
||||||
|
if (service.saveLifecycle.closingRequests.contains(requestId)) {
|
||||||
|
return "This Jigsaw Studio is closing and cannot update its graph.";
|
||||||
|
}
|
||||||
|
if (service.saveLifecycle.savesInProgress.contains(requestId)) {
|
||||||
|
return "Wait for the current Jigsaw Studio save to finish.";
|
||||||
|
}
|
||||||
|
if (graphMutationsInProgress.contains(requestId)) {
|
||||||
|
return "A Jigsaw Studio graph update is already running.";
|
||||||
|
}
|
||||||
|
if (service.saveLifecycle.exportsInProgress.contains(requestId)) {
|
||||||
|
return "Wait for the current Jigsaw Studio export to finish.";
|
||||||
|
}
|
||||||
|
if (service.materializer.materializationsInProgress.contains(requestId)) {
|
||||||
|
return "Wait for the current Jigsaw Studio variant load or rollback to finish.";
|
||||||
|
}
|
||||||
|
if (session.operationInProgress()) {
|
||||||
|
return "Wait for the current workcell operation to finish.";
|
||||||
|
}
|
||||||
|
if (service.saveLifecycle.reopenRequiredRequests.contains(requestId)) {
|
||||||
|
return "Close and reopen Jigsaw Studio before making another graph change.";
|
||||||
|
}
|
||||||
|
if (service.tileWatcher.hasJigsawTileWatch(requestId)) {
|
||||||
|
return "Finish or close the open vanilla jigsaw-block editor before changing graph metadata.";
|
||||||
|
}
|
||||||
|
if (session.isDirty()) {
|
||||||
|
return "Wait for every dirty workcell to finish autosaving before changing graph metadata.";
|
||||||
|
}
|
||||||
|
graphMutationsInProgress.add(requestId);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean runCommandGraphMutation(
|
||||||
|
Player player,
|
||||||
|
UUID expectedRequestId,
|
||||||
|
CommandGraphMutation task
|
||||||
|
) {
|
||||||
|
Objects.requireNonNull(expectedRequestId, "Jigsaw Studio command graph request ID");
|
||||||
|
Objects.requireNonNull(task, "Jigsaw Studio command graph mutation");
|
||||||
|
if (player == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!J.isOwnedByCurrentRegion(player)) {
|
||||||
|
return J.runEntity(player, () -> runCommandGraphMutation(player, expectedRequestId, task));
|
||||||
|
}
|
||||||
|
ActiveStudio studio = service.studios.get(player.getWorld().getUID());
|
||||||
|
if (studio == null
|
||||||
|
|| !studio.generator().getRequest().requestId().equals(expectedRequestId)) {
|
||||||
|
message(player, "This Jigsaw Studio session is no longer active.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!authorizeOwner(player, studio)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String reservationFailure = beginGraphMutation(studio, studio.generator().getSession());
|
||||||
|
if (!reservationFailure.isEmpty()) {
|
||||||
|
message(player, reservationFailure);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return scheduleGraphMutation(player, studio, task);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean scheduleGraphMutation(
|
||||||
|
Player player,
|
||||||
|
ActiveStudio studio,
|
||||||
|
CommandGraphMutation task
|
||||||
|
) {
|
||||||
|
UUID requestId = studio.generator().getRequest().requestId();
|
||||||
|
J.a(() -> {
|
||||||
|
CommandGraphMutationResult result = null;
|
||||||
|
Throwable failure = null;
|
||||||
|
try {
|
||||||
|
result = task.run();
|
||||||
|
} catch (Throwable exception) {
|
||||||
|
failure = exception;
|
||||||
|
}
|
||||||
|
CommandGraphMutationResult completedResult = result;
|
||||||
|
Throwable completedFailure = failure;
|
||||||
|
boolean scheduled = J.runEntity(
|
||||||
|
player,
|
||||||
|
() -> completeGraphMutation(
|
||||||
|
player,
|
||||||
|
studio,
|
||||||
|
requestId,
|
||||||
|
completedResult,
|
||||||
|
completedFailure),
|
||||||
|
0,
|
||||||
|
() -> finishGraphMutation(requestId));
|
||||||
|
if (!scheduled) {
|
||||||
|
finishGraphMutation(requestId);
|
||||||
|
if (completedFailure != null) {
|
||||||
|
IrisLogging.reportError(completedFailure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void completeGraphMutation(
|
||||||
|
Player player,
|
||||||
|
ActiveStudio studio,
|
||||||
|
UUID requestId,
|
||||||
|
CommandGraphMutationResult result,
|
||||||
|
Throwable failure
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
if (failure != null) {
|
||||||
|
IrisLogging.reportError(failure);
|
||||||
|
message(player, "Graph update failed: " + failureMessage(failure));
|
||||||
|
finishGraphMutation(requestId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (result == null) {
|
||||||
|
message(player, "Graph update completed without a result; no Studio state changed.");
|
||||||
|
finishGraphMutation(requestId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!service.isCurrentRequest(studio, requestId)) {
|
||||||
|
message(player, "The graph updated on disk after this Studio session changed; reopen it to continue.");
|
||||||
|
finishGraphMutation(requestId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
JigsawStudioSession session = studio.generator().getSession();
|
||||||
|
JigsawStudioLayout previousLayout = session.layout();
|
||||||
|
if (result.rebindActiveVariants().isEmpty()) {
|
||||||
|
session.replaceLayout(result.layout());
|
||||||
|
} else {
|
||||||
|
session.replaceLayoutAndRebind(result.layout(), result.rebindActiveVariants());
|
||||||
|
}
|
||||||
|
for (JigsawStudioBay workcell : result.layout().bays()) {
|
||||||
|
studio.generator().invalidateRender(workcell.stableId());
|
||||||
|
}
|
||||||
|
if (layoutGeometryChanged(previousLayout, result.layout())) {
|
||||||
|
scheduleLiveRelayout(player, studio, requestId, previousLayout, result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
finishGraphMutationSuccess(player, studio, requestId, result);
|
||||||
|
} catch (Throwable exception) {
|
||||||
|
IrisLogging.reportError(exception);
|
||||||
|
message(player, "The graph updated on disk, but Studio could not refresh: "
|
||||||
|
+ failureMessage(exception) + ". Reopen this project before editing further.");
|
||||||
|
service.saveLifecycle.reopenRequiredRequests.add(requestId);
|
||||||
|
finishGraphMutation(requestId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void finishGraphMutationSuccess(
|
||||||
|
Player player,
|
||||||
|
ActiveStudio studio,
|
||||||
|
UUID requestId,
|
||||||
|
CommandGraphMutationResult result
|
||||||
|
) {
|
||||||
|
for (JigsawStudioBay workcell : result.layout().bays()) {
|
||||||
|
service.playerContext.refreshWorkcellContext(studio.worldId(), workcell.stableId());
|
||||||
|
}
|
||||||
|
service.evaluator.scheduleEvaluation(studio);
|
||||||
|
message(player, result.message());
|
||||||
|
finishGraphMutation(requestId);
|
||||||
|
if (!result.activatePieceKey().isEmpty()) {
|
||||||
|
service.variantEditor.switchVariant(
|
||||||
|
player,
|
||||||
|
result.activateWorkcellId(),
|
||||||
|
result.activatePieceKey(),
|
||||||
|
false);
|
||||||
|
} else if (result.reload().isPresent()) {
|
||||||
|
service.variantEditor.reloadActiveVariant(player, studio, result.reload().orElseThrow());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void scheduleLiveRelayout(
|
||||||
|
Player player,
|
||||||
|
ActiveStudio studio,
|
||||||
|
UUID requestId,
|
||||||
|
JigsawStudioLayout previousLayout,
|
||||||
|
CommandGraphMutationResult result
|
||||||
|
) {
|
||||||
|
Set<Long> chunks = relayoutChunks(previousLayout, result.layout());
|
||||||
|
AtomicInteger remaining = new AtomicInteger(chunks.size());
|
||||||
|
AtomicReference<String> failure = new AtomicReference<>("");
|
||||||
|
AtomicReference<Throwable> cause = new AtomicReference<>();
|
||||||
|
if (chunks.isEmpty()) {
|
||||||
|
completeLiveRelayout(player, studio, requestId, result, "", null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (long chunkKey : chunks) {
|
||||||
|
int chunkX = (int) (chunkKey >> 32);
|
||||||
|
int chunkZ = (int) chunkKey;
|
||||||
|
boolean scheduled = J.runRegion(studio.world(), chunkX, chunkZ, () -> {
|
||||||
|
try {
|
||||||
|
repaintStudioChunk(studio, chunkX, chunkZ);
|
||||||
|
} catch (Throwable exception) {
|
||||||
|
failure.compareAndSet("", "chunk " + chunkX + "," + chunkZ
|
||||||
|
+ " could not regenerate: " + failureMessage(exception));
|
||||||
|
cause.compareAndSet(null, exception);
|
||||||
|
}
|
||||||
|
if (remaining.decrementAndGet() == 0) {
|
||||||
|
scheduleLiveRelayoutCompletion(
|
||||||
|
player,
|
||||||
|
studio,
|
||||||
|
requestId,
|
||||||
|
result,
|
||||||
|
failure.get(),
|
||||||
|
cause.get());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!scheduled) {
|
||||||
|
failure.compareAndSet("", "chunk " + chunkX + "," + chunkZ
|
||||||
|
+ " could not be scheduled on its owning region");
|
||||||
|
if (remaining.decrementAndGet() == 0) {
|
||||||
|
scheduleLiveRelayoutCompletion(
|
||||||
|
player,
|
||||||
|
studio,
|
||||||
|
requestId,
|
||||||
|
result,
|
||||||
|
failure.get(),
|
||||||
|
cause.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void scheduleLiveRelayoutCompletion(
|
||||||
|
Player player,
|
||||||
|
ActiveStudio studio,
|
||||||
|
UUID requestId,
|
||||||
|
CommandGraphMutationResult result,
|
||||||
|
String failure,
|
||||||
|
Throwable cause
|
||||||
|
) {
|
||||||
|
boolean scheduled = J.runEntity(
|
||||||
|
player,
|
||||||
|
() -> completeLiveRelayout(player, studio, requestId, result, failure, cause));
|
||||||
|
if (!scheduled) {
|
||||||
|
service.saveLifecycle.reopenRequiredRequests.add(requestId);
|
||||||
|
finishGraphMutation(requestId);
|
||||||
|
if (cause != null) {
|
||||||
|
IrisLogging.reportError(cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void completeLiveRelayout(
|
||||||
|
Player player,
|
||||||
|
ActiveStudio studio,
|
||||||
|
UUID requestId,
|
||||||
|
CommandGraphMutationResult result,
|
||||||
|
String failure,
|
||||||
|
Throwable cause
|
||||||
|
) {
|
||||||
|
if (!failure.isEmpty() || !service.isCurrentRequest(studio, requestId)) {
|
||||||
|
service.saveLifecycle.reopenRequiredRequests.add(requestId);
|
||||||
|
studio.populations().clear();
|
||||||
|
if (cause != null) {
|
||||||
|
IrisLogging.reportError(cause);
|
||||||
|
}
|
||||||
|
message(player, "The workcell size was saved, but live regeneration failed"
|
||||||
|
+ (failure.isEmpty() ? "." : ": " + failure + ".")
|
||||||
|
+ " Close and reopen this project before editing further.");
|
||||||
|
finishGraphMutation(requestId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
studio.populations().clear();
|
||||||
|
for (JigsawStudioBay workcell : result.layout().bays()) {
|
||||||
|
JigsawStudioGenerator.RenderedBay rendered = studio.generator().renderBay(workcell);
|
||||||
|
studio.replacePopulation(
|
||||||
|
workcell,
|
||||||
|
rendered,
|
||||||
|
rendered.valid() ? "" : rendered.failure(),
|
||||||
|
rendered.valid());
|
||||||
|
}
|
||||||
|
service.saveLifecycle.reopenRequiredRequests.remove(requestId);
|
||||||
|
finishGraphMutationSuccess(player, studio, requestId, result);
|
||||||
|
studio.generator().getSession().selectedBayId().ifPresent(workcellId -> service.teleportTo(player, workcellId));
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean layoutGeometryChanged(
|
||||||
|
JigsawStudioLayout previous,
|
||||||
|
JigsawStudioLayout current
|
||||||
|
) {
|
||||||
|
if (previous.bays().size() != current.bays().size()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
for (JigsawStudioBay previousBay : previous.bays()) {
|
||||||
|
JigsawStudioBay currentBay = current.get(previousBay.stableId());
|
||||||
|
if (currentBay == null || !previousBay.bounds().equals(currentBay.bounds())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Set<Long> relayoutChunks(
|
||||||
|
JigsawStudioLayout previous,
|
||||||
|
JigsawStudioLayout current
|
||||||
|
) {
|
||||||
|
Set<Long> chunks = new HashSet<>();
|
||||||
|
addRelayoutChunks(chunks, previous);
|
||||||
|
addRelayoutChunks(chunks, current);
|
||||||
|
return Set.copyOf(chunks);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addRelayoutChunks(Set<Long> chunks, JigsawStudioLayout layout) {
|
||||||
|
for (JigsawStudioBay workcell : layout.bays()) {
|
||||||
|
JigsawStudioBounds bounds = workcell.bounds();
|
||||||
|
int minimumChunkX = (bounds.originX() - 1) >> 4;
|
||||||
|
int maximumChunkX = (bounds.maxX() + 1) >> 4;
|
||||||
|
int minimumChunkZ = (bounds.originZ() - 1) >> 4;
|
||||||
|
int maximumChunkZ = (bounds.maxZ() + 1) >> 4;
|
||||||
|
for (int chunkX = minimumChunkX; chunkX <= maximumChunkX; chunkX++) {
|
||||||
|
for (int chunkZ = minimumChunkZ; chunkZ <= maximumChunkZ; chunkZ++) {
|
||||||
|
chunks.add(JigsawStudioService.chunkKey(chunkX, chunkZ));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void repaintStudioChunk(ActiveStudio studio, int chunkX, int chunkZ) throws IOException {
|
||||||
|
World world = studio.world();
|
||||||
|
Chunk chunk = world.getChunkAt(chunkX, chunkZ);
|
||||||
|
TerrainChunk generated = TerrainChunk.create(world);
|
||||||
|
studio.generator().paintChunk(generated, chunkX, chunkZ);
|
||||||
|
if (!INMS.get().applyChunkBlocks(chunk, generated)) {
|
||||||
|
InPlaceChunkRegenerator.applyBlockDiffs(
|
||||||
|
chunk,
|
||||||
|
generated.getChunkData(),
|
||||||
|
world.getMinHeight(),
|
||||||
|
world.getMaxHeight());
|
||||||
|
}
|
||||||
|
for (JigsawStudioBay workcell : studio.generator().getLayout().bays()) {
|
||||||
|
JigsawStudioGenerator.RenderedBay rendered = studio.generator().renderBay(workcell);
|
||||||
|
if (!rendered.valid()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
boolean connectorsVisible = studio.generator().getSession()
|
||||||
|
.workcellSnapshot(workcell.stableId())
|
||||||
|
.connectorsVisible();
|
||||||
|
applyRenderedBayChunk(world, workcell, rendered, chunkX, chunkZ, connectorsVisible);
|
||||||
|
verifyRenderedBayChunk(world, workcell, rendered, chunkX, chunkZ, connectorsVisible);
|
||||||
|
}
|
||||||
|
world.refreshChunk(chunkX, chunkZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean graphMutationInProgress(UUID requestId) {
|
||||||
|
synchronized (service.saveLifecycleLock) {
|
||||||
|
return graphMutationsInProgress.contains(requestId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void finishGraphMutation(UUID requestId) {
|
||||||
|
synchronized (service.saveLifecycleLock) {
|
||||||
|
graphMutationsInProgress.remove(requestId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static JigsawStudioLayout loadMappedLayout(ActiveStudio studio) throws IOException {
|
||||||
|
JigsawStudioActivation.Request request = studio.generator().getRequest();
|
||||||
|
IrisData source = request.source();
|
||||||
|
source.invalidateStructureResources();
|
||||||
|
IrisStructure structure = source.load(IrisStructure.class, request.structureKey(), false);
|
||||||
|
if (structure == null) {
|
||||||
|
throw new IOException("The structure could not be reloaded after the graph transaction");
|
||||||
|
}
|
||||||
|
return JigsawStudioGraphMapper.map(source, structure);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,632 @@
|
|||||||
|
package art.arcane.iris.core.service;
|
||||||
|
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioBay;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioBounds;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioSession;
|
||||||
|
import art.arcane.iris.core.service.JigsawStudioCapture.ChunkCaptureArea;
|
||||||
|
import art.arcane.iris.core.service.JigsawStudioCapture.LocalPosition;
|
||||||
|
import art.arcane.iris.core.service.JigsawStudioService.ActiveStudio;
|
||||||
|
import art.arcane.iris.engine.platform.studio.generators.JigsawStudioGenerator;
|
||||||
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
|
import art.arcane.iris.util.common.scheduling.J;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioCapture.chunkIntersections;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioChunkWriter.hydrateChunk;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioChunkWriter.restoreConnectorChunk;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioChunkWriter.validateMaterialization;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioChunkWriter.writeMaterializedChunk;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioService.chunkKey;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioService.failureMessage;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioService.message;
|
||||||
|
|
||||||
|
final class JigsawStudioMaterializer {
|
||||||
|
private static final int HYDRATION_RETRY_TICKS = 2;
|
||||||
|
private static final int MAX_HYDRATION_ATTEMPTS = 40;
|
||||||
|
|
||||||
|
private final JigsawStudioService service;
|
||||||
|
final Set<UUID> materializationsInProgress = new HashSet<>();
|
||||||
|
|
||||||
|
JigsawStudioMaterializer(JigsawStudioService service) {
|
||||||
|
this.service = Objects.requireNonNull(service, "Jigsaw Studio service");
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean scheduleMaterialization(MaterializationWork work) {
|
||||||
|
String validationFailure = validateMaterialization(work.target());
|
||||||
|
if (!validationFailure.isEmpty()) {
|
||||||
|
work.studio().generator().getSession().abortVariantSwitch(work.token());
|
||||||
|
service.playerContext.refreshWorkcellContext(work.studio().worldId(), work.workcell().stableId());
|
||||||
|
message(work.player(), "Variant '" + work.token().targetVariant().pieceKey()
|
||||||
|
+ "' cannot load: " + validationFailure);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String reservationFailure = beginMaterialization(work);
|
||||||
|
if (!reservationFailure.isEmpty()) {
|
||||||
|
work.studio().generator().getSession().abortVariantSwitch(work.token());
|
||||||
|
service.playerContext.refreshWorkcellContext(work.studio().worldId(), work.workcell().stableId());
|
||||||
|
message(work.player(), reservationFailure);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
List<ChunkCaptureArea> areas = chunkIntersections(work.workcell().bounds());
|
||||||
|
MaterializationCoordinator coordinator = new MaterializationCoordinator(work, areas);
|
||||||
|
for (ChunkCaptureArea area : areas) {
|
||||||
|
boolean scheduled = J.runRegion(
|
||||||
|
work.world(),
|
||||||
|
area.chunkX(),
|
||||||
|
area.chunkZ(),
|
||||||
|
() -> materializeCandidateChunk(coordinator, area));
|
||||||
|
if (!scheduled) {
|
||||||
|
coordinator.candidateComplete(
|
||||||
|
area,
|
||||||
|
"Iris could not schedule workcell chunk "
|
||||||
|
+ area.chunkX() + "," + area.chunkZ() + " on its owning region.",
|
||||||
|
null);
|
||||||
|
} else {
|
||||||
|
coordinator.markScheduled();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return coordinator.scheduledAny();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String beginMaterialization(MaterializationWork work) {
|
||||||
|
UUID requestId = work.studio().generator().getRequest().requestId();
|
||||||
|
synchronized (service.saveLifecycleLock) {
|
||||||
|
if (!isCurrentVariantSwitch(work)) {
|
||||||
|
return "The Jigsaw Studio changed before the variant load could start.";
|
||||||
|
}
|
||||||
|
if (service.saveLifecycle.closingRequests.contains(requestId)) {
|
||||||
|
return "This Jigsaw Studio is closing and cannot load another variant.";
|
||||||
|
}
|
||||||
|
if (service.saveLifecycle.savesInProgress.contains(requestId)) {
|
||||||
|
return "Wait for the current Jigsaw Studio save to finish.";
|
||||||
|
}
|
||||||
|
if (service.graphMutations.graphMutationsInProgress.contains(requestId)) {
|
||||||
|
return "Wait for the current Jigsaw Studio graph update to finish.";
|
||||||
|
}
|
||||||
|
if (service.saveLifecycle.exportsInProgress.contains(requestId)) {
|
||||||
|
return "Wait for the current Jigsaw Studio export to finish.";
|
||||||
|
}
|
||||||
|
if (service.tileWatcher.hasJigsawTileWatch(requestId)) {
|
||||||
|
return "Finish or close the open vanilla jigsaw-block editor before loading another variant.";
|
||||||
|
}
|
||||||
|
if (!materializationsInProgress.add(requestId)) {
|
||||||
|
return "Another Jigsaw Studio variant load or rollback is already running.";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String beginConnectorRepair(ActiveStudio studio) {
|
||||||
|
UUID requestId = studio.generator().getRequest().requestId();
|
||||||
|
service.tileWatcher.finalizeJigsawTileWatches(requestId);
|
||||||
|
synchronized (service.saveLifecycleLock) {
|
||||||
|
if (!service.isCurrentRequest(studio, requestId)) {
|
||||||
|
return "This Jigsaw Studio session is no longer active.";
|
||||||
|
}
|
||||||
|
if (service.saveLifecycle.closingRequests.contains(requestId)) {
|
||||||
|
return "This Jigsaw Studio is closing and cannot reset connector blocks.";
|
||||||
|
}
|
||||||
|
if (service.saveLifecycle.savesInProgress.contains(requestId)) {
|
||||||
|
return "Wait for the current Jigsaw Studio save to finish.";
|
||||||
|
}
|
||||||
|
if (service.graphMutations.graphMutationsInProgress.contains(requestId)) {
|
||||||
|
return "Wait for the current Jigsaw Studio graph update to finish.";
|
||||||
|
}
|
||||||
|
if (service.saveLifecycle.exportsInProgress.contains(requestId)) {
|
||||||
|
return "Wait for the current Jigsaw Studio export to finish.";
|
||||||
|
}
|
||||||
|
if (studio.generator().getSession().operationInProgress()) {
|
||||||
|
return "Wait for the current workcell operation to finish.";
|
||||||
|
}
|
||||||
|
if (service.tileWatcher.hasJigsawTileWatch(requestId)) {
|
||||||
|
return "Finish or close the open vanilla jigsaw-block editor before resetting connectors.";
|
||||||
|
}
|
||||||
|
if (!materializationsInProgress.add(requestId)) {
|
||||||
|
return "Another Jigsaw Studio variant load or repair is already running.";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean scheduleConnectorRepair(
|
||||||
|
Player player,
|
||||||
|
ActiveStudio studio,
|
||||||
|
JigsawStudioBay workcell,
|
||||||
|
JigsawStudioGenerator.RenderedBay rendered,
|
||||||
|
boolean connectorsVisible
|
||||||
|
) {
|
||||||
|
Map<Long, List<JigsawStudioGenerator.RenderedConnector>> connectorsByChunk = new HashMap<>();
|
||||||
|
JigsawStudioBounds bounds = workcell.bounds();
|
||||||
|
for (JigsawStudioGenerator.RenderedConnector connector : rendered.connectors()) {
|
||||||
|
int worldX = bounds.originX() + connector.x();
|
||||||
|
int worldZ = bounds.originZ() + connector.z();
|
||||||
|
connectorsByChunk.computeIfAbsent(
|
||||||
|
chunkKey(worldX >> 4, worldZ >> 4),
|
||||||
|
ignored -> new ArrayList<>()).add(connector);
|
||||||
|
}
|
||||||
|
AtomicInteger remaining = new AtomicInteger(connectorsByChunk.size());
|
||||||
|
AtomicReference<String> failure = new AtomicReference<>("");
|
||||||
|
AtomicReference<Throwable> cause = new AtomicReference<>();
|
||||||
|
AtomicBoolean scheduledAny = new AtomicBoolean(false);
|
||||||
|
Map<LocalPosition, JigsawStudioGenerator.RenderedBlock> renderedBlocks = new HashMap<>();
|
||||||
|
for (JigsawStudioGenerator.RenderedBlock block : rendered.blocks()) {
|
||||||
|
renderedBlocks.put(new LocalPosition(block.x(), block.y(), block.z()), block);
|
||||||
|
}
|
||||||
|
for (Map.Entry<Long, List<JigsawStudioGenerator.RenderedConnector>> entry
|
||||||
|
: connectorsByChunk.entrySet()) {
|
||||||
|
int chunkX = (int) (entry.getKey() >> 32);
|
||||||
|
int chunkZ = (int) entry.getKey().longValue();
|
||||||
|
boolean scheduled = J.runRegion(studio.world(), chunkX, chunkZ, () -> {
|
||||||
|
try {
|
||||||
|
if (!studio.world().isChunkLoaded(chunkX, chunkZ)) {
|
||||||
|
throw new IOException("connector chunk " + chunkX + "," + chunkZ
|
||||||
|
+ " is not loaded");
|
||||||
|
}
|
||||||
|
restoreConnectorChunk(
|
||||||
|
studio.world(),
|
||||||
|
workcell,
|
||||||
|
entry.getValue(),
|
||||||
|
renderedBlocks,
|
||||||
|
connectorsVisible);
|
||||||
|
} catch (Throwable exception) {
|
||||||
|
failure.compareAndSet("", failureMessage(exception));
|
||||||
|
cause.compareAndSet(null, exception);
|
||||||
|
}
|
||||||
|
if (remaining.decrementAndGet() == 0) {
|
||||||
|
completeConnectorRepair(
|
||||||
|
player,
|
||||||
|
studio,
|
||||||
|
rendered.connectors().size(),
|
||||||
|
failure.get(),
|
||||||
|
cause.get());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (scheduled) {
|
||||||
|
scheduledAny.set(true);
|
||||||
|
} else {
|
||||||
|
failure.compareAndSet("", "connector chunk " + chunkX + "," + chunkZ
|
||||||
|
+ " could not be scheduled on its owning region");
|
||||||
|
if (remaining.decrementAndGet() == 0) {
|
||||||
|
completeConnectorRepair(
|
||||||
|
player,
|
||||||
|
studio,
|
||||||
|
rendered.connectors().size(),
|
||||||
|
failure.get(),
|
||||||
|
cause.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!scheduledAny.get()) {
|
||||||
|
finishMaterialization(studio.generator().getRequest().requestId());
|
||||||
|
}
|
||||||
|
return scheduledAny.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void completeConnectorRepair(
|
||||||
|
Player player,
|
||||||
|
ActiveStudio studio,
|
||||||
|
int connectorCount,
|
||||||
|
String failure,
|
||||||
|
Throwable cause
|
||||||
|
) {
|
||||||
|
UUID requestId = studio.generator().getRequest().requestId();
|
||||||
|
boolean scheduled = J.runEntity(player, () -> {
|
||||||
|
finishMaterialization(requestId);
|
||||||
|
if (cause != null) {
|
||||||
|
IrisLogging.reportError(cause);
|
||||||
|
}
|
||||||
|
if (!failure.isEmpty()) {
|
||||||
|
message(player, "Connector reset was incomplete: " + failure + ". Retry after visiting the workcell.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
message(player, "Restored " + connectorCount
|
||||||
|
+ " connector block(s) from the last saved iteration.");
|
||||||
|
});
|
||||||
|
if (!scheduled) {
|
||||||
|
finishMaterialization(requestId);
|
||||||
|
if (cause != null) {
|
||||||
|
IrisLogging.reportError(cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void finishMaterialization(UUID requestId) {
|
||||||
|
synchronized (service.saveLifecycleLock) {
|
||||||
|
materializationsInProgress.remove(requestId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void materializeCandidateChunk(
|
||||||
|
MaterializationCoordinator coordinator,
|
||||||
|
ChunkCaptureArea area
|
||||||
|
) {
|
||||||
|
MaterializationWork work = coordinator.work();
|
||||||
|
if (!isCurrentVariantSwitch(work)) {
|
||||||
|
coordinator.candidateComplete(
|
||||||
|
area,
|
||||||
|
"Jigsaw Studio changed before the variant could finish loading.",
|
||||||
|
null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!work.world().isChunkLoaded(area.chunkX(), area.chunkZ())) {
|
||||||
|
coordinator.candidateComplete(
|
||||||
|
area,
|
||||||
|
"Workcell chunk " + area.chunkX() + "," + area.chunkZ()
|
||||||
|
+ " is not loaded. Visit the whole workcell and try again.",
|
||||||
|
null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
coordinator.markCandidateTouched(area);
|
||||||
|
writeMaterializedChunk(
|
||||||
|
work.world(),
|
||||||
|
work.workcell(),
|
||||||
|
work.target(),
|
||||||
|
area,
|
||||||
|
work.targetConnectorsVisible());
|
||||||
|
coordinator.candidateComplete(area, "", null);
|
||||||
|
} catch (Throwable exception) {
|
||||||
|
coordinator.candidateComplete(
|
||||||
|
area,
|
||||||
|
"Variant materialization failed in chunk "
|
||||||
|
+ area.chunkX() + "," + area.chunkZ() + ": " + failureMessage(exception),
|
||||||
|
exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void materializeRollbackChunk(
|
||||||
|
MaterializationCoordinator coordinator,
|
||||||
|
ChunkCaptureArea area
|
||||||
|
) {
|
||||||
|
MaterializationWork work = coordinator.work();
|
||||||
|
if (!isRollbackVariantSwitch(work)) {
|
||||||
|
coordinator.rollbackComplete(
|
||||||
|
area,
|
||||||
|
"Jigsaw Studio changed before the previous variant could be restored.",
|
||||||
|
null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!work.world().isChunkLoaded(area.chunkX(), area.chunkZ())) {
|
||||||
|
coordinator.rollbackComplete(
|
||||||
|
area,
|
||||||
|
"Rollback chunk " + area.chunkX() + "," + area.chunkZ() + " is not loaded.",
|
||||||
|
null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
writeMaterializedChunk(
|
||||||
|
work.world(),
|
||||||
|
work.workcell(),
|
||||||
|
work.previous(),
|
||||||
|
area,
|
||||||
|
work.previousConnectorsVisible());
|
||||||
|
coordinator.rollbackComplete(area, "", null);
|
||||||
|
} catch (Throwable exception) {
|
||||||
|
coordinator.rollbackComplete(
|
||||||
|
area,
|
||||||
|
"Rollback failed in chunk " + area.chunkX() + "," + area.chunkZ()
|
||||||
|
+ ": " + failureMessage(exception),
|
||||||
|
exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isCurrentVariantSwitch(MaterializationWork work) {
|
||||||
|
return service.isCurrentRequest(
|
||||||
|
work.studio(),
|
||||||
|
work.studio().generator().getRequest().requestId())
|
||||||
|
&& work.studio().generator().getSession().isVariantSwitchCurrent(work.token());
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isRollbackVariantSwitch(MaterializationWork work) {
|
||||||
|
return service.studios.get(work.studio().worldId()) == work.studio()
|
||||||
|
&& service.protection.materializationInProgress(work.studio());
|
||||||
|
}
|
||||||
|
|
||||||
|
void scheduleHydration(
|
||||||
|
ActiveStudio studio,
|
||||||
|
World world,
|
||||||
|
int chunkX,
|
||||||
|
int chunkZ,
|
||||||
|
int attempt
|
||||||
|
) {
|
||||||
|
if (!service.enabled || service.studios.get(world.getUID()) != studio) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
long key = chunkKey(chunkX, chunkZ);
|
||||||
|
if (!studio.hydrationsInProgress().add(key)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
boolean scheduled = J.runRegion(
|
||||||
|
world,
|
||||||
|
chunkX,
|
||||||
|
chunkZ,
|
||||||
|
() -> runHydrationAttempt(studio, world, chunkX, chunkZ, attempt, key),
|
||||||
|
HYDRATION_RETRY_TICKS);
|
||||||
|
if (!scheduled) {
|
||||||
|
studio.hydrationsInProgress().remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void runHydrationAttempt(
|
||||||
|
ActiveStudio studio,
|
||||||
|
World world,
|
||||||
|
int chunkX,
|
||||||
|
int chunkZ,
|
||||||
|
int attempt,
|
||||||
|
long key
|
||||||
|
) {
|
||||||
|
studio.hydrationsInProgress().remove(key);
|
||||||
|
if (!service.enabled || service.studios.get(world.getUID()) != studio) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!world.isChunkLoaded(chunkX, chunkZ)) {
|
||||||
|
if (attempt < MAX_HYDRATION_ATTEMPTS) {
|
||||||
|
scheduleHydration(studio, world, chunkX, chunkZ, attempt + 1);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (hydrateChunk(studio, world, chunkX, chunkZ)) {
|
||||||
|
scheduleHydration(studio, world, chunkX, chunkZ, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class MaterializationCoordinator {
|
||||||
|
private final MaterializationWork work;
|
||||||
|
private final List<ChunkCaptureArea> areas;
|
||||||
|
private final Set<Long> candidateCompleted = new HashSet<>();
|
||||||
|
private final Set<Long> candidateTouched = new HashSet<>();
|
||||||
|
private final Set<Long> rollbackCompleted = new HashSet<>();
|
||||||
|
private int candidateRemaining;
|
||||||
|
private int rollbackRemaining;
|
||||||
|
private String candidateFailure = "";
|
||||||
|
private String rollbackFailure = "";
|
||||||
|
private boolean rollbackStarted;
|
||||||
|
private boolean finished;
|
||||||
|
private boolean leaseReleased;
|
||||||
|
private boolean scheduledAny;
|
||||||
|
|
||||||
|
private MaterializationCoordinator(
|
||||||
|
MaterializationWork work,
|
||||||
|
List<ChunkCaptureArea> areas
|
||||||
|
) {
|
||||||
|
this.work = Objects.requireNonNull(work, "Jigsaw Studio materialization work");
|
||||||
|
this.areas = List.copyOf(areas);
|
||||||
|
candidateRemaining = this.areas.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
private MaterializationWork work() {
|
||||||
|
return work;
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void markScheduled() {
|
||||||
|
scheduledAny = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized boolean scheduledAny() {
|
||||||
|
return scheduledAny;
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void markCandidateTouched(ChunkCaptureArea area) {
|
||||||
|
candidateTouched.add(chunkKey(area.chunkX(), area.chunkZ()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void candidateComplete(
|
||||||
|
ChunkCaptureArea area,
|
||||||
|
String failure,
|
||||||
|
Throwable exception
|
||||||
|
) {
|
||||||
|
boolean finishSuccess = false;
|
||||||
|
boolean startRollback = false;
|
||||||
|
synchronized (this) {
|
||||||
|
if (finished || rollbackStarted
|
||||||
|
|| !candidateCompleted.add(chunkKey(area.chunkX(), area.chunkZ()))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (failure != null && !failure.isBlank() && candidateFailure.isEmpty()) {
|
||||||
|
candidateFailure = failure;
|
||||||
|
}
|
||||||
|
candidateRemaining--;
|
||||||
|
if (candidateRemaining == 0) {
|
||||||
|
if (candidateFailure.isEmpty()) {
|
||||||
|
finishSuccess = true;
|
||||||
|
} else {
|
||||||
|
rollbackStarted = true;
|
||||||
|
startRollback = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (exception != null) {
|
||||||
|
IrisLogging.reportError(exception);
|
||||||
|
}
|
||||||
|
if (finishSuccess) {
|
||||||
|
finishSuccess();
|
||||||
|
} else if (startRollback) {
|
||||||
|
scheduleRollback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void finishSuccess() {
|
||||||
|
JigsawStudioSession session = work.studio().generator().getSession();
|
||||||
|
if (!isCurrentVariantSwitch(work) || !session.completeVariantSwitch(work.token())) {
|
||||||
|
beginLateRollback("Jigsaw Studio changed before the loaded variant could be activated.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (work.connectorVisibilityChange()) {
|
||||||
|
session.setConnectorsVisible(work.workcell().stableId(), work.targetConnectorsVisible());
|
||||||
|
}
|
||||||
|
synchronized (this) {
|
||||||
|
if (finished) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
finished = true;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
work.studio().generator().invalidateRender(work.workcell().stableId());
|
||||||
|
work.studio().replacePopulation(work.workcell(), work.target(), "", true);
|
||||||
|
service.playerContext.refreshWorkcellContext(work.studio().worldId(), work.workcell().stableId());
|
||||||
|
message(work.player(), work.connectorVisibilityChange()
|
||||||
|
? "Connector blocks are now "
|
||||||
|
+ (work.targetConnectorsVisible() ? "visible." : "hidden.")
|
||||||
|
: "Loaded variant '" + work.token().targetVariant().pieceKey()
|
||||||
|
+ "' into " + work.workcell().stableId() + ".");
|
||||||
|
} finally {
|
||||||
|
releaseLease();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void beginLateRollback(String failure) {
|
||||||
|
synchronized (this) {
|
||||||
|
if (finished || rollbackStarted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
candidateFailure = failure;
|
||||||
|
rollbackStarted = true;
|
||||||
|
}
|
||||||
|
scheduleRollback();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void scheduleRollback() {
|
||||||
|
List<ChunkCaptureArea> rollbackAreas = new ArrayList<>();
|
||||||
|
synchronized (this) {
|
||||||
|
for (ChunkCaptureArea area : areas) {
|
||||||
|
if (candidateTouched.contains(chunkKey(area.chunkX(), area.chunkZ()))) {
|
||||||
|
rollbackAreas.add(area);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rollbackRemaining = rollbackAreas.size();
|
||||||
|
if (rollbackRemaining == 0) {
|
||||||
|
finished = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (rollbackAreas.isEmpty()) {
|
||||||
|
finishRollback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (ChunkCaptureArea area : rollbackAreas) {
|
||||||
|
boolean scheduled = J.runRegion(
|
||||||
|
work.world(),
|
||||||
|
area.chunkX(),
|
||||||
|
area.chunkZ(),
|
||||||
|
() -> materializeRollbackChunk(this, area));
|
||||||
|
if (!scheduled) {
|
||||||
|
rollbackComplete(
|
||||||
|
area,
|
||||||
|
"Iris could not schedule rollback for workcell chunk "
|
||||||
|
+ area.chunkX() + "," + area.chunkZ() + ".",
|
||||||
|
null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void rollbackComplete(
|
||||||
|
ChunkCaptureArea area,
|
||||||
|
String failure,
|
||||||
|
Throwable exception
|
||||||
|
) {
|
||||||
|
boolean finalizeRollback = false;
|
||||||
|
synchronized (this) {
|
||||||
|
if (finished || !rollbackStarted
|
||||||
|
|| !rollbackCompleted.add(chunkKey(area.chunkX(), area.chunkZ()))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (failure != null && !failure.isBlank() && rollbackFailure.isEmpty()) {
|
||||||
|
rollbackFailure = failure;
|
||||||
|
}
|
||||||
|
rollbackRemaining--;
|
||||||
|
if (rollbackRemaining == 0) {
|
||||||
|
finished = true;
|
||||||
|
finalizeRollback = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (exception != null) {
|
||||||
|
IrisLogging.reportError(exception);
|
||||||
|
}
|
||||||
|
if (finalizeRollback) {
|
||||||
|
finishRollback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void finishRollback() {
|
||||||
|
JigsawStudioSession session = work.studio().generator().getSession();
|
||||||
|
boolean released = session.abortVariantSwitch(work.token());
|
||||||
|
try {
|
||||||
|
work.studio().generator().invalidateRender(work.workcell().stableId());
|
||||||
|
if (rollbackFailure.isEmpty() && released) {
|
||||||
|
work.studio().replacePopulation(work.workcell(), work.previous(), "", true);
|
||||||
|
service.playerContext.refreshWorkcellContext(work.studio().worldId(), work.workcell().stableId());
|
||||||
|
message(work.player(), candidateFailure + " The previous variant was restored.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String failure = rollbackFailure.isEmpty()
|
||||||
|
? "The previous variant could not be restored because the Studio session changed."
|
||||||
|
: rollbackFailure;
|
||||||
|
work.studio().replacePopulation(work.workcell(), work.previous(), failure, false);
|
||||||
|
service.playerContext.refreshWorkcellContext(work.studio().worldId(), work.workcell().stableId());
|
||||||
|
message(work.player(), candidateFailure + " Automatic rollback also failed: " + failure
|
||||||
|
+ " Do not save this workcell until it is reopened.");
|
||||||
|
} finally {
|
||||||
|
releaseLease();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void releaseLease() {
|
||||||
|
synchronized (this) {
|
||||||
|
if (leaseReleased) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
leaseReleased = true;
|
||||||
|
}
|
||||||
|
finishMaterialization(work.studio().generator().getRequest().requestId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record MaterializationWork(
|
||||||
|
ActiveStudio studio,
|
||||||
|
World world,
|
||||||
|
Player player,
|
||||||
|
JigsawStudioBay workcell,
|
||||||
|
JigsawStudioSession.VariantSwitchToken token,
|
||||||
|
JigsawStudioGenerator.RenderedBay previous,
|
||||||
|
JigsawStudioGenerator.RenderedBay target,
|
||||||
|
boolean previousConnectorsVisible,
|
||||||
|
boolean targetConnectorsVisible,
|
||||||
|
boolean connectorVisibilityChange
|
||||||
|
) {
|
||||||
|
MaterializationWork {
|
||||||
|
Objects.requireNonNull(studio, "Jigsaw Studio materialization studio");
|
||||||
|
Objects.requireNonNull(world, "Jigsaw Studio materialization world");
|
||||||
|
Objects.requireNonNull(player, "Jigsaw Studio materialization player");
|
||||||
|
Objects.requireNonNull(workcell, "Jigsaw Studio materialization workcell");
|
||||||
|
Objects.requireNonNull(token, "Jigsaw Studio materialization token");
|
||||||
|
Objects.requireNonNull(previous, "Jigsaw Studio previous rendered variant");
|
||||||
|
Objects.requireNonNull(target, "Jigsaw Studio target rendered variant");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record VariantReloadRequest(
|
||||||
|
String workcellId,
|
||||||
|
JigsawStudioGenerator.RenderedBay previous
|
||||||
|
) {
|
||||||
|
public VariantReloadRequest {
|
||||||
|
workcellId = workcellId == null ? "" : workcellId.trim();
|
||||||
|
if (workcellId.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("Jigsaw Studio reload workcell ID cannot be blank");
|
||||||
|
}
|
||||||
|
Objects.requireNonNull(previous, "Jigsaw Studio previous rendered variant");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
package art.arcane.iris.core.service;
|
||||||
|
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioBay;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioSession;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioVariant;
|
||||||
|
import art.arcane.iris.core.service.JigsawStudioSaveLifecycle.BayReadiness;
|
||||||
|
import art.arcane.iris.core.service.JigsawStudioService.ActiveStudio;
|
||||||
|
import art.arcane.iris.spi.IrisServices;
|
||||||
|
import art.arcane.iris.util.common.scheduling.J;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioProtection.ownerMatches;
|
||||||
|
|
||||||
|
final class JigsawStudioPlayerContext {
|
||||||
|
private final JigsawStudioService service;
|
||||||
|
final Map<UUID, PlayerWorkcellContext> playerWorkcells = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
JigsawStudioPlayerContext(JigsawStudioService service) {
|
||||||
|
this.service = Objects.requireNonNull(service, "Jigsaw Studio service");
|
||||||
|
}
|
||||||
|
|
||||||
|
void refreshAllWorkcellContexts(ActiveStudio studio) {
|
||||||
|
scheduleOnlinePlayers(studio.worldId());
|
||||||
|
}
|
||||||
|
|
||||||
|
void scheduleOnlinePlayers(UUID worldId) {
|
||||||
|
J.runGlobal(() -> {
|
||||||
|
for (Player player : Bukkit.getOnlinePlayers()) {
|
||||||
|
J.runEntity(player, () -> {
|
||||||
|
if (player.getWorld().getUID().equals(worldId)) {
|
||||||
|
service.visualization.ensureVisualizationLoop(player);
|
||||||
|
reconcilePlayerContext(player, player.getLocation());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void reconcilePlayerContext(Player player, Location location) {
|
||||||
|
if (player == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Location target = location == null ? player.getLocation() : location;
|
||||||
|
if (!J.isOwnedByCurrentRegion(player)
|
||||||
|
|| target.getWorld() == null
|
||||||
|
|| !target.getWorld().getUID().equals(player.getWorld().getUID())) {
|
||||||
|
J.runEntity(player, () -> reconcilePlayerContext(player, player.getLocation()), 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ActiveStudio studio = service.studios.get(target.getWorld().getUID());
|
||||||
|
if (studio == null) {
|
||||||
|
clearPlayerContext(player);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
JigsawStudioSession session = studio.generator().getSession();
|
||||||
|
JigsawStudioBay workcell = session.layout().findAt(
|
||||||
|
target.getBlockX(), target.getBlockY(), target.getBlockZ());
|
||||||
|
boolean owner = ownerMatches(player, studio);
|
||||||
|
selectEnteredWorkcell(session, workcell, owner);
|
||||||
|
String workcellId = workcell == null ? "" : workcell.stableId();
|
||||||
|
playerWorkcells.put(player.getUniqueId(), new PlayerWorkcellContext(
|
||||||
|
studio.worldId(),
|
||||||
|
studio.generator().getRequest().requestId(),
|
||||||
|
workcellId));
|
||||||
|
|
||||||
|
JigsawStudioVariant variant = workcell == null
|
||||||
|
? null
|
||||||
|
: session.activeVariant(workcell.stableId()).orElse(null);
|
||||||
|
JigsawStudioBoardState state = boardState(studio, session, workcell, variant);
|
||||||
|
String workcellRole = workcell == null ? "" : workcell.canonicalDisplayName();
|
||||||
|
String workcellName = workcell == null ? "" : workcell.displayName();
|
||||||
|
String variantName = variant == null ? "" : variant.resolvedDisplayName();
|
||||||
|
String hint = "Triple-sneak for controls";
|
||||||
|
boardService().applyJigsawContext(player, new JigsawStudioBoardContext(
|
||||||
|
studio.worldId(),
|
||||||
|
studio.generator().getRequest().requestId(),
|
||||||
|
studio.generator().getRequest().structureKey(),
|
||||||
|
session.layout().mode(),
|
||||||
|
workcellRole,
|
||||||
|
workcellName,
|
||||||
|
variantName,
|
||||||
|
state,
|
||||||
|
hint));
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean selectEnteredWorkcell(
|
||||||
|
JigsawStudioSession session,
|
||||||
|
JigsawStudioBay workcell,
|
||||||
|
boolean owner
|
||||||
|
) {
|
||||||
|
return owner && workcell != null && session.selectBay(workcell.stableId());
|
||||||
|
}
|
||||||
|
|
||||||
|
void refreshWorkcellContext(UUID worldId, String workcellId) {
|
||||||
|
if (worldId == null || workcellId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
J.runGlobal(() -> {
|
||||||
|
for (Player player : Bukkit.getOnlinePlayers()) {
|
||||||
|
J.runEntity(player, () -> {
|
||||||
|
if (!player.getWorld().getUID().equals(worldId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
PlayerWorkcellContext context = playerWorkcells.get(player.getUniqueId());
|
||||||
|
if (context != null && workcellId.equals(context.workcellId())) {
|
||||||
|
reconcilePlayerContext(player, player.getLocation());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearWorldPlayerContexts(UUID worldId) {
|
||||||
|
J.runGlobal(() -> {
|
||||||
|
for (Player player : Bukkit.getOnlinePlayers()) {
|
||||||
|
J.runEntity(player, () -> {
|
||||||
|
PlayerWorkcellContext context = playerWorkcells.get(player.getUniqueId());
|
||||||
|
if (context != null && context.worldId().equals(worldId)) {
|
||||||
|
service.toolbelt.closeMenu(player);
|
||||||
|
clearPlayerContext(player);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearPlayerContext(Player player) {
|
||||||
|
if (player == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
playerWorkcells.remove(player.getUniqueId());
|
||||||
|
boardService().clearJigsawContext(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
static BoardSVC boardService() {
|
||||||
|
return IrisServices.get(BoardSVC.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JigsawStudioBoardState boardState(
|
||||||
|
ActiveStudio studio,
|
||||||
|
JigsawStudioSession session,
|
||||||
|
JigsawStudioBay workcell,
|
||||||
|
JigsawStudioVariant variant
|
||||||
|
) {
|
||||||
|
if (workcell == null) {
|
||||||
|
return JigsawStudioBoardState.SAVED;
|
||||||
|
}
|
||||||
|
JigsawStudioSession.WorkcellSnapshot snapshot = session.workcellSnapshot(workcell.stableId());
|
||||||
|
if (snapshot.switchInProgress()) {
|
||||||
|
return JigsawStudioBoardState.LOADING;
|
||||||
|
}
|
||||||
|
if (snapshot.saveInProgress()) {
|
||||||
|
return JigsawStudioBoardState.SAVING;
|
||||||
|
}
|
||||||
|
if (!workcell.enabled()) {
|
||||||
|
return JigsawStudioBoardState.DISABLED;
|
||||||
|
}
|
||||||
|
if (variant != null && !variant.owned()) {
|
||||||
|
return JigsawStudioBoardState.READ_ONLY;
|
||||||
|
}
|
||||||
|
BayReadiness readiness = studio.population(workcell).readiness();
|
||||||
|
if (!readiness.failure().isEmpty()) {
|
||||||
|
return JigsawStudioBoardState.INVALID;
|
||||||
|
}
|
||||||
|
return snapshot.dirty() ? JigsawStudioBoardState.UNSAVED : JigsawStudioBoardState.SAVED;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String displayKey(String resourceKey) {
|
||||||
|
int separator = resourceKey.lastIndexOf('/');
|
||||||
|
return separator < 0 ? resourceKey : resourceKey.substring(separator + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private record PlayerWorkcellContext(
|
||||||
|
UUID worldId,
|
||||||
|
UUID requestId,
|
||||||
|
String workcellId
|
||||||
|
) {
|
||||||
|
PlayerWorkcellContext {
|
||||||
|
Objects.requireNonNull(worldId, "Jigsaw Studio player-context world ID");
|
||||||
|
Objects.requireNonNull(requestId, "Jigsaw Studio player-context request ID");
|
||||||
|
workcellId = workcellId == null ? "" : workcellId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,396 @@
|
|||||||
|
package art.arcane.iris.core.service;
|
||||||
|
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioActivation;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioBay;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioCompatibilityTarget;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioControlPosition;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioLayout;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioSession;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioVariant;
|
||||||
|
import art.arcane.iris.core.service.JigsawStudioService.ActiveStudio;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.block.Block;
|
||||||
|
import org.bukkit.block.BlockFace;
|
||||||
|
import org.bukkit.block.BlockState;
|
||||||
|
import org.bukkit.block.DoubleChest;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.inventory.BlockInventoryHolder;
|
||||||
|
import org.bukkit.inventory.Inventory;
|
||||||
|
import org.bukkit.inventory.InventoryHolder;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioService.message;
|
||||||
|
|
||||||
|
final class JigsawStudioProtection {
|
||||||
|
private static final Set<String> MUTATING_COMMANDS = Set.of(
|
||||||
|
"biome", "brush", "chunk", "clone", "copy", "cut", "data", "deform", "drain",
|
||||||
|
"execute", "faces", "fill", "fillbiome", "fixlava", "fixwater", "flip", "flora",
|
||||||
|
"forest", "function", "gmask", "green", "hollow", "item", "load", "mask", "move",
|
||||||
|
"naturalize", "overlay", "paste", "place", "redo", "regen", "replace", "replacenear",
|
||||||
|
"restore", "rotate", "schedule", "schem", "schematic", "set", "setblock", "smooth",
|
||||||
|
"snow", "sphere", "stack", "thaw", "undo", "walls");
|
||||||
|
|
||||||
|
private static final Set<String> SAFE_NON_OWNER_COMMANDS = Set.of(
|
||||||
|
"help", "list", "me", "msg", "ping", "pl", "plugins", "r", "reply", "rules",
|
||||||
|
"say", "tell", "ver", "version", "w", "whisper");
|
||||||
|
|
||||||
|
private final JigsawStudioService service;
|
||||||
|
|
||||||
|
JigsawStudioProtection(JigsawStudioService service) {
|
||||||
|
this.service = Objects.requireNonNull(service, "Jigsaw Studio service");
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean blocksInventoryMutation(Inventory inventory, UUID actorId) {
|
||||||
|
for (Block block : inventoryBlocks(inventory)) {
|
||||||
|
StudioBlockMutationContext context = studioBlockMutationContext(block);
|
||||||
|
if (context != null && blocksStudioInventoryMutation(
|
||||||
|
true,
|
||||||
|
context.immutable(),
|
||||||
|
materializationInProgress(context.studio()),
|
||||||
|
service.saveLifecycle.reopenRequiredRequests.contains(context.requestId()),
|
||||||
|
context.ownerId(),
|
||||||
|
actorId)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean blocksMachineMutation(Block block) {
|
||||||
|
StudioBlockMutationContext context = studioBlockMutationContext(block);
|
||||||
|
return context != null && blocksStudioInventoryMutation(
|
||||||
|
true,
|
||||||
|
context.immutable(),
|
||||||
|
materializationInProgress(context.studio()),
|
||||||
|
service.saveLifecycle.reopenRequiredRequests.contains(context.requestId()),
|
||||||
|
context.ownerId(),
|
||||||
|
null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private StudioBlockMutationContext studioBlockMutationContext(Block block) {
|
||||||
|
if (block == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
ActiveStudio studio = service.studios.get(block.getWorld().getUID());
|
||||||
|
if (studio == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
JigsawStudioActivation.Request request = studio.generator().getRequest();
|
||||||
|
boolean preview = service.previewRenderer.contains(
|
||||||
|
request.requestId(), block.getX(), block.getY(), block.getZ());
|
||||||
|
JigsawStudioControlPosition control = studio.generator().getLayout().controlPosition();
|
||||||
|
boolean controlChest = block.getType() == Material.CHEST
|
||||||
|
&& block.getX() == control.worldX()
|
||||||
|
&& block.getY() == control.worldY()
|
||||||
|
&& block.getZ() == control.worldZ();
|
||||||
|
JigsawStudioSession session = studio.generator().getSession();
|
||||||
|
JigsawStudioBay bay = session.layout().findAt(
|
||||||
|
block.getX(), block.getY(), block.getZ());
|
||||||
|
boolean workcell = bay != null;
|
||||||
|
if (!preview && !controlChest && !workcell) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
JigsawStudioVariant variant = bay == null
|
||||||
|
? null
|
||||||
|
: session.activeVariant(bay.stableId()).orElse(null);
|
||||||
|
boolean nonEditableWorkcell = bay != null && (variant == null || !variant.owned());
|
||||||
|
return new StudioBlockMutationContext(
|
||||||
|
studio,
|
||||||
|
request.requestId(),
|
||||||
|
request.ownerId(),
|
||||||
|
preview || controlChest || nonEditableWorkcell);
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<Block> inventoryBlocks(Inventory inventory) {
|
||||||
|
if (inventory == null) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
List<Block> blocks = new ArrayList<>(2);
|
||||||
|
InventoryHolder holder = inventory.getHolder();
|
||||||
|
if (holder instanceof BlockInventoryHolder blockInventoryHolder) {
|
||||||
|
blocks.add(blockInventoryHolder.getBlock());
|
||||||
|
} else if (holder instanceof DoubleChest doubleChest) {
|
||||||
|
addInventoryBlock(blocks, doubleChest.getLeftSide());
|
||||||
|
addInventoryBlock(blocks, doubleChest.getRightSide());
|
||||||
|
}
|
||||||
|
return blocks;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addInventoryBlock(List<Block> blocks, InventoryHolder holder) {
|
||||||
|
if (holder instanceof BlockInventoryHolder blockInventoryHolder
|
||||||
|
&& !blocks.contains(blockInventoryHolder.getBlock())) {
|
||||||
|
blocks.add(blockInventoryHolder.getBlock());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean isMutatingCommand(String commandLine) {
|
||||||
|
return MUTATING_COMMANDS.contains(commandLabel(commandLine));
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean blocksMutatingCommand(UUID ownerId, UUID actorId, String commandLine) {
|
||||||
|
return !ownerMatches(ownerId, actorId)
|
||||||
|
&& !isSafeNonOwnerCommand(commandLine);
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean blocksNonEditableWorkcellMutation(
|
||||||
|
boolean hasNonEditableWorkcell,
|
||||||
|
String commandLine
|
||||||
|
) {
|
||||||
|
return hasNonEditableWorkcell && isMutatingCommand(commandLine);
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean canToggleVariantRotation(
|
||||||
|
JigsawStudioCompatibilityTarget compatibilityTarget,
|
||||||
|
JigsawStudioVariant variant
|
||||||
|
) {
|
||||||
|
JigsawStudioCompatibilityTarget target = Objects.requireNonNull(
|
||||||
|
compatibilityTarget,
|
||||||
|
"Jigsaw Studio compatibility target");
|
||||||
|
JigsawStudioVariant activeVariant = Objects.requireNonNull(variant, "Jigsaw Studio variant");
|
||||||
|
return activeVariant.owned()
|
||||||
|
&& (target != JigsawStudioCompatibilityTarget.VANILLA_PORTABLE
|
||||||
|
|| !activeVariant.rotatable());
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean isSafeNonOwnerCommand(String commandLine) {
|
||||||
|
String normalized = normalizeCommandLine(commandLine);
|
||||||
|
String label = commandLabel(normalized);
|
||||||
|
if (SAFE_NON_OWNER_COMMANDS.contains(label)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
int separator = normalized.indexOf(' ');
|
||||||
|
if (!label.equals("iris") || separator < 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String arguments = normalized.substring(separator + 1).trim();
|
||||||
|
return arguments.equals("jigsaw status") || arguments.startsWith("jigsaw status ");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String commandLabel(String commandLine) {
|
||||||
|
String normalized = normalizeCommandLine(commandLine);
|
||||||
|
int separator = normalized.indexOf(' ');
|
||||||
|
String label = separator < 0 ? normalized : normalized.substring(0, separator);
|
||||||
|
int namespace = label.lastIndexOf(':');
|
||||||
|
return namespace < 0 ? label : label.substring(namespace + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String normalizeCommandLine(String commandLine) {
|
||||||
|
if (commandLine == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
String normalized = commandLine.trim().toLowerCase(Locale.ROOT);
|
||||||
|
while (normalized.startsWith("/")) {
|
||||||
|
normalized = normalized.substring(1);
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean ownerMatches(Player player, ActiveStudio studio) {
|
||||||
|
if (player == null || studio == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
UUID ownerId = studio.generator().getRequest().ownerId();
|
||||||
|
return ownerMatches(ownerId, player.getUniqueId());
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean canCreateVariants(JigsawStudioLayout layout) {
|
||||||
|
return Objects.requireNonNull(layout, "Jigsaw Studio layout")
|
||||||
|
.variantCatalog()
|
||||||
|
.editableGraph();
|
||||||
|
}
|
||||||
|
|
||||||
|
static String variantCreationSourceFailure(
|
||||||
|
JigsawStudioVariant activeVariant,
|
||||||
|
boolean duplicateActive
|
||||||
|
) {
|
||||||
|
if (activeVariant == null) {
|
||||||
|
return duplicateActive
|
||||||
|
? "This workcell has no active variant to duplicate."
|
||||||
|
: "This workcell has no active variant whose pool role can be copied. Use "
|
||||||
|
+ "/iris jigsaw piece create <poolKey> <pieceKey> to choose an owned pool explicitly.";
|
||||||
|
}
|
||||||
|
if (!activeVariant.owned()) {
|
||||||
|
return "Adopt or clone this graph before creating from its read-only variant.";
|
||||||
|
}
|
||||||
|
if (activeVariant.memberships().isEmpty()) {
|
||||||
|
return "The active variant has no owned pool membership to copy. Use "
|
||||||
|
+ "/iris jigsaw piece create <poolKey> <pieceKey> to choose an owned pool explicitly.";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean ownerMatches(UUID ownerId, UUID actorId) {
|
||||||
|
return ownerId == null || ownerId.equals(actorId);
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean authorizeOwner(Player player, ActiveStudio studio) {
|
||||||
|
if (ownerMatches(player, studio)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
message(player, "This Jigsaw Studio is owned by another player session.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isUnauthorizedStudioEdit(Player player, Block block) {
|
||||||
|
if (player == null || block == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ActiveStudio studio = service.studios.get(block.getWorld().getUID());
|
||||||
|
if (studio == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
UUID requestId = studio.generator().getRequest().requestId();
|
||||||
|
if (service.previewRenderer.contains(requestId, block.getX(), block.getY(), block.getZ())) {
|
||||||
|
message(player, "The seed-1337 Jigsaw preview is read-only and refreshes automatically.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
JigsawStudioSession session = studio.generator().getSession();
|
||||||
|
JigsawStudioBay bay = session.layout().findAt(block.getX(), block.getY(), block.getZ());
|
||||||
|
JigsawStudioVariant variant = bay == null
|
||||||
|
? null
|
||||||
|
: session.activeVariant(bay.stableId()).orElse(null);
|
||||||
|
if (bay != null && (variant == null || !variant.owned())) {
|
||||||
|
message(player, variant == null
|
||||||
|
? "This empty Jigsaw Studio workcell has no loaded editable variant. Load one before editing it."
|
||||||
|
: "This Jigsaw Studio variant is read-only. Adopt or clone its graph before editing it.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (service.saveLifecycle.reopenRequiredRequests.contains(requestId)) {
|
||||||
|
message(player, "Close and reopen Jigsaw Studio before editing the resized layout.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (materializationInProgress(studio)) {
|
||||||
|
message(player, "Wait for the current Jigsaw Studio variant load or rollback to finish.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!blocksStudioEdit(studio.generator().getRequest().ownerId(), player.getUniqueId())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
message(player, "This Jigsaw Studio is owned by another player session.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean materializationInProgress(ActiveStudio studio) {
|
||||||
|
if (studio == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
UUID requestId = studio.generator().getRequest().requestId();
|
||||||
|
synchronized (service.saveLifecycleLock) {
|
||||||
|
return service.materializer.materializationsInProgress.contains(requestId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean anyMaterializationInProgress() {
|
||||||
|
synchronized (service.saveLifecycleLock) {
|
||||||
|
return !service.materializer.materializationsInProgress.isEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean anyNonEditableWorkcell() {
|
||||||
|
for (ActiveStudio studio : service.studios.values()) {
|
||||||
|
if (hasNonEditableWorkcell(studio)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean hasNonEditableWorkcell(ActiveStudio studio) {
|
||||||
|
JigsawStudioSession session = studio.generator().getSession();
|
||||||
|
for (JigsawStudioBay bay : session.layout().bays()) {
|
||||||
|
JigsawStudioVariant variant = session.activeVariant(bay.stableId()).orElse(null);
|
||||||
|
if (variant == null || !variant.owned()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean blocksStudioEdit(UUID ownerId, UUID actorId) {
|
||||||
|
return !ownerMatches(ownerId, actorId);
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean blocksStudioInventoryMutation(
|
||||||
|
boolean studioBlockInventory,
|
||||||
|
boolean immutable,
|
||||||
|
boolean materializing,
|
||||||
|
boolean reopenRequired,
|
||||||
|
UUID ownerId,
|
||||||
|
UUID actorId
|
||||||
|
) {
|
||||||
|
return studioBlockInventory
|
||||||
|
&& (immutable
|
||||||
|
|| materializing
|
||||||
|
|| reopenRequired
|
||||||
|
|| actorId != null && blocksStudioEdit(ownerId, actorId));
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean movesProtectedBlocks(List<Block> blocks, BlockFace direction) {
|
||||||
|
for (Block block : blocks) {
|
||||||
|
if (isImmutableStudioBlock(block)
|
||||||
|
|| isImmutableStudioBlock(block.getRelative(direction))
|
||||||
|
|| isImmutableStudioBlock(block.getRelative(direction.getOppositeFace()))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean containsImmutableStudioBlock(List<BlockState> states) {
|
||||||
|
for (BlockState state : states) {
|
||||||
|
if (isImmutableStudioBlock(state.getBlock())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isImmutableStudioBlock(Block block) {
|
||||||
|
StudioBlockMutationContext context = studioBlockMutationContext(block);
|
||||||
|
return context != null && context.immutable();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isPreviewBlock(Block block) {
|
||||||
|
if (block == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ActiveStudio studio = service.studios.get(block.getWorld().getUID());
|
||||||
|
if (studio == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
UUID requestId = studio.generator().getRequest().requestId();
|
||||||
|
return service.previewRenderer.contains(requestId, block.getX(), block.getY(), block.getZ());
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isControlChest(Block block) {
|
||||||
|
if (block == null || block.getType() != Material.CHEST) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ActiveStudio studio = service.studios.get(block.getWorld().getUID());
|
||||||
|
if (studio == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioControlPosition control = studio.generator().getLayout().controlPosition();
|
||||||
|
return block.getX() == control.worldX()
|
||||||
|
&& block.getY() == control.worldY()
|
||||||
|
&& block.getZ() == control.worldZ();
|
||||||
|
}
|
||||||
|
|
||||||
|
private record StudioBlockMutationContext(
|
||||||
|
ActiveStudio studio,
|
||||||
|
UUID requestId,
|
||||||
|
UUID ownerId,
|
||||||
|
boolean immutable
|
||||||
|
) {
|
||||||
|
private StudioBlockMutationContext {
|
||||||
|
Objects.requireNonNull(studio, "Jigsaw Studio block mutation studio");
|
||||||
|
Objects.requireNonNull(requestId, "Jigsaw Studio block mutation request ID");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,710 @@
|
|||||||
|
package art.arcane.iris.core.service;
|
||||||
|
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioToolPayload;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioTripleSneakTracker;
|
||||||
|
import art.arcane.iris.core.service.JigsawStudioService.ActiveStudio;
|
||||||
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.block.Block;
|
||||||
|
import org.bukkit.block.BlockState;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.event.EventHandler;
|
||||||
|
import org.bukkit.event.EventPriority;
|
||||||
|
import org.bukkit.event.Listener;
|
||||||
|
import org.bukkit.event.block.Action;
|
||||||
|
import org.bukkit.event.block.BlockBreakEvent;
|
||||||
|
import org.bukkit.event.block.BlockBurnEvent;
|
||||||
|
import org.bukkit.event.block.BlockCookEvent;
|
||||||
|
import org.bukkit.event.block.BlockDispenseEvent;
|
||||||
|
import org.bukkit.event.block.BlockExplodeEvent;
|
||||||
|
import org.bukkit.event.block.BlockFadeEvent;
|
||||||
|
import org.bukkit.event.block.BlockFormEvent;
|
||||||
|
import org.bukkit.event.block.BlockFromToEvent;
|
||||||
|
import org.bukkit.event.block.BlockGrowEvent;
|
||||||
|
import org.bukkit.event.block.BlockMultiPlaceEvent;
|
||||||
|
import org.bukkit.event.block.BlockPistonExtendEvent;
|
||||||
|
import org.bukkit.event.block.BlockPistonRetractEvent;
|
||||||
|
import org.bukkit.event.block.BlockPlaceEvent;
|
||||||
|
import org.bukkit.event.block.BlockRedstoneEvent;
|
||||||
|
import org.bukkit.event.block.BlockSpreadEvent;
|
||||||
|
import org.bukkit.event.block.BrewingStartEvent;
|
||||||
|
import org.bukkit.event.block.CrafterCraftEvent;
|
||||||
|
import org.bukkit.event.entity.CreatureSpawnEvent;
|
||||||
|
import org.bukkit.event.entity.EntityChangeBlockEvent;
|
||||||
|
import org.bukkit.event.entity.EntityExplodeEvent;
|
||||||
|
import org.bukkit.event.inventory.BrewEvent;
|
||||||
|
import org.bukkit.event.inventory.BrewingStandFuelEvent;
|
||||||
|
import org.bukkit.event.inventory.FurnaceBurnEvent;
|
||||||
|
import org.bukkit.event.inventory.FurnaceStartSmeltEvent;
|
||||||
|
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||||
|
import org.bukkit.event.inventory.InventoryCloseEvent;
|
||||||
|
import org.bukkit.event.inventory.InventoryDragEvent;
|
||||||
|
import org.bukkit.event.inventory.InventoryMoveItemEvent;
|
||||||
|
import org.bukkit.event.inventory.InventoryPickupItemEvent;
|
||||||
|
import org.bukkit.event.player.PlayerBucketEmptyEvent;
|
||||||
|
import org.bukkit.event.player.PlayerBucketFillEvent;
|
||||||
|
import org.bukkit.event.player.PlayerChangedWorldEvent;
|
||||||
|
import org.bukkit.event.player.PlayerCommandPreprocessEvent;
|
||||||
|
import org.bukkit.event.player.PlayerInteractEvent;
|
||||||
|
import org.bukkit.event.player.PlayerJoinEvent;
|
||||||
|
import org.bukkit.event.player.PlayerMoveEvent;
|
||||||
|
import org.bukkit.event.player.PlayerPortalEvent;
|
||||||
|
import org.bukkit.event.player.PlayerQuitEvent;
|
||||||
|
import org.bukkit.event.player.PlayerRespawnEvent;
|
||||||
|
import org.bukkit.event.player.PlayerTeleportEvent;
|
||||||
|
import org.bukkit.event.player.PlayerToggleSneakEvent;
|
||||||
|
import org.bukkit.event.server.ServerCommandEvent;
|
||||||
|
import org.bukkit.event.world.ChunkLoadEvent;
|
||||||
|
import org.bukkit.event.world.StructureGrowEvent;
|
||||||
|
import org.bukkit.event.world.WorldUnloadEvent;
|
||||||
|
import org.bukkit.inventory.EquipmentSlot;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioPlayerContext.boardService;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioProtection.authorizeOwner;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioProtection.blocksMutatingCommand;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioProtection.blocksNonEditableWorkcellMutation;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioProtection.hasNonEditableWorkcell;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioProtection.isMutatingCommand;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioProtection.isSafeNonOwnerCommand;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioProtection.ownerMatches;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioService.isNaturalStudioSpawn;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioService.message;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioService.sameBlockPosition;
|
||||||
|
|
||||||
|
final class JigsawStudioProtectionListener implements Listener {
|
||||||
|
private final JigsawStudioService service;
|
||||||
|
|
||||||
|
JigsawStudioProtectionListener(JigsawStudioService service) {
|
||||||
|
this.service = Objects.requireNonNull(service, "Jigsaw Studio service");
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||||
|
public void onWorldUnload(WorldUnloadEvent event) {
|
||||||
|
ActiveStudio studio = service.studios.get(event.getWorld().getUID());
|
||||||
|
if (studio != null) {
|
||||||
|
UUID requestId = studio.generator().getRequest().requestId();
|
||||||
|
service.tileWatcher.finalizeJigsawTileWatches(requestId);
|
||||||
|
if (service.saveLifecycle.requiresLifecycleDrain(studio)
|
||||||
|
&& !service.saveLifecycle.discardingRequest(requestId)) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
service.autosaveScheduler.expediteAutosaves(requestId);
|
||||||
|
IrisLogging.warn("Jigsaw Studio kept world %s loaded while pending edits finish autosaving",
|
||||||
|
event.getWorld().getName());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
service.registry.unregister(event.getWorld());
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onChunkLoad(ChunkLoadEvent event) {
|
||||||
|
ActiveStudio studio = service.studios.get(event.getWorld().getUID());
|
||||||
|
if (studio != null) {
|
||||||
|
service.registry.markChunkAvailable(studio, event.getChunk().getX(), event.getChunk().getZ());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onPlayerJoin(PlayerJoinEvent event) {
|
||||||
|
service.visualization.ensureVisualizationLoop(event.getPlayer());
|
||||||
|
service.playerContext.reconcilePlayerContext(event.getPlayer(), event.getPlayer().getLocation());
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onPlayerChangedWorld(PlayerChangedWorldEvent event) {
|
||||||
|
service.toolbelt.closeMenu(event.getPlayer());
|
||||||
|
service.tileWatcher.finalizeJigsawTileWatchesForPlayer(event.getPlayer().getUniqueId());
|
||||||
|
service.tripleSneakTracker.clearPlayer(event.getPlayer().getUniqueId());
|
||||||
|
service.visualization.visualizationLoops.remove(event.getPlayer().getUniqueId());
|
||||||
|
service.visualization.assemblyPreviews.remove(event.getPlayer().getUniqueId());
|
||||||
|
service.playerContext.playerWorkcells.remove(event.getPlayer().getUniqueId());
|
||||||
|
service.visualization.ensureVisualizationLoop(event.getPlayer());
|
||||||
|
service.playerContext.reconcilePlayerContext(event.getPlayer(), event.getPlayer().getLocation());
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onPlayerQuit(PlayerQuitEvent event) {
|
||||||
|
UUID playerId = event.getPlayer().getUniqueId();
|
||||||
|
service.toolbelt.closeMenu(event.getPlayer());
|
||||||
|
service.tileWatcher.finalizeJigsawTileWatchesForPlayer(playerId);
|
||||||
|
service.tripleSneakTracker.clearPlayer(playerId);
|
||||||
|
service.toolbelt.toolConfirmations.remove(playerId);
|
||||||
|
service.visualization.visualizationLoops.remove(playerId);
|
||||||
|
service.particlesDisabled.remove(playerId);
|
||||||
|
service.visualization.assemblyPreviews.remove(playerId);
|
||||||
|
service.playerContext.playerWorkcells.remove(playerId);
|
||||||
|
service.toolbelt.deferredDuplications.entrySet().removeIf(
|
||||||
|
entry -> entry.getValue().playerId().equals(playerId));
|
||||||
|
boardService().clearJigsawContext(event.getPlayer());
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onTripleSneak(PlayerToggleSneakEvent event) {
|
||||||
|
if (!event.isSneaking()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Player player = event.getPlayer();
|
||||||
|
ActiveStudio studio = service.studios.get(player.getWorld().getUID());
|
||||||
|
if (studio == null || !ownerMatches(player, studio)) {
|
||||||
|
service.tripleSneakTracker.clearPlayer(player.getUniqueId());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
JigsawStudioTripleSneakTracker.Progress progress = service.tripleSneakTracker.recordSneak(
|
||||||
|
player.getUniqueId(),
|
||||||
|
studio.worldId(),
|
||||||
|
studio.generator().getRequest().requestId(),
|
||||||
|
System.nanoTime());
|
||||||
|
if (progress == JigsawStudioTripleSneakTracker.Progress.TRIGGERED) {
|
||||||
|
service.toolbelt.openControlMenu(player);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
|
||||||
|
public void onUseTool(PlayerInteractEvent event) {
|
||||||
|
if (event.getHand() != EquipmentSlot.HAND
|
||||||
|
|| (event.getAction() != Action.RIGHT_CLICK_AIR
|
||||||
|
&& event.getAction() != Action.RIGHT_CLICK_BLOCK)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Optional<JigsawStudioToolPayload> decoded = service.toolbelt.toolCodec.decode(event.getItem());
|
||||||
|
if (decoded.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
service.tileWatcher.finalizeJigsawTileWatchesForPlayer(event.getPlayer().getUniqueId());
|
||||||
|
event.setCancelled(true);
|
||||||
|
service.toolbelt.useTool(event.getPlayer(), decoded.get(), event.getItem(), event.getPlayer().isSneaking());
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||||
|
public void onPlayerMove(PlayerMoveEvent event) {
|
||||||
|
if (sameBlockPosition(event.getFrom(), event.getTo())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
service.playerContext.reconcilePlayerContext(event.getPlayer(), event.getTo());
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||||
|
public void onPlayerTeleport(PlayerTeleportEvent event) {
|
||||||
|
service.tileWatcher.finalizeJigsawTileWatchesForPlayer(event.getPlayer().getUniqueId());
|
||||||
|
service.playerContext.reconcilePlayerContext(event.getPlayer(), event.getTo());
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||||
|
public void onPlayerPortal(PlayerPortalEvent event) {
|
||||||
|
service.tileWatcher.finalizeJigsawTileWatchesForPlayer(event.getPlayer().getUniqueId());
|
||||||
|
service.playerContext.reconcilePlayerContext(event.getPlayer(), event.getTo());
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR)
|
||||||
|
public void onPlayerRespawn(PlayerRespawnEvent event) {
|
||||||
|
service.tileWatcher.finalizeJigsawTileWatchesForPlayer(event.getPlayer().getUniqueId());
|
||||||
|
service.playerContext.reconcilePlayerContext(event.getPlayer(), event.getRespawnLocation());
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||||
|
public void onNaturalCreatureSpawn(CreatureSpawnEvent event) {
|
||||||
|
if (service.studios.containsKey(event.getLocation().getWorld().getUID())
|
||||||
|
&& isNaturalStudioSpawn(event.getSpawnReason())) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||||
|
public void onUnauthorizedBlockPlace(BlockPlaceEvent event) {
|
||||||
|
if (service.protection.isUnauthorizedStudioEdit(event.getPlayer(), event.getBlockPlaced())) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||||
|
public void onUnauthorizedBlockMultiPlace(BlockMultiPlaceEvent event) {
|
||||||
|
for (BlockState state : event.getReplacedBlockStates()) {
|
||||||
|
if (service.protection.isUnauthorizedStudioEdit(event.getPlayer(), state.getBlock())) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||||
|
public void onProtectedBlockBreak(BlockBreakEvent event) {
|
||||||
|
if (service.protection.isControlChest(event.getBlock())) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
message(event.getPlayer(), "The Jigsaw Studio control chest cannot be removed.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (service.protection.isUnauthorizedStudioEdit(event.getPlayer(), event.getBlock())) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||||
|
public void onUnauthorizedBucketEmpty(PlayerBucketEmptyEvent event) {
|
||||||
|
Block target = event.getBlockClicked().getRelative(event.getBlockFace());
|
||||||
|
if (service.protection.isUnauthorizedStudioEdit(event.getPlayer(), target)) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||||
|
public void onUnauthorizedBucketFill(PlayerBucketFillEvent event) {
|
||||||
|
if (service.protection.isUnauthorizedStudioEdit(event.getPlayer(), event.getBlockClicked())) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||||
|
public void onUnauthorizedInteract(PlayerInteractEvent event) {
|
||||||
|
if (event.getAction() == Action.RIGHT_CLICK_BLOCK
|
||||||
|
&& !service.protection.isControlChest(event.getClickedBlock())
|
||||||
|
&& service.protection.isUnauthorizedStudioEdit(event.getPlayer(), event.getClickedBlock())) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||||
|
public void onBlockPlace(BlockPlaceEvent event) {
|
||||||
|
service.dirtyTracker.markDirty(event.getBlockPlaced());
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||||
|
public void onBlockMultiPlace(BlockMultiPlaceEvent event) {
|
||||||
|
for (BlockState state : event.getReplacedBlockStates()) {
|
||||||
|
service.dirtyTracker.markDirty(state.getBlock());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||||
|
public void onBlockBreak(BlockBreakEvent event) {
|
||||||
|
service.dirtyTracker.markDirty(event.getBlock());
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||||
|
public void onBucketEmpty(PlayerBucketEmptyEvent event) {
|
||||||
|
service.dirtyTracker.markDirty(event.getBlockClicked().getRelative(event.getBlockFace()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||||
|
public void onBucketFill(PlayerBucketFillEvent event) {
|
||||||
|
service.dirtyTracker.markDirty(event.getBlockClicked());
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||||
|
public void onControlChest(PlayerInteractEvent event) {
|
||||||
|
if (event.getAction() != Action.RIGHT_CLICK_BLOCK
|
||||||
|
|| event.getHand() != EquipmentSlot.HAND
|
||||||
|
|| !service.protection.isControlChest(event.getClickedBlock())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.setCancelled(true);
|
||||||
|
ActiveStudio studio = service.studios.get(event.getPlayer().getWorld().getUID());
|
||||||
|
if (!authorizeOwner(event.getPlayer(), studio)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
service.toolbelt.openControlMenu(event.getPlayer());
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||||
|
public void onProtectedBlockExplode(BlockExplodeEvent event) {
|
||||||
|
if (service.protection.materializationInProgress(service.studios.get(event.getBlock().getWorld().getUID()))) {
|
||||||
|
event.blockList().clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.blockList().removeIf(service.protection::isImmutableStudioBlock);
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||||
|
public void onProtectedEntityExplode(EntityExplodeEvent event) {
|
||||||
|
if (service.protection.materializationInProgress(service.studios.get(event.getEntity().getWorld().getUID()))) {
|
||||||
|
event.blockList().clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.blockList().removeIf(service.protection::isImmutableStudioBlock);
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||||
|
public void onProtectedPistonExtend(BlockPistonExtendEvent event) {
|
||||||
|
if (service.protection.materializationInProgress(service.studios.get(event.getBlock().getWorld().getUID()))
|
||||||
|
|| service.protection.movesProtectedBlocks(event.getBlocks(), event.getDirection())) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||||
|
public void onProtectedPistonRetract(BlockPistonRetractEvent event) {
|
||||||
|
if (service.protection.materializationInProgress(service.studios.get(event.getBlock().getWorld().getUID()))
|
||||||
|
|| service.protection.movesProtectedBlocks(event.getBlocks(), event.getDirection())) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||||
|
public void onProtectedEntityChangeBlock(EntityChangeBlockEvent event) {
|
||||||
|
if (service.protection.materializationInProgress(service.studios.get(event.getBlock().getWorld().getUID()))
|
||||||
|
|| service.protection.isImmutableStudioBlock(event.getBlock())) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||||
|
public void onProtectedBlockFromTo(BlockFromToEvent event) {
|
||||||
|
if (service.protection.materializationInProgress(service.studios.get(event.getBlock().getWorld().getUID()))
|
||||||
|
|| service.protection.isImmutableStudioBlock(event.getBlock())
|
||||||
|
|| service.protection.isImmutableStudioBlock(event.getToBlock())) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||||
|
public void onProtectedBlockForm(BlockFormEvent event) {
|
||||||
|
if (service.protection.materializationInProgress(service.studios.get(event.getBlock().getWorld().getUID()))
|
||||||
|
|| service.protection.isImmutableStudioBlock(event.getBlock())) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||||
|
public void onProtectedBlockGrow(BlockGrowEvent event) {
|
||||||
|
if (service.protection.materializationInProgress(service.studios.get(event.getBlock().getWorld().getUID()))
|
||||||
|
|| service.protection.isImmutableStudioBlock(event.getBlock())) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||||
|
public void onProtectedBlockSpread(BlockSpreadEvent event) {
|
||||||
|
if (service.protection.materializationInProgress(service.studios.get(event.getBlock().getWorld().getUID()))
|
||||||
|
|| service.protection.isImmutableStudioBlock(event.getBlock())
|
||||||
|
|| service.protection.isImmutableStudioBlock(event.getSource())) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||||
|
public void onProtectedBlockFade(BlockFadeEvent event) {
|
||||||
|
if (service.protection.materializationInProgress(service.studios.get(event.getBlock().getWorld().getUID()))
|
||||||
|
|| service.protection.isImmutableStudioBlock(event.getBlock())) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||||
|
public void onProtectedBlockBurn(BlockBurnEvent event) {
|
||||||
|
if (service.protection.materializationInProgress(service.studios.get(event.getBlock().getWorld().getUID()))
|
||||||
|
|| service.protection.isImmutableStudioBlock(event.getBlock())) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST)
|
||||||
|
public void onProtectedBlockRedstone(BlockRedstoneEvent event) {
|
||||||
|
if (service.protection.materializationInProgress(service.studios.get(event.getBlock().getWorld().getUID()))
|
||||||
|
|| service.protection.isImmutableStudioBlock(event.getBlock())) {
|
||||||
|
event.setNewCurrent(event.getOldCurrent());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||||
|
public void onProtectedStructureGrow(StructureGrowEvent event) {
|
||||||
|
if (event.getLocation().getWorld() != null
|
||||||
|
&& (service.protection.materializationInProgress(
|
||||||
|
service.studios.get(event.getLocation().getWorld().getUID()))
|
||||||
|
|| service.protection.containsImmutableStudioBlock(event.getBlocks()))) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||||
|
public void onProtectedInventoryClick(InventoryClickEvent event) {
|
||||||
|
if (!service.protection.blocksInventoryMutation(
|
||||||
|
event.getView().getTopInventory(),
|
||||||
|
event.getWhoClicked().getUniqueId())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.setCancelled(true);
|
||||||
|
if (event.getWhoClicked() instanceof Player player) {
|
||||||
|
message(player, "This Jigsaw Studio container is read-only during the current operation or owner session.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||||
|
public void onProtectedInventoryDrag(InventoryDragEvent event) {
|
||||||
|
if (!service.protection.blocksInventoryMutation(
|
||||||
|
event.getView().getTopInventory(),
|
||||||
|
event.getWhoClicked().getUniqueId())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.setCancelled(true);
|
||||||
|
if (event.getWhoClicked() instanceof Player player) {
|
||||||
|
message(player, "This Jigsaw Studio container is read-only during the current operation or owner session.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||||
|
public void onProtectedInventoryMove(InventoryMoveItemEvent event) {
|
||||||
|
if (service.protection.blocksInventoryMutation(event.getSource(), null)
|
||||||
|
|| service.protection.blocksInventoryMutation(event.getDestination(), null)) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||||
|
public void onProtectedInventoryPickup(InventoryPickupItemEvent event) {
|
||||||
|
if (service.protection.blocksInventoryMutation(event.getInventory(), null)) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||||
|
public void onProtectedBlockCook(BlockCookEvent event) {
|
||||||
|
if (service.protection.blocksMachineMutation(event.getBlock())) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||||
|
public void onProtectedFurnaceBurn(FurnaceBurnEvent event) {
|
||||||
|
if (service.protection.blocksMachineMutation(event.getBlock())) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||||
|
public void onProtectedBrew(BrewEvent event) {
|
||||||
|
if (service.protection.blocksMachineMutation(event.getBlock())) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||||
|
public void onProtectedBrewingStandFuel(BrewingStandFuelEvent event) {
|
||||||
|
if (service.protection.blocksMachineMutation(event.getBlock())) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||||
|
public void onProtectedBlockDispense(BlockDispenseEvent event) {
|
||||||
|
if (service.protection.blocksMachineMutation(event.getBlock())) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||||
|
public void onProtectedCrafterCraft(CrafterCraftEvent event) {
|
||||||
|
if (service.protection.blocksMachineMutation(event.getBlock())) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||||
|
public void onPlayerInteract(PlayerInteractEvent event) {
|
||||||
|
if (event.getAction() != Action.RIGHT_CLICK_BLOCK && event.getAction() != Action.PHYSICAL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Block clicked = event.getClickedBlock();
|
||||||
|
if (clicked != null) {
|
||||||
|
if (event.getAction() == Action.RIGHT_CLICK_BLOCK) {
|
||||||
|
service.tileWatcher.finalizeJigsawTileWatchesForPlayer(event.getPlayer().getUniqueId());
|
||||||
|
if (clicked.getType() == Material.JIGSAW) {
|
||||||
|
service.tileWatcher.startJigsawTileWatch(event.getPlayer(), clicked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
service.dirtyTracker.markDirty(clicked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||||
|
public void onInventoryClick(InventoryClickEvent event) {
|
||||||
|
service.dirtyTracker.markDirty(event.getView().getTopInventory());
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||||
|
public void onInventoryDrag(InventoryDragEvent event) {
|
||||||
|
service.dirtyTracker.markDirty(event.getView().getTopInventory());
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR)
|
||||||
|
public void onInventoryClose(InventoryCloseEvent event) {
|
||||||
|
service.dirtyTracker.markDirty(event.getView().getTopInventory());
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||||
|
public void onInventoryMove(InventoryMoveItemEvent event) {
|
||||||
|
service.dirtyTracker.markDirty(event.getSource());
|
||||||
|
service.dirtyTracker.markDirty(event.getDestination());
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||||
|
public void onInventoryPickup(InventoryPickupItemEvent event) {
|
||||||
|
service.dirtyTracker.markDirty(event.getInventory());
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||||
|
public void onBlockCook(BlockCookEvent event) {
|
||||||
|
service.dirtyTracker.markDirty(event.getBlock());
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||||
|
public void onFurnaceBurn(FurnaceBurnEvent event) {
|
||||||
|
service.dirtyTracker.markDirty(event.getBlock());
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR)
|
||||||
|
public void onFurnaceStartSmelt(FurnaceStartSmeltEvent event) {
|
||||||
|
service.dirtyTracker.markDirty(event.getBlock());
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||||
|
public void onBrew(BrewEvent event) {
|
||||||
|
service.dirtyTracker.markDirty(event.getBlock());
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR)
|
||||||
|
public void onBrewingStart(BrewingStartEvent event) {
|
||||||
|
service.dirtyTracker.markDirty(event.getBlock());
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||||
|
public void onBrewingStandFuel(BrewingStandFuelEvent event) {
|
||||||
|
service.dirtyTracker.markDirty(event.getBlock());
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||||
|
public void onBlockDispense(BlockDispenseEvent event) {
|
||||||
|
service.dirtyTracker.markDirty(event.getBlock());
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||||
|
public void onCrafterCraft(CrafterCraftEvent event) {
|
||||||
|
service.dirtyTracker.markDirty(event.getBlock());
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||||
|
public void onBlockExplode(BlockExplodeEvent event) {
|
||||||
|
service.dirtyTracker.markDirty(event.blockList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||||
|
public void onEntityExplode(EntityExplodeEvent event) {
|
||||||
|
service.dirtyTracker.markDirty(event.blockList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||||
|
public void onEntityChangeBlock(EntityChangeBlockEvent event) {
|
||||||
|
service.dirtyTracker.markDirty(event.getBlock());
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||||
|
public void onBlockFromTo(BlockFromToEvent event) {
|
||||||
|
service.dirtyTracker.markDirty(event.getToBlock());
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||||
|
public void onBlockForm(BlockFormEvent event) {
|
||||||
|
service.dirtyTracker.markDirty(event.getBlock());
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||||
|
public void onBlockGrow(BlockGrowEvent event) {
|
||||||
|
service.dirtyTracker.markDirty(event.getBlock());
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||||
|
public void onBlockSpread(BlockSpreadEvent event) {
|
||||||
|
service.dirtyTracker.markDirty(event.getBlock());
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||||
|
public void onBlockFade(BlockFadeEvent event) {
|
||||||
|
service.dirtyTracker.markDirty(event.getBlock());
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||||
|
public void onBlockBurn(BlockBurnEvent event) {
|
||||||
|
service.dirtyTracker.markDirty(event.getBlock());
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||||
|
public void onBlockRedstone(BlockRedstoneEvent event) {
|
||||||
|
service.dirtyTracker.markDirty(event.getBlock());
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||||
|
public void onPistonExtend(BlockPistonExtendEvent event) {
|
||||||
|
service.dirtyTracker.markDirty(event.getBlock());
|
||||||
|
service.dirtyTracker.markDirty(event.getBlock().getRelative(event.getDirection()));
|
||||||
|
for (Block block : event.getBlocks()) {
|
||||||
|
service.dirtyTracker.markDirty(block);
|
||||||
|
service.dirtyTracker.markDirty(block.getRelative(event.getDirection()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||||
|
public void onPistonRetract(BlockPistonRetractEvent event) {
|
||||||
|
service.dirtyTracker.markDirty(event.getBlock());
|
||||||
|
service.dirtyTracker.markDirty(event.getBlock().getRelative(event.getDirection()));
|
||||||
|
for (Block block : event.getBlocks()) {
|
||||||
|
service.dirtyTracker.markDirty(block);
|
||||||
|
service.dirtyTracker.markDirty(block.getRelative(event.getDirection()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||||
|
public void onStructureGrow(StructureGrowEvent event) {
|
||||||
|
for (BlockState state : event.getBlocks()) {
|
||||||
|
service.dirtyTracker.markDirty(state.getBlock());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||||
|
public void onUnauthorizedPlayerCommand(PlayerCommandPreprocessEvent event) {
|
||||||
|
service.tileWatcher.finalizeJigsawTileWatchesForPlayer(event.getPlayer().getUniqueId());
|
||||||
|
ActiveStudio studio = service.studios.get(event.getPlayer().getWorld().getUID());
|
||||||
|
if (studio != null
|
||||||
|
&& service.protection.materializationInProgress(studio)
|
||||||
|
&& !isSafeNonOwnerCommand(event.getMessage())) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
message(event.getPlayer(), "Wait for the current Jigsaw Studio variant load or rollback to finish.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (studio != null && blocksNonEditableWorkcellMutation(
|
||||||
|
hasNonEditableWorkcell(studio), event.getMessage())) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
message(event.getPlayer(), "Mutating commands are disabled while this Studio has an empty or read-only workcell.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
UUID ownerId = studio == null ? null : studio.generator().getRequest().ownerId();
|
||||||
|
if (!blocksMutatingCommand(ownerId, event.getPlayer().getUniqueId(), event.getMessage())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.setCancelled(true);
|
||||||
|
message(event.getPlayer(), "This Jigsaw Studio is owned by another player session.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||||
|
public void onPlayerCommand(PlayerCommandPreprocessEvent event) {
|
||||||
|
if (isMutatingCommand(event.getMessage())) {
|
||||||
|
service.dirtyTracker.markAllDirty(event.getPlayer().getWorld());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||||
|
public void onServerCommand(ServerCommandEvent event) {
|
||||||
|
if (!isSafeNonOwnerCommand(event.getCommand()) && service.protection.anyMaterializationInProgress()) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isMutatingCommand(event.getCommand()) && service.protection.anyNonEditableWorkcell()) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isMutatingCommand(event.getCommand())) {
|
||||||
|
service.dirtyTracker.markAllDirty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,271 @@
|
|||||||
|
package art.arcane.iris.core.service;
|
||||||
|
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioActivation;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioBay;
|
||||||
|
import art.arcane.iris.core.service.JigsawStudioChunkWriter.BayPopulation;
|
||||||
|
import art.arcane.iris.core.service.JigsawStudioService.ActiveStudio;
|
||||||
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
|
import art.arcane.iris.engine.platform.studio.generators.JigsawStudioGenerator;
|
||||||
|
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
||||||
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
|
import art.arcane.iris.util.common.scheduling.J;
|
||||||
|
import org.bukkit.World;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioService.AUTOSAVE_RETRY_TICKS;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioService.INSTANCE;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioService.disableNaturalStudioSpawning;
|
||||||
|
|
||||||
|
final class JigsawStudioRegistry {
|
||||||
|
private final JigsawStudioService service;
|
||||||
|
private final Set<UUID> unregisterRetries = ConcurrentHashMap.newKeySet();
|
||||||
|
private final Set<UUID> unregisterDrainWarnings = ConcurrentHashMap.newKeySet();
|
||||||
|
|
||||||
|
JigsawStudioRegistry(JigsawStudioService service) {
|
||||||
|
this.service = Objects.requireNonNull(service, "Jigsaw Studio service");
|
||||||
|
}
|
||||||
|
|
||||||
|
void quiesceForServerShutdown() {
|
||||||
|
if (!service.disableStarted.compareAndSet(false, true)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
service.tileWatcher.finalizeAllJigsawTileWatches();
|
||||||
|
} catch (Throwable exception) {
|
||||||
|
IrisLogging.reportError("Failed to finalize Jigsaw Studio tile watches during shutdown.", exception);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
service.autosaveScheduler.drainAutosavesBeforeDisable();
|
||||||
|
} catch (Throwable exception) {
|
||||||
|
IrisLogging.reportError("Failed to drain Jigsaw Studio autosaves during shutdown.", exception);
|
||||||
|
}
|
||||||
|
service.enabled = false;
|
||||||
|
JigsawStudioMenuController activeMenuController = service.toolbelt.menuController;
|
||||||
|
service.toolbelt.menuController = null;
|
||||||
|
if (activeMenuController != null) {
|
||||||
|
try {
|
||||||
|
activeMenuController.closeAll();
|
||||||
|
} catch (Throwable exception) {
|
||||||
|
IrisLogging.reportError("Failed to close Jigsaw Studio menus during shutdown.", exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
service.particlesDisabled.clear();
|
||||||
|
service.visualization.visualizationLoops.clear();
|
||||||
|
service.visualization.assemblyPreviews.clear();
|
||||||
|
service.playerContext.playerWorkcells.clear();
|
||||||
|
service.autosaveScheduler.autosaves.clear();
|
||||||
|
service.toolbelt.deferredDuplications.clear();
|
||||||
|
service.tileWatcher.jigsawTileWatches.clear();
|
||||||
|
service.toolbelt.toolConfirmations.clear();
|
||||||
|
service.tripleSneakTracker.clearAll();
|
||||||
|
service.evaluator.evaluations.clear();
|
||||||
|
try {
|
||||||
|
service.previewRenderer.removeAll();
|
||||||
|
} catch (Throwable exception) {
|
||||||
|
IrisLogging.reportError("Failed to remove Jigsaw Studio previews during shutdown.", exception);
|
||||||
|
}
|
||||||
|
service.saveLifecycle.reopenRequiredRequests.clear();
|
||||||
|
unregisterRetries.clear();
|
||||||
|
unregisterDrainWarnings.clear();
|
||||||
|
synchronized (service.saveLifecycleLock) {
|
||||||
|
service.studios.clear();
|
||||||
|
service.saveLifecycle.savesInProgress.clear();
|
||||||
|
service.graphMutations.graphMutationsInProgress.clear();
|
||||||
|
service.materializer.materializationsInProgress.clear();
|
||||||
|
service.saveLifecycle.exportsInProgress.clear();
|
||||||
|
service.saveLifecycle.closingRequests.clear();
|
||||||
|
service.saveLifecycle.discardingRequests.clear();
|
||||||
|
}
|
||||||
|
INSTANCE = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void register(Engine engine, JigsawStudioGenerator generator) {
|
||||||
|
if (!service.enabled || service.disableStarted.get()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Engine activeEngine = Objects.requireNonNull(engine, "Jigsaw Studio engine");
|
||||||
|
JigsawStudioGenerator activeGenerator = Objects.requireNonNull(generator, "Jigsaw Studio generator");
|
||||||
|
World world = BukkitWorldBinding.world(activeEngine.getTarget().getWorld());
|
||||||
|
if (world == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ActiveStudio existing = service.studios.get(world.getUID());
|
||||||
|
if (existing != null && existing.generator() == activeGenerator) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (existing != null) {
|
||||||
|
UUID existingRequestId = existing.generator().getRequest().requestId();
|
||||||
|
service.tileWatcher.finalizeJigsawTileWatches(existingRequestId);
|
||||||
|
service.autosaveScheduler.drainAutosavesBeforeRemoval(existing);
|
||||||
|
if (service.saveLifecycle.requiresLifecycleDrain(existing)
|
||||||
|
&& !service.saveLifecycle.discardingRequest(existingRequestId)) {
|
||||||
|
IrisLogging.warn("Jigsaw Studio registration deferred while request %s finishes its final autosave drain",
|
||||||
|
existingRequestId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ActiveStudio next = new ActiveStudio(
|
||||||
|
world.getUID(),
|
||||||
|
world,
|
||||||
|
activeEngine,
|
||||||
|
activeGenerator,
|
||||||
|
new ConcurrentHashMap<>(),
|
||||||
|
ConcurrentHashMap.newKeySet(),
|
||||||
|
new AtomicLong());
|
||||||
|
UUID displacedRequestId = null;
|
||||||
|
synchronized (service.saveLifecycleLock) {
|
||||||
|
if (!service.enabled || service.disableStarted.get()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ActiveStudio previous = service.studios.get(world.getUID());
|
||||||
|
if (previous != null && previous.generator() == activeGenerator) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ActiveStudio displaced = service.studios.put(world.getUID(), next);
|
||||||
|
if (displaced != null) {
|
||||||
|
displacedRequestId = displaced.generator().getRequest().requestId();
|
||||||
|
service.saveLifecycle.savesInProgress.remove(displacedRequestId);
|
||||||
|
service.graphMutations.graphMutationsInProgress.remove(displacedRequestId);
|
||||||
|
service.materializer.materializationsInProgress.remove(displacedRequestId);
|
||||||
|
service.saveLifecycle.exportsInProgress.remove(displacedRequestId);
|
||||||
|
service.saveLifecycle.closingRequests.remove(displacedRequestId);
|
||||||
|
service.saveLifecycle.discardingRequests.remove(displacedRequestId);
|
||||||
|
service.saveLifecycle.reopenRequiredRequests.remove(displacedRequestId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (displacedRequestId != null) {
|
||||||
|
service.autosaveScheduler.clearAutosaves(displacedRequestId);
|
||||||
|
service.tileWatcher.clearJigsawTileWatches(displacedRequestId);
|
||||||
|
unregisterRetries.remove(displacedRequestId);
|
||||||
|
unregisterDrainWarnings.remove(displacedRequestId);
|
||||||
|
service.tripleSneakTracker.clearRequest(displacedRequestId);
|
||||||
|
service.evaluator.evaluations.remove(displacedRequestId);
|
||||||
|
service.previewRenderer.removeRequest(displacedRequestId);
|
||||||
|
}
|
||||||
|
IrisLogging.debug("Jigsaw Studio authoring registered: world=%s structure=%s bays=%d",
|
||||||
|
world.getName(), activeGenerator.getSession().structureKey(), activeGenerator.getLayout().bays().size());
|
||||||
|
service.evaluator.scheduleInitialEvaluation(next);
|
||||||
|
service.playerContext.scheduleOnlinePlayers(world.getUID());
|
||||||
|
}
|
||||||
|
|
||||||
|
void activationCommitted(World world, UUID requestId) {
|
||||||
|
if (!service.enabled || service.disableStarted.get() || world == null || requestId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ActiveStudio studio = service.studios.get(world.getUID());
|
||||||
|
if (studio == null || !requestId.equals(studio.generator().getRequest().requestId())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
disableNaturalStudioSpawning(world);
|
||||||
|
service.evaluator.scheduleInitialEvaluation(studio);
|
||||||
|
}
|
||||||
|
|
||||||
|
void markChunkGenerated(
|
||||||
|
Engine engine,
|
||||||
|
JigsawStudioGenerator generator,
|
||||||
|
int chunkX,
|
||||||
|
int chunkZ
|
||||||
|
) {
|
||||||
|
if (!service.enabled || service.disableStarted.get() || engine == null || generator == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
World world = BukkitWorldBinding.world(engine.getTarget().getWorld());
|
||||||
|
if (world == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ActiveStudio studio = service.studios.get(world.getUID());
|
||||||
|
if (!service.enabled || service.disableStarted.get()
|
||||||
|
|| studio == null || studio.engine() != engine || studio.generator() != generator) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
markChunkAvailable(studio, chunkX, chunkZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
void markChunkAvailable(ActiveStudio studio, int chunkX, int chunkZ) {
|
||||||
|
long chunkKey = JigsawStudioService.chunkKey(chunkX, chunkZ);
|
||||||
|
boolean relevant = false;
|
||||||
|
for (JigsawStudioBay bay : studio.generator().getLayout().bays()) {
|
||||||
|
BayPopulation population = studio.population(bay);
|
||||||
|
if (population.markGenerated(chunkKey)) {
|
||||||
|
relevant = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (relevant) {
|
||||||
|
service.materializer.scheduleHydration(studio, studio.world(), chunkX, chunkZ, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void unregister(World world) {
|
||||||
|
if (world == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ActiveStudio active = service.studios.get(world.getUID());
|
||||||
|
if (active != null) {
|
||||||
|
UUID activeRequestId = active.generator().getRequest().requestId();
|
||||||
|
service.tileWatcher.finalizeJigsawTileWatches(activeRequestId);
|
||||||
|
service.autosaveScheduler.drainAutosavesBeforeRemoval(active);
|
||||||
|
if (service.saveLifecycle.requiresLifecycleDrain(active)
|
||||||
|
&& !service.saveLifecycle.discardingRequest(activeRequestId)) {
|
||||||
|
if (unregisterDrainWarnings.add(activeRequestId)) {
|
||||||
|
IrisLogging.warn("Jigsaw Studio unregister deferred while request %s finishes its final autosave drain",
|
||||||
|
activeRequestId);
|
||||||
|
}
|
||||||
|
scheduleUnregisterRetry(world, activeRequestId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ActiveStudio removed;
|
||||||
|
JigsawStudioActivation.Request request;
|
||||||
|
synchronized (service.saveLifecycleLock) {
|
||||||
|
removed = service.studios.remove(world.getUID());
|
||||||
|
if (removed == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
request = removed.generator().getRequest();
|
||||||
|
service.saveLifecycle.savesInProgress.remove(request.requestId());
|
||||||
|
service.graphMutations.graphMutationsInProgress.remove(request.requestId());
|
||||||
|
service.materializer.materializationsInProgress.remove(request.requestId());
|
||||||
|
service.saveLifecycle.exportsInProgress.remove(request.requestId());
|
||||||
|
service.saveLifecycle.closingRequests.remove(request.requestId());
|
||||||
|
service.saveLifecycle.discardingRequests.remove(request.requestId());
|
||||||
|
service.saveLifecycle.reopenRequiredRequests.remove(request.requestId());
|
||||||
|
}
|
||||||
|
service.autosaveScheduler.clearAutosaves(request.requestId());
|
||||||
|
service.toolbelt.deferredDuplications.remove(request.requestId());
|
||||||
|
service.tileWatcher.clearJigsawTileWatches(request.requestId());
|
||||||
|
unregisterRetries.remove(request.requestId());
|
||||||
|
unregisterDrainWarnings.remove(request.requestId());
|
||||||
|
service.toolbelt.toolConfirmations.entrySet().removeIf(
|
||||||
|
entry -> entry.getValue().payload().requestId().equals(request.requestId()));
|
||||||
|
service.tripleSneakTracker.clearRequest(request.requestId());
|
||||||
|
service.evaluator.evaluations.remove(request.requestId());
|
||||||
|
service.previewRenderer.forgetRequest(request.requestId());
|
||||||
|
JigsawStudioActivation.deactivate(request.packKey(), request.requestId());
|
||||||
|
service.playerContext.clearWorldPlayerContexts(world.getUID());
|
||||||
|
IrisLogging.debug("Jigsaw Studio authoring unregistered: world=%s", world.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void scheduleUnregisterRetry(World world, UUID requestId) {
|
||||||
|
if (!service.enabled || !unregisterRetries.add(requestId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
J.s(() -> {
|
||||||
|
unregisterRetries.remove(requestId);
|
||||||
|
ActiveStudio current = service.studios.get(world.getUID());
|
||||||
|
if (current != null
|
||||||
|
&& current.generator().getRequest().requestId().equals(requestId)) {
|
||||||
|
unregister(world);
|
||||||
|
}
|
||||||
|
}, AUTOSAVE_RETRY_TICKS);
|
||||||
|
} catch (Throwable exception) {
|
||||||
|
unregisterRetries.remove(requestId);
|
||||||
|
IrisLogging.reportError(exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,512 @@
|
|||||||
|
package art.arcane.iris.core.service;
|
||||||
|
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioActivation;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioBay;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioSession;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioVariant;
|
||||||
|
import art.arcane.iris.core.service.JigsawStudioAutosaveScheduler.AutosaveKey;
|
||||||
|
import art.arcane.iris.core.service.JigsawStudioAutosaveScheduler.AutosavePersistentFailure;
|
||||||
|
import art.arcane.iris.core.service.JigsawStudioCaptureScheduler.CaptureTarget;
|
||||||
|
import art.arcane.iris.core.service.JigsawStudioService.ActiveStudio;
|
||||||
|
import art.arcane.iris.core.service.JigsawStudioService.CloseStart;
|
||||||
|
import art.arcane.iris.core.service.JigsawStudioService.ExportStart;
|
||||||
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
|
import art.arcane.iris.util.common.scheduling.J;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioCaptureScheduler.resolveCaptureTarget;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioProtection.authorizeOwner;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioService.AUTOSAVE_RETRY_TICKS;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioService.failureMessage;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioService.findBay;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioService.message;
|
||||||
|
|
||||||
|
final class JigsawStudioSaveLifecycle {
|
||||||
|
private static final int REPLACEMENT_CLOSE_WAIT_TICKS = 2_400;
|
||||||
|
|
||||||
|
private final JigsawStudioService service;
|
||||||
|
final Set<UUID> savesInProgress = new HashSet<>();
|
||||||
|
final Set<UUID> exportsInProgress = new HashSet<>();
|
||||||
|
final Set<UUID> closingRequests = new HashSet<>();
|
||||||
|
final Set<UUID> discardingRequests = new HashSet<>();
|
||||||
|
final Set<UUID> reopenRequiredRequests = ConcurrentHashMap.newKeySet();
|
||||||
|
|
||||||
|
JigsawStudioSaveLifecycle(JigsawStudioService service) {
|
||||||
|
this.service = Objects.requireNonNull(service, "Jigsaw Studio service");
|
||||||
|
}
|
||||||
|
|
||||||
|
CloseStart tryBeginClose(UUID requestId, UUID ownerId, boolean discard) {
|
||||||
|
if (requestId == null) {
|
||||||
|
return CloseStart.NOT_ACTIVE;
|
||||||
|
}
|
||||||
|
service.tileWatcher.finalizeJigsawTileWatches(requestId);
|
||||||
|
synchronized (service.saveLifecycleLock) {
|
||||||
|
JigsawStudioActivation.Request request = JigsawStudioActivation.getRequest(requestId);
|
||||||
|
JigsawStudioSession session = JigsawStudioActivation.getSession(requestId);
|
||||||
|
if (request == null || session == null) {
|
||||||
|
return CloseStart.NOT_ACTIVE;
|
||||||
|
}
|
||||||
|
if (request.ownerId() != null && !request.ownerId().equals(ownerId)) {
|
||||||
|
return CloseStart.NOT_OWNER;
|
||||||
|
}
|
||||||
|
if (savesInProgress.contains(requestId)) {
|
||||||
|
return CloseStart.SAVE_IN_PROGRESS;
|
||||||
|
}
|
||||||
|
if (service.graphMutations.graphMutationsInProgress.contains(requestId)) {
|
||||||
|
return CloseStart.OPERATION_IN_PROGRESS;
|
||||||
|
}
|
||||||
|
if (exportsInProgress.contains(requestId)) {
|
||||||
|
return CloseStart.OPERATION_IN_PROGRESS;
|
||||||
|
}
|
||||||
|
if (service.materializer.materializationsInProgress.contains(requestId)) {
|
||||||
|
return CloseStart.OPERATION_IN_PROGRESS;
|
||||||
|
}
|
||||||
|
if (session.operationInProgress()) {
|
||||||
|
return CloseStart.OPERATION_IN_PROGRESS;
|
||||||
|
}
|
||||||
|
if (service.tileWatcher.hasJigsawTileWatch(requestId)) {
|
||||||
|
return CloseStart.OPERATION_IN_PROGRESS;
|
||||||
|
}
|
||||||
|
if (session.isDirty() && !discard) {
|
||||||
|
return CloseStart.DIRTY;
|
||||||
|
}
|
||||||
|
closingRequests.add(requestId);
|
||||||
|
if (discard) {
|
||||||
|
discardingRequests.add(requestId);
|
||||||
|
} else {
|
||||||
|
discardingRequests.remove(requestId);
|
||||||
|
}
|
||||||
|
return CloseStart.STARTED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String closeProtectionFailure(UUID requestId) {
|
||||||
|
if (requestId == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
synchronized (service.saveLifecycleLock) {
|
||||||
|
if (closingRequests.contains(requestId) && !savesInProgress.contains(requestId)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (savesInProgress.contains(requestId)) {
|
||||||
|
return "The active Jigsaw Studio is saving and cannot be closed or replaced yet.";
|
||||||
|
}
|
||||||
|
if (service.graphMutations.graphMutationsInProgress.contains(requestId)) {
|
||||||
|
return "The active Jigsaw Studio is updating its graph and cannot be closed or replaced yet.";
|
||||||
|
}
|
||||||
|
if (exportsInProgress.contains(requestId)) {
|
||||||
|
return "The active Jigsaw Studio is exporting and cannot be closed or replaced yet.";
|
||||||
|
}
|
||||||
|
if (service.materializer.materializationsInProgress.contains(requestId)) {
|
||||||
|
return "The active Jigsaw Studio is loading or restoring a variant and cannot be closed yet.";
|
||||||
|
}
|
||||||
|
JigsawStudioSession session = JigsawStudioActivation.getSession(requestId);
|
||||||
|
if (session != null && session.operationInProgress()) {
|
||||||
|
return "The active Jigsaw Studio is loading a variant and cannot be closed or replaced yet.";
|
||||||
|
}
|
||||||
|
if (service.tileWatcher.hasJigsawTileWatch(requestId)) {
|
||||||
|
return "The active Jigsaw Studio is finalizing an open vanilla jigsaw-block editor.";
|
||||||
|
}
|
||||||
|
if (session != null && session.isDirty()) {
|
||||||
|
return "The active Jigsaw Studio is waiting for autosave. Let it finish before closing.";
|
||||||
|
}
|
||||||
|
return "The active Jigsaw Studio is owner-controlled. Close it with /iris jigsaw close.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CompletableFuture<Void> awaitCloseForReplacement(UUID requestId, UUID ownerId) {
|
||||||
|
CompletableFuture<Void> readiness = new CompletableFuture<>();
|
||||||
|
awaitCloseForReplacement(requestId, ownerId, readiness, 0);
|
||||||
|
return readiness;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void awaitCloseForReplacement(
|
||||||
|
UUID requestId,
|
||||||
|
UUID ownerId,
|
||||||
|
CompletableFuture<Void> readiness,
|
||||||
|
int waitedTicks
|
||||||
|
) {
|
||||||
|
if (readiness.isDone()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
CloseStart closeStart = tryBeginClose(requestId, ownerId, false);
|
||||||
|
switch (closeStart) {
|
||||||
|
case STARTED, NOT_ACTIVE -> readiness.complete(null);
|
||||||
|
case NOT_OWNER -> readiness.completeExceptionally(new IllegalStateException(
|
||||||
|
"The active Jigsaw Studio is owned by another player session."));
|
||||||
|
case DIRTY, SAVE_IN_PROGRESS, OPERATION_IN_PROGRESS -> {
|
||||||
|
if (waitedTicks == 0) {
|
||||||
|
service.autosaveScheduler.expediteAutosaves(requestId);
|
||||||
|
}
|
||||||
|
if (waitedTicks >= REPLACEMENT_CLOSE_WAIT_TICKS) {
|
||||||
|
String failure = closeProtectionFailure(requestId);
|
||||||
|
readiness.completeExceptionally(new IllegalStateException(
|
||||||
|
failure == null
|
||||||
|
? "The active Jigsaw Studio did not become ready for replacement."
|
||||||
|
: failure));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
J.s(() -> awaitCloseForReplacement(
|
||||||
|
requestId,
|
||||||
|
ownerId,
|
||||||
|
readiness,
|
||||||
|
waitedTicks + AUTOSAVE_RETRY_TICKS), AUTOSAVE_RETRY_TICKS);
|
||||||
|
} catch (Throwable exception) {
|
||||||
|
readiness.completeExceptionally(exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void cancelClose(UUID requestId) {
|
||||||
|
if (requestId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
synchronized (service.saveLifecycleLock) {
|
||||||
|
closingRequests.remove(requestId);
|
||||||
|
discardingRequests.remove(requestId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SaveStart tryBeginSave(UUID requestId) {
|
||||||
|
Objects.requireNonNull(requestId, "Jigsaw Studio save request ID");
|
||||||
|
synchronized (service.saveLifecycleLock) {
|
||||||
|
if (closingRequests.contains(requestId)) {
|
||||||
|
return SaveStart.CLOSING;
|
||||||
|
}
|
||||||
|
if (service.graphMutations.graphMutationsInProgress.contains(requestId)) {
|
||||||
|
return SaveStart.GRAPH_OPERATION;
|
||||||
|
}
|
||||||
|
if (exportsInProgress.contains(requestId)) {
|
||||||
|
return SaveStart.EXPORT_OPERATION;
|
||||||
|
}
|
||||||
|
if (service.materializer.materializationsInProgress.contains(requestId)) {
|
||||||
|
return SaveStart.VARIANT_OPERATION;
|
||||||
|
}
|
||||||
|
if (!savesInProgress.add(requestId)) {
|
||||||
|
return SaveStart.IN_PROGRESS;
|
||||||
|
}
|
||||||
|
return SaveStart.STARTED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void finishSave(UUID requestId) {
|
||||||
|
if (requestId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
synchronized (service.saveLifecycleLock) {
|
||||||
|
savesInProgress.remove(requestId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ExportStart tryBeginExport(UUID requestId, UUID ownerId) {
|
||||||
|
Objects.requireNonNull(requestId, "Jigsaw Studio export request ID");
|
||||||
|
Objects.requireNonNull(ownerId, "Jigsaw Studio export owner ID");
|
||||||
|
service.tileWatcher.finalizeJigsawTileWatches(requestId);
|
||||||
|
synchronized (service.saveLifecycleLock) {
|
||||||
|
JigsawStudioActivation.Request request = JigsawStudioActivation.getRequest(requestId);
|
||||||
|
JigsawStudioSession session = JigsawStudioActivation.getSession(requestId);
|
||||||
|
if (request == null || session == null) {
|
||||||
|
return ExportStart.NOT_ACTIVE;
|
||||||
|
}
|
||||||
|
if (request.ownerId() != null && !request.ownerId().equals(ownerId)) {
|
||||||
|
return ExportStart.NOT_OWNER;
|
||||||
|
}
|
||||||
|
if (closingRequests.contains(requestId)) {
|
||||||
|
return ExportStart.CLOSING;
|
||||||
|
}
|
||||||
|
if (savesInProgress.contains(requestId)) {
|
||||||
|
return ExportStart.SAVE_IN_PROGRESS;
|
||||||
|
}
|
||||||
|
if (service.graphMutations.graphMutationsInProgress.contains(requestId)) {
|
||||||
|
return ExportStart.OPERATION_IN_PROGRESS;
|
||||||
|
}
|
||||||
|
if (service.materializer.materializationsInProgress.contains(requestId) || session.operationInProgress()) {
|
||||||
|
return ExportStart.OPERATION_IN_PROGRESS;
|
||||||
|
}
|
||||||
|
if (service.tileWatcher.hasJigsawTileWatch(requestId)) {
|
||||||
|
return ExportStart.OPERATION_IN_PROGRESS;
|
||||||
|
}
|
||||||
|
if (session.isDirty()) {
|
||||||
|
return ExportStart.DIRTY;
|
||||||
|
}
|
||||||
|
if (!exportsInProgress.add(requestId)) {
|
||||||
|
return ExportStart.IN_PROGRESS;
|
||||||
|
}
|
||||||
|
return ExportStart.STARTED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void finishExport(UUID requestId) {
|
||||||
|
if (requestId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
synchronized (service.saveLifecycleLock) {
|
||||||
|
exportsInProgress.remove(requestId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean saveSelected(Player player) {
|
||||||
|
if (player == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!J.isOwnedByCurrentRegion(player)) {
|
||||||
|
return J.runEntity(player, () -> saveSelected(player));
|
||||||
|
}
|
||||||
|
ActiveStudio studio = service.studios.get(player.getWorld().getUID());
|
||||||
|
if (studio == null) {
|
||||||
|
message(player, "Iris Jigsaw Studio is not active in this world.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!authorizeOwner(player, studio)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (reopenRequiredRequests.contains(studio.generator().getRequest().requestId())) {
|
||||||
|
message(player, "Close and reopen Jigsaw Studio before loading variants in the resized layout.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (service.graphMutations.graphMutationInProgress(studio.generator().getRequest().requestId())) {
|
||||||
|
message(player, "Wait for the current Jigsaw Studio graph update to finish.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
UUID requestId = studio.generator().getRequest().requestId();
|
||||||
|
service.tileWatcher.finalizeJigsawTileWatches(requestId);
|
||||||
|
if (service.tileWatcher.hasJigsawTileWatch(requestId)) {
|
||||||
|
message(player, "Wait for the open vanilla jigsaw-block editor to finish its final snapshot.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioSession session = studio.generator().getSession();
|
||||||
|
String selected = session.selectedBayId().orElse(null);
|
||||||
|
if (selected == null) {
|
||||||
|
Location location = player.getLocation();
|
||||||
|
JigsawStudioBay underPlayer = session.layout().findAt(
|
||||||
|
location.getBlockX(), location.getBlockY(), location.getBlockZ());
|
||||||
|
if (underPlayer != null) {
|
||||||
|
selected = underPlayer.stableId();
|
||||||
|
session.selectBay(selected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (selected == null) {
|
||||||
|
message(player, "Stand inside or select a Jigsaw Studio workcell before saving.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return saveBay(player, selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean saveBay(Player player, String bayId) {
|
||||||
|
if (player == null || bayId == null || bayId.isBlank()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!J.isOwnedByCurrentRegion(player)) {
|
||||||
|
return J.runEntity(player, () -> saveBay(player, bayId));
|
||||||
|
}
|
||||||
|
ActiveStudio studio = service.studios.get(player.getWorld().getUID());
|
||||||
|
if (studio == null) {
|
||||||
|
message(player, "Iris Jigsaw Studio is not active in this world.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!authorizeOwner(player, studio)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioBay bay = findBay(studio.generator().getSession().layout(), bayId);
|
||||||
|
if (bay == null) {
|
||||||
|
message(player, "No Jigsaw Studio bay matches '" + bayId + "'.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return startSave(studio, player.getWorld(), bay, player, true) == SaveAttempt.STARTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
SaveAttempt startSave(
|
||||||
|
ActiveStudio studio,
|
||||||
|
World world,
|
||||||
|
JigsawStudioBay bay,
|
||||||
|
Player player,
|
||||||
|
boolean report
|
||||||
|
) {
|
||||||
|
UUID requestId = studio.generator().getRequest().requestId();
|
||||||
|
if (reopenRequiredRequests.contains(requestId)) {
|
||||||
|
JigsawStudioService.report(
|
||||||
|
player, report, "Close and reopen Jigsaw Studio before saving the resized layout.");
|
||||||
|
return SaveAttempt.DEFERRED;
|
||||||
|
}
|
||||||
|
JigsawStudioSession session = studio.generator().getSession();
|
||||||
|
JigsawStudioVariant activeVariant = session.activeVariant(bay.stableId()).orElse(null);
|
||||||
|
if (activeVariant == null) {
|
||||||
|
JigsawStudioService.report(
|
||||||
|
player, report, "Workcell '" + bay.stableId() + "' has no active variant to save.");
|
||||||
|
return SaveAttempt.DEFERRED;
|
||||||
|
}
|
||||||
|
if (!activeVariant.owned()) {
|
||||||
|
JigsawStudioService.report(player, report, "Variant '" + activeVariant.pieceKey()
|
||||||
|
+ "' is read-only. Adopt or clone its graph before editing it.");
|
||||||
|
return SaveAttempt.DEFERRED;
|
||||||
|
}
|
||||||
|
BayReadiness readiness = studio.population(bay).readiness();
|
||||||
|
if (!readiness.ready()) {
|
||||||
|
JigsawStudioService.report(player, report, readinessMessage(bay, readiness));
|
||||||
|
return readiness.failure().isEmpty() ? SaveAttempt.RETRY : SaveAttempt.DEFERRED;
|
||||||
|
}
|
||||||
|
CaptureTarget captureTarget;
|
||||||
|
try {
|
||||||
|
captureTarget = resolveCaptureTarget(studio, bay, activeVariant);
|
||||||
|
} catch (IOException exception) {
|
||||||
|
String failure = "Jigsaw Studio cannot capture this workcell: " + failureMessage(exception);
|
||||||
|
JigsawStudioService.report(player, report, failure);
|
||||||
|
return service.autosaveScheduler.retainCurrentAutosaveFailure(
|
||||||
|
studio,
|
||||||
|
bay.stableId(),
|
||||||
|
new AutosavePersistentFailure(failure, exception))
|
||||||
|
? SaveAttempt.PERSISTENT_FAILURE
|
||||||
|
: SaveAttempt.DEFERRED;
|
||||||
|
}
|
||||||
|
JigsawStudioSession.SaveStart sessionSave = session.beginSave(bay.stableId());
|
||||||
|
if (sessionSave.status() != JigsawStudioSession.SaveStatus.STARTED) {
|
||||||
|
JigsawStudioService.report(player, report, switch (sessionSave.status()) {
|
||||||
|
case SWITCH_IN_PROGRESS -> "This workcell is loading another variant.";
|
||||||
|
case SAVE_IN_PROGRESS -> "This workcell is already saving.";
|
||||||
|
case NO_ACTIVE_VARIANT -> "This workcell has no active variant.";
|
||||||
|
case UNKNOWN_WORKCELL -> "This workcell is no longer active.";
|
||||||
|
case STARTED -> "The workcell save could not start.";
|
||||||
|
});
|
||||||
|
return sessionSave.status() == JigsawStudioSession.SaveStatus.SAVE_IN_PROGRESS
|
||||||
|
|| sessionSave.status() == JigsawStudioSession.SaveStatus.SWITCH_IN_PROGRESS
|
||||||
|
? SaveAttempt.RETRY
|
||||||
|
: SaveAttempt.DEFERRED;
|
||||||
|
}
|
||||||
|
JigsawStudioSession.SaveIdentity saveIdentity = sessionSave.identity().orElseThrow();
|
||||||
|
SaveStart saveStart = tryBeginSave(requestId);
|
||||||
|
if (saveStart == SaveStart.CLOSING) {
|
||||||
|
session.abortSave(saveIdentity);
|
||||||
|
JigsawStudioService.report(player, report, "This Jigsaw Studio is closing and cannot start another save.");
|
||||||
|
return SaveAttempt.DEFERRED;
|
||||||
|
}
|
||||||
|
if (saveStart == SaveStart.IN_PROGRESS) {
|
||||||
|
session.abortSave(saveIdentity);
|
||||||
|
JigsawStudioService.report(player, report, "A save is already running for this Jigsaw Studio.");
|
||||||
|
return SaveAttempt.RETRY;
|
||||||
|
}
|
||||||
|
if (saveStart == SaveStart.GRAPH_OPERATION) {
|
||||||
|
session.abortSave(saveIdentity);
|
||||||
|
JigsawStudioService.report(player, report, "Wait for the current Jigsaw Studio graph update to finish.");
|
||||||
|
return SaveAttempt.RETRY;
|
||||||
|
}
|
||||||
|
if (saveStart == SaveStart.EXPORT_OPERATION) {
|
||||||
|
session.abortSave(saveIdentity);
|
||||||
|
JigsawStudioService.report(player, report, "Wait for the current Jigsaw Studio export to finish.");
|
||||||
|
return SaveAttempt.RETRY;
|
||||||
|
}
|
||||||
|
if (saveStart == SaveStart.VARIANT_OPERATION) {
|
||||||
|
session.abortSave(saveIdentity);
|
||||||
|
JigsawStudioService.report(
|
||||||
|
player, report, "Wait for the current Jigsaw Studio variant load or rollback to finish.");
|
||||||
|
return SaveAttempt.RETRY;
|
||||||
|
}
|
||||||
|
if (report) {
|
||||||
|
session.selectBay(bay.stableId());
|
||||||
|
}
|
||||||
|
service.playerContext.refreshWorkcellContext(studio.worldId(), bay.stableId());
|
||||||
|
JigsawStudioService.report(player, report, "Capturing variant '" + activeVariant.pieceKey() + "' from "
|
||||||
|
+ bay.stableId() + "...");
|
||||||
|
try {
|
||||||
|
boolean scheduled = service.captureScheduler.scheduleCapture(
|
||||||
|
studio,
|
||||||
|
world,
|
||||||
|
bay,
|
||||||
|
captureTarget,
|
||||||
|
player,
|
||||||
|
requestId,
|
||||||
|
saveIdentity,
|
||||||
|
service.autosaveScheduler.autosaveFailureState(studio, saveIdentity));
|
||||||
|
return scheduled ? SaveAttempt.STARTED : SaveAttempt.DEFERRED;
|
||||||
|
} catch (Throwable exception) {
|
||||||
|
session.abortSave(saveIdentity);
|
||||||
|
finishSave(requestId);
|
||||||
|
service.playerContext.refreshWorkcellContext(studio.worldId(), bay.stableId());
|
||||||
|
IrisLogging.reportError(exception);
|
||||||
|
JigsawStudioService.report(player, report, "Jigsaw Studio could not schedule the workcell capture: "
|
||||||
|
+ failureMessage(exception));
|
||||||
|
return SaveAttempt.DEFERRED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean requiresLifecycleDrain(ActiveStudio studio) {
|
||||||
|
UUID requestId = studio.generator().getRequest().requestId();
|
||||||
|
if (service.tileWatcher.hasJigsawTileWatch(requestId)
|
||||||
|
|| studio.generator().getSession().isDirty()
|
||||||
|
|| studio.generator().getSession().operationInProgress()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
for (AutosaveKey key : service.autosaveScheduler.autosaves.keySet()) {
|
||||||
|
if (requestId.equals(key.requestId())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
synchronized (service.saveLifecycleLock) {
|
||||||
|
return savesInProgress.contains(requestId)
|
||||||
|
|| service.graphMutations.graphMutationsInProgress.contains(requestId)
|
||||||
|
|| service.materializer.materializationsInProgress.contains(requestId)
|
||||||
|
|| exportsInProgress.contains(requestId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean discardingRequest(UUID requestId) {
|
||||||
|
synchronized (service.saveLifecycleLock) {
|
||||||
|
return discardingRequests.contains(requestId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isCurrentSave(
|
||||||
|
ActiveStudio studio,
|
||||||
|
UUID requestId,
|
||||||
|
JigsawStudioSession.SaveIdentity identity
|
||||||
|
) {
|
||||||
|
return service.isCurrentRequest(studio, requestId)
|
||||||
|
&& studio.generator().getSession().isSaveCurrent(identity);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String readinessMessage(JigsawStudioBay bay, BayReadiness readiness) {
|
||||||
|
if (!readiness.failure().isEmpty()) {
|
||||||
|
return "Bay '" + bay.stableId() + "' is invalid and cannot be saved: " + readiness.failure();
|
||||||
|
}
|
||||||
|
return "Bay '" + bay.stableId() + "' is not ready to save: "
|
||||||
|
+ readiness.generatedChunks() + "/" + readiness.requiredChunks() + " chunk(s) populated, "
|
||||||
|
+ readiness.hydratedChunks() + "/" + readiness.requiredChunks()
|
||||||
|
+ " chunk(s) hydrated. Visit the whole bay and wait for its markers to finish loading.";
|
||||||
|
}
|
||||||
|
|
||||||
|
record BayReadiness(
|
||||||
|
boolean ready,
|
||||||
|
String failure,
|
||||||
|
int requiredChunks,
|
||||||
|
int generatedChunks,
|
||||||
|
int hydratedChunks
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SaveStart {
|
||||||
|
STARTED,
|
||||||
|
IN_PROGRESS,
|
||||||
|
CLOSING,
|
||||||
|
GRAPH_OPERATION,
|
||||||
|
EXPORT_OPERATION,
|
||||||
|
VARIANT_OPERATION
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SaveAttempt {
|
||||||
|
STARTED,
|
||||||
|
RETRY,
|
||||||
|
DEFERRED,
|
||||||
|
PERSISTENT_FAILURE
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,340 @@
|
|||||||
|
package art.arcane.iris.core.service;
|
||||||
|
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioActivation;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioBay;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioSession;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioVariant;
|
||||||
|
import art.arcane.iris.core.service.JigsawStudioService.ActiveStudio;
|
||||||
|
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||||
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
|
import art.arcane.iris.util.common.scheduling.J;
|
||||||
|
import art.arcane.volmlib.util.collection.KMap;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.block.Block;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioService.AUTOSAVE_RETRY_TICKS;
|
||||||
|
|
||||||
|
final class JigsawStudioTileWatcher {
|
||||||
|
private static final int JIGSAW_TILE_WATCH_INTERVAL_TICKS = 5;
|
||||||
|
|
||||||
|
private final JigsawStudioService service;
|
||||||
|
final Map<JigsawTileWatchKey, JigsawTileWatch> jigsawTileWatches = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
JigsawStudioTileWatcher(JigsawStudioService service) {
|
||||||
|
this.service = Objects.requireNonNull(service, "Jigsaw Studio service");
|
||||||
|
}
|
||||||
|
|
||||||
|
void startJigsawTileWatch(Player player, Block block) {
|
||||||
|
if (player == null || block == null || block.getType() != Material.JIGSAW) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
World world = block.getWorld();
|
||||||
|
int worldX = block.getX();
|
||||||
|
int worldY = block.getY();
|
||||||
|
int worldZ = block.getZ();
|
||||||
|
UUID playerId = player.getUniqueId();
|
||||||
|
if (J.isOwnedByCurrentRegion(world, worldX >> 4, worldZ >> 4)) {
|
||||||
|
initializeJigsawTileWatch(playerId, world, worldX, worldY, worldZ);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
boolean scheduled = J.runRegion(
|
||||||
|
world,
|
||||||
|
worldX >> 4,
|
||||||
|
worldZ >> 4,
|
||||||
|
() -> initializeJigsawTileWatch(playerId, world, worldX, worldY, worldZ));
|
||||||
|
if (!scheduled) {
|
||||||
|
IrisLogging.warn("Jigsaw Studio could not watch jigsaw marker NBT at %d,%d,%d in %s",
|
||||||
|
worldX, worldY, worldZ, world.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initializeJigsawTileWatch(
|
||||||
|
UUID playerId,
|
||||||
|
World world,
|
||||||
|
int worldX,
|
||||||
|
int worldY,
|
||||||
|
int worldZ
|
||||||
|
) {
|
||||||
|
ActiveStudio studio = service.studios.get(world.getUID());
|
||||||
|
if (!service.enabled || studio == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Block block = world.getBlockAt(worldX, worldY, worldZ);
|
||||||
|
if (block.getType() != Material.JIGSAW) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
JigsawStudioSession session = studio.generator().getSession();
|
||||||
|
JigsawStudioBay bay = session.layout().findAt(worldX, worldY, worldZ);
|
||||||
|
JigsawStudioVariant variant = bay == null
|
||||||
|
? null
|
||||||
|
: session.activeVariant(bay.stableId()).orElse(null);
|
||||||
|
if (bay == null || variant == null || !variant.owned()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
KMap<String, Object> snapshot;
|
||||||
|
try {
|
||||||
|
snapshot = BukkitPlatform.serializeTile(block.getLocation());
|
||||||
|
} catch (Throwable exception) {
|
||||||
|
IrisLogging.reportError(exception);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (snapshot == null) {
|
||||||
|
IrisLogging.warn("Jigsaw Studio could not read jigsaw marker NBT at %d,%d,%d in %s",
|
||||||
|
worldX, worldY, worldZ, world.getName());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
JigsawStudioActivation.Request request = studio.generator().getRequest();
|
||||||
|
JigsawTileWatchKey key = new JigsawTileWatchKey(
|
||||||
|
request.requestId(), world.getUID(), worldX, worldY, worldZ);
|
||||||
|
JigsawTileWatch watch = new JigsawTileWatch(
|
||||||
|
key,
|
||||||
|
studio,
|
||||||
|
playerId,
|
||||||
|
bay.stableId(),
|
||||||
|
snapshot,
|
||||||
|
new AtomicBoolean(false),
|
||||||
|
new AtomicBoolean(false));
|
||||||
|
jigsawTileWatches.put(key, watch);
|
||||||
|
scheduleJigsawTileWatch(watch);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void scheduleJigsawTileWatch(JigsawTileWatch watch) {
|
||||||
|
boolean scheduled = J.runRegion(
|
||||||
|
watch.studio().world(),
|
||||||
|
watch.key().worldX() >> 4,
|
||||||
|
watch.key().worldZ() >> 4,
|
||||||
|
() -> runJigsawTileWatch(watch, false),
|
||||||
|
JIGSAW_TILE_WATCH_INTERVAL_TICKS);
|
||||||
|
if (!scheduled && jigsawTileWatches.get(watch.key()) == watch) {
|
||||||
|
warnJigsawTileWatchScheduleFailure(watch);
|
||||||
|
scheduleJigsawTileWatchReconciliation(watch, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void runJigsawTileWatch(JigsawTileWatch watch, boolean release) {
|
||||||
|
if (jigsawTileWatches.get(watch.key()) != watch) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ActiveStudio studio = service.studios.get(watch.key().worldId());
|
||||||
|
if (!service.enabled || studio != watch.studio()
|
||||||
|
|| !service.isCurrentRequest(studio, watch.key().requestId())) {
|
||||||
|
jigsawTileWatches.remove(watch.key(), watch);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Block block = studio.world().getBlockAt(
|
||||||
|
watch.key().worldX(), watch.key().worldY(), watch.key().worldZ());
|
||||||
|
JigsawStudioBay bay = studio.generator().getSession().layout().findAt(
|
||||||
|
watch.key().worldX(), watch.key().worldY(), watch.key().worldZ());
|
||||||
|
if (block.getType() != Material.JIGSAW
|
||||||
|
|| bay == null
|
||||||
|
|| !bay.stableId().equals(watch.workcellId())) {
|
||||||
|
jigsawTileWatches.remove(watch.key(), watch);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
KMap<String, Object> snapshot;
|
||||||
|
try {
|
||||||
|
snapshot = BukkitPlatform.serializeTile(block.getLocation());
|
||||||
|
} catch (Throwable exception) {
|
||||||
|
jigsawTileWatches.remove(watch.key(), watch);
|
||||||
|
IrisLogging.reportError(exception);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (snapshot == null) {
|
||||||
|
jigsawTileWatches.remove(watch.key(), watch);
|
||||||
|
IrisLogging.warn("Jigsaw Studio stopped watching unreadable jigsaw marker NBT at %d,%d,%d in %s",
|
||||||
|
watch.key().worldX(),
|
||||||
|
watch.key().worldY(),
|
||||||
|
watch.key().worldZ(),
|
||||||
|
studio.world().getName());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
JigsawTilePollDecision decision = jigsawTilePollDecision(
|
||||||
|
watch.snapshot(), snapshot, release);
|
||||||
|
if (decision.changed()) {
|
||||||
|
service.dirtyTracker.markDirty(
|
||||||
|
studio.world(),
|
||||||
|
watch.key().worldX(),
|
||||||
|
watch.key().worldY(),
|
||||||
|
watch.key().worldZ());
|
||||||
|
}
|
||||||
|
if (!decision.continueWatching()) {
|
||||||
|
jigsawTileWatches.remove(watch.key(), watch);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
JigsawTileWatch next = new JigsawTileWatch(
|
||||||
|
watch.key(),
|
||||||
|
watch.studio(),
|
||||||
|
watch.playerId(),
|
||||||
|
watch.workcellId(),
|
||||||
|
snapshot,
|
||||||
|
watch.scheduleFailureLogged(),
|
||||||
|
new AtomicBoolean(false));
|
||||||
|
if (jigsawTileWatches.replace(watch.key(), watch, next)) {
|
||||||
|
scheduleJigsawTileWatch(next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean hasJigsawTileWatch(UUID requestId) {
|
||||||
|
if (requestId == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (JigsawTileWatchKey key : jigsawTileWatches.keySet()) {
|
||||||
|
if (requestId.equals(key.requestId())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearJigsawTileWatches(UUID requestId) {
|
||||||
|
if (requestId != null) {
|
||||||
|
jigsawTileWatches.keySet().removeIf(key -> requestId.equals(key.requestId()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void finalizeJigsawTileWatchesForPlayer(UUID playerId) {
|
||||||
|
if (playerId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (JigsawTileWatch watch : List.copyOf(jigsawTileWatches.values())) {
|
||||||
|
if (playerId.equals(watch.playerId())) {
|
||||||
|
finalizeJigsawTileWatch(watch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void finalizeJigsawTileWatches(UUID requestId) {
|
||||||
|
if (requestId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (JigsawTileWatch watch : List.copyOf(jigsawTileWatches.values())) {
|
||||||
|
if (requestId.equals(watch.key().requestId())) {
|
||||||
|
finalizeJigsawTileWatch(watch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void finalizeAllJigsawTileWatches() {
|
||||||
|
for (JigsawTileWatch watch : List.copyOf(jigsawTileWatches.values())) {
|
||||||
|
try {
|
||||||
|
finalizeJigsawTileWatch(watch);
|
||||||
|
} catch (Throwable exception) {
|
||||||
|
IrisLogging.reportError("Failed to finalize a Jigsaw Studio tile watch during shutdown.", exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void finalizeJigsawTileWatch(JigsawTileWatch watch) {
|
||||||
|
if (jigsawTileWatches.get(watch.key()) != watch) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
World world = watch.studio().world();
|
||||||
|
int chunkX = watch.key().worldX() >> 4;
|
||||||
|
int chunkZ = watch.key().worldZ() >> 4;
|
||||||
|
if (J.isOwnedByCurrentRegion(world, chunkX, chunkZ)) {
|
||||||
|
runJigsawTileWatch(watch, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
boolean scheduled = J.runRegion(
|
||||||
|
world,
|
||||||
|
chunkX,
|
||||||
|
chunkZ,
|
||||||
|
() -> runJigsawTileWatch(watch, true));
|
||||||
|
if (!scheduled) {
|
||||||
|
warnJigsawTileWatchScheduleFailure(watch);
|
||||||
|
scheduleJigsawTileWatchReconciliation(watch, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void warnJigsawTileWatchScheduleFailure(JigsawTileWatch watch) {
|
||||||
|
if (watch.scheduleFailureLogged().compareAndSet(false, true)) {
|
||||||
|
IrisLogging.warn("Jigsaw Studio could not schedule jigsaw marker NBT at %d,%d,%d in %s; Iris will keep retrying",
|
||||||
|
watch.key().worldX(),
|
||||||
|
watch.key().worldY(),
|
||||||
|
watch.key().worldZ(),
|
||||||
|
watch.studio().world().getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void scheduleJigsawTileWatchReconciliation(JigsawTileWatch watch, boolean release) {
|
||||||
|
if (!watch.reconciliationScheduled().compareAndSet(false, true)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
J.s(() -> {
|
||||||
|
watch.reconciliationScheduled().set(false);
|
||||||
|
if (jigsawTileWatches.get(watch.key()) != watch) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (release) {
|
||||||
|
finalizeJigsawTileWatch(watch);
|
||||||
|
} else {
|
||||||
|
scheduleJigsawTileWatch(watch);
|
||||||
|
}
|
||||||
|
}, AUTOSAVE_RETRY_TICKS);
|
||||||
|
} catch (Throwable exception) {
|
||||||
|
watch.reconciliationScheduled().set(false);
|
||||||
|
IrisLogging.reportError(exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean tileSnapshotChanged(
|
||||||
|
KMap<String, Object> previous,
|
||||||
|
KMap<String, Object> current
|
||||||
|
) {
|
||||||
|
return !Objects.equals(previous, current);
|
||||||
|
}
|
||||||
|
|
||||||
|
static JigsawTilePollDecision jigsawTilePollDecision(
|
||||||
|
KMap<String, Object> previous,
|
||||||
|
KMap<String, Object> current,
|
||||||
|
boolean release
|
||||||
|
) {
|
||||||
|
return new JigsawTilePollDecision(tileSnapshotChanged(previous, current), !release);
|
||||||
|
}
|
||||||
|
|
||||||
|
private record JigsawTileWatchKey(
|
||||||
|
UUID requestId,
|
||||||
|
UUID worldId,
|
||||||
|
int worldX,
|
||||||
|
int worldY,
|
||||||
|
int worldZ
|
||||||
|
) {
|
||||||
|
private JigsawTileWatchKey {
|
||||||
|
Objects.requireNonNull(requestId, "Jigsaw Studio tile watch request ID");
|
||||||
|
Objects.requireNonNull(worldId, "Jigsaw Studio tile watch world ID");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record JigsawTilePollDecision(boolean changed, boolean continueWatching) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private record JigsawTileWatch(
|
||||||
|
JigsawTileWatchKey key,
|
||||||
|
ActiveStudio studio,
|
||||||
|
UUID playerId,
|
||||||
|
String workcellId,
|
||||||
|
KMap<String, Object> snapshot,
|
||||||
|
AtomicBoolean scheduleFailureLogged,
|
||||||
|
AtomicBoolean reconciliationScheduled
|
||||||
|
) {
|
||||||
|
private JigsawTileWatch {
|
||||||
|
Objects.requireNonNull(key, "Jigsaw Studio tile watch key");
|
||||||
|
Objects.requireNonNull(studio, "Jigsaw Studio tile watch studio");
|
||||||
|
Objects.requireNonNull(playerId, "Jigsaw Studio tile watch player ID");
|
||||||
|
Objects.requireNonNull(workcellId, "Jigsaw Studio tile watch workcell ID");
|
||||||
|
Objects.requireNonNull(snapshot, "Jigsaw Studio tile watch snapshot");
|
||||||
|
Objects.requireNonNull(scheduleFailureLogged, "Jigsaw Studio tile watch schedule warning state");
|
||||||
|
Objects.requireNonNull(reconciliationScheduled, "Jigsaw Studio tile watch retry schedule state");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,692 @@
|
|||||||
|
package art.arcane.iris.core.service;
|
||||||
|
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioActivation;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioBay;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioLayout;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioPoolMembership;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioSession;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioToolPayload;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioVariant;
|
||||||
|
import art.arcane.iris.core.service.JigsawStudioService.ActiveStudio;
|
||||||
|
import art.arcane.iris.engine.object.IrisJigsawThemeSet;
|
||||||
|
import art.arcane.iris.engine.object.IrisStructure;
|
||||||
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
|
import art.arcane.iris.util.common.scheduling.J;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.ChatColor;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioEvaluator.loadStudioStructure;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioProtection.authorizeOwner;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioProtection.canToggleVariantRotation;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioProtection.ownerMatches;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioService.AUTOSAVE_RETRY_TICKS;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioService.message;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioVariantProperties.canResizeVariantToCapacity;
|
||||||
|
|
||||||
|
final class JigsawStudioToolbelt {
|
||||||
|
private static final long TOOL_CONFIRM_NANOS = 10_000_000_000L;
|
||||||
|
|
||||||
|
private final JigsawStudioService service;
|
||||||
|
final Map<UUID, DeferredDuplication> deferredDuplications = new ConcurrentHashMap<>();
|
||||||
|
final Map<UUID, ToolConfirmation> toolConfirmations = new ConcurrentHashMap<>();
|
||||||
|
final JigsawStudioToolCodec toolCodec = new JigsawStudioToolCodec();
|
||||||
|
volatile JigsawStudioMenuController menuController;
|
||||||
|
|
||||||
|
JigsawStudioToolbelt(JigsawStudioService service) {
|
||||||
|
this.service = Objects.requireNonNull(service, "Jigsaw Studio service");
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<JigsawStudioMenuState> menuState(Player player) {
|
||||||
|
if (player == null || !J.isOwnedByCurrentRegion(player)) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
ActiveStudio studio = service.studios.get(player.getWorld().getUID());
|
||||||
|
if (studio == null) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
if (!ownerMatches(player, studio)) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
JigsawStudioActivation.Request request = studio.generator().getRequest();
|
||||||
|
JigsawStudioSession session = studio.generator().getSession();
|
||||||
|
JigsawStudioLayout layout = session.layout();
|
||||||
|
IrisStructure structure = loadStudioStructure(studio);
|
||||||
|
List<JigsawStudioMenuState.ThemeSet> themeSets = new ArrayList<>();
|
||||||
|
if (structure != null && structure.getThemeSets() != null) {
|
||||||
|
for (IrisJigsawThemeSet themeSet : structure.getThemeSets()) {
|
||||||
|
if (themeSet != null) {
|
||||||
|
themeSets.add(new JigsawStudioMenuState.ThemeSet(
|
||||||
|
themeSet.getKey(),
|
||||||
|
themeSet.getWeight()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<JigsawStudioMenuState.Workcell> workcells = new ArrayList<>(layout.bays().size());
|
||||||
|
for (JigsawStudioBay workcell : layout.bays()) {
|
||||||
|
JigsawStudioSession.WorkcellSnapshot snapshot = session.workcellSnapshot(workcell.stableId());
|
||||||
|
List<JigsawStudioMenuState.Variant> variants = new ArrayList<>();
|
||||||
|
for (JigsawStudioVariant variant : layout.variants(workcell)) {
|
||||||
|
boolean active = variant.pieceKey().equals(snapshot.activeVariantKey());
|
||||||
|
List<JigsawStudioMenuState.Membership> memberships = new ArrayList<>(
|
||||||
|
variant.memberships().size());
|
||||||
|
for (JigsawStudioPoolMembership membership : variant.memberships()) {
|
||||||
|
memberships.add(new JigsawStudioMenuState.Membership(
|
||||||
|
membership.poolKey(),
|
||||||
|
membership.entryIndex(),
|
||||||
|
membership.weight(),
|
||||||
|
membership.chance()));
|
||||||
|
}
|
||||||
|
variants.add(new JigsawStudioMenuState.Variant(
|
||||||
|
variant.pieceKey(),
|
||||||
|
variant.resolvedDisplayName(),
|
||||||
|
variant.dimensions(),
|
||||||
|
active,
|
||||||
|
variant.owned(),
|
||||||
|
variant.rotatable(),
|
||||||
|
canToggleVariantRotation(request.compatibilityTarget(), variant),
|
||||||
|
canResizeVariantToCapacity(workcell, variant, active),
|
||||||
|
variant.themes(),
|
||||||
|
variant.rules(),
|
||||||
|
memberships));
|
||||||
|
}
|
||||||
|
workcells.add(new JigsawStudioMenuState.Workcell(
|
||||||
|
workcell.stableId(),
|
||||||
|
workcell.canonicalDisplayName(),
|
||||||
|
workcell.displayName(),
|
||||||
|
workcell.capacity(),
|
||||||
|
workcell.enabled(),
|
||||||
|
snapshot.activeVariantKey(),
|
||||||
|
snapshot.dirty(),
|
||||||
|
snapshot.saveInProgress(),
|
||||||
|
snapshot.switchInProgress(),
|
||||||
|
snapshot.connectorsVisible(),
|
||||||
|
variants));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Optional.of(new JigsawStudioMenuState(
|
||||||
|
studio.worldId(),
|
||||||
|
request.requestId(),
|
||||||
|
request.structureKey(),
|
||||||
|
layout.mode(),
|
||||||
|
request.compatibilityTarget(),
|
||||||
|
structure != null && structure.isRequireCaps(),
|
||||||
|
themeSets,
|
||||||
|
session.selectedBayId().orElse(""),
|
||||||
|
Optional.ofNullable(service.evaluator.evaluations.get(request.requestId()))
|
||||||
|
.map(JigsawStudioMenuState.Evaluation::from)
|
||||||
|
.orElseGet(JigsawStudioMenuState.Evaluation::pending),
|
||||||
|
workcells));
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean selectWorkcell(Player player, String workcellId) {
|
||||||
|
if (player == null || workcellId == null || workcellId.isBlank()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!J.isOwnedByCurrentRegion(player)) {
|
||||||
|
return J.runEntity(player, () -> selectWorkcell(player, workcellId));
|
||||||
|
}
|
||||||
|
ActiveStudio studio = service.studios.get(player.getWorld().getUID());
|
||||||
|
if (studio == null) {
|
||||||
|
message(player, "Iris Jigsaw Studio is not active in this world.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!authorizeOwner(player, studio)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioSession session = studio.generator().getSession();
|
||||||
|
if (!session.selectBay(workcellId)) {
|
||||||
|
message(player, "Unknown Jigsaw Studio workcell '" + workcellId + "'.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
service.visualization.ensureVisualizationLoop(player);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean openControlMenu(Player player) {
|
||||||
|
if (player != null && J.isOwnedByCurrentRegion(player)) {
|
||||||
|
service.tileWatcher.finalizeJigsawTileWatchesForPlayer(player.getUniqueId());
|
||||||
|
ActiveStudio studio = service.studios.get(player.getWorld().getUID());
|
||||||
|
if (studio != null
|
||||||
|
&& service.saveLifecycle.reopenRequiredRequests.contains(
|
||||||
|
studio.generator().getRequest().requestId())) {
|
||||||
|
message(player, "Close and reopen Jigsaw Studio to apply the resized workcell layout.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
JigsawStudioMenuController activeMenuController = menuController;
|
||||||
|
return activeMenuController != null && activeMenuController.open(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
void closeMenu(Player player) {
|
||||||
|
JigsawStudioMenuController activeMenuController = menuController;
|
||||||
|
if (activeMenuController != null) {
|
||||||
|
activeMenuController.close(player);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean useTool(
|
||||||
|
Player player,
|
||||||
|
JigsawStudioToolPayload payload,
|
||||||
|
ItemStack tool,
|
||||||
|
boolean resetLabel
|
||||||
|
) {
|
||||||
|
if (!J.isOwnedByCurrentRegion(player)) {
|
||||||
|
return J.runEntity(player, () -> useTool(player, payload, tool, resetLabel));
|
||||||
|
}
|
||||||
|
ActiveStudio studio = service.studios.get(player.getWorld().getUID());
|
||||||
|
if (studio == null
|
||||||
|
|| !studio.generator().getRequest().requestId().equals(payload.requestId())
|
||||||
|
|| !authorizeOwner(player, studio)) {
|
||||||
|
message(player, "This Jigsaw Studio tool is stale or belongs to another session.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (payload.action().destructive() && !confirmTool(player, payload)) {
|
||||||
|
message(player, "Right-click the same tool again within 10 seconds to confirm "
|
||||||
|
+ payload.action().displayName() + ".");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return switch (payload.action()) {
|
||||||
|
case OPEN_MENU -> openControlMenu(player);
|
||||||
|
case SELECT_WORKCELL -> selectWorkcell(player, payload.workcellId());
|
||||||
|
case LOAD_VARIANT -> service.variantEditor.switchVariant(
|
||||||
|
player,
|
||||||
|
payload.workcellId(),
|
||||||
|
payload.pieceKey(),
|
||||||
|
false);
|
||||||
|
case CREATE_VARIANT -> service.variantEditor.createVariant(player, payload.workcellId(), false);
|
||||||
|
case DUPLICATE_VARIANT -> service.variantEditor.createVariant(player, payload.workcellId(), true);
|
||||||
|
case DUPLICATE_FAMILY -> duplicateFamilyFromTool(player);
|
||||||
|
case PREVIEW_GRAPH -> service.goToPreview(player);
|
||||||
|
case FLUSH_AUTOSAVE -> service.autosaveScheduler.flushAutosave(player, payload.workcellId());
|
||||||
|
case TOGGLE_ROTATION -> service.variantProperties.toggleVariantRotatable(player, payload.workcellId());
|
||||||
|
case EXPAND_TO_CELL -> service.variantProperties.expandVariantToCell(player, payload.workcellId());
|
||||||
|
case RESIZE_VARIANT -> openVariantSizeSettingsFromTool(player, payload);
|
||||||
|
case RENAME_VARIANT -> renameVariantFromTool(player, payload, tool, resetLabel);
|
||||||
|
case ADJUST_VARIANT_WEIGHT -> service.variantProperties.adjustVariantWeight(
|
||||||
|
player,
|
||||||
|
payload.workcellId(),
|
||||||
|
payload.pieceKey(),
|
||||||
|
payload.poolKey(),
|
||||||
|
payload.entryIndex(),
|
||||||
|
payload.amount());
|
||||||
|
case ADJUST_VARIANT_CHANCE -> service.variantProperties.adjustVariantChance(
|
||||||
|
player,
|
||||||
|
payload.workcellId(),
|
||||||
|
payload.pieceKey(),
|
||||||
|
payload.poolKey(),
|
||||||
|
payload.entryIndex(),
|
||||||
|
payload.amount());
|
||||||
|
case UNLINK_MEMBERSHIP -> service.variantEditor.unlinkVariantMembership(
|
||||||
|
player,
|
||||||
|
payload.workcellId(),
|
||||||
|
payload.pieceKey(),
|
||||||
|
payload.poolKey(),
|
||||||
|
payload.entryIndex());
|
||||||
|
case TOGGLE_WORKCELL -> toggleWorkcellFromTool(player, payload.workcellId());
|
||||||
|
case RESIZE_WORKCELL -> openWorkcellSettingsFromTool(player, payload.workcellId());
|
||||||
|
case RENAME_WORKCELL -> renameWorkcellFromTool(player, payload, tool, resetLabel);
|
||||||
|
case SET_THEME -> useThemeTool(player, payload);
|
||||||
|
case SET_PIECE_RULES -> openVariantSettingsFromTool(player, payload);
|
||||||
|
case DELETE_VARIANT -> service.variantEditor.deleteVariant(
|
||||||
|
player, payload.workcellId(), payload.pieceKey());
|
||||||
|
case DELETE_PROJECT -> service.variantEditor.deleteProject(player);
|
||||||
|
case TOGGLE_REQUIRE_CAPS -> toggleRequireCapsFromTool(player);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean toggleWorkcellFromTool(Player player, String workcellId) {
|
||||||
|
Optional<JigsawStudioMenuState> state = menuState(player);
|
||||||
|
JigsawStudioMenuState.Workcell workcell = state
|
||||||
|
.map(menu -> menu.workcell(workcellId))
|
||||||
|
.orElse(null);
|
||||||
|
if (workcell == null) {
|
||||||
|
message(player, "This bound workcell no longer exists.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return service.workcellEditor.setWorkcellEnabled(player, workcellId, !workcell.enabled());
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean openWorkcellSettingsFromTool(Player player, String workcellId) {
|
||||||
|
JigsawStudioMenuController activeMenuController = menuController;
|
||||||
|
return activeMenuController != null
|
||||||
|
&& activeMenuController.openWorkcellSettings(player, workcellId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean openVariantSettingsFromTool(Player player, JigsawStudioToolPayload payload) {
|
||||||
|
JigsawStudioMenuController activeMenuController = menuController;
|
||||||
|
return activeMenuController != null
|
||||||
|
&& activeMenuController.openVariantSettings(
|
||||||
|
player,
|
||||||
|
payload.workcellId(),
|
||||||
|
payload.pieceKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean openVariantSizeSettingsFromTool(Player player, JigsawStudioToolPayload payload) {
|
||||||
|
JigsawStudioMenuController activeMenuController = menuController;
|
||||||
|
return activeMenuController != null
|
||||||
|
&& activeMenuController.openVariantSizeSettings(
|
||||||
|
player,
|
||||||
|
payload.workcellId(),
|
||||||
|
payload.pieceKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean renameVariantFromTool(
|
||||||
|
Player player,
|
||||||
|
JigsawStudioToolPayload payload,
|
||||||
|
ItemStack tool,
|
||||||
|
boolean resetLabel
|
||||||
|
) {
|
||||||
|
Optional<String> displayName = toolDisplayName(player, payload, tool, resetLabel);
|
||||||
|
return displayName.isPresent() && service.variantProperties.updateVariantDisplayName(
|
||||||
|
player,
|
||||||
|
payload.workcellId(),
|
||||||
|
payload.pieceKey(),
|
||||||
|
displayName.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean renameWorkcellFromTool(
|
||||||
|
Player player,
|
||||||
|
JigsawStudioToolPayload payload,
|
||||||
|
ItemStack tool,
|
||||||
|
boolean resetLabel
|
||||||
|
) {
|
||||||
|
Optional<String> displayName = toolDisplayName(player, payload, tool, resetLabel);
|
||||||
|
return displayName.isPresent() && service.workcellEditor.updateWorkcellDisplayName(
|
||||||
|
player,
|
||||||
|
payload.workcellId(),
|
||||||
|
displayName.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Optional<String> toolDisplayName(
|
||||||
|
Player player,
|
||||||
|
JigsawStudioToolPayload payload,
|
||||||
|
ItemStack tool,
|
||||||
|
boolean resetLabel
|
||||||
|
) {
|
||||||
|
if (resetLabel) {
|
||||||
|
return Optional.of("");
|
||||||
|
}
|
||||||
|
String displayName = tool == null || !tool.hasItemMeta() || tool.getItemMeta() == null
|
||||||
|
? ""
|
||||||
|
: tool.getItemMeta().getDisplayName();
|
||||||
|
String normalized = ChatColor.stripColor(displayName);
|
||||||
|
normalized = normalized == null ? "" : normalized.trim();
|
||||||
|
String defaultName = "Jigsaw Studio: " + payload.action().displayName();
|
||||||
|
if (normalized.isEmpty() || normalized.equals(defaultName)) {
|
||||||
|
message(player, "Rename this bound stick in an anvil to the desired label, then right-click it. "
|
||||||
|
+ "Sneak-right-click resets the label.");
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
return Optional.of(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean useThemeTool(Player player, JigsawStudioToolPayload payload) {
|
||||||
|
if (!payload.pieceKey().isBlank()) {
|
||||||
|
return openVariantSettingsFromTool(player, payload);
|
||||||
|
}
|
||||||
|
Optional<JigsawStudioMenuState> state = menuState(player);
|
||||||
|
if (state.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return service.variantEditor.duplicateActiveFamily(
|
||||||
|
player,
|
||||||
|
JigsawStudioMenuFormat.nextThemeSetKey(state.get().themeSets()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean duplicateFamilyFromTool(Player player) {
|
||||||
|
Optional<JigsawStudioMenuState> state = menuState(player);
|
||||||
|
return state.isPresent() && service.variantEditor.duplicateActiveFamily(
|
||||||
|
player,
|
||||||
|
JigsawStudioMenuFormat.nextThemeSetKey(state.get().themeSets()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean toggleRequireCapsFromTool(Player player) {
|
||||||
|
Optional<JigsawStudioMenuState> state = menuState(player);
|
||||||
|
return state.isPresent() && service.workcellEditor.setRequireCaps(player, !state.get().requireCaps());
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean confirmTool(Player player, JigsawStudioToolPayload payload) {
|
||||||
|
long now = System.nanoTime();
|
||||||
|
ToolConfirmation previous = toolConfirmations.get(player.getUniqueId());
|
||||||
|
if (previous != null
|
||||||
|
&& previous.expiresAtNanos() >= now
|
||||||
|
&& previous.payload().equals(payload)) {
|
||||||
|
toolConfirmations.remove(player.getUniqueId(), previous);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
toolConfirmations.put(
|
||||||
|
player.getUniqueId(),
|
||||||
|
new ToolConfirmation(payload, now + TOOL_CONFIRM_NANOS));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
JigsawStudioVariant activeOwnedVariant(
|
||||||
|
Player player,
|
||||||
|
ActiveStudio studio,
|
||||||
|
String workcellId
|
||||||
|
) {
|
||||||
|
if (studio == null) {
|
||||||
|
message(player, "Iris Jigsaw Studio is not active in this world.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
JigsawStudioSession session = studio.generator().getSession();
|
||||||
|
if (session.layout().get(workcellId) == null) {
|
||||||
|
message(player, "Unknown Jigsaw Studio workcell '" + workcellId + "'.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
JigsawStudioVariant variant = session.activeVariant(workcellId).orElse(null);
|
||||||
|
if (variant == null) {
|
||||||
|
message(player, "This workcell has no active variant.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!variant.owned()) {
|
||||||
|
message(player, "Variant '" + variant.pieceKey()
|
||||||
|
+ "' is read-only. Adopt or clone its graph before editing it.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return variant;
|
||||||
|
}
|
||||||
|
|
||||||
|
JigsawStudioVariant activeOwnedVariant(
|
||||||
|
Player player,
|
||||||
|
ActiveStudio studio,
|
||||||
|
String workcellId,
|
||||||
|
String expectedPieceKey
|
||||||
|
) {
|
||||||
|
JigsawStudioVariant variant = activeOwnedVariant(player, studio, workcellId);
|
||||||
|
if (variant == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!variant.pieceKey().equals(expectedPieceKey)) {
|
||||||
|
message(player, "The bound variant changed. Open the control menu or request a fresh tool.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return variant;
|
||||||
|
}
|
||||||
|
|
||||||
|
static JigsawStudioPoolMembership findMembership(
|
||||||
|
JigsawStudioVariant variant,
|
||||||
|
String poolKey,
|
||||||
|
int entryIndex
|
||||||
|
) {
|
||||||
|
for (JigsawStudioPoolMembership membership : variant.memberships()) {
|
||||||
|
if (membership.poolKey().equals(poolKey) && membership.entryIndex() == entryIndex) {
|
||||||
|
return membership;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean deferDuplicationUntilAutosaved(
|
||||||
|
Player player,
|
||||||
|
ActiveStudio studio,
|
||||||
|
DeferredDuplication requested
|
||||||
|
) {
|
||||||
|
JigsawStudioSession session = studio.generator().getSession();
|
||||||
|
UUID requestId = studio.generator().getRequest().requestId();
|
||||||
|
boolean autosavePending;
|
||||||
|
synchronized (service.saveLifecycleLock) {
|
||||||
|
autosavePending = session.isDirty() || service.saveLifecycle.savesInProgress.contains(requestId);
|
||||||
|
}
|
||||||
|
if (!autosavePending) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
DeferredDuplication existing = deferredDuplications.putIfAbsent(requestId, requested);
|
||||||
|
if (existing == null) {
|
||||||
|
message(player, requested.kind() == DeferredDuplicationKind.SINGLE
|
||||||
|
? "Autosave is finishing first; this cell's variant will duplicate automatically."
|
||||||
|
: "Autosave is finishing every edited cell first; the coherent family will duplicate automatically.");
|
||||||
|
service.autosaveScheduler.expediteAutosaves(requestId);
|
||||||
|
scheduleDeferredDuplication(requested);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
message(player, existing.sameIntent(requested)
|
||||||
|
? "That duplicate action is already queued behind autosave."
|
||||||
|
: "Another duplicate action is already queued behind autosave for this Studio.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void scheduleDeferredDuplication(DeferredDuplication deferred) {
|
||||||
|
if (deferredDuplications.get(deferred.requestId()) != deferred
|
||||||
|
|| !deferred.scheduled().compareAndSet(false, true)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
J.s(() -> {
|
||||||
|
deferred.scheduled().set(false);
|
||||||
|
if (deferredDuplications.get(deferred.requestId()) != deferred) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Player player = Bukkit.getPlayer(deferred.playerId());
|
||||||
|
if (player == null) {
|
||||||
|
deferredDuplications.remove(deferred.requestId(), deferred);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
boolean scheduled = J.runEntity(
|
||||||
|
player,
|
||||||
|
() -> resumeDeferredDuplication(player, deferred),
|
||||||
|
0,
|
||||||
|
() -> deferredDuplications.remove(deferred.requestId(), deferred));
|
||||||
|
if (!scheduled) {
|
||||||
|
deferredDuplications.remove(deferred.requestId(), deferred);
|
||||||
|
}
|
||||||
|
}, AUTOSAVE_RETRY_TICKS);
|
||||||
|
} catch (Throwable exception) {
|
||||||
|
deferred.scheduled().set(false);
|
||||||
|
deferredDuplications.remove(deferred.requestId(), deferred);
|
||||||
|
IrisLogging.reportError(exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void resumeDeferredDuplication(Player player, DeferredDuplication deferred) {
|
||||||
|
if (deferredDuplications.get(deferred.requestId()) != deferred) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ActiveStudio studio = service.studios.get(deferred.worldId());
|
||||||
|
JigsawStudioSession session = studio == null ? null : studio.generator().getSession();
|
||||||
|
boolean currentRequest = studio != null
|
||||||
|
&& studio.generator().getRequest().requestId().equals(deferred.requestId())
|
||||||
|
&& session.sessionId().equals(deferred.sessionId())
|
||||||
|
&& player.getWorld().getUID().equals(deferred.worldId());
|
||||||
|
boolean sourcesCurrent = currentRequest && sourceBindingsCurrent(session, deferred.sourcePieces());
|
||||||
|
boolean autosavePending;
|
||||||
|
boolean operationPending;
|
||||||
|
boolean terminal;
|
||||||
|
synchronized (service.saveLifecycleLock) {
|
||||||
|
autosavePending = currentRequest
|
||||||
|
&& (session.isDirty() || service.saveLifecycle.savesInProgress.contains(deferred.requestId()));
|
||||||
|
operationPending = currentRequest
|
||||||
|
&& (service.graphMutations.graphMutationsInProgress.contains(deferred.requestId())
|
||||||
|
|| service.materializer.materializationsInProgress.contains(deferred.requestId())
|
||||||
|
|| service.saveLifecycle.exportsInProgress.contains(deferred.requestId())
|
||||||
|
|| session.operationInProgress()
|
||||||
|
|| service.tileWatcher.hasJigsawTileWatch(deferred.requestId()));
|
||||||
|
terminal = !currentRequest
|
||||||
|
|| service.saveLifecycle.closingRequests.contains(deferred.requestId())
|
||||||
|
|| service.saveLifecycle.reopenRequiredRequests.contains(deferred.requestId());
|
||||||
|
}
|
||||||
|
DeferredDuplicationReadiness readiness = deferredDuplicationReadiness(
|
||||||
|
currentRequest,
|
||||||
|
sourcesCurrent,
|
||||||
|
autosavePending,
|
||||||
|
operationPending,
|
||||||
|
terminal);
|
||||||
|
if (readiness == DeferredDuplicationReadiness.STALE) {
|
||||||
|
deferredDuplications.remove(deferred.requestId(), deferred);
|
||||||
|
message(player, "The queued duplicate was cancelled because its Studio or source variant changed.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (readiness == DeferredDuplicationReadiness.WAITING_FOR_AUTOSAVE) {
|
||||||
|
service.autosaveScheduler.expediteAutosaves(deferred.requestId());
|
||||||
|
scheduleDeferredDuplication(deferred);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (readiness == DeferredDuplicationReadiness.WAITING_FOR_OPERATION) {
|
||||||
|
scheduleDeferredDuplication(deferred);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
boolean started = deferred.kind() == DeferredDuplicationKind.SINGLE
|
||||||
|
? service.variantEditor.createVariant(player, deferred.workcellId(), true)
|
||||||
|
: service.variantEditor.duplicateActiveFamily(player, deferred.themeKey());
|
||||||
|
if (started) {
|
||||||
|
deferredDuplications.remove(deferred.requestId(), deferred);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (service.isCurrentRequest(studio, deferred.requestId())
|
||||||
|
&& sourceBindingsCurrent(session, deferred.sourcePieces())) {
|
||||||
|
scheduleDeferredDuplication(deferred);
|
||||||
|
} else {
|
||||||
|
deferredDuplications.remove(deferred.requestId(), deferred);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static DeferredDuplicationReadiness deferredDuplicationReadiness(
|
||||||
|
boolean currentRequest,
|
||||||
|
boolean sourcesCurrent,
|
||||||
|
boolean autosavePending,
|
||||||
|
boolean operationPending,
|
||||||
|
boolean terminal
|
||||||
|
) {
|
||||||
|
if (!currentRequest || !sourcesCurrent || terminal) {
|
||||||
|
return DeferredDuplicationReadiness.STALE;
|
||||||
|
}
|
||||||
|
if (autosavePending) {
|
||||||
|
return DeferredDuplicationReadiness.WAITING_FOR_AUTOSAVE;
|
||||||
|
}
|
||||||
|
if (operationPending) {
|
||||||
|
return DeferredDuplicationReadiness.WAITING_FOR_OPERATION;
|
||||||
|
}
|
||||||
|
return DeferredDuplicationReadiness.READY;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean sourceBindingsCurrent(
|
||||||
|
JigsawStudioSession session,
|
||||||
|
Map<String, String> expectedSources
|
||||||
|
) {
|
||||||
|
if (session == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (Map.Entry<String, String> source : expectedSources.entrySet()) {
|
||||||
|
if (session.activeVariant(source.getKey())
|
||||||
|
.map(JigsawStudioVariant::pieceKey)
|
||||||
|
.filter(source.getValue()::equals)
|
||||||
|
.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum DeferredDuplicationReadiness {
|
||||||
|
READY,
|
||||||
|
WAITING_FOR_AUTOSAVE,
|
||||||
|
WAITING_FOR_OPERATION,
|
||||||
|
STALE
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum DeferredDuplicationKind {
|
||||||
|
SINGLE,
|
||||||
|
FAMILY
|
||||||
|
}
|
||||||
|
|
||||||
|
record DeferredDuplication(
|
||||||
|
UUID requestId,
|
||||||
|
UUID sessionId,
|
||||||
|
UUID playerId,
|
||||||
|
UUID worldId,
|
||||||
|
DeferredDuplicationKind kind,
|
||||||
|
String workcellId,
|
||||||
|
Map<String, String> sourcePieces,
|
||||||
|
String themeKey,
|
||||||
|
AtomicBoolean scheduled
|
||||||
|
) {
|
||||||
|
DeferredDuplication {
|
||||||
|
Objects.requireNonNull(requestId, "Jigsaw Studio deferred duplicate request ID");
|
||||||
|
Objects.requireNonNull(sessionId, "Jigsaw Studio deferred duplicate session ID");
|
||||||
|
Objects.requireNonNull(playerId, "Jigsaw Studio deferred duplicate player ID");
|
||||||
|
Objects.requireNonNull(worldId, "Jigsaw Studio deferred duplicate world ID");
|
||||||
|
Objects.requireNonNull(kind, "Jigsaw Studio deferred duplicate kind");
|
||||||
|
workcellId = workcellId == null ? "" : workcellId;
|
||||||
|
sourcePieces = Map.copyOf(Objects.requireNonNull(
|
||||||
|
sourcePieces,
|
||||||
|
"Jigsaw Studio deferred duplicate source pieces"));
|
||||||
|
themeKey = themeKey == null ? "" : themeKey;
|
||||||
|
Objects.requireNonNull(scheduled, "Jigsaw Studio deferred duplicate schedule state");
|
||||||
|
if (sourcePieces.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("A deferred duplicate requires at least one source variant");
|
||||||
|
}
|
||||||
|
if (kind == DeferredDuplicationKind.SINGLE && workcellId.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("A deferred single duplicate requires a workcell");
|
||||||
|
}
|
||||||
|
if (kind == DeferredDuplicationKind.FAMILY && themeKey.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("A deferred family duplicate requires a theme key");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static DeferredDuplication single(
|
||||||
|
UUID requestId,
|
||||||
|
UUID sessionId,
|
||||||
|
Player player,
|
||||||
|
UUID worldId,
|
||||||
|
String workcellId,
|
||||||
|
String sourcePiece
|
||||||
|
) {
|
||||||
|
return new DeferredDuplication(
|
||||||
|
requestId,
|
||||||
|
sessionId,
|
||||||
|
player.getUniqueId(),
|
||||||
|
worldId,
|
||||||
|
DeferredDuplicationKind.SINGLE,
|
||||||
|
workcellId,
|
||||||
|
Map.of(workcellId, sourcePiece),
|
||||||
|
"",
|
||||||
|
new AtomicBoolean());
|
||||||
|
}
|
||||||
|
|
||||||
|
static DeferredDuplication family(
|
||||||
|
UUID requestId,
|
||||||
|
UUID sessionId,
|
||||||
|
Player player,
|
||||||
|
UUID worldId,
|
||||||
|
Map<String, String> sourcePieces,
|
||||||
|
String themeKey
|
||||||
|
) {
|
||||||
|
return new DeferredDuplication(
|
||||||
|
requestId,
|
||||||
|
sessionId,
|
||||||
|
player.getUniqueId(),
|
||||||
|
worldId,
|
||||||
|
DeferredDuplicationKind.FAMILY,
|
||||||
|
"",
|
||||||
|
sourcePieces,
|
||||||
|
themeKey,
|
||||||
|
new AtomicBoolean());
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean sameIntent(DeferredDuplication other) {
|
||||||
|
return other != null
|
||||||
|
&& playerId.equals(other.playerId)
|
||||||
|
&& kind == other.kind
|
||||||
|
&& workcellId.equals(other.workcellId)
|
||||||
|
&& sourcePieces.equals(other.sourcePieces)
|
||||||
|
&& themeKey.equals(other.themeKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record ToolConfirmation(JigsawStudioToolPayload payload, long expiresAtNanos) {
|
||||||
|
ToolConfirmation {
|
||||||
|
Objects.requireNonNull(payload, "Jigsaw Studio tool confirmation payload");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,590 @@
|
|||||||
|
package art.arcane.iris.core.service;
|
||||||
|
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioActivation;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioBay;
|
||||||
|
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.JigsawStudioPoolMembership;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioProjectDeletionService;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioSession;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioVariant;
|
||||||
|
import art.arcane.iris.core.service.JigsawStudioMaterializer.MaterializationWork;
|
||||||
|
import art.arcane.iris.core.service.JigsawStudioMaterializer.VariantReloadRequest;
|
||||||
|
import art.arcane.iris.core.service.JigsawStudioService.ActiveStudio;
|
||||||
|
import art.arcane.iris.core.service.JigsawStudioService.CloseStart;
|
||||||
|
import art.arcane.iris.core.service.JigsawStudioService.CommandGraphMutationResult;
|
||||||
|
import art.arcane.iris.core.service.JigsawStudioToolbelt.DeferredDuplication;
|
||||||
|
import art.arcane.iris.engine.platform.studio.generators.JigsawStudioGenerator;
|
||||||
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
|
import art.arcane.iris.spi.IrisServices;
|
||||||
|
import art.arcane.iris.util.common.scheduling.J;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioChunkWriter.validateMaterialization;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioGraphMutations.loadMappedLayout;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioProtection.authorizeOwner;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioProtection.canCreateVariants;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioProtection.variantCreationSourceFailure;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioService.clearAutosaveHistory;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioService.failureMessage;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioService.message;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioToolbelt.findMembership;
|
||||||
|
|
||||||
|
final class JigsawStudioVariantEditor {
|
||||||
|
private final JigsawStudioService service;
|
||||||
|
|
||||||
|
JigsawStudioVariantEditor(JigsawStudioService service) {
|
||||||
|
this.service = Objects.requireNonNull(service, "Jigsaw Studio service");
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean switchVariant(
|
||||||
|
Player player,
|
||||||
|
String workcellId,
|
||||||
|
String targetPieceKey,
|
||||||
|
boolean discardDirty
|
||||||
|
) {
|
||||||
|
if (player == null || workcellId == null || targetPieceKey == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!J.isOwnedByCurrentRegion(player)) {
|
||||||
|
return J.runEntity(player, () -> switchVariant(
|
||||||
|
player, workcellId, targetPieceKey, discardDirty));
|
||||||
|
}
|
||||||
|
ActiveStudio studio = service.studios.get(player.getWorld().getUID());
|
||||||
|
if (studio == null) {
|
||||||
|
message(player, "Iris Jigsaw Studio is not active in this world.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!authorizeOwner(player, studio)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (service.graphMutations.graphMutationInProgress(studio.generator().getRequest().requestId())) {
|
||||||
|
message(player, "Wait for the current Jigsaw Studio graph update to finish.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
UUID requestId = studio.generator().getRequest().requestId();
|
||||||
|
service.tileWatcher.finalizeJigsawTileWatches(requestId);
|
||||||
|
if (service.tileWatcher.hasJigsawTileWatch(requestId)) {
|
||||||
|
message(player, "Wait for the open vanilla jigsaw-block editor to finish its final snapshot.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioSession session = studio.generator().getSession();
|
||||||
|
JigsawStudioBay workcell = session.layout().get(workcellId);
|
||||||
|
if (workcell == null) {
|
||||||
|
message(player, "Unknown Jigsaw Studio workcell '" + workcellId + "'.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioSession.SwitchStart start = session.beginVariantSwitch(
|
||||||
|
workcell.stableId(), targetPieceKey, discardDirty);
|
||||||
|
if (start.status() != JigsawStudioSession.SwitchStatus.STARTED) {
|
||||||
|
message(player, switch (start.status()) {
|
||||||
|
case DIRTY -> "Wait for the current variant to finish autosaving before switching.";
|
||||||
|
case SAVE_IN_PROGRESS -> "Wait for the current variant save to finish.";
|
||||||
|
case SWITCH_IN_PROGRESS -> "This workcell is already loading another variant.";
|
||||||
|
case ALREADY_ACTIVE -> "Variant '" + targetPieceKey + "' is already active.";
|
||||||
|
case WRONG_WORKCELL -> "Variant '" + targetPieceKey + "' belongs to another workcell.";
|
||||||
|
case UNKNOWN_VARIANT -> "No variant '" + targetPieceKey + "' exists in this Studio catalog.";
|
||||||
|
case UNKNOWN_WORKCELL -> "The selected workcell no longer exists.";
|
||||||
|
case STARTED -> "The variant switch could not start.";
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return materializeVariant(player, studio, workcell, start, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
void reloadActiveVariant(
|
||||||
|
Player player,
|
||||||
|
ActiveStudio studio,
|
||||||
|
VariantReloadRequest reload
|
||||||
|
) {
|
||||||
|
if (!service.isCurrentRequest(studio, studio.generator().getRequest().requestId())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
JigsawStudioSession session = studio.generator().getSession();
|
||||||
|
JigsawStudioBay workcell = session.layout().get(reload.workcellId());
|
||||||
|
if (workcell == null) {
|
||||||
|
message(player, "The resized variant was saved, but its workcell is no longer active. Reopen Studio.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
JigsawStudioSession.SwitchStart start = session.beginVariantReload(workcell.stableId());
|
||||||
|
if (start.status() != JigsawStudioSession.SwitchStatus.STARTED) {
|
||||||
|
service.saveLifecycle.reopenRequiredRequests.add(studio.generator().getRequest().requestId());
|
||||||
|
message(player, "The resized variant was saved, but Studio could not reload it: "
|
||||||
|
+ start.status() + ". Close and reopen this project before editing that workcell.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
materializeVariant(player, studio, workcell, start, reload.previous());
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean materializeVariant(
|
||||||
|
Player player,
|
||||||
|
ActiveStudio studio,
|
||||||
|
JigsawStudioBay workcell,
|
||||||
|
JigsawStudioSession.SwitchStart start,
|
||||||
|
JigsawStudioGenerator.RenderedBay previousOverride
|
||||||
|
) {
|
||||||
|
JigsawStudioSession session = studio.generator().getSession();
|
||||||
|
JigsawStudioSession.VariantSwitchToken token = start.token().orElseThrow();
|
||||||
|
String targetPieceKey = token.targetVariant().pieceKey();
|
||||||
|
JigsawStudioGenerator.RenderedBay target = studio.generator().renderVariant(
|
||||||
|
workcell,
|
||||||
|
token.targetVariant());
|
||||||
|
if (!target.valid()) {
|
||||||
|
session.abortVariantSwitch(token);
|
||||||
|
service.playerContext.refreshWorkcellContext(studio.worldId(), workcell.stableId());
|
||||||
|
message(player, "Variant '" + targetPieceKey + "' cannot load: " + target.failure());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioGenerator.RenderedBay previous = previousOverride == null
|
||||||
|
? token.previousVariant()
|
||||||
|
.map(variant -> studio.generator().renderVariant(workcell, variant))
|
||||||
|
.orElseGet(() -> JigsawStudioGenerator.RenderedBay.empty(workcell.bounds().dimensions()))
|
||||||
|
: previousOverride;
|
||||||
|
if (!previous.valid()) {
|
||||||
|
session.abortVariantSwitch(token);
|
||||||
|
service.playerContext.refreshWorkcellContext(studio.worldId(), workcell.stableId());
|
||||||
|
message(player, "The current variant cannot be retained for rollback: " + previous.failure());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioVariant previousVariant = token.previousVariant().orElse(null);
|
||||||
|
if (previousVariant != null) {
|
||||||
|
String rollbackFailure = validateMaterialization(previous);
|
||||||
|
if (!rollbackFailure.isEmpty()) {
|
||||||
|
session.abortVariantSwitch(token);
|
||||||
|
service.playerContext.refreshWorkcellContext(studio.worldId(), workcell.stableId());
|
||||||
|
message(player, "The current variant cannot be retained for rollback: " + rollbackFailure);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
session.selectBay(workcell.stableId());
|
||||||
|
service.playerContext.refreshWorkcellContext(studio.worldId(), workcell.stableId());
|
||||||
|
message(player, "Loading variant '" + targetPieceKey + "' into " + workcell.stableId() + "...");
|
||||||
|
return service.materializer.scheduleMaterialization(new MaterializationWork(
|
||||||
|
studio,
|
||||||
|
player.getWorld(),
|
||||||
|
player,
|
||||||
|
workcell,
|
||||||
|
token,
|
||||||
|
previous,
|
||||||
|
target,
|
||||||
|
session.workcellSnapshot(workcell.stableId()).connectorsVisible(),
|
||||||
|
session.workcellSnapshot(workcell.stableId()).connectorsVisible(),
|
||||||
|
false));
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean createVariant(
|
||||||
|
Player player,
|
||||||
|
String workcellId,
|
||||||
|
boolean duplicateActive
|
||||||
|
) {
|
||||||
|
if (player == null || workcellId == null || workcellId.isBlank()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!J.isOwnedByCurrentRegion(player)) {
|
||||||
|
return J.runEntity(player, () -> createVariant(player, workcellId, duplicateActive));
|
||||||
|
}
|
||||||
|
ActiveStudio studio = service.studios.get(player.getWorld().getUID());
|
||||||
|
if (studio == null) {
|
||||||
|
message(player, "Iris Jigsaw Studio is not active in this world.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!authorizeOwner(player, studio)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioSession session = studio.generator().getSession();
|
||||||
|
JigsawStudioBay workcell = session.layout().get(workcellId);
|
||||||
|
if (workcell == null) {
|
||||||
|
message(player, "Unknown Jigsaw Studio workcell '" + workcellId + "'.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!canCreateVariants(session.layout())) {
|
||||||
|
message(player, "This Jigsaw Studio graph is read-only. "
|
||||||
|
+ "Adopt or clone it before creating variants.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioVariant activeVariant = session.activeVariant(workcellId).orElse(null);
|
||||||
|
String sourceFailure = variantCreationSourceFailure(activeVariant, duplicateActive);
|
||||||
|
if (!sourceFailure.isEmpty()) {
|
||||||
|
message(player, sourceFailure);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioActivation.Request request = studio.generator().getRequest();
|
||||||
|
if (duplicateActive && service.toolbelt.deferDuplicationUntilAutosaved(
|
||||||
|
player,
|
||||||
|
studio,
|
||||||
|
DeferredDuplication.single(
|
||||||
|
request.requestId(),
|
||||||
|
session.sessionId(),
|
||||||
|
player,
|
||||||
|
studio.worldId(),
|
||||||
|
workcell.stableId(),
|
||||||
|
activeVariant.pieceKey()))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
String reservationFailure = service.graphMutations.beginGraphMutation(studio, session);
|
||||||
|
if (!reservationFailure.isEmpty()) {
|
||||||
|
message(player, reservationFailure);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String archetypeKey = workcell.archetype()
|
||||||
|
.map(archetype -> archetype.name().toLowerCase(Locale.ROOT))
|
||||||
|
.orElse("spatial");
|
||||||
|
String sourcePieceKey = activeVariant.pieceKey();
|
||||||
|
message(player, duplicateActive ? "Duplicating the active variant..." : "Creating a new variant...");
|
||||||
|
return service.graphMutations.scheduleGraphMutation(
|
||||||
|
player,
|
||||||
|
studio,
|
||||||
|
() -> {
|
||||||
|
Path packRoot = request.source().getDataFolder().toPath();
|
||||||
|
String pieceKey = JigsawStudioGraphEditor.nextVariantKey(
|
||||||
|
packRoot, request.structureKey(), archetypeKey);
|
||||||
|
if (duplicateActive) {
|
||||||
|
JigsawStudioGraphEditor.duplicatePiece(
|
||||||
|
packRoot,
|
||||||
|
request.structureKey(),
|
||||||
|
sourcePieceKey,
|
||||||
|
pieceKey);
|
||||||
|
} else {
|
||||||
|
JigsawStudioGraphEditor.createBlankVariant(
|
||||||
|
packRoot,
|
||||||
|
request.structureKey(),
|
||||||
|
sourcePieceKey,
|
||||||
|
pieceKey);
|
||||||
|
}
|
||||||
|
JigsawStudioLayout updatedLayout = loadMappedLayout(studio);
|
||||||
|
String targetWorkcellId = updatedLayout.workcellForVariant(pieceKey)
|
||||||
|
.map(JigsawStudioBay::stableId)
|
||||||
|
.orElseThrow(() -> new IOException(
|
||||||
|
"Created variant '" + pieceKey + "' has no Studio workcell"));
|
||||||
|
if (updatedLayout.mode() == JigsawStudioMode.SPATIAL_JIGSAW) {
|
||||||
|
return new CommandGraphMutationResult(
|
||||||
|
updatedLayout,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
(duplicateActive ? "Duplicated" : "Created") + " variant '" + pieceKey
|
||||||
|
+ "' in " + targetWorkcellId + ".");
|
||||||
|
}
|
||||||
|
return new CommandGraphMutationResult(
|
||||||
|
updatedLayout,
|
||||||
|
targetWorkcellId,
|
||||||
|
pieceKey,
|
||||||
|
(duplicateActive ? "Duplicated" : "Created") + " variant '" + pieceKey + "'.");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean duplicateActiveFamily(Player player, String themeKey) {
|
||||||
|
if (player == null || themeKey == null || themeKey.isBlank()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!J.isOwnedByCurrentRegion(player)) {
|
||||||
|
return J.runEntity(player, () -> duplicateActiveFamily(player, themeKey));
|
||||||
|
}
|
||||||
|
ActiveStudio studio = service.studios.get(player.getWorld().getUID());
|
||||||
|
if (studio == null || !authorizeOwner(player, studio)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioActivation.Request request = studio.generator().getRequest();
|
||||||
|
if (request.compatibilityTarget() == JigsawStudioCompatibilityTarget.VANILLA_PORTABLE) {
|
||||||
|
message(player, "Coherent variant families are Iris-only and cannot be added to a vanilla-portable graph.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioSession session = studio.generator().getSession();
|
||||||
|
Map<String, String> sourcePieces = new LinkedHashMap<>();
|
||||||
|
for (JigsawStudioBay workcell : session.layout().bays()) {
|
||||||
|
if (!workcell.enabled()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
JigsawStudioVariant active = session.activeVariant(workcell.stableId()).orElse(null);
|
||||||
|
if (active == null || !active.owned()) {
|
||||||
|
message(player, "Load an owned source variant in every enabled workcell before duplicating a family.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
sourcePieces.put(workcell.stableId(), active.pieceKey());
|
||||||
|
}
|
||||||
|
if (service.toolbelt.deferDuplicationUntilAutosaved(
|
||||||
|
player,
|
||||||
|
studio,
|
||||||
|
DeferredDuplication.family(
|
||||||
|
request.requestId(),
|
||||||
|
session.sessionId(),
|
||||||
|
player,
|
||||||
|
studio.worldId(),
|
||||||
|
sourcePieces,
|
||||||
|
themeKey))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
String reservationFailure = service.graphMutations.beginGraphMutation(studio, session);
|
||||||
|
if (!reservationFailure.isEmpty()) {
|
||||||
|
message(player, reservationFailure);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return service.graphMutations.scheduleGraphMutation(
|
||||||
|
player,
|
||||||
|
studio,
|
||||||
|
() -> {
|
||||||
|
JigsawStudioGraphEditor.VariantFamilyCreation creation =
|
||||||
|
JigsawStudioGraphEditor.duplicateActiveFamily(
|
||||||
|
request.source().getDataFolder().toPath(),
|
||||||
|
request.structureKey(),
|
||||||
|
sourcePieces,
|
||||||
|
themeKey);
|
||||||
|
if (!creation.writeResult().successful()) {
|
||||||
|
throw new IOException("Variant-family transaction failed with "
|
||||||
|
+ creation.writeResult().status());
|
||||||
|
}
|
||||||
|
JigsawStudioLayout updatedLayout = loadMappedLayout(studio);
|
||||||
|
Map<String, String> rebinds = updatedLayout.mode() == JigsawStudioMode.SPATIAL_JIGSAW
|
||||||
|
? Map.of()
|
||||||
|
: creation.pieceKeysByWorkcell();
|
||||||
|
return new CommandGraphMutationResult(
|
||||||
|
updatedLayout,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
rebinds,
|
||||||
|
Optional.empty(),
|
||||||
|
"Duplicated every enabled workcell as coherent family '" + themeKey + "' with "
|
||||||
|
+ creation.pieceKeysByWorkcell().size() + " variant(s).");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean deleteVariant(Player player, String workcellId, String pieceKey) {
|
||||||
|
if (player == null || workcellId == null || workcellId.isBlank()
|
||||||
|
|| pieceKey == null || pieceKey.isBlank()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!J.isOwnedByCurrentRegion(player)) {
|
||||||
|
return J.runEntity(player, () -> deleteVariant(player, workcellId, pieceKey));
|
||||||
|
}
|
||||||
|
ActiveStudio studio = service.studios.get(player.getWorld().getUID());
|
||||||
|
if (studio == null || !authorizeOwner(player, studio)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioSession session = studio.generator().getSession();
|
||||||
|
JigsawStudioBay workcell = session.layout().get(workcellId);
|
||||||
|
JigsawStudioVariant variant = session.layout().variantCatalog().find(pieceKey).orElse(null);
|
||||||
|
if (workcell == null || variant == null || !session.layout().accepts(workcell, variant)) {
|
||||||
|
message(player, "Variant '" + pieceKey + "' no longer belongs to workcell '" + workcellId + "'.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!variant.owned()) {
|
||||||
|
message(player, "Variant '" + pieceKey + "' is read-only. Adopt or clone it before deletion.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String activePieceKey = session.activeVariant(workcellId)
|
||||||
|
.map(JigsawStudioVariant::pieceKey)
|
||||||
|
.orElse("");
|
||||||
|
if (session.layout().mode() == JigsawStudioMode.PLANAR_JIGSAW
|
||||||
|
&& activePieceKey.equals(pieceKey)) {
|
||||||
|
message(player, "Load another variant in this workcell before deleting '" + pieceKey + "'.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String reservationFailure = service.graphMutations.beginGraphMutation(studio, session);
|
||||||
|
if (!reservationFailure.isEmpty()) {
|
||||||
|
message(player, reservationFailure);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioActivation.Request request = studio.generator().getRequest();
|
||||||
|
return service.graphMutations.scheduleGraphMutation(
|
||||||
|
player,
|
||||||
|
studio,
|
||||||
|
() -> {
|
||||||
|
JigsawStudioGraphEditor.PieceDeletionResult deletion =
|
||||||
|
JigsawStudioGraphEditor.deletePieceVariant(
|
||||||
|
request.source().getDataFolder().toPath(),
|
||||||
|
request.structureKey(),
|
||||||
|
pieceKey);
|
||||||
|
return new CommandGraphMutationResult(
|
||||||
|
loadMappedLayout(studio),
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"Deleted variant '" + pieceKey + "', removed "
|
||||||
|
+ deletion.removedPoolMemberships() + " pool membership(s), and removed "
|
||||||
|
+ deletion.removedObjectResources() + " unshared object(s).");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean deleteProject(Player player) {
|
||||||
|
if (player == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!J.isOwnedByCurrentRegion(player)) {
|
||||||
|
return J.runEntity(player, () -> deleteProject(player));
|
||||||
|
}
|
||||||
|
ActiveStudio studio = service.studios.get(player.getWorld().getUID());
|
||||||
|
if (studio == null || !authorizeOwner(player, studio)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioActivation.Request request = studio.generator().getRequest();
|
||||||
|
CloseStart closeStart = service.saveLifecycle.tryBeginClose(request.requestId(), player.getUniqueId(), false);
|
||||||
|
if (closeStart != CloseStart.STARTED) {
|
||||||
|
message(player, switch (closeStart) {
|
||||||
|
case DIRTY -> "Wait for autosave to finish before deleting this project.";
|
||||||
|
case SAVE_IN_PROGRESS -> "Wait for the active autosave to finish before deleting this project.";
|
||||||
|
case OPERATION_IN_PROGRESS -> "Wait for the active Jigsaw Studio operation to finish.";
|
||||||
|
case NOT_OWNER -> "This Jigsaw Studio is owned by another player session.";
|
||||||
|
case NOT_ACTIVE -> "This Jigsaw Studio session is no longer active.";
|
||||||
|
case STARTED -> "Project deletion could not start.";
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Path packRoot = request.source().getDataFolder().toPath();
|
||||||
|
message(player, "Inspecting owned resources and reverse references before project deletion...");
|
||||||
|
J.a(() -> inspectProjectDeletion(player, studio, request, packRoot));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void inspectProjectDeletion(
|
||||||
|
Player player,
|
||||||
|
ActiveStudio studio,
|
||||||
|
JigsawStudioActivation.Request request,
|
||||||
|
Path packRoot
|
||||||
|
) {
|
||||||
|
JigsawStudioProjectDeletionService.DeletionPlan plan;
|
||||||
|
try {
|
||||||
|
plan = JigsawStudioProjectDeletionService.inspect(packRoot, request.structureKey());
|
||||||
|
} catch (Throwable exception) {
|
||||||
|
service.saveLifecycle.cancelClose(request.requestId());
|
||||||
|
IrisLogging.reportError(exception);
|
||||||
|
message(player, "Project deletion inspection failed: " + failureMessage(exception));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!plan.deletable()) {
|
||||||
|
service.saveLifecycle.cancelClose(request.requestId());
|
||||||
|
message(player, "Project deletion is blocked by " + plan.blockers().size()
|
||||||
|
+ " external reference(s). The first is "
|
||||||
|
+ plan.blockers().getFirst().ownerPath() + " at "
|
||||||
|
+ plan.blockers().getFirst().location() + ".");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
boolean scheduled = J.runEntity(
|
||||||
|
player,
|
||||||
|
() -> closeAndDeleteProject(player, studio, request, plan),
|
||||||
|
0,
|
||||||
|
() -> service.saveLifecycle.cancelClose(request.requestId()));
|
||||||
|
if (!scheduled) {
|
||||||
|
service.saveLifecycle.cancelClose(request.requestId());
|
||||||
|
message(player, "Project deletion stopped because the owner session ended.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void closeAndDeleteProject(
|
||||||
|
Player player,
|
||||||
|
ActiveStudio studio,
|
||||||
|
JigsawStudioActivation.Request request,
|
||||||
|
JigsawStudioProjectDeletionService.DeletionPlan plan
|
||||||
|
) {
|
||||||
|
if (!service.isCurrentRequest(studio, request.requestId())) {
|
||||||
|
service.saveLifecycle.cancelClose(request.requestId());
|
||||||
|
message(player, "Project deletion stopped because the Jigsaw Studio session changed.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
IrisServices.get(StudioSVC.class).close().whenComplete((result, throwable) -> {
|
||||||
|
Throwable failure = throwable != null
|
||||||
|
? throwable
|
||||||
|
: result == null ? new IllegalStateException("Studio close completed without a result")
|
||||||
|
: result.failureCause();
|
||||||
|
if (failure != null) {
|
||||||
|
service.saveLifecycle.cancelClose(request.requestId());
|
||||||
|
IrisLogging.reportError(failure);
|
||||||
|
message(player, "Project deletion stopped because Studio could not close: "
|
||||||
|
+ failureMessage(failure));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
J.a(() -> applyProjectDeletion(player, request, plan));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyProjectDeletion(
|
||||||
|
Player player,
|
||||||
|
JigsawStudioActivation.Request request,
|
||||||
|
JigsawStudioProjectDeletionService.DeletionPlan plan
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
JigsawStudioProjectDeletionService.ProjectDeletionResult result =
|
||||||
|
JigsawStudioProjectDeletionService.delete(plan);
|
||||||
|
try {
|
||||||
|
clearAutosaveHistory(
|
||||||
|
request.source().getDataFolder().toPath(),
|
||||||
|
request.structureKey());
|
||||||
|
} catch (IOException historyFailure) {
|
||||||
|
IrisLogging.reportError(historyFailure);
|
||||||
|
message(player, "The project graph was deleted, but its autosave history could not be removed: "
|
||||||
|
+ failureMessage(historyFailure));
|
||||||
|
}
|
||||||
|
request.source().invalidateStructureResources();
|
||||||
|
message(player, "Deleted Jigsaw project '" + request.structureKey() + "' and "
|
||||||
|
+ result.removedResourceCount() + " owned resource(s).");
|
||||||
|
IrisLogging.debug("Jigsaw Studio project deleted: structure=%s resources=%d",
|
||||||
|
request.structureKey(), result.removedResourceCount());
|
||||||
|
} catch (Throwable exception) {
|
||||||
|
IrisLogging.reportError(exception);
|
||||||
|
message(player, "Studio closed, but the project was not deleted: "
|
||||||
|
+ failureMessage(exception) + ". Its files remain recoverable on disk.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean unlinkVariantMembership(
|
||||||
|
Player player,
|
||||||
|
String workcellId,
|
||||||
|
String pieceKey,
|
||||||
|
String poolKey,
|
||||||
|
int entryIndex
|
||||||
|
) {
|
||||||
|
if (player == null || workcellId == null || pieceKey == null || poolKey == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!J.isOwnedByCurrentRegion(player)) {
|
||||||
|
return J.runEntity(player, () -> unlinkVariantMembership(
|
||||||
|
player, workcellId, pieceKey, poolKey, entryIndex));
|
||||||
|
}
|
||||||
|
ActiveStudio studio = service.studios.get(player.getWorld().getUID());
|
||||||
|
if (studio != null && !authorizeOwner(player, studio)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioVariant variant = service.toolbelt.activeOwnedVariant(player, studio, workcellId, pieceKey);
|
||||||
|
if (variant == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioPoolMembership membership = findMembership(variant, poolKey, entryIndex);
|
||||||
|
if (membership == null) {
|
||||||
|
message(player, "That exact pool membership is no longer present.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioSession session = studio.generator().getSession();
|
||||||
|
String reservationFailure = service.graphMutations.beginGraphMutation(studio, session);
|
||||||
|
if (!reservationFailure.isEmpty()) {
|
||||||
|
message(player, reservationFailure);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioActivation.Request request = studio.generator().getRequest();
|
||||||
|
return service.graphMutations.scheduleGraphMutation(
|
||||||
|
player,
|
||||||
|
studio,
|
||||||
|
() -> {
|
||||||
|
JigsawStudioPoolEditor.removeEntry(
|
||||||
|
request.source().getDataFolder().toPath(),
|
||||||
|
request.structureKey(),
|
||||||
|
membership.poolKey(),
|
||||||
|
membership.entryIndex(),
|
||||||
|
variant.pieceKey());
|
||||||
|
return new CommandGraphMutationResult(
|
||||||
|
loadMappedLayout(studio),
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"Unlinked '" + variant.pieceKey() + "' from pool '"
|
||||||
|
+ membership.poolKey() + "'.");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,550 @@
|
|||||||
|
package art.arcane.iris.core.service;
|
||||||
|
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioActivation;
|
||||||
|
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.JigsawStudioMode;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioPieceRules;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioPoolEditor;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioPoolMembership;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioSession;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioVariant;
|
||||||
|
import art.arcane.iris.core.service.JigsawStudioMaterializer.VariantReloadRequest;
|
||||||
|
import art.arcane.iris.core.service.JigsawStudioService.ActiveStudio;
|
||||||
|
import art.arcane.iris.core.service.JigsawStudioService.CommandGraphMutationResult;
|
||||||
|
import art.arcane.iris.engine.object.IrisJigsawThemeSet;
|
||||||
|
import art.arcane.iris.engine.object.IrisStructure;
|
||||||
|
import art.arcane.iris.engine.platform.studio.generators.JigsawStudioGenerator;
|
||||||
|
import art.arcane.iris.util.common.scheduling.J;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioEvaluator.loadStudioStructure;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioGraphMutations.loadMappedLayout;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioProtection.authorizeOwner;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioProtection.canToggleVariantRotation;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioService.DEFAULT_PIECE_RULES;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioService.message;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioToolbelt.findMembership;
|
||||||
|
|
||||||
|
final class JigsawStudioVariantProperties {
|
||||||
|
private final JigsawStudioService service;
|
||||||
|
|
||||||
|
JigsawStudioVariantProperties(JigsawStudioService service) {
|
||||||
|
this.service = Objects.requireNonNull(service, "Jigsaw Studio service");
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean updateVariantThemes(
|
||||||
|
Player player,
|
||||||
|
String workcellId,
|
||||||
|
String pieceKey,
|
||||||
|
List<String> themes
|
||||||
|
) {
|
||||||
|
if (player == null || pieceKey == null || themes == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!J.isOwnedByCurrentRegion(player)) {
|
||||||
|
return J.runEntity(player, () -> updateVariantThemes(
|
||||||
|
player, workcellId, pieceKey, themes));
|
||||||
|
}
|
||||||
|
ActiveStudio studio = service.studios.get(player.getWorld().getUID());
|
||||||
|
if (studio == null || !authorizeOwner(player, studio)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioVariant variant = service.toolbelt.activeOwnedVariant(player, studio, workcellId, pieceKey);
|
||||||
|
if (variant == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioActivation.Request request = studio.generator().getRequest();
|
||||||
|
List<String> targetThemes = List.copyOf(themes);
|
||||||
|
if (request.compatibilityTarget() == JigsawStudioCompatibilityTarget.VANILLA_PORTABLE
|
||||||
|
&& !targetThemes.isEmpty()) {
|
||||||
|
message(player, "Piece themes are Iris-only and cannot be added to a vanilla-portable graph.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
IrisStructure structure = loadStudioStructure(studio);
|
||||||
|
Set<String> declaredThemes = new HashSet<>();
|
||||||
|
if (structure != null && structure.getThemeSets() != null) {
|
||||||
|
for (IrisJigsawThemeSet theme : structure.getThemeSets()) {
|
||||||
|
if (theme != null) {
|
||||||
|
declaredThemes.add(theme.getKey());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!declaredThemes.containsAll(targetThemes)) {
|
||||||
|
message(player, "One or more selected theme sets no longer exist.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (variant.themes().equals(targetThemes)) {
|
||||||
|
message(player, "Variant theme membership is unchanged.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioSession session = studio.generator().getSession();
|
||||||
|
String reservationFailure = service.graphMutations.beginGraphMutation(studio, session);
|
||||||
|
if (!reservationFailure.isEmpty()) {
|
||||||
|
message(player, reservationFailure);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return service.graphMutations.scheduleGraphMutation(
|
||||||
|
player,
|
||||||
|
studio,
|
||||||
|
() -> {
|
||||||
|
JigsawStudioGraphEditor.updatePieceThemes(
|
||||||
|
request.source().getDataFolder().toPath(),
|
||||||
|
request.structureKey(),
|
||||||
|
variant.pieceKey(),
|
||||||
|
targetThemes);
|
||||||
|
return new CommandGraphMutationResult(
|
||||||
|
loadMappedLayout(studio),
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"Updated theme membership for '" + variant.pieceKey() + "'.");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean updateVariantRules(
|
||||||
|
Player player,
|
||||||
|
String workcellId,
|
||||||
|
String pieceKey,
|
||||||
|
JigsawStudioPieceRules rules
|
||||||
|
) {
|
||||||
|
if (player == null || pieceKey == null || rules == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!J.isOwnedByCurrentRegion(player)) {
|
||||||
|
return J.runEntity(player, () -> updateVariantRules(
|
||||||
|
player, workcellId, pieceKey, rules));
|
||||||
|
}
|
||||||
|
ActiveStudio studio = service.studios.get(player.getWorld().getUID());
|
||||||
|
if (studio == null || !authorizeOwner(player, studio)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioVariant variant = service.toolbelt.activeOwnedVariant(player, studio, workcellId, pieceKey);
|
||||||
|
if (variant == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioActivation.Request request = studio.generator().getRequest();
|
||||||
|
if (request.compatibilityTarget() == JigsawStudioCompatibilityTarget.VANILLA_PORTABLE
|
||||||
|
&& !DEFAULT_PIECE_RULES.equals(rules)) {
|
||||||
|
message(player, "Piece placement rules are Iris-only for a vanilla-portable graph.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (variant.rules().equals(rules)) {
|
||||||
|
message(player, "Variant piece rules are unchanged.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioSession session = studio.generator().getSession();
|
||||||
|
String reservationFailure = service.graphMutations.beginGraphMutation(studio, session);
|
||||||
|
if (!reservationFailure.isEmpty()) {
|
||||||
|
message(player, reservationFailure);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return service.graphMutations.scheduleGraphMutation(
|
||||||
|
player,
|
||||||
|
studio,
|
||||||
|
() -> {
|
||||||
|
JigsawStudioGraphEditor.updatePieceRules(
|
||||||
|
request.source().getDataFolder().toPath(),
|
||||||
|
request.structureKey(),
|
||||||
|
variant.pieceKey(),
|
||||||
|
rules);
|
||||||
|
return new CommandGraphMutationResult(
|
||||||
|
loadMappedLayout(studio),
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"Updated placement rules for '" + variant.pieceKey() + "'.");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean toggleVariantRotatable(Player player, String workcellId) {
|
||||||
|
if (player == null || workcellId == null || workcellId.isBlank()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!J.isOwnedByCurrentRegion(player)) {
|
||||||
|
return J.runEntity(player, () -> toggleVariantRotatable(player, workcellId));
|
||||||
|
}
|
||||||
|
ActiveStudio studio = service.studios.get(player.getWorld().getUID());
|
||||||
|
if (studio != null && !authorizeOwner(player, studio)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioVariant variant = service.toolbelt.activeOwnedVariant(player, studio, workcellId);
|
||||||
|
if (variant == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioActivation.Request request = studio.generator().getRequest();
|
||||||
|
if (!canToggleVariantRotation(request.compatibilityTarget(), variant)) {
|
||||||
|
message(player, "Vanilla-portable variants must remain rotatable.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioSession session = studio.generator().getSession();
|
||||||
|
String reservationFailure = service.graphMutations.beginGraphMutation(studio, session);
|
||||||
|
if (!reservationFailure.isEmpty()) {
|
||||||
|
message(player, reservationFailure);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
boolean rotatable = !variant.rotatable();
|
||||||
|
return service.graphMutations.scheduleGraphMutation(
|
||||||
|
player,
|
||||||
|
studio,
|
||||||
|
() -> {
|
||||||
|
JigsawStudioGraphEditor.updateRotatable(
|
||||||
|
request.source().getDataFolder().toPath(),
|
||||||
|
request.structureKey(),
|
||||||
|
variant.pieceKey(),
|
||||||
|
rotatable);
|
||||||
|
return new CommandGraphMutationResult(
|
||||||
|
loadMappedLayout(studio),
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"Variant rotation is now " + (rotatable ? "enabled." : "disabled."));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean expandVariantToCell(Player player, String workcellId) {
|
||||||
|
if (player == null || workcellId == null || workcellId.isBlank()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!J.isOwnedByCurrentRegion(player)) {
|
||||||
|
return J.runEntity(player, () -> expandVariantToCell(player, workcellId));
|
||||||
|
}
|
||||||
|
ActiveStudio studio = service.studios.get(player.getWorld().getUID());
|
||||||
|
if (studio != null && !authorizeOwner(player, studio)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (studio == null) {
|
||||||
|
message(player, "Iris Jigsaw Studio is not active in this world.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioSession session = studio.generator().getSession();
|
||||||
|
JigsawStudioVariant variant = service.toolbelt.activeOwnedVariant(player, studio, workcellId);
|
||||||
|
JigsawStudioBay workcell = session.layout().get(workcellId);
|
||||||
|
if (variant == null || workcell == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return resizeVariant(player, workcellId, variant.pieceKey(), workcell.capacity());
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean resizeVariant(
|
||||||
|
Player player,
|
||||||
|
String workcellId,
|
||||||
|
String pieceKey,
|
||||||
|
JigsawStudioCellDimensions dimensions
|
||||||
|
) {
|
||||||
|
if (player == null || workcellId == null || workcellId.isBlank()
|
||||||
|
|| pieceKey == null || pieceKey.isBlank() || dimensions == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!J.isOwnedByCurrentRegion(player)) {
|
||||||
|
return J.runEntity(player, () -> resizeVariant(player, workcellId, pieceKey, dimensions));
|
||||||
|
}
|
||||||
|
ActiveStudio studio = service.studios.get(player.getWorld().getUID());
|
||||||
|
if (studio == null || !authorizeOwner(player, studio)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioSession session = studio.generator().getSession();
|
||||||
|
JigsawStudioBay workcell = session.layout().get(workcellId);
|
||||||
|
JigsawStudioVariant variant = session.layout().variantCatalog().find(pieceKey).orElse(null);
|
||||||
|
if (workcell == null || variant == null || !session.layout().accepts(workcell, variant)) {
|
||||||
|
message(player, "Variant '" + pieceKey + "' does not belong to workcell '" + workcellId + "'.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!variant.owned()) {
|
||||||
|
message(player, "Variant '" + pieceKey + "' is read-only. Duplicate it before resizing.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (dimensions.width() > workcell.capacity().width()
|
||||||
|
|| dimensions.height() > workcell.capacity().height()
|
||||||
|
|| dimensions.depth() > workcell.capacity().depth()) {
|
||||||
|
message(player, "Requested variant size " + dimensions.width() + "x" + dimensions.height() + "x"
|
||||||
|
+ dimensions.depth() + " exceeds workcell capacity " + workcell.capacity().width() + "x"
|
||||||
|
+ workcell.capacity().height() + "x" + workcell.capacity().depth()
|
||||||
|
+ ". Increase the workcell capacity first.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (variant.mode() == JigsawStudioMode.PLANAR_JIGSAW
|
||||||
|
&& (dimensions.width() < 3 || dimensions.depth() < 3)) {
|
||||||
|
message(player, "Planar variant width and depth must each be at least 3 blocks.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (variant.dimensions().filter(dimensions::equals).isPresent()) {
|
||||||
|
message(player, "Variant '" + variant.resolvedDisplayName() + "' already uses "
|
||||||
|
+ dimensions.width() + "x" + dimensions.height() + "x" + dimensions.depth() + ".");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
boolean active = session.activeVariant(workcellId)
|
||||||
|
.map(activeVariant -> activeVariant.pieceKey().equals(pieceKey))
|
||||||
|
.orElse(false);
|
||||||
|
JigsawStudioGenerator.RenderedBay previous = active
|
||||||
|
? studio.generator().renderVariant(workcell, variant)
|
||||||
|
: null;
|
||||||
|
if (previous != null && !previous.valid()) {
|
||||||
|
message(player, "The active variant cannot be retained for live resize rollback: " + previous.failure());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String reservationFailure = service.graphMutations.beginGraphMutation(studio, session);
|
||||||
|
if (!reservationFailure.isEmpty()) {
|
||||||
|
message(player, reservationFailure);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioActivation.Request request = studio.generator().getRequest();
|
||||||
|
message(player, "Resizing variant '" + variant.resolvedDisplayName() + "' to "
|
||||||
|
+ dimensions.width() + "x" + dimensions.height() + "x" + dimensions.depth() + "...");
|
||||||
|
return service.graphMutations.scheduleGraphMutation(
|
||||||
|
player,
|
||||||
|
studio,
|
||||||
|
() -> {
|
||||||
|
JigsawStudioGraphEditor.VariantResizeResult resize =
|
||||||
|
JigsawStudioGraphEditor.resizePieceObject(
|
||||||
|
request.source().getDataFolder().toPath(),
|
||||||
|
request.structureKey(),
|
||||||
|
variant.pieceKey(),
|
||||||
|
dimensions);
|
||||||
|
return new CommandGraphMutationResult(
|
||||||
|
loadMappedLayout(studio),
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
Map.of(),
|
||||||
|
active ? Optional.of(new VariantReloadRequest(workcellId, previous)) : Optional.empty(),
|
||||||
|
"Resized variant '" + variant.resolvedDisplayName() + "' from "
|
||||||
|
+ resize.previousDimensions().width() + "x"
|
||||||
|
+ resize.previousDimensions().height() + "x"
|
||||||
|
+ resize.previousDimensions().depth() + " to "
|
||||||
|
+ dimensions.width() + "x" + dimensions.height() + "x"
|
||||||
|
+ dimensions.depth() + ". " + resize.relocatedConnectors()
|
||||||
|
+ " connector(s) moved with the new bounds.");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean updateVariantDisplayName(
|
||||||
|
Player player,
|
||||||
|
String workcellId,
|
||||||
|
String pieceKey,
|
||||||
|
String displayName
|
||||||
|
) {
|
||||||
|
if (player == null || workcellId == null || pieceKey == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!J.isOwnedByCurrentRegion(player)) {
|
||||||
|
return J.runEntity(player, () -> updateVariantDisplayName(
|
||||||
|
player, workcellId, pieceKey, displayName));
|
||||||
|
}
|
||||||
|
ActiveStudio studio = service.studios.get(player.getWorld().getUID());
|
||||||
|
if (studio == null || !authorizeOwner(player, studio)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioSession session = studio.generator().getSession();
|
||||||
|
JigsawStudioBay workcell = session.layout().get(workcellId);
|
||||||
|
JigsawStudioVariant variant = session.layout().variantCatalog().find(pieceKey).orElse(null);
|
||||||
|
if (workcell == null || variant == null || !session.layout().accepts(workcell, variant)) {
|
||||||
|
message(player, "Variant '" + pieceKey + "' does not belong to workcell '" + workcellId + "'.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!variant.owned()) {
|
||||||
|
message(player, "Variant '" + pieceKey + "' is read-only. Duplicate it before renaming.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String normalizedName;
|
||||||
|
try {
|
||||||
|
normalizedName = JigsawStudioGraphEditor.normalizeDisplayName(displayName);
|
||||||
|
} catch (IllegalArgumentException exception) {
|
||||||
|
message(player, exception.getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String reservationFailure = service.graphMutations.beginGraphMutation(studio, session);
|
||||||
|
if (!reservationFailure.isEmpty()) {
|
||||||
|
message(player, reservationFailure);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioActivation.Request request = studio.generator().getRequest();
|
||||||
|
return service.graphMutations.scheduleGraphMutation(
|
||||||
|
player,
|
||||||
|
studio,
|
||||||
|
() -> {
|
||||||
|
JigsawStudioGraphEditor.updatePieceDisplayName(
|
||||||
|
request.source().getDataFolder().toPath(),
|
||||||
|
request.structureKey(),
|
||||||
|
pieceKey,
|
||||||
|
normalizedName);
|
||||||
|
return new CommandGraphMutationResult(
|
||||||
|
loadMappedLayout(studio),
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
normalizedName.isEmpty()
|
||||||
|
? "Reset the variant label to its resource-key fallback."
|
||||||
|
: "Renamed the variant to '" + normalizedName + "'.");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean adjustVariantWeight(
|
||||||
|
Player player,
|
||||||
|
String workcellId,
|
||||||
|
String pieceKey,
|
||||||
|
String poolKey,
|
||||||
|
int entryIndex,
|
||||||
|
int delta
|
||||||
|
) {
|
||||||
|
if (delta == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (player == null || workcellId == null || pieceKey == null || poolKey == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!J.isOwnedByCurrentRegion(player)) {
|
||||||
|
return J.runEntity(player, () -> adjustVariantWeight(
|
||||||
|
player, workcellId, pieceKey, poolKey, entryIndex, delta));
|
||||||
|
}
|
||||||
|
ActiveStudio studio = service.studios.get(player.getWorld().getUID());
|
||||||
|
if (studio != null && !authorizeOwner(player, studio)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioVariant variant = service.toolbelt.activeOwnedVariant(player, studio, workcellId, pieceKey);
|
||||||
|
if (variant == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioPoolMembership membership = findMembership(
|
||||||
|
variant, poolKey, entryIndex);
|
||||||
|
if (membership == null) {
|
||||||
|
message(player, "That exact pool membership is no longer present.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int weight;
|
||||||
|
try {
|
||||||
|
weight = Math.addExact(membership.weight(), delta);
|
||||||
|
} catch (ArithmeticException exception) {
|
||||||
|
message(player, "The requested variant weight is outside the supported integer range.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (weight < 1) {
|
||||||
|
message(player, "Variant weights cannot be lower than 1; unlink the membership instead.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioSession session = studio.generator().getSession();
|
||||||
|
String reservationFailure = service.graphMutations.beginGraphMutation(studio, session);
|
||||||
|
if (!reservationFailure.isEmpty()) {
|
||||||
|
message(player, reservationFailure);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioActivation.Request request = studio.generator().getRequest();
|
||||||
|
return service.graphMutations.scheduleGraphMutation(
|
||||||
|
player,
|
||||||
|
studio,
|
||||||
|
() -> {
|
||||||
|
JigsawStudioPoolEditor.updateWeightAtIndex(
|
||||||
|
request.source().getDataFolder().toPath(),
|
||||||
|
request.structureKey(),
|
||||||
|
membership.poolKey(),
|
||||||
|
membership.entryIndex(),
|
||||||
|
variant.pieceKey(),
|
||||||
|
weight);
|
||||||
|
return new CommandGraphMutationResult(
|
||||||
|
loadMappedLayout(studio),
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"Set '" + variant.pieceKey() + "' weight to " + weight
|
||||||
|
+ " in pool '" + membership.poolKey() + "'.");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean adjustVariantChance(
|
||||||
|
Player player,
|
||||||
|
String workcellId,
|
||||||
|
String pieceKey,
|
||||||
|
String poolKey,
|
||||||
|
int entryIndex,
|
||||||
|
int deltaPercentagePoints
|
||||||
|
) {
|
||||||
|
if (deltaPercentagePoints == 0 || player == null || workcellId == null
|
||||||
|
|| pieceKey == null || poolKey == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!J.isOwnedByCurrentRegion(player)) {
|
||||||
|
return J.runEntity(player, () -> adjustVariantChance(
|
||||||
|
player,
|
||||||
|
workcellId,
|
||||||
|
pieceKey,
|
||||||
|
poolKey,
|
||||||
|
entryIndex,
|
||||||
|
deltaPercentagePoints));
|
||||||
|
}
|
||||||
|
ActiveStudio studio = service.studios.get(player.getWorld().getUID());
|
||||||
|
if (studio != null && !authorizeOwner(player, studio)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioVariant variant = service.toolbelt.activeOwnedVariant(player, studio, workcellId, pieceKey);
|
||||||
|
if (variant == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioPoolMembership membership = findMembership(variant, poolKey, entryIndex);
|
||||||
|
if (membership == null) {
|
||||||
|
message(player, "That exact pool membership is no longer present.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
double chance = membership.chance() + deltaPercentagePoints / 100.0D;
|
||||||
|
if (!Double.isFinite(chance) || chance < 0.0D || chance > 1.0D) {
|
||||||
|
message(player, "Variant chance must stay between 0% and 100%.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
chance = Math.round(chance * 1_000_000.0D) / 1_000_000.0D;
|
||||||
|
JigsawStudioActivation.Request request = studio.generator().getRequest();
|
||||||
|
if (request.compatibilityTarget() == JigsawStudioCompatibilityTarget.VANILLA_PORTABLE
|
||||||
|
&& chance != 1.0D) {
|
||||||
|
message(player, "Chance gates are Iris-only and cannot be added to a vanilla-portable graph.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioSession session = studio.generator().getSession();
|
||||||
|
String reservationFailure = service.graphMutations.beginGraphMutation(studio, session);
|
||||||
|
if (!reservationFailure.isEmpty()) {
|
||||||
|
message(player, reservationFailure);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
double targetChance = chance;
|
||||||
|
return service.graphMutations.scheduleGraphMutation(
|
||||||
|
player,
|
||||||
|
studio,
|
||||||
|
() -> {
|
||||||
|
JigsawStudioPoolEditor.updateChanceAtIndex(
|
||||||
|
request.source().getDataFolder().toPath(),
|
||||||
|
request.structureKey(),
|
||||||
|
membership.poolKey(),
|
||||||
|
membership.entryIndex(),
|
||||||
|
variant.pieceKey(),
|
||||||
|
targetChance);
|
||||||
|
return new CommandGraphMutationResult(
|
||||||
|
loadMappedLayout(studio),
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"Set '" + variant.pieceKey() + "' chance to "
|
||||||
|
+ Math.round(targetChance * 100.0D) + "% in pool '"
|
||||||
|
+ membership.poolKey() + "'.");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean canResizeVariantToCapacity(
|
||||||
|
JigsawStudioBay workcell,
|
||||||
|
JigsawStudioVariant variant,
|
||||||
|
boolean active
|
||||||
|
) {
|
||||||
|
if (!active || !variant.owned()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioCellDimensions canonical = variant.dimensions().orElse(null);
|
||||||
|
if (canonical == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioCellDimensions target = workcell.capacity();
|
||||||
|
return canonical.width() <= target.width()
|
||||||
|
&& canonical.height() <= target.height()
|
||||||
|
&& canonical.depth() <= target.depth()
|
||||||
|
&& (canonical.width() < target.width()
|
||||||
|
|| canonical.height() < target.height()
|
||||||
|
|| canonical.depth() < target.depth());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,404 @@
|
|||||||
|
package art.arcane.iris.core.service;
|
||||||
|
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioBay;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioBounds;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioLayout;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioSession;
|
||||||
|
import art.arcane.iris.core.service.JigsawStudioService.ActiveStudio;
|
||||||
|
import art.arcane.iris.engine.framework.PlacedStructurePiece;
|
||||||
|
import art.arcane.iris.engine.object.IrisDirection;
|
||||||
|
import art.arcane.iris.engine.object.IrisJigsawConnector;
|
||||||
|
import art.arcane.iris.engine.platform.studio.generators.JigsawStudioGenerator;
|
||||||
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
|
import art.arcane.iris.util.common.scheduling.J;
|
||||||
|
import org.bukkit.Color;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.Particle;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioService.message;
|
||||||
|
|
||||||
|
final class JigsawStudioVisualization {
|
||||||
|
private static final int VISUAL_INTERVAL_TICKS = 8;
|
||||||
|
private static final int PARTICLE_BUDGET = 384;
|
||||||
|
private static final double VISUAL_RANGE = 96.0D;
|
||||||
|
private static final double VISUAL_RANGE_SQUARED = VISUAL_RANGE * VISUAL_RANGE;
|
||||||
|
private static final Color SELECTED_COLOR = Color.AQUA;
|
||||||
|
private static final Color NEARBY_COLOR = Color.fromRGB(92, 102, 118);
|
||||||
|
private static final Color INVALID_BAY_COLOR = Color.RED;
|
||||||
|
private static final Color INVALID_CONNECTOR_COLOR = Color.RED;
|
||||||
|
private static final Color VALID_CONNECTOR_COLOR = Color.LIME;
|
||||||
|
private static final Color ASSEMBLY_PREVIEW_COLOR = Color.fromRGB(180, 90, 255);
|
||||||
|
private static final Color LIVE_PREVIEW_WARNING_COLOR = Color.fromRGB(255, 180, 55);
|
||||||
|
private static final long ASSEMBLY_PREVIEW_MILLIS = 10_000L;
|
||||||
|
|
||||||
|
private final JigsawStudioService service;
|
||||||
|
final Set<UUID> visualizationLoops = ConcurrentHashMap.newKeySet();
|
||||||
|
final Map<UUID, AssemblyPreview> assemblyPreviews = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
JigsawStudioVisualization(JigsawStudioService service) {
|
||||||
|
this.service = Objects.requireNonNull(service, "Jigsaw Studio service");
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean showAssemblyPreview(Player player, List<PlacedStructurePiece> pieces) {
|
||||||
|
if (player == null || pieces == null || pieces.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!J.isOwnedByCurrentRegion(player)) {
|
||||||
|
return J.runEntity(player, () -> showAssemblyPreview(player, pieces));
|
||||||
|
}
|
||||||
|
if (!service.studios.containsKey(player.getWorld().getUID())) {
|
||||||
|
message(player, "Iris Jigsaw Studio is not active in this world.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
List<JigsawStudioPreviewRenderer.PreviewBounds> bounds = new ArrayList<>(pieces.size());
|
||||||
|
for (PlacedStructurePiece piece : pieces) {
|
||||||
|
bounds.add(new JigsawStudioPreviewRenderer.PreviewBounds(
|
||||||
|
piece.getMinX(),
|
||||||
|
piece.getMinY(),
|
||||||
|
piece.getMinZ(),
|
||||||
|
piece.getMaxX(),
|
||||||
|
piece.getMaxY(),
|
||||||
|
piece.getMaxZ()));
|
||||||
|
}
|
||||||
|
assemblyPreviews.put(player.getUniqueId(), new AssemblyPreview(
|
||||||
|
player.getWorld().getUID(),
|
||||||
|
System.currentTimeMillis() + ASSEMBLY_PREVIEW_MILLIS,
|
||||||
|
List.copyOf(bounds)));
|
||||||
|
service.particlesDisabled.remove(player.getUniqueId());
|
||||||
|
ensureVisualizationLoop(player);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ensureVisualizationLoop(Player player) {
|
||||||
|
if (player == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!J.isOwnedByCurrentRegion(player)) {
|
||||||
|
J.runEntity(player, () -> ensureVisualizationLoop(player));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
UUID playerId = player.getUniqueId();
|
||||||
|
if (!service.enabled || service.particlesDisabled.contains(playerId)
|
||||||
|
|| !service.studios.containsKey(player.getWorld().getUID())
|
||||||
|
|| !visualizationLoops.add(playerId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
visualizationTick(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void visualizationTick(Player player) {
|
||||||
|
UUID playerId = player.getUniqueId();
|
||||||
|
if (!service.enabled || service.particlesDisabled.contains(playerId)) {
|
||||||
|
visualizationLoops.remove(playerId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ActiveStudio studio = service.studios.get(player.getWorld().getUID());
|
||||||
|
if (studio == null) {
|
||||||
|
visualizationLoops.remove(playerId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
renderVisualization(player, studio);
|
||||||
|
} catch (Throwable exception) {
|
||||||
|
visualizationLoops.remove(playerId);
|
||||||
|
IrisLogging.reportError(exception);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
boolean scheduled = J.runEntity(
|
||||||
|
player,
|
||||||
|
() -> visualizationTick(player),
|
||||||
|
VISUAL_INTERVAL_TICKS,
|
||||||
|
() -> visualizationLoops.remove(playerId));
|
||||||
|
if (!scheduled) {
|
||||||
|
visualizationLoops.remove(playerId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void renderVisualization(Player player, ActiveStudio studio) {
|
||||||
|
JigsawStudioSession session = studio.generator().getSession();
|
||||||
|
JigsawStudioLayout layout = session.layout();
|
||||||
|
Location playerLocation = player.getLocation();
|
||||||
|
JigsawStudioBay focused = layout.findAt(
|
||||||
|
playerLocation.getBlockX(),
|
||||||
|
playerLocation.getBlockY(),
|
||||||
|
playerLocation.getBlockZ());
|
||||||
|
ParticleBudget budget = new ParticleBudget(PARTICLE_BUDGET);
|
||||||
|
if (focused != null) {
|
||||||
|
Color focusedColor = bayValid(studio, focused) ? SELECTED_COLOR : INVALID_BAY_COLOR;
|
||||||
|
drawConnectors(player, playerLocation, studio, focused, budget);
|
||||||
|
drawBounds(player, playerLocation, focused.bounds(), focusedColor, 1.15F, 1.0D, budget);
|
||||||
|
}
|
||||||
|
drawLivePreview(player, playerLocation, studio, budget);
|
||||||
|
drawAssemblyPreview(player, playerLocation, budget);
|
||||||
|
for (JigsawStudioBay bay : layout.bays()) {
|
||||||
|
if (bay == focused || !isNearby(playerLocation, bay.bounds())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Color nearbyColor = bayValid(studio, bay) ? NEARBY_COLOR : INVALID_BAY_COLOR;
|
||||||
|
drawBounds(player, playerLocation, bay.bounds(), nearbyColor, 0.55F, 4.0D, budget);
|
||||||
|
if (budget.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawLivePreview(
|
||||||
|
Player player,
|
||||||
|
Location playerLocation,
|
||||||
|
ActiveStudio studio,
|
||||||
|
ParticleBudget budget
|
||||||
|
) {
|
||||||
|
UUID requestId = studio.generator().getRequest().requestId();
|
||||||
|
JigsawStudioGraphEvaluation evaluation = service.evaluator.evaluations.get(requestId);
|
||||||
|
if (evaluation == null || evaluation.previewBounds().isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
JigsawStudioPreviewRenderer.PreviewBounds preview = evaluation.previewBounds();
|
||||||
|
Color color = switch (evaluation.state()) {
|
||||||
|
case VALID -> ASSEMBLY_PREVIEW_COLOR;
|
||||||
|
case PENDING, WARNING, STALE -> LIVE_PREVIEW_WARNING_COLOR;
|
||||||
|
case INVALID -> INVALID_BAY_COLOR;
|
||||||
|
};
|
||||||
|
drawBounds(
|
||||||
|
player,
|
||||||
|
playerLocation,
|
||||||
|
preview,
|
||||||
|
color,
|
||||||
|
0.85F,
|
||||||
|
2.0D,
|
||||||
|
budget);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawAssemblyPreview(
|
||||||
|
Player player,
|
||||||
|
Location playerLocation,
|
||||||
|
ParticleBudget budget
|
||||||
|
) {
|
||||||
|
AssemblyPreview preview = assemblyPreviews.get(player.getUniqueId());
|
||||||
|
if (preview == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!preview.worldId().equals(player.getWorld().getUID())
|
||||||
|
|| preview.expiresAtMillis() < System.currentTimeMillis()) {
|
||||||
|
assemblyPreviews.remove(player.getUniqueId(), preview);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (JigsawStudioPreviewRenderer.PreviewBounds bounds : preview.bounds()) {
|
||||||
|
drawBounds(player, playerLocation, bounds, ASSEMBLY_PREVIEW_COLOR, 0.85F, 2.0D, budget);
|
||||||
|
if (budget.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void drawConnectors(
|
||||||
|
Player player,
|
||||||
|
Location playerLocation,
|
||||||
|
ActiveStudio studio,
|
||||||
|
JigsawStudioBay bay,
|
||||||
|
ParticleBudget budget
|
||||||
|
) {
|
||||||
|
JigsawStudioGenerator.RenderedBay rendered = studio.generator().renderBay(bay);
|
||||||
|
if (!rendered.valid()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (JigsawStudioGenerator.RenderedConnector renderedConnector : rendered.connectors()) {
|
||||||
|
IrisJigsawConnector connector = renderedConnector.connector();
|
||||||
|
IrisDirection direction = connector.getDirection();
|
||||||
|
drawConnectorLine(
|
||||||
|
player,
|
||||||
|
playerLocation,
|
||||||
|
bay.bounds().originX() + renderedConnector.x() + 0.5D,
|
||||||
|
bay.bounds().originY() + renderedConnector.y() + 0.5D,
|
||||||
|
bay.bounds().originZ() + renderedConnector.z() + 0.5D,
|
||||||
|
direction,
|
||||||
|
connectorColor(connector),
|
||||||
|
budget);
|
||||||
|
if (budget.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void drawConnectorLine(
|
||||||
|
Player player,
|
||||||
|
Location playerLocation,
|
||||||
|
double startX,
|
||||||
|
double startY,
|
||||||
|
double startZ,
|
||||||
|
IrisDirection direction,
|
||||||
|
Color color,
|
||||||
|
ParticleBudget budget
|
||||||
|
) {
|
||||||
|
drawLine(
|
||||||
|
player,
|
||||||
|
playerLocation,
|
||||||
|
startX,
|
||||||
|
startY,
|
||||||
|
startZ,
|
||||||
|
startX + direction.x() * 1.75D,
|
||||||
|
startY + direction.y() * 1.75D,
|
||||||
|
startZ + direction.z() * 1.75D,
|
||||||
|
color,
|
||||||
|
1.05F,
|
||||||
|
0.35D,
|
||||||
|
budget);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean bayValid(ActiveStudio studio, JigsawStudioBay bay) {
|
||||||
|
JigsawStudioGenerator.RenderedBay rendered = studio.generator().renderBay(bay);
|
||||||
|
return rendered.valid() && studio.population(bay).readiness().failure().isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Color connectorColor(IrisJigsawConnector connector) {
|
||||||
|
if (connector.getPool() == null || connector.getPool().isBlank()
|
||||||
|
|| connector.getName() == null || connector.getName().isBlank()
|
||||||
|
|| connector.getTargetName() == null || connector.getTargetName().isBlank()) {
|
||||||
|
return INVALID_CONNECTOR_COLOR;
|
||||||
|
}
|
||||||
|
String channel = connector.getChannel();
|
||||||
|
if (channel == null || channel.isBlank()) {
|
||||||
|
return VALID_CONNECTOR_COLOR;
|
||||||
|
}
|
||||||
|
int hash = channel.toLowerCase(Locale.ROOT).hashCode();
|
||||||
|
int red = 80 + (hash & 127);
|
||||||
|
int green = 80 + ((hash >>> 8) & 127);
|
||||||
|
int blue = 80 + ((hash >>> 16) & 127);
|
||||||
|
return Color.fromRGB(red, green, blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void drawBounds(
|
||||||
|
Player player,
|
||||||
|
Location playerLocation,
|
||||||
|
JigsawStudioBounds bounds,
|
||||||
|
Color color,
|
||||||
|
float size,
|
||||||
|
double step,
|
||||||
|
ParticleBudget budget
|
||||||
|
) {
|
||||||
|
drawBounds(player, playerLocation, new JigsawStudioPreviewRenderer.PreviewBounds(
|
||||||
|
bounds.originX(), bounds.originY(), bounds.originZ(), bounds.maxX(), bounds.maxY(), bounds.maxZ()),
|
||||||
|
color, size, step, budget);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void drawBounds(
|
||||||
|
Player player,
|
||||||
|
Location playerLocation,
|
||||||
|
JigsawStudioPreviewRenderer.PreviewBounds bounds,
|
||||||
|
Color color,
|
||||||
|
float size,
|
||||||
|
double step,
|
||||||
|
ParticleBudget budget
|
||||||
|
) {
|
||||||
|
double minX = bounds.minimumX();
|
||||||
|
double minY = bounds.minimumY();
|
||||||
|
double minZ = bounds.minimumZ();
|
||||||
|
double maxX = bounds.maximumX() + 1.0D;
|
||||||
|
double maxY = bounds.maximumY() + 1.0D;
|
||||||
|
double maxZ = bounds.maximumZ() + 1.0D;
|
||||||
|
drawLine(player, playerLocation, minX, minY, minZ, maxX, minY, minZ, color, size, step, budget);
|
||||||
|
drawLine(player, playerLocation, minX, minY, maxZ, maxX, minY, maxZ, color, size, step, budget);
|
||||||
|
drawLine(player, playerLocation, minX, maxY, minZ, maxX, maxY, minZ, color, size, step, budget);
|
||||||
|
drawLine(player, playerLocation, minX, maxY, maxZ, maxX, maxY, maxZ, color, size, step, budget);
|
||||||
|
drawLine(player, playerLocation, minX, minY, minZ, minX, maxY, minZ, color, size, step, budget);
|
||||||
|
drawLine(player, playerLocation, maxX, minY, minZ, maxX, maxY, minZ, color, size, step, budget);
|
||||||
|
drawLine(player, playerLocation, minX, minY, maxZ, minX, maxY, maxZ, color, size, step, budget);
|
||||||
|
drawLine(player, playerLocation, maxX, minY, maxZ, maxX, maxY, maxZ, color, size, step, budget);
|
||||||
|
drawLine(player, playerLocation, minX, minY, minZ, minX, minY, maxZ, color, size, step, budget);
|
||||||
|
drawLine(player, playerLocation, maxX, minY, minZ, maxX, minY, maxZ, color, size, step, budget);
|
||||||
|
drawLine(player, playerLocation, minX, maxY, minZ, minX, maxY, maxZ, color, size, step, budget);
|
||||||
|
drawLine(player, playerLocation, maxX, maxY, minZ, maxX, maxY, maxZ, color, size, step, budget);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void drawLine(
|
||||||
|
Player player,
|
||||||
|
Location playerLocation,
|
||||||
|
double startX,
|
||||||
|
double startY,
|
||||||
|
double startZ,
|
||||||
|
double endX,
|
||||||
|
double endY,
|
||||||
|
double endZ,
|
||||||
|
Color color,
|
||||||
|
float size,
|
||||||
|
double step,
|
||||||
|
ParticleBudget budget
|
||||||
|
) {
|
||||||
|
if (budget.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
double deltaX = endX - startX;
|
||||||
|
double deltaY = endY - startY;
|
||||||
|
double deltaZ = endZ - startZ;
|
||||||
|
double length = Math.sqrt(deltaX * deltaX + deltaY * deltaY + deltaZ * deltaZ);
|
||||||
|
int samples = Math.max(1, (int) Math.ceil(length / step));
|
||||||
|
Particle.DustOptions dust = new Particle.DustOptions(color, size);
|
||||||
|
for (int index = 0; index <= samples; index++) {
|
||||||
|
double progress = (double) index / samples;
|
||||||
|
double x = startX + deltaX * progress;
|
||||||
|
double y = startY + deltaY * progress;
|
||||||
|
double z = startZ + deltaZ * progress;
|
||||||
|
double distanceX = playerLocation.getX() - x;
|
||||||
|
double distanceY = playerLocation.getY() - y;
|
||||||
|
double distanceZ = playerLocation.getZ() - z;
|
||||||
|
if (distanceX * distanceX + distanceY * distanceY + distanceZ * distanceZ > VISUAL_RANGE_SQUARED) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!budget.consume()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
player.spawnParticle(Particle.DUST, x, y, z, 1, 0.0D, 0.0D, 0.0D, 0.0D, dust);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isNearby(Location location, JigsawStudioBounds bounds) {
|
||||||
|
double centerX = bounds.originX() + bounds.dimensions().width() / 2.0D;
|
||||||
|
double centerY = bounds.originY() + bounds.dimensions().height() / 2.0D;
|
||||||
|
double centerZ = bounds.originZ() + bounds.dimensions().depth() / 2.0D;
|
||||||
|
double deltaX = location.getX() - centerX;
|
||||||
|
double deltaY = location.getY() - centerY;
|
||||||
|
double deltaZ = location.getZ() - centerZ;
|
||||||
|
return deltaX * deltaX + deltaY * deltaY + deltaZ * deltaZ <= VISUAL_RANGE_SQUARED;
|
||||||
|
}
|
||||||
|
|
||||||
|
private record AssemblyPreview(
|
||||||
|
UUID worldId,
|
||||||
|
long expiresAtMillis,
|
||||||
|
List<JigsawStudioPreviewRenderer.PreviewBounds> bounds
|
||||||
|
) {
|
||||||
|
AssemblyPreview {
|
||||||
|
Objects.requireNonNull(worldId, "Jigsaw Studio preview world");
|
||||||
|
bounds = List.copyOf(bounds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class ParticleBudget {
|
||||||
|
private int remaining;
|
||||||
|
|
||||||
|
private ParticleBudget(int remaining) {
|
||||||
|
this.remaining = remaining;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean consume() {
|
||||||
|
if (remaining < 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
remaining--;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean empty() {
|
||||||
|
return remaining < 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,434 @@
|
|||||||
|
package art.arcane.iris.core.service;
|
||||||
|
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawPlanarArchetype;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioActivation;
|
||||||
|
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.JigsawStudioSession;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioStructureEditor;
|
||||||
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioVariant;
|
||||||
|
import art.arcane.iris.core.service.JigsawStudioMaterializer.MaterializationWork;
|
||||||
|
import art.arcane.iris.core.service.JigsawStudioService.ActiveStudio;
|
||||||
|
import art.arcane.iris.core.service.JigsawStudioService.CommandGraphMutationResult;
|
||||||
|
import art.arcane.iris.engine.object.IrisJigsawThemeSet;
|
||||||
|
import art.arcane.iris.engine.object.IrisStructure;
|
||||||
|
import art.arcane.iris.engine.platform.studio.generators.JigsawStudioGenerator;
|
||||||
|
import art.arcane.iris.util.common.scheduling.J;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioChunkWriter.validateMaterialization;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioEvaluator.loadStudioStructure;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioGraphMutations.loadMappedLayout;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioProtection.authorizeOwner;
|
||||||
|
import static art.arcane.iris.core.service.JigsawStudioService.message;
|
||||||
|
|
||||||
|
final class JigsawStudioWorkcellEditor {
|
||||||
|
private final JigsawStudioService service;
|
||||||
|
|
||||||
|
JigsawStudioWorkcellEditor(JigsawStudioService service) {
|
||||||
|
this.service = Objects.requireNonNull(service, "Jigsaw Studio service");
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean setConnectorBlocksVisible(Player player, String workcellId, boolean visible) {
|
||||||
|
if (player == null || workcellId == null || workcellId.isBlank()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!J.isOwnedByCurrentRegion(player)) {
|
||||||
|
return J.runEntity(player, () -> setConnectorBlocksVisible(player, workcellId, visible));
|
||||||
|
}
|
||||||
|
ActiveStudio studio = service.studios.get(player.getWorld().getUID());
|
||||||
|
if (studio == null || !authorizeOwner(player, studio)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioSession session = studio.generator().getSession();
|
||||||
|
JigsawStudioBay workcell = session.layout().get(workcellId);
|
||||||
|
if (workcell == null) {
|
||||||
|
message(player, "Unknown Jigsaw Studio workcell '" + workcellId + "'.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioSession.WorkcellSnapshot snapshot = session.workcellSnapshot(workcellId);
|
||||||
|
if (snapshot.connectorsVisible() == visible) {
|
||||||
|
message(player, "Connector blocks are already " + (visible ? "visible." : "hidden."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioSession.SwitchStart start = session.beginVariantReload(workcellId);
|
||||||
|
if (start.status() != JigsawStudioSession.SwitchStatus.STARTED) {
|
||||||
|
message(player, switch (start.status()) {
|
||||||
|
case DIRTY -> "Wait for this workcell to finish autosaving before changing connector visibility.";
|
||||||
|
case SAVE_IN_PROGRESS -> "Wait for the current workcell save to finish.";
|
||||||
|
case SWITCH_IN_PROGRESS -> "This workcell is already loading another view.";
|
||||||
|
case UNKNOWN_WORKCELL -> "The selected workcell no longer exists.";
|
||||||
|
case UNKNOWN_VARIANT -> "This workcell has no active variant.";
|
||||||
|
case ALREADY_ACTIVE, WRONG_WORKCELL, STARTED ->
|
||||||
|
"Connector visibility could not change: " + start.status() + ".";
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioSession.VariantSwitchToken token = start.token().orElseThrow();
|
||||||
|
JigsawStudioGenerator.RenderedBay rendered = studio.generator().renderVariant(
|
||||||
|
workcell,
|
||||||
|
token.targetVariant());
|
||||||
|
if (!rendered.valid()) {
|
||||||
|
session.abortVariantSwitch(token);
|
||||||
|
message(player, "Connector visibility cannot change: " + rendered.failure());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
message(player, (visible ? "Showing" : "Hiding") + " connector blocks in " + workcellId + "...");
|
||||||
|
return service.materializer.scheduleMaterialization(new MaterializationWork(
|
||||||
|
studio,
|
||||||
|
player.getWorld(),
|
||||||
|
player,
|
||||||
|
workcell,
|
||||||
|
token,
|
||||||
|
rendered,
|
||||||
|
rendered,
|
||||||
|
snapshot.connectorsVisible(),
|
||||||
|
visible,
|
||||||
|
true));
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean resetConnectorBlocks(Player player, String workcellId) {
|
||||||
|
if (player == null || workcellId == null || workcellId.isBlank()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!J.isOwnedByCurrentRegion(player)) {
|
||||||
|
return J.runEntity(player, () -> resetConnectorBlocks(player, workcellId));
|
||||||
|
}
|
||||||
|
ActiveStudio studio = service.studios.get(player.getWorld().getUID());
|
||||||
|
if (studio == null || !authorizeOwner(player, studio)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioSession session = studio.generator().getSession();
|
||||||
|
JigsawStudioBay workcell = session.layout().get(workcellId);
|
||||||
|
if (workcell == null) {
|
||||||
|
message(player, "Unknown Jigsaw Studio workcell '" + workcellId + "'.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioVariant activeVariant = session.activeVariant(workcellId).orElse(null);
|
||||||
|
if (activeVariant == null || !activeVariant.owned()) {
|
||||||
|
message(player, "Load an owned variant before resetting its connector blocks.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioGenerator.RenderedBay rendered = studio.generator().renderVariant(
|
||||||
|
workcell,
|
||||||
|
activeVariant);
|
||||||
|
String validationFailure = validateMaterialization(rendered);
|
||||||
|
if (!validationFailure.isEmpty()) {
|
||||||
|
message(player, "Connector blocks cannot reset: " + validationFailure);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (rendered.connectors().isEmpty()) {
|
||||||
|
message(player, "The active variant has no saved connectors to reset.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String reservationFailure = service.materializer.beginConnectorRepair(studio);
|
||||||
|
if (!reservationFailure.isEmpty()) {
|
||||||
|
message(player, reservationFailure);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
boolean connectorsVisible = session.workcellSnapshot(workcellId).connectorsVisible();
|
||||||
|
message(player, "Resetting " + rendered.connectors().size()
|
||||||
|
+ " connector block(s) from the last saved iteration...");
|
||||||
|
return service.materializer.scheduleConnectorRepair(
|
||||||
|
player,
|
||||||
|
studio,
|
||||||
|
workcell,
|
||||||
|
rendered,
|
||||||
|
connectorsVisible);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean setWorkcellEnabled(Player player, String workcellId, boolean enabled) {
|
||||||
|
if (player == null || workcellId == null || workcellId.isBlank()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!J.isOwnedByCurrentRegion(player)) {
|
||||||
|
return J.runEntity(player, () -> setWorkcellEnabled(player, workcellId, enabled));
|
||||||
|
}
|
||||||
|
ActiveStudio studio = service.studios.get(player.getWorld().getUID());
|
||||||
|
if (studio == null || !authorizeOwner(player, studio)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioSession session = studio.generator().getSession();
|
||||||
|
JigsawStudioBay workcell = session.layout().get(workcellId);
|
||||||
|
if (workcell == null || workcell.archetype().isEmpty()) {
|
||||||
|
message(player, "Only planar Jigsaw Studio workcells can be enabled or disabled.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (workcell.enabled() == enabled) {
|
||||||
|
message(player, "Workcell '" + workcellId + "' is already "
|
||||||
|
+ (enabled ? "enabled." : "disabled."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String reservationFailure = service.graphMutations.beginGraphMutation(studio, session);
|
||||||
|
if (!reservationFailure.isEmpty()) {
|
||||||
|
message(player, reservationFailure);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioActivation.Request request = studio.generator().getRequest();
|
||||||
|
JigsawPlanarArchetype archetype = workcell.archetype().orElseThrow();
|
||||||
|
return service.graphMutations.scheduleGraphMutation(
|
||||||
|
player,
|
||||||
|
studio,
|
||||||
|
() -> {
|
||||||
|
JigsawStudioStructureEditor.updateWorkcellEnabled(
|
||||||
|
request.source().getDataFolder().toPath(),
|
||||||
|
request.structureKey(),
|
||||||
|
archetype,
|
||||||
|
enabled);
|
||||||
|
return new CommandGraphMutationResult(
|
||||||
|
loadMappedLayout(studio),
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"Workcell '" + workcellId + "' is now "
|
||||||
|
+ (enabled ? "enabled." : "disabled for assembly and export."));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean updateWorkcellDimensions(
|
||||||
|
Player player,
|
||||||
|
String workcellId,
|
||||||
|
JigsawStudioCellDimensions dimensions
|
||||||
|
) {
|
||||||
|
if (player == null || workcellId == null || workcellId.isBlank() || dimensions == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!J.isOwnedByCurrentRegion(player)) {
|
||||||
|
return J.runEntity(player, () -> updateWorkcellDimensions(player, workcellId, dimensions));
|
||||||
|
}
|
||||||
|
ActiveStudio studio = service.studios.get(player.getWorld().getUID());
|
||||||
|
if (studio == null || !authorizeOwner(player, studio)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioSession session = studio.generator().getSession();
|
||||||
|
JigsawStudioBay workcell = session.layout().get(workcellId);
|
||||||
|
if (workcell == null) {
|
||||||
|
message(player, "Unknown Jigsaw Studio workcell '" + workcellId + "'.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (workcell.capacity().equals(dimensions)) {
|
||||||
|
message(player, "Workcell '" + workcellId + "' already has capacity "
|
||||||
|
+ dimensions.width() + "x" + dimensions.height() + "x" + dimensions.depth() + ".");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (workcell.archetype().isPresent()
|
||||||
|
&& (dimensions.width() < 3 || dimensions.depth() < 3)) {
|
||||||
|
message(player, "Planar workcell width and depth must each be at least 3 blocks.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String reservationFailure = service.graphMutations.beginGraphMutation(studio, session);
|
||||||
|
if (!reservationFailure.isEmpty()) {
|
||||||
|
message(player, reservationFailure);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioActivation.Request request = studio.generator().getRequest();
|
||||||
|
JigsawPlanarArchetype archetype = workcell.archetype().orElse(null);
|
||||||
|
return service.graphMutations.scheduleGraphMutation(
|
||||||
|
player,
|
||||||
|
studio,
|
||||||
|
() -> {
|
||||||
|
JigsawStudioGraphEditor.WorkcellCapacityResult capacityResult = null;
|
||||||
|
if (archetype == null) {
|
||||||
|
JigsawStudioStructureEditor.updateCellSize(
|
||||||
|
request.source().getDataFolder().toPath(),
|
||||||
|
request.structureKey(),
|
||||||
|
dimensions);
|
||||||
|
} else {
|
||||||
|
capacityResult = JigsawStudioGraphEditor.updatePlanarWorkcellCapacity(
|
||||||
|
request.source().getDataFolder().toPath(),
|
||||||
|
request.structureKey(),
|
||||||
|
archetype,
|
||||||
|
dimensions);
|
||||||
|
}
|
||||||
|
request.source().invalidateStructureResources();
|
||||||
|
String resizeSummary = capacityResult == null
|
||||||
|
? ""
|
||||||
|
: " Verified " + capacityResult.checkedVariants() + " existing variant"
|
||||||
|
+ (capacityResult.checkedVariants() == 1 ? "" : "s") + "; no variant object was resized.";
|
||||||
|
return new CommandGraphMutationResult(
|
||||||
|
loadMappedLayout(studio),
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"Updated '" + workcellId + "' capacity to "
|
||||||
|
+ dimensions.width() + "x" + dimensions.height() + "x"
|
||||||
|
+ dimensions.depth() + "." + resizeSummary
|
||||||
|
+ " The live workcell layout was regenerated.");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean setRequireCaps(Player player, boolean requireCaps) {
|
||||||
|
if (player == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!J.isOwnedByCurrentRegion(player)) {
|
||||||
|
return J.runEntity(player, () -> setRequireCaps(player, requireCaps));
|
||||||
|
}
|
||||||
|
ActiveStudio studio = service.studios.get(player.getWorld().getUID());
|
||||||
|
if (studio == null || !authorizeOwner(player, studio)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioActivation.Request request = studio.generator().getRequest();
|
||||||
|
if (request.compatibilityTarget() == JigsawStudioCompatibilityTarget.VANILLA_PORTABLE
|
||||||
|
&& requireCaps) {
|
||||||
|
message(player, "Mandatory caps are Iris-only and cannot be enabled for a vanilla-portable graph.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
IrisStructure structure = loadStudioStructure(studio);
|
||||||
|
if (structure == null) {
|
||||||
|
message(player, "The active jigsaw structure could not be loaded.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (structure.isRequireCaps() == requireCaps) {
|
||||||
|
message(player, "Mandatory caps are already " + (requireCaps ? "enabled." : "disabled."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioSession session = studio.generator().getSession();
|
||||||
|
String reservationFailure = service.graphMutations.beginGraphMutation(studio, session);
|
||||||
|
if (!reservationFailure.isEmpty()) {
|
||||||
|
message(player, reservationFailure);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return service.graphMutations.scheduleGraphMutation(
|
||||||
|
player,
|
||||||
|
studio,
|
||||||
|
() -> {
|
||||||
|
JigsawStudioStructureEditor.updateRequireCaps(
|
||||||
|
request.source().getDataFolder().toPath(),
|
||||||
|
request.structureKey(),
|
||||||
|
requireCaps);
|
||||||
|
return new CommandGraphMutationResult(
|
||||||
|
loadMappedLayout(studio),
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"Mandatory physical caps are now "
|
||||||
|
+ (requireCaps ? "enabled." : "disabled."));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean updateThemeSetWeight(Player player, String themeKey, int weight) {
|
||||||
|
if (player == null || themeKey == null || themeKey.isBlank() || weight < 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!J.isOwnedByCurrentRegion(player)) {
|
||||||
|
return J.runEntity(player, () -> updateThemeSetWeight(player, themeKey, weight));
|
||||||
|
}
|
||||||
|
ActiveStudio studio = service.studios.get(player.getWorld().getUID());
|
||||||
|
if (studio == null || !authorizeOwner(player, studio)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioActivation.Request request = studio.generator().getRequest();
|
||||||
|
if (request.compatibilityTarget() == JigsawStudioCompatibilityTarget.VANILLA_PORTABLE) {
|
||||||
|
message(player, "Coherent theme sets are Iris-only.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
IrisStructure structure = loadStudioStructure(studio);
|
||||||
|
if (structure == null || structure.getThemeSets() == null) {
|
||||||
|
message(player, "The active jigsaw structure has no theme sets.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
List<IrisJigsawThemeSet> updated = new ArrayList<>(structure.getThemeSets().size());
|
||||||
|
boolean found = false;
|
||||||
|
for (IrisJigsawThemeSet theme : structure.getThemeSets()) {
|
||||||
|
if (theme != null && themeKey.equals(theme.getKey())) {
|
||||||
|
if (theme.getWeight() == weight) {
|
||||||
|
message(player, "Theme '" + themeKey + "' already has weight " + weight + ".");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
updated.add(new IrisJigsawThemeSet(themeKey, weight));
|
||||||
|
found = true;
|
||||||
|
} else if (theme != null) {
|
||||||
|
updated.add(new IrisJigsawThemeSet(theme.getKey(), theme.getWeight()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) {
|
||||||
|
message(player, "Theme set '" + themeKey + "' no longer exists.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioSession session = studio.generator().getSession();
|
||||||
|
String reservationFailure = service.graphMutations.beginGraphMutation(studio, session);
|
||||||
|
if (!reservationFailure.isEmpty()) {
|
||||||
|
message(player, reservationFailure);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return service.graphMutations.scheduleGraphMutation(
|
||||||
|
player,
|
||||||
|
studio,
|
||||||
|
() -> {
|
||||||
|
JigsawStudioStructureEditor.updateThemeSets(
|
||||||
|
request.source().getDataFolder().toPath(),
|
||||||
|
request.structureKey(),
|
||||||
|
updated);
|
||||||
|
return new CommandGraphMutationResult(
|
||||||
|
loadMappedLayout(studio),
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"Set theme '" + themeKey + "' weight to " + weight + ".");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean updateWorkcellDisplayName(
|
||||||
|
Player player,
|
||||||
|
String workcellId,
|
||||||
|
String displayName
|
||||||
|
) {
|
||||||
|
if (player == null || workcellId == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!J.isOwnedByCurrentRegion(player)) {
|
||||||
|
return J.runEntity(player, () -> updateWorkcellDisplayName(player, workcellId, displayName));
|
||||||
|
}
|
||||||
|
ActiveStudio studio = service.studios.get(player.getWorld().getUID());
|
||||||
|
if (studio == null || !authorizeOwner(player, studio)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioSession session = studio.generator().getSession();
|
||||||
|
JigsawStudioBay workcell = session.layout().get(workcellId);
|
||||||
|
if (workcell == null) {
|
||||||
|
message(player, "Unknown Jigsaw Studio workcell '" + workcellId + "'.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String normalizedName;
|
||||||
|
try {
|
||||||
|
normalizedName = JigsawStudioGraphEditor.normalizeDisplayName(displayName);
|
||||||
|
} catch (IllegalArgumentException exception) {
|
||||||
|
message(player, exception.getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String reservationFailure = service.graphMutations.beginGraphMutation(studio, session);
|
||||||
|
if (!reservationFailure.isEmpty()) {
|
||||||
|
message(player, reservationFailure);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JigsawStudioActivation.Request request = studio.generator().getRequest();
|
||||||
|
JigsawPlanarArchetype archetype = workcell.archetype().orElse(null);
|
||||||
|
return service.graphMutations.scheduleGraphMutation(
|
||||||
|
player,
|
||||||
|
studio,
|
||||||
|
() -> {
|
||||||
|
if (archetype == null) {
|
||||||
|
JigsawStudioStructureEditor.updateSpatialWorkcellDisplayName(
|
||||||
|
request.source().getDataFolder().toPath(),
|
||||||
|
request.structureKey(),
|
||||||
|
normalizedName);
|
||||||
|
} else {
|
||||||
|
JigsawStudioStructureEditor.updateWorkcellDisplayName(
|
||||||
|
request.source().getDataFolder().toPath(),
|
||||||
|
request.structureKey(),
|
||||||
|
archetype,
|
||||||
|
normalizedName);
|
||||||
|
}
|
||||||
|
return new CommandGraphMutationResult(
|
||||||
|
loadMappedLayout(studio),
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
normalizedName.isEmpty()
|
||||||
|
? "Reset the workcell label to '" + workcell.canonicalDisplayName() + "'."
|
||||||
|
: "Renamed the workcell to '" + normalizedName + "'.");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -112,18 +112,18 @@ public class JigsawStudioLifecycleTest {
|
|||||||
assertTrue(service.closeProtectionFailure(request.requestId()).contains("loading a variant"));
|
assertTrue(service.closeProtectionFailure(request.requestId()).contains("loading a variant"));
|
||||||
assertTrue(session.abortVariantSwitch(switchToken));
|
assertTrue(session.abortVariantSwitch(switchToken));
|
||||||
|
|
||||||
assertEquals(JigsawStudioService.SaveStart.STARTED, service.tryBeginSave(request.requestId()));
|
assertEquals(JigsawStudioSaveLifecycle.SaveStart.STARTED, service.saveLifecycle.tryBeginSave(request.requestId()));
|
||||||
assertEquals(
|
assertEquals(
|
||||||
JigsawStudioService.CloseStart.SAVE_IN_PROGRESS,
|
JigsawStudioService.CloseStart.SAVE_IN_PROGRESS,
|
||||||
service.tryBeginClose(request.requestId(), OWNER, true));
|
service.tryBeginClose(request.requestId(), OWNER, true));
|
||||||
assertTrue(service.closeProtectionFailure(request.requestId()).contains("saving"));
|
assertTrue(service.closeProtectionFailure(request.requestId()).contains("saving"));
|
||||||
|
|
||||||
service.finishSave(request.requestId());
|
service.saveLifecycle.finishSave(request.requestId());
|
||||||
assertEquals(
|
assertEquals(
|
||||||
JigsawStudioService.CloseStart.STARTED,
|
JigsawStudioService.CloseStart.STARTED,
|
||||||
service.tryBeginClose(request.requestId(), OWNER, true));
|
service.tryBeginClose(request.requestId(), OWNER, true));
|
||||||
assertNull(service.closeProtectionFailure(request.requestId()));
|
assertNull(service.closeProtectionFailure(request.requestId()));
|
||||||
assertEquals(JigsawStudioService.SaveStart.CLOSING, service.tryBeginSave(request.requestId()));
|
assertEquals(JigsawStudioSaveLifecycle.SaveStart.CLOSING, service.saveLifecycle.tryBeginSave(request.requestId()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -143,7 +143,7 @@ public class JigsawStudioLifecycleTest {
|
|||||||
retry.set(invocation.getArgument(0));
|
retry.set(invocation.getArgument(0));
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
CompletableFuture<Void> readiness = service.awaitCloseForReplacement(
|
CompletableFuture<Void> readiness = service.saveLifecycle.awaitCloseForReplacement(
|
||||||
request.requestId(), OWNER);
|
request.requestId(), OWNER);
|
||||||
|
|
||||||
assertFalse(readiness.isDone());
|
assertFalse(readiness.isDone());
|
||||||
@@ -167,7 +167,7 @@ public class JigsawStudioLifecycleTest {
|
|||||||
JigsawStudioService service = new JigsawStudioService();
|
JigsawStudioService service = new JigsawStudioService();
|
||||||
|
|
||||||
try (MockedStatic<J> scheduling = mockStatic(J.class)) {
|
try (MockedStatic<J> scheduling = mockStatic(J.class)) {
|
||||||
CompletableFuture<Void> readiness = service.awaitCloseForReplacement(
|
CompletableFuture<Void> readiness = service.saveLifecycle.awaitCloseForReplacement(
|
||||||
request.requestId(), OTHER_OWNER);
|
request.requestId(), OTHER_OWNER);
|
||||||
|
|
||||||
assertTrue(readiness.isCompletedExceptionally());
|
assertTrue(readiness.isCompletedExceptionally());
|
||||||
@@ -187,9 +187,9 @@ public class JigsawStudioLifecycleTest {
|
|||||||
baseline.put("name", "iris:start");
|
baseline.put("name", "iris:start");
|
||||||
KMap<String, Object> updated = new KMap<>();
|
KMap<String, Object> updated = new KMap<>();
|
||||||
updated.put("name", "iris:hall");
|
updated.put("name", "iris:hall");
|
||||||
assertTrue(JigsawStudioService.tileSnapshotChanged(baseline, updated));
|
assertTrue(JigsawStudioTileWatcher.tileSnapshotChanged(baseline, updated));
|
||||||
|
|
||||||
Class<?> keyType = Class.forName(JigsawStudioService.class.getName() + "$JigsawTileWatchKey");
|
Class<?> keyType = Class.forName(JigsawStudioTileWatcher.class.getName() + "$JigsawTileWatchKey");
|
||||||
Constructor<?> constructor = keyType.getDeclaredConstructor(
|
Constructor<?> constructor = keyType.getDeclaredConstructor(
|
||||||
UUID.class, UUID.class, int.class, int.class, int.class);
|
UUID.class, UUID.class, int.class, int.class, int.class);
|
||||||
constructor.setAccessible(true);
|
constructor.setAccessible(true);
|
||||||
@@ -213,7 +213,7 @@ public class JigsawStudioLifecycleTest {
|
|||||||
new ConcurrentHashMap<>(),
|
new ConcurrentHashMap<>(),
|
||||||
ConcurrentHashMap.newKeySet(),
|
ConcurrentHashMap.newKeySet(),
|
||||||
new AtomicLong());
|
new AtomicLong());
|
||||||
Class<?> watchType = Class.forName(JigsawStudioService.class.getName() + "$JigsawTileWatch");
|
Class<?> watchType = Class.forName(JigsawStudioTileWatcher.class.getName() + "$JigsawTileWatch");
|
||||||
Constructor<?> watchConstructor = watchType.getDeclaredConstructor(
|
Constructor<?> watchConstructor = watchType.getDeclaredConstructor(
|
||||||
keyType,
|
keyType,
|
||||||
studioType,
|
studioType,
|
||||||
@@ -231,10 +231,10 @@ public class JigsawStudioLifecycleTest {
|
|||||||
baseline,
|
baseline,
|
||||||
new AtomicBoolean(),
|
new AtomicBoolean(),
|
||||||
new AtomicBoolean());
|
new AtomicBoolean());
|
||||||
Field watchesField = JigsawStudioService.class.getDeclaredField("jigsawTileWatches");
|
Field watchesField = JigsawStudioTileWatcher.class.getDeclaredField("jigsawTileWatches");
|
||||||
watchesField.setAccessible(true);
|
watchesField.setAccessible(true);
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
Map<Object, Object> watches = (Map<Object, Object>) watchesField.get(service);
|
Map<Object, Object> watches = (Map<Object, Object>) watchesField.get(service.tileWatcher);
|
||||||
watches.put(key, watch);
|
watches.put(key, watch);
|
||||||
|
|
||||||
try (MockedStatic<J> scheduling = mockStatic(J.class)) {
|
try (MockedStatic<J> scheduling = mockStatic(J.class)) {
|
||||||
@@ -277,55 +277,55 @@ public class JigsawStudioLifecycleTest {
|
|||||||
JigsawStudioService.ExportStart.IN_PROGRESS,
|
JigsawStudioService.ExportStart.IN_PROGRESS,
|
||||||
service.tryBeginExport(request.requestId(), OWNER));
|
service.tryBeginExport(request.requestId(), OWNER));
|
||||||
assertEquals(
|
assertEquals(
|
||||||
JigsawStudioService.SaveStart.EXPORT_OPERATION,
|
JigsawStudioSaveLifecycle.SaveStart.EXPORT_OPERATION,
|
||||||
service.tryBeginSave(request.requestId()));
|
service.saveLifecycle.tryBeginSave(request.requestId()));
|
||||||
assertEquals(
|
assertEquals(
|
||||||
JigsawStudioService.CloseStart.OPERATION_IN_PROGRESS,
|
JigsawStudioService.CloseStart.OPERATION_IN_PROGRESS,
|
||||||
service.tryBeginClose(request.requestId(), OWNER, false));
|
service.tryBeginClose(request.requestId(), OWNER, false));
|
||||||
assertTrue(service.closeProtectionFailure(request.requestId()).contains("exporting"));
|
assertTrue(service.closeProtectionFailure(request.requestId()).contains("exporting"));
|
||||||
|
|
||||||
service.finishExport(request.requestId());
|
service.finishExport(request.requestId());
|
||||||
assertEquals(JigsawStudioService.SaveStart.STARTED, service.tryBeginSave(request.requestId()));
|
assertEquals(JigsawStudioSaveLifecycle.SaveStart.STARTED, service.saveLifecycle.tryBeginSave(request.requestId()));
|
||||||
service.finishSave(request.requestId());
|
service.saveLifecycle.finishSave(request.requestId());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void immediateSingleAndFamilyDuplicatesWaitForAutosaveThenRunExactlyOnce() {
|
public void immediateSingleAndFamilyDuplicatesWaitForAutosaveThenRunExactlyOnce() {
|
||||||
assertEquals(
|
assertEquals(
|
||||||
JigsawStudioService.DeferredDuplicationReadiness.WAITING_FOR_AUTOSAVE,
|
JigsawStudioToolbelt.DeferredDuplicationReadiness.WAITING_FOR_AUTOSAVE,
|
||||||
JigsawStudioService.deferredDuplicationReadiness(
|
JigsawStudioToolbelt.deferredDuplicationReadiness(
|
||||||
true, true, true, false, false));
|
true, true, true, false, false));
|
||||||
assertEquals(
|
assertEquals(
|
||||||
JigsawStudioService.DeferredDuplicationReadiness.WAITING_FOR_AUTOSAVE,
|
JigsawStudioToolbelt.DeferredDuplicationReadiness.WAITING_FOR_AUTOSAVE,
|
||||||
JigsawStudioService.deferredDuplicationReadiness(
|
JigsawStudioToolbelt.deferredDuplicationReadiness(
|
||||||
true, true, true, true, false));
|
true, true, true, true, false));
|
||||||
assertEquals(
|
assertEquals(
|
||||||
JigsawStudioService.DeferredDuplicationReadiness.WAITING_FOR_OPERATION,
|
JigsawStudioToolbelt.DeferredDuplicationReadiness.WAITING_FOR_OPERATION,
|
||||||
JigsawStudioService.deferredDuplicationReadiness(
|
JigsawStudioToolbelt.deferredDuplicationReadiness(
|
||||||
true, true, false, true, false));
|
true, true, false, true, false));
|
||||||
assertEquals(
|
assertEquals(
|
||||||
JigsawStudioService.DeferredDuplicationReadiness.READY,
|
JigsawStudioToolbelt.DeferredDuplicationReadiness.READY,
|
||||||
JigsawStudioService.deferredDuplicationReadiness(
|
JigsawStudioToolbelt.deferredDuplicationReadiness(
|
||||||
true, true, false, false, false));
|
true, true, false, false, false));
|
||||||
assertEquals(
|
assertEquals(
|
||||||
JigsawStudioService.DeferredDuplicationReadiness.STALE,
|
JigsawStudioToolbelt.DeferredDuplicationReadiness.STALE,
|
||||||
JigsawStudioService.deferredDuplicationReadiness(
|
JigsawStudioToolbelt.deferredDuplicationReadiness(
|
||||||
true, false, false, false, false));
|
true, false, false, false, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void initialEvaluationWaitsForCommitAndSchedulesExactlyOnce() {
|
public void initialEvaluationWaitsForCommitAndSchedulesExactlyOnce() {
|
||||||
AtomicLong registrationBeforeCommit = new AtomicLong();
|
AtomicLong registrationBeforeCommit = new AtomicLong();
|
||||||
assertFalse(JigsawStudioService.claimInitialEvaluation(registrationBeforeCommit, false));
|
assertFalse(JigsawStudioEvaluator.claimInitialEvaluation(registrationBeforeCommit, false));
|
||||||
assertEquals(0L, registrationBeforeCommit.get());
|
assertEquals(0L, registrationBeforeCommit.get());
|
||||||
assertTrue(JigsawStudioService.claimInitialEvaluation(registrationBeforeCommit, true));
|
assertTrue(JigsawStudioEvaluator.claimInitialEvaluation(registrationBeforeCommit, true));
|
||||||
assertEquals(1L, registrationBeforeCommit.get());
|
assertEquals(1L, registrationBeforeCommit.get());
|
||||||
assertFalse(JigsawStudioService.claimInitialEvaluation(registrationBeforeCommit, true));
|
assertFalse(JigsawStudioEvaluator.claimInitialEvaluation(registrationBeforeCommit, true));
|
||||||
|
|
||||||
AtomicLong registrationAfterCommit = new AtomicLong();
|
AtomicLong registrationAfterCommit = new AtomicLong();
|
||||||
assertTrue(JigsawStudioService.claimInitialEvaluation(registrationAfterCommit, true));
|
assertTrue(JigsawStudioEvaluator.claimInitialEvaluation(registrationAfterCommit, true));
|
||||||
assertEquals(1L, registrationAfterCommit.get());
|
assertEquals(1L, registrationAfterCommit.get());
|
||||||
assertFalse(JigsawStudioService.claimInitialEvaluation(registrationAfterCommit, true));
|
assertFalse(JigsawStudioEvaluator.claimInitialEvaluation(registrationAfterCommit, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -339,9 +339,9 @@ public class JigsawStudioLifecycleTest {
|
|||||||
JigsawStudioBay blank = layout.get("workcell/blank");
|
JigsawStudioBay blank = layout.get("workcell/blank");
|
||||||
|
|
||||||
assertTrue(session.selectedBayId().isEmpty());
|
assertTrue(session.selectedBayId().isEmpty());
|
||||||
assertTrue(JigsawStudioService.selectEnteredWorkcell(session, end, true));
|
assertTrue(JigsawStudioPlayerContext.selectEnteredWorkcell(session, end, true));
|
||||||
assertEquals(end.stableId(), session.selectedBayId().orElseThrow());
|
assertEquals(end.stableId(), session.selectedBayId().orElseThrow());
|
||||||
assertFalse(JigsawStudioService.selectEnteredWorkcell(session, blank, false));
|
assertFalse(JigsawStudioPlayerContext.selectEnteredWorkcell(session, blank, false));
|
||||||
assertEquals(end.stableId(), session.selectedBayId().orElseThrow());
|
assertEquals(end.stableId(), session.selectedBayId().orElseThrow());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -571,14 +571,14 @@ public class JigsawStudioMenuControllerTest {
|
|||||||
when(world.getUID()).thenReturn(worldId);
|
when(world.getUID()).thenReturn(worldId);
|
||||||
when(location.getWorld()).thenReturn(world);
|
when(location.getWorld()).thenReturn(world);
|
||||||
|
|
||||||
Field menuField = JigsawStudioService.class.getDeclaredField("menuController");
|
Field menuField = JigsawStudioToolbelt.class.getDeclaredField("menuController");
|
||||||
menuField.setAccessible(true);
|
menuField.setAccessible(true);
|
||||||
menuField.set(service, menu);
|
menuField.set(service.toolbelt, menu);
|
||||||
IrisServices.register(BoardSVC.class, board);
|
IrisServices.register(BoardSVC.class, board);
|
||||||
try (MockedStatic<J> scheduling = mockStatic(J.class)) {
|
try (MockedStatic<J> scheduling = mockStatic(J.class)) {
|
||||||
scheduling.when(() -> J.isOwnedByCurrentRegion(player)).thenReturn(true);
|
scheduling.when(() -> J.isOwnedByCurrentRegion(player)).thenReturn(true);
|
||||||
|
|
||||||
service.onPlayerChangedWorld(event);
|
service.protectionListener.onPlayerChangedWorld(event);
|
||||||
|
|
||||||
verify(menu).close(player);
|
verify(menu).close(player);
|
||||||
verify(board).clearJigsawContext(player);
|
verify(board).clearJigsawContext(player);
|
||||||
|
|||||||
+2
-2
@@ -79,10 +79,10 @@ public class JigsawStudioPreviewRendererTest {
|
|||||||
10,
|
10,
|
||||||
IrisObjectRotation.of(0, 0, 0));
|
IrisObjectRotation.of(0, 0, 0));
|
||||||
|
|
||||||
PlacedStructurePiece planar = JigsawStudioService.alignPreviewPieces(
|
PlacedStructurePiece planar = JigsawStudioEvaluator.alignPreviewPieces(
|
||||||
List.of(source),
|
List.of(source),
|
||||||
JigsawStudioMode.PLANAR_JIGSAW).getFirst();
|
JigsawStudioMode.PLANAR_JIGSAW).getFirst();
|
||||||
PlacedStructurePiece spatial = JigsawStudioService.alignPreviewPieces(
|
PlacedStructurePiece spatial = JigsawStudioEvaluator.alignPreviewPieces(
|
||||||
List.of(source),
|
List.of(source),
|
||||||
JigsawStudioMode.SPATIAL_JIGSAW).getFirst();
|
JigsawStudioMode.SPATIAL_JIGSAW).getFirst();
|
||||||
|
|
||||||
|
|||||||
+164
-164
@@ -197,11 +197,11 @@ public class JigsawStudioServiceCaptureTest {
|
|||||||
when(state.isCustom()).thenReturn(false);
|
when(state.isCustom()).thenReturn(false);
|
||||||
when(state.nativeHandle()).thenReturn(blockData);
|
when(state.nativeHandle()).thenReturn(blockData);
|
||||||
|
|
||||||
JigsawStudioService.restoreConnectorChunk(
|
JigsawStudioChunkWriter.restoreConnectorChunk(
|
||||||
world,
|
world,
|
||||||
workcell,
|
workcell,
|
||||||
List.of(renderedConnector),
|
List.of(renderedConnector),
|
||||||
Map.of(new JigsawStudioService.LocalPosition(1, 2, 3), renderedBlock),
|
Map.of(new JigsawStudioCapture.LocalPosition(1, 2, 3), renderedBlock),
|
||||||
false);
|
false);
|
||||||
|
|
||||||
verify(world).getBlockAt(11, 22, 33);
|
verify(world).getBlockAt(11, 22, 33);
|
||||||
@@ -229,9 +229,9 @@ public class JigsawStudioServiceCaptureTest {
|
|||||||
expandedSpecs,
|
expandedSpecs,
|
||||||
JigsawStudioVariantCatalog.empty());
|
JigsawStudioVariantCatalog.empty());
|
||||||
|
|
||||||
assertFalse(JigsawStudioService.layoutGeometryChanged(original, original));
|
assertFalse(JigsawStudioGraphMutations.layoutGeometryChanged(original, original));
|
||||||
assertTrue(JigsawStudioService.layoutGeometryChanged(original, expanded));
|
assertTrue(JigsawStudioGraphMutations.layoutGeometryChanged(original, expanded));
|
||||||
Set<Long> chunks = JigsawStudioService.relayoutChunks(original, expanded);
|
Set<Long> chunks = JigsawStudioGraphMutations.relayoutChunks(original, expanded);
|
||||||
assertTrue(chunks.contains(0L));
|
assertTrue(chunks.contains(0L));
|
||||||
assertTrue(chunks.contains(((long) 4 << 32)));
|
assertTrue(chunks.contains(((long) 4 << 32)));
|
||||||
}
|
}
|
||||||
@@ -248,8 +248,8 @@ public class JigsawStudioServiceCaptureTest {
|
|||||||
dimensions,
|
dimensions,
|
||||||
new JigsawStudioVariantCatalog(List.of(), false));
|
new JigsawStudioVariantCatalog(List.of(), false));
|
||||||
|
|
||||||
assertTrue(JigsawStudioService.canCreateVariants(editable));
|
assertTrue(JigsawStudioProtection.canCreateVariants(editable));
|
||||||
assertFalse(JigsawStudioService.canCreateVariants(managed));
|
assertFalse(JigsawStudioProtection.canCreateVariants(managed));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -269,27 +269,27 @@ public class JigsawStudioServiceCaptureTest {
|
|||||||
new JigsawStudioPoolMembership("test/pieces", 0, 4, 0.35D),
|
new JigsawStudioPoolMembership("test/pieces", 0, 4, 0.35D),
|
||||||
new JigsawStudioPoolMembership("test/caps", 0, 7, 0.8D)));
|
new JigsawStudioPoolMembership("test/caps", 0, 7, 0.8D)));
|
||||||
|
|
||||||
assertEquals("", JigsawStudioService.variantCreationSourceFailure(assigned, false));
|
assertEquals("", JigsawStudioProtection.variantCreationSourceFailure(assigned, false));
|
||||||
assertEquals("", JigsawStudioService.variantCreationSourceFailure(assigned, true));
|
assertEquals("", JigsawStudioProtection.variantCreationSourceFailure(assigned, true));
|
||||||
assertTrue(JigsawStudioService.variantCreationSourceFailure(null, false)
|
assertTrue(JigsawStudioProtection.variantCreationSourceFailure(null, false)
|
||||||
.contains("piece create <poolKey> <pieceKey>"));
|
.contains("piece create <poolKey> <pieceKey>"));
|
||||||
assertTrue(JigsawStudioService.variantCreationSourceFailure(planarVariant(true, true), false)
|
assertTrue(JigsawStudioProtection.variantCreationSourceFailure(planarVariant(true, true), false)
|
||||||
.contains("no owned pool membership"));
|
.contains("no owned pool membership"));
|
||||||
assertTrue(JigsawStudioService.variantCreationSourceFailure(planarVariant(true, false), true)
|
assertTrue(JigsawStudioProtection.variantCreationSourceFailure(planarVariant(true, false), true)
|
||||||
.contains("read-only variant"));
|
.contains("read-only variant"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void recognizesVanillaNamespacedAndWorldEditMutationCommands() {
|
public void recognizesVanillaNamespacedAndWorldEditMutationCommands() {
|
||||||
assertTrue(JigsawStudioService.isMutatingCommand("/fill 0 0 0 1 1 1 stone"));
|
assertTrue(JigsawStudioProtection.isMutatingCommand("/fill 0 0 0 1 1 1 stone"));
|
||||||
assertTrue(JigsawStudioService.isMutatingCommand("minecraft:setblock 0 0 0 air"));
|
assertTrue(JigsawStudioProtection.isMutatingCommand("minecraft:setblock 0 0 0 air"));
|
||||||
assertTrue(JigsawStudioService.isMutatingCommand("//paste -a"));
|
assertTrue(JigsawStudioProtection.isMutatingCommand("//paste -a"));
|
||||||
assertTrue(JigsawStudioService.isMutatingCommand("/execute as @s run setblock 0 0 0 stone"));
|
assertTrue(JigsawStudioProtection.isMutatingCommand("/execute as @s run setblock 0 0 0 stone"));
|
||||||
assertTrue(JigsawStudioService.isMutatingCommand("/function test:build"));
|
assertTrue(JigsawStudioProtection.isMutatingCommand("/function test:build"));
|
||||||
assertTrue(JigsawStudioService.isMutatingCommand("/data merge block 0 0 0 {}"));
|
assertTrue(JigsawStudioProtection.isMutatingCommand("/data merge block 0 0 0 {}"));
|
||||||
assertTrue(JigsawStudioService.isMutatingCommand("/item replace block 0 0 0 container.0 with stone"));
|
assertTrue(JigsawStudioProtection.isMutatingCommand("/item replace block 0 0 0 container.0 with stone"));
|
||||||
assertFalse(JigsawStudioService.isMutatingCommand("/iris jigsaw status"));
|
assertFalse(JigsawStudioProtection.isMutatingCommand("/iris jigsaw status"));
|
||||||
assertFalse(JigsawStudioService.isMutatingCommand("/tp 0 80 0"));
|
assertFalse(JigsawStudioProtection.isMutatingCommand("/tp 0 80 0"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -297,28 +297,28 @@ public class JigsawStudioServiceCaptureTest {
|
|||||||
UUID ownerId = UUID.fromString("11111111-1111-1111-1111-111111111111");
|
UUID ownerId = UUID.fromString("11111111-1111-1111-1111-111111111111");
|
||||||
UUID otherId = UUID.fromString("22222222-2222-2222-2222-222222222222");
|
UUID otherId = UUID.fromString("22222222-2222-2222-2222-222222222222");
|
||||||
|
|
||||||
assertTrue(JigsawStudioService.ownerMatches(ownerId, ownerId));
|
assertTrue(JigsawStudioProtection.ownerMatches(ownerId, ownerId));
|
||||||
assertTrue(JigsawStudioService.ownerMatches(null, otherId));
|
assertTrue(JigsawStudioProtection.ownerMatches(null, otherId));
|
||||||
assertFalse(JigsawStudioService.ownerMatches(ownerId, otherId));
|
assertFalse(JigsawStudioProtection.ownerMatches(ownerId, otherId));
|
||||||
assertTrue(JigsawStudioService.blocksStudioEdit(ownerId, otherId));
|
assertTrue(JigsawStudioProtection.blocksStudioEdit(ownerId, otherId));
|
||||||
assertFalse(JigsawStudioService.blocksStudioEdit(ownerId, ownerId));
|
assertFalse(JigsawStudioProtection.blocksStudioEdit(ownerId, ownerId));
|
||||||
assertFalse(JigsawStudioService.blocksStudioEdit(null, otherId));
|
assertFalse(JigsawStudioProtection.blocksStudioEdit(null, otherId));
|
||||||
assertTrue(JigsawStudioService.blocksMutatingCommand(ownerId, otherId, "//paste -a"));
|
assertTrue(JigsawStudioProtection.blocksMutatingCommand(ownerId, otherId, "//paste -a"));
|
||||||
assertFalse(JigsawStudioService.blocksMutatingCommand(ownerId, ownerId, "//paste -a"));
|
assertFalse(JigsawStudioProtection.blocksMutatingCommand(ownerId, ownerId, "//paste -a"));
|
||||||
assertFalse(JigsawStudioService.blocksMutatingCommand(null, otherId, "//paste -a"));
|
assertFalse(JigsawStudioProtection.blocksMutatingCommand(null, otherId, "//paste -a"));
|
||||||
assertFalse(JigsawStudioService.blocksMutatingCommand(ownerId, otherId, "/iris jigsaw status"));
|
assertFalse(JigsawStudioProtection.blocksMutatingCommand(ownerId, otherId, "/iris jigsaw status"));
|
||||||
assertFalse(JigsawStudioService.blocksMutatingCommand(ownerId, otherId, "/msg owner hello"));
|
assertFalse(JigsawStudioProtection.blocksMutatingCommand(ownerId, otherId, "/msg owner hello"));
|
||||||
assertTrue(JigsawStudioService.blocksMutatingCommand(ownerId, otherId, "/execute run say bypass"));
|
assertTrue(JigsawStudioProtection.blocksMutatingCommand(ownerId, otherId, "/execute run say bypass"));
|
||||||
assertTrue(JigsawStudioService.blocksMutatingCommand(ownerId, otherId, "/unknownplugin mutate"));
|
assertTrue(JigsawStudioProtection.blocksMutatingCommand(ownerId, otherId, "/unknownplugin mutate"));
|
||||||
assertTrue(JigsawStudioService.blocksNonEditableWorkcellMutation(
|
assertTrue(JigsawStudioProtection.blocksNonEditableWorkcellMutation(
|
||||||
true, "/fill 0 0 0 1 1 1 stone"));
|
true, "/fill 0 0 0 1 1 1 stone"));
|
||||||
assertTrue(JigsawStudioService.blocksNonEditableWorkcellMutation(true, "//paste -a"));
|
assertTrue(JigsawStudioProtection.blocksNonEditableWorkcellMutation(true, "//paste -a"));
|
||||||
assertFalse(JigsawStudioService.blocksNonEditableWorkcellMutation(
|
assertFalse(JigsawStudioProtection.blocksNonEditableWorkcellMutation(
|
||||||
true, "/iris jigsaw variant load test/end"));
|
true, "/iris jigsaw variant load test/end"));
|
||||||
assertFalse(JigsawStudioService.blocksNonEditableWorkcellMutation(
|
assertFalse(JigsawStudioProtection.blocksNonEditableWorkcellMutation(
|
||||||
false, "/fill 0 0 0 1 1 1 stone"));
|
false, "/fill 0 0 0 1 1 1 stone"));
|
||||||
|
|
||||||
Method method = JigsawStudioService.class.getMethod(
|
Method method = JigsawStudioProtectionListener.class.getMethod(
|
||||||
"onUnauthorizedPlayerCommand",
|
"onUnauthorizedPlayerCommand",
|
||||||
PlayerCommandPreprocessEvent.class);
|
PlayerCommandPreprocessEvent.class);
|
||||||
EventHandler handler = method.getAnnotation(EventHandler.class);
|
EventHandler handler = method.getAnnotation(EventHandler.class);
|
||||||
@@ -340,21 +340,21 @@ public class JigsawStudioServiceCaptureTest {
|
|||||||
UUID ownerId = UUID.fromString("11111111-1111-1111-1111-111111111111");
|
UUID ownerId = UUID.fromString("11111111-1111-1111-1111-111111111111");
|
||||||
UUID otherId = UUID.fromString("22222222-2222-2222-2222-222222222222");
|
UUID otherId = UUID.fromString("22222222-2222-2222-2222-222222222222");
|
||||||
|
|
||||||
assertFalse(JigsawStudioService.blocksStudioInventoryMutation(
|
assertFalse(JigsawStudioProtection.blocksStudioInventoryMutation(
|
||||||
false, false, true, true, ownerId, otherId));
|
false, false, true, true, ownerId, otherId));
|
||||||
assertFalse(JigsawStudioService.blocksStudioInventoryMutation(
|
assertFalse(JigsawStudioProtection.blocksStudioInventoryMutation(
|
||||||
true, false, false, false, ownerId, ownerId));
|
true, false, false, false, ownerId, ownerId));
|
||||||
assertTrue(JigsawStudioService.blocksStudioInventoryMutation(
|
assertTrue(JigsawStudioProtection.blocksStudioInventoryMutation(
|
||||||
true, false, true, false, ownerId, ownerId));
|
true, false, true, false, ownerId, ownerId));
|
||||||
assertTrue(JigsawStudioService.blocksStudioInventoryMutation(
|
assertTrue(JigsawStudioProtection.blocksStudioInventoryMutation(
|
||||||
true, false, false, true, ownerId, ownerId));
|
true, false, false, true, ownerId, ownerId));
|
||||||
assertTrue(JigsawStudioService.blocksStudioInventoryMutation(
|
assertTrue(JigsawStudioProtection.blocksStudioInventoryMutation(
|
||||||
true, false, false, false, ownerId, otherId));
|
true, false, false, false, ownerId, otherId));
|
||||||
assertFalse(JigsawStudioService.blocksStudioInventoryMutation(
|
assertFalse(JigsawStudioProtection.blocksStudioInventoryMutation(
|
||||||
true, false, false, false, ownerId, null));
|
true, false, false, false, ownerId, null));
|
||||||
assertTrue(JigsawStudioService.blocksStudioInventoryMutation(
|
assertTrue(JigsawStudioProtection.blocksStudioInventoryMutation(
|
||||||
true, true, false, false, ownerId, null));
|
true, true, false, false, ownerId, null));
|
||||||
assertTrue(JigsawStudioService.blocksStudioInventoryMutation(
|
assertTrue(JigsawStudioProtection.blocksStudioInventoryMutation(
|
||||||
true, true, false, false, ownerId, ownerId));
|
true, true, false, false, ownerId, ownerId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -394,17 +394,17 @@ public class JigsawStudioServiceCaptureTest {
|
|||||||
KMap<String, Object> changedAgain = new KMap<>();
|
KMap<String, Object> changedAgain = new KMap<>();
|
||||||
changedAgain.put("name", "iris:cap");
|
changedAgain.put("name", "iris:cap");
|
||||||
|
|
||||||
assertFalse(JigsawStudioService.tileSnapshotChanged(baseline, unchanged));
|
assertFalse(JigsawStudioTileWatcher.tileSnapshotChanged(baseline, unchanged));
|
||||||
assertTrue(JigsawStudioService.tileSnapshotChanged(baseline, changed));
|
assertTrue(JigsawStudioTileWatcher.tileSnapshotChanged(baseline, changed));
|
||||||
assertEquals(
|
assertEquals(
|
||||||
new JigsawStudioService.JigsawTilePollDecision(true, true),
|
new JigsawStudioTileWatcher.JigsawTilePollDecision(true, true),
|
||||||
JigsawStudioService.jigsawTilePollDecision(baseline, changed, false));
|
JigsawStudioTileWatcher.jigsawTilePollDecision(baseline, changed, false));
|
||||||
assertEquals(
|
assertEquals(
|
||||||
new JigsawStudioService.JigsawTilePollDecision(true, true),
|
new JigsawStudioTileWatcher.JigsawTilePollDecision(true, true),
|
||||||
JigsawStudioService.jigsawTilePollDecision(changed, changedAgain, false));
|
JigsawStudioTileWatcher.jigsawTilePollDecision(changed, changedAgain, false));
|
||||||
assertEquals(
|
assertEquals(
|
||||||
new JigsawStudioService.JigsawTilePollDecision(false, false),
|
new JigsawStudioTileWatcher.JigsawTilePollDecision(false, false),
|
||||||
JigsawStudioService.jigsawTilePollDecision(changedAgain, changedAgain, true));
|
JigsawStudioTileWatcher.jigsawTilePollDecision(changedAgain, changedAgain, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -412,14 +412,14 @@ public class JigsawStudioServiceCaptureTest {
|
|||||||
UUID requestId = UUID.fromString("33333333-3333-3333-3333-333333333333");
|
UUID requestId = UUID.fromString("33333333-3333-3333-3333-333333333333");
|
||||||
IOException validationFailure = new IOException("closure rejected an empty piece entry");
|
IOException validationFailure = new IOException("closure rejected an empty piece entry");
|
||||||
String validationDetail = "Jigsaw Studio save failed: closure rejected an empty piece entry";
|
String validationDetail = "Jigsaw Studio save failed: closure rejected an empty piece entry";
|
||||||
String validationContext = JigsawStudioService.autosaveFailureContext(
|
String validationContext = JigsawStudioAutosaveScheduler.autosaveFailureContext(
|
||||||
requestId,
|
requestId,
|
||||||
"qa/profile_final",
|
"qa/profile_final",
|
||||||
"workcell/end",
|
"workcell/end",
|
||||||
"qa/profile_final/end",
|
"qa/profile_final/end",
|
||||||
validationDetail);
|
validationDetail);
|
||||||
JigsawStudioService.AutosaveFailureState validationState =
|
JigsawStudioAutosaveScheduler.AutosaveFailureState validationState =
|
||||||
new JigsawStudioService.AutosaveFailureState();
|
new JigsawStudioAutosaveScheduler.AutosaveFailureState();
|
||||||
StructureWriteResult writerResult = new StructureWriteResult(
|
StructureWriteResult writerResult = new StructureWriteResult(
|
||||||
StructureWriteResult.Status.OWNERSHIP_CONFLICT,
|
StructureWriteResult.Status.OWNERSHIP_CONFLICT,
|
||||||
StructureWriteResult.Action.NONE,
|
StructureWriteResult.Action.NONE,
|
||||||
@@ -430,18 +430,18 @@ public class JigsawStudioServiceCaptureTest {
|
|||||||
"",
|
"",
|
||||||
Optional.empty());
|
Optional.empty());
|
||||||
String writerDetail = JigsawStudioService.writeFailure(writerResult);
|
String writerDetail = JigsawStudioService.writeFailure(writerResult);
|
||||||
String writerContext = JigsawStudioService.autosaveFailureContext(
|
String writerContext = JigsawStudioAutosaveScheduler.autosaveFailureContext(
|
||||||
requestId,
|
requestId,
|
||||||
"qa/profile_final",
|
"qa/profile_final",
|
||||||
"workcell/end",
|
"workcell/end",
|
||||||
"qa/profile_final/end",
|
"qa/profile_final/end",
|
||||||
writerDetail);
|
writerDetail);
|
||||||
JigsawStudioService.AutosaveFailureState writerState =
|
JigsawStudioAutosaveScheduler.AutosaveFailureState writerState =
|
||||||
new JigsawStudioService.AutosaveFailureState();
|
new JigsawStudioAutosaveScheduler.AutosaveFailureState();
|
||||||
|
|
||||||
try (MockedStatic<IrisLogging> logging = mockStatic(IrisLogging.class)) {
|
try (MockedStatic<IrisLogging> logging = mockStatic(IrisLogging.class)) {
|
||||||
JigsawStudioService.AutosaveFailureDecision firstValidation =
|
JigsawStudioAutosaveScheduler.AutosaveFailureDecision firstValidation =
|
||||||
JigsawStudioService.recordPersistentAutosaveFailure(
|
JigsawStudioAutosaveScheduler.recordPersistentAutosaveFailure(
|
||||||
validationState,
|
validationState,
|
||||||
requestId,
|
requestId,
|
||||||
"qa/profile_final",
|
"qa/profile_final",
|
||||||
@@ -449,8 +449,8 @@ public class JigsawStudioServiceCaptureTest {
|
|||||||
"qa/profile_final/end",
|
"qa/profile_final/end",
|
||||||
validationDetail,
|
validationDetail,
|
||||||
validationFailure);
|
validationFailure);
|
||||||
JigsawStudioService.AutosaveFailureDecision secondValidation =
|
JigsawStudioAutosaveScheduler.AutosaveFailureDecision secondValidation =
|
||||||
JigsawStudioService.recordPersistentAutosaveFailure(
|
JigsawStudioAutosaveScheduler.recordPersistentAutosaveFailure(
|
||||||
validationState,
|
validationState,
|
||||||
requestId,
|
requestId,
|
||||||
"qa/profile_final",
|
"qa/profile_final",
|
||||||
@@ -460,7 +460,7 @@ public class JigsawStudioServiceCaptureTest {
|
|||||||
validationFailure);
|
validationFailure);
|
||||||
List<Integer> writerDelays = new ArrayList<>();
|
List<Integer> writerDelays = new ArrayList<>();
|
||||||
for (int attempt = 0; attempt < 7; attempt++) {
|
for (int attempt = 0; attempt < 7; attempt++) {
|
||||||
writerDelays.add(JigsawStudioService.recordPersistentAutosaveFailure(
|
writerDelays.add(JigsawStudioAutosaveScheduler.recordPersistentAutosaveFailure(
|
||||||
writerState,
|
writerState,
|
||||||
requestId,
|
requestId,
|
||||||
"qa/profile_final",
|
"qa/profile_final",
|
||||||
@@ -481,8 +481,8 @@ public class JigsawStudioServiceCaptureTest {
|
|||||||
logging.verify(() -> IrisLogging.warn("%s", writerContext), times(1));
|
logging.verify(() -> IrisLogging.warn("%s", writerContext), times(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
JigsawStudioService.AutosaveFailureDecision newIdentity =
|
JigsawStudioAutosaveScheduler.AutosaveFailureDecision newIdentity =
|
||||||
new JigsawStudioService.AutosaveFailureState().recordPersistentFailure();
|
new JigsawStudioAutosaveScheduler.AutosaveFailureState().recordPersistentFailure();
|
||||||
assertEquals(40, newIdentity.retryTicks());
|
assertEquals(40, newIdentity.retryTicks());
|
||||||
assertTrue(newIdentity.logFailure());
|
assertTrue(newIdentity.logFailure());
|
||||||
}
|
}
|
||||||
@@ -545,11 +545,11 @@ public class JigsawStudioServiceCaptureTest {
|
|||||||
when(generator.getLayout()).thenAnswer(invocation -> session.layout());
|
when(generator.getLayout()).thenAnswer(invocation -> session.layout());
|
||||||
World world = mock(World.class);
|
World world = mock(World.class);
|
||||||
when(world.getUID()).thenReturn(worldId);
|
when(world.getUID()).thenReturn(worldId);
|
||||||
ConcurrentHashMap<String, JigsawStudioService.BayPopulation> populations = new ConcurrentHashMap<>();
|
ConcurrentHashMap<String, JigsawStudioChunkWriter.BayPopulation> populations = new ConcurrentHashMap<>();
|
||||||
JigsawStudioService.BayPopulation endPopulation =
|
JigsawStudioChunkWriter.BayPopulation endPopulation =
|
||||||
new JigsawStudioService.BayPopulation(Set.of(0L), "");
|
new JigsawStudioChunkWriter.BayPopulation(Set.of(0L), "");
|
||||||
JigsawStudioService.BayPopulation straightPopulation =
|
JigsawStudioChunkWriter.BayPopulation straightPopulation =
|
||||||
new JigsawStudioService.BayPopulation(Set.of(0L), "");
|
new JigsawStudioChunkWriter.BayPopulation(Set.of(0L), "");
|
||||||
endPopulation.markFullyReady();
|
endPopulation.markFullyReady();
|
||||||
straightPopulation.markFullyReady();
|
straightPopulation.markFullyReady();
|
||||||
populations.put(initialEnd.stableId(), endPopulation);
|
populations.put(initialEnd.stableId(), endPopulation);
|
||||||
@@ -572,7 +572,7 @@ public class JigsawStudioServiceCaptureTest {
|
|||||||
populations,
|
populations,
|
||||||
ConcurrentHashMap.newKeySet(),
|
ConcurrentHashMap.newKeySet(),
|
||||||
new AtomicLong());
|
new AtomicLong());
|
||||||
Method scheduleAutosave = JigsawStudioService.class.getDeclaredMethod(
|
Method scheduleAutosave = JigsawStudioAutosaveScheduler.class.getDeclaredMethod(
|
||||||
"scheduleAutosave",
|
"scheduleAutosave",
|
||||||
studioType,
|
studioType,
|
||||||
JigsawStudioBay.class,
|
JigsawStudioBay.class,
|
||||||
@@ -606,8 +606,8 @@ public class JigsawStudioServiceCaptureTest {
|
|||||||
anyInt(),
|
anyInt(),
|
||||||
any(Runnable.class)))
|
any(Runnable.class)))
|
||||||
.thenReturn(true);
|
.thenReturn(true);
|
||||||
scheduleAutosave.invoke(service, studio, initialEnd, endDirty, 40);
|
scheduleAutosave.invoke(service.autosaveScheduler, studio, initialEnd, endDirty, 40);
|
||||||
scheduleAutosave.invoke(service, studio, initialStraight, straightDirty, 40);
|
scheduleAutosave.invoke(service.autosaveScheduler, studio, initialStraight, straightDirty, 40);
|
||||||
assertEquals(2, delayedAutosaves.size());
|
assertEquals(2, delayedAutosaves.size());
|
||||||
|
|
||||||
JigsawStudioSession.SaveIdentity committedEnd = session.beginSave(initialEnd.stableId())
|
JigsawStudioSession.SaveIdentity committedEnd = session.beginSave(initialEnd.stableId())
|
||||||
@@ -619,7 +619,7 @@ public class JigsawStudioServiceCaptureTest {
|
|||||||
dimensions,
|
dimensions,
|
||||||
catalog);
|
catalog);
|
||||||
assertTrue(session.replaceLayout(replacementLayout));
|
assertTrue(session.replaceLayout(replacementLayout));
|
||||||
JigsawStudioBay currentStraight = JigsawStudioService.resolveCurrentAutosaveBay(
|
JigsawStudioBay currentStraight = JigsawStudioAutosaveScheduler.resolveCurrentAutosaveBay(
|
||||||
session,
|
session,
|
||||||
initialStraight.stableId());
|
initialStraight.stableId());
|
||||||
assertNotSame(initialStraight, currentStraight);
|
assertNotSame(initialStraight, currentStraight);
|
||||||
@@ -676,10 +676,10 @@ public class JigsawStudioServiceCaptureTest {
|
|||||||
when(player.getWorld()).thenReturn(world);
|
when(player.getWorld()).thenReturn(world);
|
||||||
|
|
||||||
JigsawStudioService service = new JigsawStudioService();
|
JigsawStudioService service = new JigsawStudioService();
|
||||||
ConcurrentHashMap<String, JigsawStudioService.BayPopulation> populations = new ConcurrentHashMap<>();
|
ConcurrentHashMap<String, JigsawStudioChunkWriter.BayPopulation> populations = new ConcurrentHashMap<>();
|
||||||
populations.put(
|
populations.put(
|
||||||
bay.stableId(),
|
bay.stableId(),
|
||||||
new JigsawStudioService.BayPopulation(Set.of(0L), "workcell hydration failed"));
|
new JigsawStudioChunkWriter.BayPopulation(Set.of(0L), "workcell hydration failed"));
|
||||||
Class<?> studioType = Class.forName(JigsawStudioService.class.getName() + "$ActiveStudio");
|
Class<?> studioType = Class.forName(JigsawStudioService.class.getName() + "$ActiveStudio");
|
||||||
Constructor<?> studioConstructor = studioType.getDeclaredConstructor(
|
Constructor<?> studioConstructor = studioType.getDeclaredConstructor(
|
||||||
UUID.class,
|
UUID.class,
|
||||||
@@ -704,11 +704,11 @@ public class JigsawStudioServiceCaptureTest {
|
|||||||
Map<UUID, Object> studios = (Map<UUID, Object>) studiosField.get(service);
|
Map<UUID, Object> studios = (Map<UUID, Object>) studiosField.get(service);
|
||||||
studios.put(worldId, studio);
|
studios.put(worldId, studio);
|
||||||
|
|
||||||
Class<?> keyType = Class.forName(JigsawStudioService.class.getName() + "$AutosaveKey");
|
Class<?> keyType = Class.forName(JigsawStudioAutosaveScheduler.class.getName() + "$AutosaveKey");
|
||||||
Constructor<?> keyConstructor = keyType.getDeclaredConstructor(UUID.class, String.class);
|
Constructor<?> keyConstructor = keyType.getDeclaredConstructor(UUID.class, String.class);
|
||||||
keyConstructor.setAccessible(true);
|
keyConstructor.setAccessible(true);
|
||||||
Object key = keyConstructor.newInstance(requestId, bay.stableId());
|
Object key = keyConstructor.newInstance(requestId, bay.stableId());
|
||||||
Class<?> ticketType = Class.forName(JigsawStudioService.class.getName() + "$AutosaveTicket");
|
Class<?> ticketType = Class.forName(JigsawStudioAutosaveScheduler.class.getName() + "$AutosaveTicket");
|
||||||
Constructor<?> ticketConstructor = ticketType.getDeclaredConstructor(
|
Constructor<?> ticketConstructor = ticketType.getDeclaredConstructor(
|
||||||
keyType,
|
keyType,
|
||||||
studioType,
|
studioType,
|
||||||
@@ -722,15 +722,15 @@ public class JigsawStudioServiceCaptureTest {
|
|||||||
identity,
|
identity,
|
||||||
new AtomicBoolean(false),
|
new AtomicBoolean(false),
|
||||||
new AtomicBoolean(false));
|
new AtomicBoolean(false));
|
||||||
Field autosavesField = JigsawStudioService.class.getDeclaredField("autosaves");
|
Field autosavesField = JigsawStudioAutosaveScheduler.class.getDeclaredField("autosaves");
|
||||||
autosavesField.setAccessible(true);
|
autosavesField.setAccessible(true);
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
Map<Object, Object> autosaves = (Map<Object, Object>) autosavesField.get(service);
|
Map<Object, Object> autosaves = (Map<Object, Object>) autosavesField.get(service.autosaveScheduler);
|
||||||
autosaves.put(key, ticket);
|
autosaves.put(key, ticket);
|
||||||
Method failureStateAccessor = ticketType.getDeclaredMethod("failureState");
|
Method failureStateAccessor = ticketType.getDeclaredMethod("failureState");
|
||||||
failureStateAccessor.setAccessible(true);
|
failureStateAccessor.setAccessible(true);
|
||||||
JigsawStudioService.AutosaveFailureState failureState =
|
JigsawStudioAutosaveScheduler.AutosaveFailureState failureState =
|
||||||
(JigsawStudioService.AutosaveFailureState) failureStateAccessor.invoke(ticket);
|
(JigsawStudioAutosaveScheduler.AutosaveFailureState) failureStateAccessor.invoke(ticket);
|
||||||
assertEquals(40, failureState.recordPersistentFailure().retryTicks());
|
assertEquals(40, failureState.recordPersistentFailure().retryTicks());
|
||||||
|
|
||||||
try (MockedStatic<J> scheduling = mockStatic(J.class)) {
|
try (MockedStatic<J> scheduling = mockStatic(J.class)) {
|
||||||
@@ -745,13 +745,13 @@ public class JigsawStudioServiceCaptureTest {
|
|||||||
|
|
||||||
populations.put(
|
populations.put(
|
||||||
bay.stableId(),
|
bay.stableId(),
|
||||||
new JigsawStudioService.BayPopulation(Set.of(0L), ""));
|
new JigsawStudioChunkWriter.BayPopulation(Set.of(0L), ""));
|
||||||
assertFalse(service.flushAutosave(player, bay.stableId()));
|
assertFalse(service.flushAutosave(player, bay.stableId()));
|
||||||
assertEquals(1, autosaves.size());
|
assertEquals(1, autosaves.size());
|
||||||
assertTrue(session.isDirtyCurrent(identity));
|
assertTrue(session.isDirtyCurrent(identity));
|
||||||
|
|
||||||
JigsawStudioService.BayPopulation ready =
|
JigsawStudioChunkWriter.BayPopulation ready =
|
||||||
new JigsawStudioService.BayPopulation(Set.of(0L), "");
|
new JigsawStudioChunkWriter.BayPopulation(Set.of(0L), "");
|
||||||
ready.markFullyReady();
|
ready.markFullyReady();
|
||||||
populations.put(bay.stableId(), ready);
|
populations.put(bay.stableId(), ready);
|
||||||
JigsawStudioSession.SaveIdentity inProgress = session.beginSave(bay.stableId())
|
JigsawStudioSession.SaveIdentity inProgress = session.beginSave(bay.stableId())
|
||||||
@@ -773,13 +773,13 @@ public class JigsawStudioServiceCaptureTest {
|
|||||||
JigsawStudioVariant fixed = planarVariant(false, true);
|
JigsawStudioVariant fixed = planarVariant(false, true);
|
||||||
JigsawStudioVariant readOnly = planarVariant(true, false);
|
JigsawStudioVariant readOnly = planarVariant(true, false);
|
||||||
|
|
||||||
assertFalse(JigsawStudioService.canToggleVariantRotation(
|
assertFalse(JigsawStudioProtection.canToggleVariantRotation(
|
||||||
JigsawStudioCompatibilityTarget.VANILLA_PORTABLE, rotatable));
|
JigsawStudioCompatibilityTarget.VANILLA_PORTABLE, rotatable));
|
||||||
assertTrue(JigsawStudioService.canToggleVariantRotation(
|
assertTrue(JigsawStudioProtection.canToggleVariantRotation(
|
||||||
JigsawStudioCompatibilityTarget.VANILLA_PORTABLE, fixed));
|
JigsawStudioCompatibilityTarget.VANILLA_PORTABLE, fixed));
|
||||||
assertTrue(JigsawStudioService.canToggleVariantRotation(
|
assertTrue(JigsawStudioProtection.canToggleVariantRotation(
|
||||||
JigsawStudioCompatibilityTarget.IRIS_EXTENDED, rotatable));
|
JigsawStudioCompatibilityTarget.IRIS_EXTENDED, rotatable));
|
||||||
assertFalse(JigsawStudioService.canToggleVariantRotation(
|
assertFalse(JigsawStudioProtection.canToggleVariantRotation(
|
||||||
JigsawStudioCompatibilityTarget.IRIS_EXTENDED, readOnly));
|
JigsawStudioCompatibilityTarget.IRIS_EXTENDED, readOnly));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -793,22 +793,22 @@ public class JigsawStudioServiceCaptureTest {
|
|||||||
IrisJigsawConnector eastSource = connector().setDirection(IrisDirection.EAST_POSITIVE_X);
|
IrisJigsawConnector eastSource = connector().setDirection(IrisDirection.EAST_POSITIVE_X);
|
||||||
IrisJigsawConnector southDisplayed = connector().setDirection(IrisDirection.SOUTH_POSITIVE_Z);
|
IrisJigsawConnector southDisplayed = connector().setDirection(IrisDirection.SOUTH_POSITIVE_Z);
|
||||||
|
|
||||||
JigsawStudioService.requireWorkcellTopology(
|
JigsawStudioCapture.requireWorkcellTopology(
|
||||||
layout.get("workcell/end"),
|
layout.get("workcell/end"),
|
||||||
List.of(eastSource),
|
List.of(eastSource),
|
||||||
3);
|
3);
|
||||||
JigsawStudioService.WorkcellTopologyException wrongDirection = assertThrows(
|
JigsawStudioCapture.WorkcellTopologyException wrongDirection = assertThrows(
|
||||||
JigsawStudioService.WorkcellTopologyException.class,
|
JigsawStudioCapture.WorkcellTopologyException.class,
|
||||||
() -> JigsawStudioService.requireWorkcellTopology(
|
() -> JigsawStudioCapture.requireWorkcellTopology(
|
||||||
layout.get("workcell/end"),
|
layout.get("workcell/end"),
|
||||||
List.of(southDisplayed),
|
List.of(southDisplayed),
|
||||||
0));
|
0));
|
||||||
assertTrue(wrongDirection.getMessage().contains("south end (1 horizontal connector)"));
|
assertTrue(wrongDirection.getMessage().contains("south end (1 horizontal connector)"));
|
||||||
assertTrue(wrongDirection.getMessage().contains("Reset Connector Blocks"));
|
assertTrue(wrongDirection.getMessage().contains("Reset Connector Blocks"));
|
||||||
|
|
||||||
JigsawStudioService.WorkcellTopologyException missingTee = assertThrows(
|
JigsawStudioCapture.WorkcellTopologyException missingTee = assertThrows(
|
||||||
JigsawStudioService.WorkcellTopologyException.class,
|
JigsawStudioCapture.WorkcellTopologyException.class,
|
||||||
() -> JigsawStudioService.requireWorkcellTopology(
|
() -> JigsawStudioCapture.requireWorkcellTopology(
|
||||||
layout.get("workcell/tee"),
|
layout.get("workcell/tee"),
|
||||||
List.of(),
|
List.of(),
|
||||||
0));
|
0));
|
||||||
@@ -823,13 +823,13 @@ public class JigsawStudioServiceCaptureTest {
|
|||||||
64,
|
64,
|
||||||
0,
|
0,
|
||||||
new JigsawStudioCellDimensions(3, 1, 3));
|
new JigsawStudioCellDimensions(3, 1, 3));
|
||||||
List<JigsawStudioService.ChunkCaptureArea> areas = JigsawStudioService.chunkIntersections(bounds);
|
List<JigsawStudioCapture.ChunkCaptureArea> areas = JigsawStudioCapture.chunkIntersections(bounds);
|
||||||
PlatformBlockState sourceState = BukkitBlockState.of(directionalBlockData(BlockFace.NORTH));
|
PlatformBlockState sourceState = BukkitBlockState.of(directionalBlockData(BlockFace.NORTH));
|
||||||
for (int quarterTurns = 1; quarterTurns <= 3; quarterTurns++) {
|
for (int quarterTurns = 1; quarterTurns <= 3; quarterTurns++) {
|
||||||
IrisObjectRotation displayRotation = IrisObjectRotation.of(0, -90.0D * quarterTurns, 0);
|
IrisObjectRotation displayRotation = IrisObjectRotation.of(0, -90.0D * quarterTurns, 0);
|
||||||
PlatformBlockState displayedState = displayRotation.rotate(sourceState, 0, 0, 0);
|
PlatformBlockState displayedState = displayRotation.rotate(sourceState, 0, 0, 0);
|
||||||
IrisDirection displayedDirection = displayRotation.rotate(IrisDirection.NORTH_NEGATIVE_Z);
|
IrisDirection displayedDirection = displayRotation.rotate(IrisDirection.NORTH_NEGATIVE_Z);
|
||||||
JigsawStudioService.CapturedConnector connector = new JigsawStudioService.CapturedConnector(
|
JigsawStudioCapture.CapturedConnector connector = new JigsawStudioCapture.CapturedConnector(
|
||||||
1,
|
1,
|
||||||
0,
|
0,
|
||||||
1,
|
1,
|
||||||
@@ -843,14 +843,14 @@ public class JigsawStudioServiceCaptureTest {
|
|||||||
displayedState.key(),
|
displayedState.key(),
|
||||||
-3,
|
-3,
|
||||||
7);
|
7);
|
||||||
JigsawStudioService.ChunkSnapshot snapshot = new JigsawStudioService.ChunkSnapshot(
|
JigsawStudioCapture.ChunkSnapshot snapshot = new JigsawStudioCapture.ChunkSnapshot(
|
||||||
areas.getFirst(),
|
areas.getFirst(),
|
||||||
List.of(),
|
List.of(),
|
||||||
List.of(connector));
|
List.of(connector));
|
||||||
try (MockedStatic<B> blocks = mockStatic(B.class)) {
|
try (MockedStatic<B> blocks = mockStatic(B.class)) {
|
||||||
blocks.when(() -> B.getStateOrNull(
|
blocks.when(() -> B.getStateOrNull(
|
||||||
displayedState.key(), false)).thenReturn(displayedState);
|
displayedState.key(), false)).thenReturn(displayedState);
|
||||||
JigsawStudioService.Capture capture = JigsawStudioService.aggregateSnapshots(
|
JigsawStudioCapture.Capture capture = JigsawStudioCapture.aggregateSnapshots(
|
||||||
bounds,
|
bounds,
|
||||||
areas,
|
areas,
|
||||||
List.of(snapshot),
|
List.of(snapshot),
|
||||||
@@ -888,12 +888,12 @@ public class JigsawStudioServiceCaptureTest {
|
|||||||
when(block.getBlockData()).thenReturn(chestData);
|
when(block.getBlockData()).thenReturn(chestData);
|
||||||
World world = mock(World.class);
|
World world = mock(World.class);
|
||||||
when(world.getBlockAt(0, 64, 0)).thenReturn(block);
|
when(world.getBlockAt(0, 64, 0)).thenReturn(block);
|
||||||
JigsawStudioService.ChunkCaptureArea area = JigsawStudioService.chunkIntersections(bounds).getFirst();
|
JigsawStudioCapture.ChunkCaptureArea area = JigsawStudioCapture.chunkIntersections(bounds).getFirst();
|
||||||
|
|
||||||
JigsawStudioService.ChunkSnapshot snapshot;
|
JigsawStudioCapture.ChunkSnapshot snapshot;
|
||||||
try (MockedStatic<TileData> tiles = mockStatic(TileData.class)) {
|
try (MockedStatic<TileData> tiles = mockStatic(TileData.class)) {
|
||||||
tiles.when(() -> TileData.getTileState(block, false)).thenReturn(tileData);
|
tiles.when(() -> TileData.getTileState(block, false)).thenReturn(tileData);
|
||||||
snapshot = JigsawStudioService.captureChunkIntersection(
|
snapshot = JigsawStudioCapture.captureChunkIntersection(
|
||||||
world,
|
world,
|
||||||
bounds,
|
bounds,
|
||||||
piece,
|
piece,
|
||||||
@@ -902,7 +902,7 @@ public class JigsawStudioServiceCaptureTest {
|
|||||||
0,
|
0,
|
||||||
false);
|
false);
|
||||||
}
|
}
|
||||||
JigsawStudioService.Capture capture = JigsawStudioService.aggregateSnapshots(
|
JigsawStudioCapture.Capture capture = JigsawStudioCapture.aggregateSnapshots(
|
||||||
bounds,
|
bounds,
|
||||||
List.of(area),
|
List.of(area),
|
||||||
List.of(snapshot));
|
List.of(snapshot));
|
||||||
@@ -945,7 +945,7 @@ public class JigsawStudioServiceCaptureTest {
|
|||||||
IrisJigsawPiece source = pieceWithPlanarConnectors(dimensions, authoredOrders.get(index));
|
IrisJigsawPiece source = pieceWithPlanarConnectors(dimensions, authoredOrders.get(index));
|
||||||
List<IrisJigsawConnector> captured = planarConnectors(dimensions, captureOrders.get(index));
|
List<IrisJigsawConnector> captured = planarConnectors(dimensions, captureOrders.get(index));
|
||||||
|
|
||||||
List<IrisJigsawConnector> ordered = JigsawStudioService.preserveCapturedConnectorOrder(
|
List<IrisJigsawConnector> ordered = JigsawStudioCapture.preserveCapturedConnectorOrder(
|
||||||
source,
|
source,
|
||||||
captured);
|
captured);
|
||||||
|
|
||||||
@@ -974,7 +974,7 @@ public class JigsawStudioServiceCaptureTest {
|
|||||||
dimensions,
|
dimensions,
|
||||||
IrisDirection.EAST_POSITIVE_X);
|
IrisDirection.EAST_POSITIVE_X);
|
||||||
|
|
||||||
List<IrisJigsawConnector> ordered = JigsawStudioService.preserveCapturedConnectorOrder(
|
List<IrisJigsawConnector> ordered = JigsawStudioCapture.preserveCapturedConnectorOrder(
|
||||||
source,
|
source,
|
||||||
List.of(capturedEast, capturedNorth));
|
List.of(capturedEast, capturedNorth));
|
||||||
|
|
||||||
@@ -998,7 +998,7 @@ public class JigsawStudioServiceCaptureTest {
|
|||||||
dimensions,
|
dimensions,
|
||||||
IrisDirection.NORTH_NEGATIVE_Z);
|
IrisDirection.NORTH_NEGATIVE_Z);
|
||||||
|
|
||||||
List<IrisJigsawConnector> ordered = JigsawStudioService.preserveCapturedConnectorOrder(
|
List<IrisJigsawConnector> ordered = JigsawStudioCapture.preserveCapturedConnectorOrder(
|
||||||
source,
|
source,
|
||||||
List.of(capturedWest, capturedNorth));
|
List.of(capturedWest, capturedNorth));
|
||||||
|
|
||||||
@@ -1020,7 +1020,7 @@ public class JigsawStudioServiceCaptureTest {
|
|||||||
IrisJigsawConnector highZ = connectorAt(2, 3, 10);
|
IrisJigsawConnector highZ = connectorAt(2, 3, 10);
|
||||||
IrisJigsawConnector lowZ = connectorAt(2, 3, 4);
|
IrisJigsawConnector lowZ = connectorAt(2, 3, 4);
|
||||||
|
|
||||||
List<IrisJigsawConnector> ordered = JigsawStudioService.preserveCapturedConnectorOrder(
|
List<IrisJigsawConnector> ordered = JigsawStudioCapture.preserveCapturedConnectorOrder(
|
||||||
source,
|
source,
|
||||||
List.of(highY, capturedNorth, highZ, lowZ));
|
List.of(highY, capturedNorth, highZ, lowZ));
|
||||||
|
|
||||||
@@ -1040,10 +1040,10 @@ public class JigsawStudioServiceCaptureTest {
|
|||||||
duplicateSource.getConnectors().add(second);
|
duplicateSource.getConnectors().add(second);
|
||||||
IrisJigsawPiece emptySource = new IrisJigsawPiece().setConnectors(new KList<>());
|
IrisJigsawPiece emptySource = new IrisJigsawPiece().setConnectors(new KList<>());
|
||||||
|
|
||||||
assertThrows(IOException.class, () -> JigsawStudioService.preserveCapturedConnectorOrder(
|
assertThrows(IOException.class, () -> JigsawStudioCapture.preserveCapturedConnectorOrder(
|
||||||
duplicateSource,
|
duplicateSource,
|
||||||
List.of(first)));
|
List.of(first)));
|
||||||
assertThrows(IOException.class, () -> JigsawStudioService.preserveCapturedConnectorOrder(
|
assertThrows(IOException.class, () -> JigsawStudioCapture.preserveCapturedConnectorOrder(
|
||||||
emptySource,
|
emptySource,
|
||||||
List.of(first, second)));
|
List.of(first, second)));
|
||||||
}
|
}
|
||||||
@@ -1055,7 +1055,7 @@ public class JigsawStudioServiceCaptureTest {
|
|||||||
64,
|
64,
|
||||||
0,
|
0,
|
||||||
new JigsawStudioCellDimensions(5, 1, 3));
|
new JigsawStudioCellDimensions(5, 1, 3));
|
||||||
List<JigsawStudioService.ChunkCaptureArea> areas = JigsawStudioService.chunkIntersections(bounds);
|
List<JigsawStudioCapture.ChunkCaptureArea> areas = JigsawStudioCapture.chunkIntersections(bounds);
|
||||||
IrisJigsawConnector sourceFirst = connectorAt(0, 0, 1)
|
IrisJigsawConnector sourceFirst = connectorAt(0, 0, 1)
|
||||||
.setDirection(IrisDirection.NORTH_NEGATIVE_Z);
|
.setDirection(IrisDirection.NORTH_NEGATIVE_Z);
|
||||||
IrisJigsawConnector sourceSecond = connectorAt(2, 0, 2)
|
IrisJigsawConnector sourceSecond = connectorAt(2, 0, 2)
|
||||||
@@ -1069,31 +1069,31 @@ public class JigsawStudioServiceCaptureTest {
|
|||||||
for (int quarterTurns = 1; quarterTurns <= 3; quarterTurns++) {
|
for (int quarterTurns = 1; quarterTurns <= 3; quarterTurns++) {
|
||||||
IrisObjectRotation displayRotation = IrisObjectRotation.of(0, -90.0D * quarterTurns, 0);
|
IrisObjectRotation displayRotation = IrisObjectRotation.of(0, -90.0D * quarterTurns, 0);
|
||||||
PlatformBlockState displayedState = displayRotation.rotate(sourceState, 0, 0, 0);
|
PlatformBlockState displayedState = displayRotation.rotate(sourceState, 0, 0, 0);
|
||||||
JigsawStudioService.CapturedConnector displayedFirst = displayedConnector(
|
JigsawStudioCapture.CapturedConnector displayedFirst = displayedConnector(
|
||||||
sourceFirst,
|
sourceFirst,
|
||||||
bounds.dimensions(),
|
bounds.dimensions(),
|
||||||
quarterTurns,
|
quarterTurns,
|
||||||
displayRotation,
|
displayRotation,
|
||||||
displayedState.key());
|
displayedState.key());
|
||||||
JigsawStudioService.CapturedConnector displayedSecond = displayedConnector(
|
JigsawStudioCapture.CapturedConnector displayedSecond = displayedConnector(
|
||||||
sourceSecond,
|
sourceSecond,
|
||||||
bounds.dimensions(),
|
bounds.dimensions(),
|
||||||
quarterTurns,
|
quarterTurns,
|
||||||
displayRotation,
|
displayRotation,
|
||||||
displayedState.key());
|
displayedState.key());
|
||||||
JigsawStudioService.ChunkSnapshot snapshot = new JigsawStudioService.ChunkSnapshot(
|
JigsawStudioCapture.ChunkSnapshot snapshot = new JigsawStudioCapture.ChunkSnapshot(
|
||||||
areas.getFirst(),
|
areas.getFirst(),
|
||||||
List.of(),
|
List.of(),
|
||||||
List.of(displayedSecond, displayedFirst));
|
List.of(displayedSecond, displayedFirst));
|
||||||
|
|
||||||
try (MockedStatic<B> blocks = mockStatic(B.class)) {
|
try (MockedStatic<B> blocks = mockStatic(B.class)) {
|
||||||
blocks.when(() -> B.getStateOrNull(displayedState.key(), false)).thenReturn(displayedState);
|
blocks.when(() -> B.getStateOrNull(displayedState.key(), false)).thenReturn(displayedState);
|
||||||
JigsawStudioService.Capture capture = JigsawStudioService.aggregateSnapshots(
|
JigsawStudioCapture.Capture capture = JigsawStudioCapture.aggregateSnapshots(
|
||||||
bounds,
|
bounds,
|
||||||
areas,
|
areas,
|
||||||
List.of(snapshot),
|
List.of(snapshot),
|
||||||
quarterTurns);
|
quarterTurns);
|
||||||
List<IrisJigsawConnector> ordered = JigsawStudioService.preserveCapturedConnectorOrder(
|
List<IrisJigsawConnector> ordered = JigsawStudioCapture.preserveCapturedConnectorOrder(
|
||||||
source,
|
source,
|
||||||
capture.connectors());
|
capture.connectors());
|
||||||
|
|
||||||
@@ -1110,7 +1110,7 @@ public class JigsawStudioServiceCaptureTest {
|
|||||||
64,
|
64,
|
||||||
0,
|
0,
|
||||||
new JigsawStudioCellDimensions(5, 2, 3));
|
new JigsawStudioCellDimensions(5, 2, 3));
|
||||||
List<JigsawStudioService.ChunkCaptureArea> areas = JigsawStudioService.chunkIntersections(bounds);
|
List<JigsawStudioCapture.ChunkCaptureArea> areas = JigsawStudioCapture.chunkIntersections(bounds);
|
||||||
KMap<String, Object> properties = new KMap<>();
|
KMap<String, Object> properties = new KMap<>();
|
||||||
properties.put("CustomName", "QA Chest");
|
properties.put("CustomName", "QA Chest");
|
||||||
properties.put("Lock", "iris:test");
|
properties.put("Lock", "iris:test");
|
||||||
@@ -1124,18 +1124,18 @@ public class JigsawStudioServiceCaptureTest {
|
|||||||
for (int quarterTurns = 1; quarterTurns <= 3; quarterTurns++) {
|
for (int quarterTurns = 1; quarterTurns <= 3; quarterTurns++) {
|
||||||
BlockData displayedData = directionalBlockData(Material.CHEST, BlockFace.EAST);
|
BlockData displayedData = directionalBlockData(Material.CHEST, BlockFace.EAST);
|
||||||
PlatformBlockState displayedState = BukkitBlockState.of(displayedData);
|
PlatformBlockState displayedState = BukkitBlockState.of(displayedData);
|
||||||
JigsawStudioService.CapturedBlock capturedBlock = new JigsawStudioService.CapturedBlock(
|
JigsawStudioCapture.CapturedBlock capturedBlock = new JigsawStudioCapture.CapturedBlock(
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
displayedState,
|
displayedState,
|
||||||
tileData);
|
tileData);
|
||||||
JigsawStudioService.ChunkSnapshot snapshot = new JigsawStudioService.ChunkSnapshot(
|
JigsawStudioCapture.ChunkSnapshot snapshot = new JigsawStudioCapture.ChunkSnapshot(
|
||||||
areas.getFirst(),
|
areas.getFirst(),
|
||||||
List.of(capturedBlock),
|
List.of(capturedBlock),
|
||||||
List.of());
|
List.of());
|
||||||
|
|
||||||
JigsawStudioService.Capture capture = JigsawStudioService.aggregateSnapshots(
|
JigsawStudioCapture.Capture capture = JigsawStudioCapture.aggregateSnapshots(
|
||||||
bounds,
|
bounds,
|
||||||
areas,
|
areas,
|
||||||
List.of(snapshot),
|
List.of(snapshot),
|
||||||
@@ -1163,7 +1163,7 @@ public class JigsawStudioServiceCaptureTest {
|
|||||||
List.of(new JigsawStudioGenerator.RenderedBlock(0, 0, 0, state, tileData)),
|
List.of(new JigsawStudioGenerator.RenderedBlock(0, 0, 0, state, tileData)),
|
||||||
List.of());
|
List.of());
|
||||||
|
|
||||||
assertEquals("", JigsawStudioService.validateMaterialization(rendered));
|
assertEquals("", JigsawStudioChunkWriter.validateMaterialization(rendered));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -1172,8 +1172,8 @@ public class JigsawStudioServiceCaptureTest {
|
|||||||
BlockData air = blockData(Material.AIR, "minecraft:air");
|
BlockData air = blockData(Material.AIR, "minecraft:air");
|
||||||
BlockData structureVoid = blockData(Material.STRUCTURE_VOID, "minecraft:structure_void");
|
BlockData structureVoid = blockData(Material.STRUCTURE_VOID, "minecraft:structure_void");
|
||||||
|
|
||||||
JigsawStudioService.storeConnectorFinalState(object, 0, 0, 0, air);
|
JigsawStudioCapture.storeConnectorFinalState(object, 0, 0, 0, air);
|
||||||
JigsawStudioService.storeConnectorFinalState(object, 1, 0, 0, structureVoid);
|
JigsawStudioCapture.storeConnectorFinalState(object, 1, 0, 0, structureVoid);
|
||||||
|
|
||||||
IrisBlockVector airPosition = object.getSigned(0, 0, 0);
|
IrisBlockVector airPosition = object.getSigned(0, 0, 0);
|
||||||
IrisBlockVector structureVoidPosition = object.getSigned(1, 0, 0);
|
IrisBlockVector structureVoidPosition = object.getSigned(1, 0, 0);
|
||||||
@@ -1194,10 +1194,10 @@ public class JigsawStudioServiceCaptureTest {
|
|||||||
when(sourceAir.key()).thenReturn("minecraft:air");
|
when(sourceAir.key()).thenReturn("minecraft:air");
|
||||||
source.setUnsigned(0, 0, 0, sourceAir);
|
source.setUnsigned(0, 0, 0, sourceAir);
|
||||||
|
|
||||||
PlatformBlockState retained = JigsawStudioService.retainedSourceAir(source, 0, 0, 0, air);
|
PlatformBlockState retained = JigsawStudioCapture.retainedSourceAir(source, 0, 0, 0, air);
|
||||||
assertNotNull(retained);
|
assertNotNull(retained);
|
||||||
assertNull(JigsawStudioService.retainedSourceAir(source, 1, 0, 0, air));
|
assertNull(JigsawStudioCapture.retainedSourceAir(source, 1, 0, 0, air));
|
||||||
assertNull(JigsawStudioService.retainedSourceAir(source, 0, 0, 0, structureVoid));
|
assertNull(JigsawStudioCapture.retainedSourceAir(source, 0, 0, 0, structureVoid));
|
||||||
|
|
||||||
IrisObject captured = new IrisObject(2, 2, 2);
|
IrisObject captured = new IrisObject(2, 2, 2);
|
||||||
captured.setUnsigned(0, 0, 0, retained);
|
captured.setUnsigned(0, 0, 0, retained);
|
||||||
@@ -1239,7 +1239,7 @@ public class JigsawStudioServiceCaptureTest {
|
|||||||
IrisJigsawPiece piece = new IrisJigsawPiece().setConnectors(new KList<>());
|
IrisJigsawPiece piece = new IrisJigsawPiece().setConnectors(new KList<>());
|
||||||
piece.getConnectors().add(source);
|
piece.getConnectors().add(source);
|
||||||
|
|
||||||
KMap<String, Object> nbt = JigsawStudioService.markerNbt(source);
|
KMap<String, Object> nbt = JigsawStudioChunkWriter.markerNbt(source);
|
||||||
assertEquals("iris:fort/start", nbt.get("pool"));
|
assertEquals("iris:fort/start", nbt.get("pool"));
|
||||||
assertEquals("iris:door", nbt.get("name"));
|
assertEquals("iris:door", nbt.get("name"));
|
||||||
assertEquals(-4, nbt.get("selection_priority"));
|
assertEquals(-4, nbt.get("selection_priority"));
|
||||||
@@ -1250,7 +1250,7 @@ public class JigsawStudioServiceCaptureTest {
|
|||||||
.setPool("fort/start")
|
.setPool("fort/start")
|
||||||
.setName("iris:door")
|
.setName("iris:door")
|
||||||
.setTargetName("iris:door");
|
.setTargetName("iris:door");
|
||||||
JigsawStudioService.restoreCapturedMetadata(captured, piece);
|
JigsawStudioChunkWriter.restoreCapturedMetadata(captured, piece);
|
||||||
|
|
||||||
assertEquals("fort/start", captured.getPool());
|
assertEquals("fort/start", captured.getPool());
|
||||||
assertEquals("door", captured.getName());
|
assertEquals("door", captured.getName());
|
||||||
@@ -1260,7 +1260,7 @@ public class JigsawStudioServiceCaptureTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void authoredBayReadinessRequiresEveryChunkToPopulateAndHydrate() {
|
public void authoredBayReadinessRequiresEveryChunkToPopulateAndHydrate() {
|
||||||
JigsawStudioService.BayPopulation population = new JigsawStudioService.BayPopulation(
|
JigsawStudioChunkWriter.BayPopulation population = new JigsawStudioChunkWriter.BayPopulation(
|
||||||
Set.of(11L, 12L), "");
|
Set.of(11L, 12L), "");
|
||||||
|
|
||||||
assertFalse(population.readiness().ready());
|
assertFalse(population.readiness().ready());
|
||||||
@@ -1275,7 +1275,7 @@ public class JigsawStudioServiceCaptureTest {
|
|||||||
population.markApplied(12L);
|
population.markApplied(12L);
|
||||||
population.markHydrated(12L);
|
population.markHydrated(12L);
|
||||||
|
|
||||||
JigsawStudioService.BayReadiness readiness = population.readiness();
|
JigsawStudioSaveLifecycle.BayReadiness readiness = population.readiness();
|
||||||
assertTrue(readiness.ready());
|
assertTrue(readiness.ready());
|
||||||
assertEquals(2, readiness.requiredChunks());
|
assertEquals(2, readiness.requiredChunks());
|
||||||
assertEquals(2, readiness.generatedChunks());
|
assertEquals(2, readiness.generatedChunks());
|
||||||
@@ -1293,9 +1293,9 @@ public class JigsawStudioServiceCaptureTest {
|
|||||||
new JigsawStudioVariantCatalog(List.of(planarVariant(true, true))));
|
new JigsawStudioVariantCatalog(List.of(planarVariant(true, true))));
|
||||||
JigsawStudioBay blank = layout.get("workcell/blank");
|
JigsawStudioBay blank = layout.get("workcell/blank");
|
||||||
long chunkKey = ((long) 1 << 32) ^ 1L;
|
long chunkKey = ((long) 1 << 32) ^ 1L;
|
||||||
JigsawStudioService.BayPopulation population =
|
JigsawStudioChunkWriter.BayPopulation population =
|
||||||
new JigsawStudioService.BayPopulation(Set.of(chunkKey), "");
|
new JigsawStudioChunkWriter.BayPopulation(Set.of(chunkKey), "");
|
||||||
ConcurrentHashMap<String, JigsawStudioService.BayPopulation> populations = new ConcurrentHashMap<>();
|
ConcurrentHashMap<String, JigsawStudioChunkWriter.BayPopulation> populations = new ConcurrentHashMap<>();
|
||||||
populations.put(blank.stableId(), population);
|
populations.put(blank.stableId(), population);
|
||||||
|
|
||||||
World world = mock(World.class);
|
World world = mock(World.class);
|
||||||
@@ -1339,7 +1339,7 @@ public class JigsawStudioServiceCaptureTest {
|
|||||||
when(event.getWorld()).thenReturn(world);
|
when(event.getWorld()).thenReturn(world);
|
||||||
when(event.getChunk()).thenReturn(chunk);
|
when(event.getChunk()).thenReturn(chunk);
|
||||||
|
|
||||||
service.onChunkLoad(event);
|
service.protectionListener.onChunkLoad(event);
|
||||||
|
|
||||||
assertTrue(population.needsApplication(chunkKey));
|
assertTrue(population.needsApplication(chunkKey));
|
||||||
assertEquals(1, population.readiness().generatedChunks());
|
assertEquals(1, population.readiness().generatedChunks());
|
||||||
@@ -1348,7 +1348,7 @@ public class JigsawStudioServiceCaptureTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void hydrationFailurePermanentlyBlocksSaveReadiness() {
|
public void hydrationFailurePermanentlyBlocksSaveReadiness() {
|
||||||
JigsawStudioService.BayPopulation population = new JigsawStudioService.BayPopulation(
|
JigsawStudioChunkWriter.BayPopulation population = new JigsawStudioChunkWriter.BayPopulation(
|
||||||
Set.of(27L), "");
|
Set.of(27L), "");
|
||||||
population.markGenerated(27L);
|
population.markGenerated(27L);
|
||||||
population.markApplied(27L);
|
population.markApplied(27L);
|
||||||
@@ -1369,20 +1369,20 @@ public class JigsawStudioServiceCaptureTest {
|
|||||||
9,
|
9,
|
||||||
new JigsawStudioCellDimensions(25, 3, 20));
|
new JigsawStudioCellDimensions(25, 3, 20));
|
||||||
|
|
||||||
List<JigsawStudioService.ChunkCaptureArea> areas = JigsawStudioService.chunkIntersections(bounds);
|
List<JigsawStudioCapture.ChunkCaptureArea> areas = JigsawStudioCapture.chunkIntersections(bounds);
|
||||||
|
|
||||||
assertEquals(List.of(
|
assertEquals(List.of(
|
||||||
new JigsawStudioService.ChunkCaptureArea(-1, 0, 0, 5, 0, 7),
|
new JigsawStudioCapture.ChunkCaptureArea(-1, 0, 0, 5, 0, 7),
|
||||||
new JigsawStudioService.ChunkCaptureArea(-1, 1, 0, 5, 7, 20),
|
new JigsawStudioCapture.ChunkCaptureArea(-1, 1, 0, 5, 7, 20),
|
||||||
new JigsawStudioService.ChunkCaptureArea(0, 0, 5, 21, 0, 7),
|
new JigsawStudioCapture.ChunkCaptureArea(0, 0, 5, 21, 0, 7),
|
||||||
new JigsawStudioService.ChunkCaptureArea(0, 1, 5, 21, 7, 20),
|
new JigsawStudioCapture.ChunkCaptureArea(0, 1, 5, 21, 7, 20),
|
||||||
new JigsawStudioService.ChunkCaptureArea(1, 0, 21, 25, 0, 7),
|
new JigsawStudioCapture.ChunkCaptureArea(1, 0, 21, 25, 0, 7),
|
||||||
new JigsawStudioService.ChunkCaptureArea(1, 1, 21, 25, 7, 20)
|
new JigsawStudioCapture.ChunkCaptureArea(1, 1, 21, 25, 7, 20)
|
||||||
), areas);
|
), areas);
|
||||||
for (int x = 0; x < bounds.dimensions().width(); x++) {
|
for (int x = 0; x < bounds.dimensions().width(); x++) {
|
||||||
for (int z = 0; z < bounds.dimensions().depth(); z++) {
|
for (int z = 0; z < bounds.dimensions().depth(); z++) {
|
||||||
int matches = 0;
|
int matches = 0;
|
||||||
for (JigsawStudioService.ChunkCaptureArea area : areas) {
|
for (JigsawStudioCapture.ChunkCaptureArea area : areas) {
|
||||||
if (area.contains(x, z)) {
|
if (area.contains(x, z)) {
|
||||||
matches++;
|
matches++;
|
||||||
}
|
}
|
||||||
@@ -1399,28 +1399,28 @@ public class JigsawStudioServiceCaptureTest {
|
|||||||
64,
|
64,
|
||||||
0,
|
0,
|
||||||
new JigsawStudioCellDimensions(20, 2, 1));
|
new JigsawStudioCellDimensions(20, 2, 1));
|
||||||
List<JigsawStudioService.ChunkCaptureArea> areas = JigsawStudioService.chunkIntersections(bounds);
|
List<JigsawStudioCapture.ChunkCaptureArea> areas = JigsawStudioCapture.chunkIntersections(bounds);
|
||||||
PlatformBlockState stone = mock(PlatformBlockState.class);
|
PlatformBlockState stone = mock(PlatformBlockState.class);
|
||||||
when(stone.key()).thenReturn("minecraft:stone");
|
when(stone.key()).thenReturn("minecraft:stone");
|
||||||
JigsawStudioService.ChunkSnapshot first = new JigsawStudioService.ChunkSnapshot(
|
JigsawStudioCapture.ChunkSnapshot first = new JigsawStudioCapture.ChunkSnapshot(
|
||||||
areas.getFirst(),
|
areas.getFirst(),
|
||||||
List.of(new JigsawStudioService.CapturedBlock(0, 0, 0, stone, null)),
|
List.of(new JigsawStudioCapture.CapturedBlock(0, 0, 0, stone, null)),
|
||||||
List.of());
|
List.of());
|
||||||
JigsawStudioService.ChunkSnapshot second = new JigsawStudioService.ChunkSnapshot(
|
JigsawStudioCapture.ChunkSnapshot second = new JigsawStudioCapture.ChunkSnapshot(
|
||||||
areas.getLast(),
|
areas.getLast(),
|
||||||
List.of(new JigsawStudioService.CapturedBlock(8, 1, 0, stone, null)),
|
List.of(new JigsawStudioCapture.CapturedBlock(8, 1, 0, stone, null)),
|
||||||
List.of());
|
List.of());
|
||||||
|
|
||||||
JigsawStudioService.Capture forward = JigsawStudioService.aggregateSnapshots(
|
JigsawStudioCapture.Capture forward = JigsawStudioCapture.aggregateSnapshots(
|
||||||
bounds, areas, List.of(first, second));
|
bounds, areas, List.of(first, second));
|
||||||
JigsawStudioService.Capture reverse = JigsawStudioService.aggregateSnapshots(
|
JigsawStudioCapture.Capture reverse = JigsawStudioCapture.aggregateSnapshots(
|
||||||
bounds, areas, List.of(second, first));
|
bounds, areas, List.of(second, first));
|
||||||
|
|
||||||
assertArrayEquals(forward.objectContent(), reverse.objectContent());
|
assertArrayEquals(forward.objectContent(), reverse.objectContent());
|
||||||
assertFalse(forward.hasBlockEntities());
|
assertFalse(forward.hasBlockEntities());
|
||||||
assertTrue(forward.connectors().isEmpty());
|
assertTrue(forward.connectors().isEmpty());
|
||||||
assertThrows(IOException.class,
|
assertThrows(IOException.class,
|
||||||
() -> JigsawStudioService.aggregateSnapshots(bounds, areas, List.of(first)));
|
() -> JigsawStudioCapture.aggregateSnapshots(bounds, areas, List.of(first)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -1429,14 +1429,14 @@ public class JigsawStudioServiceCaptureTest {
|
|||||||
UUID firstRequest = UUID.fromString("44444444-4444-4444-4444-444444444444");
|
UUID firstRequest = UUID.fromString("44444444-4444-4444-4444-444444444444");
|
||||||
UUID secondRequest = UUID.fromString("55555555-5555-5555-5555-555555555555");
|
UUID secondRequest = UUID.fromString("55555555-5555-5555-5555-555555555555");
|
||||||
|
|
||||||
assertEquals(JigsawStudioService.SaveStart.STARTED, service.tryBeginSave(firstRequest));
|
assertEquals(JigsawStudioSaveLifecycle.SaveStart.STARTED, service.saveLifecycle.tryBeginSave(firstRequest));
|
||||||
assertEquals(JigsawStudioService.SaveStart.IN_PROGRESS, service.tryBeginSave(firstRequest));
|
assertEquals(JigsawStudioSaveLifecycle.SaveStart.IN_PROGRESS, service.saveLifecycle.tryBeginSave(firstRequest));
|
||||||
assertEquals(JigsawStudioService.SaveStart.STARTED, service.tryBeginSave(secondRequest));
|
assertEquals(JigsawStudioSaveLifecycle.SaveStart.STARTED, service.saveLifecycle.tryBeginSave(secondRequest));
|
||||||
|
|
||||||
service.finishSave(firstRequest);
|
service.saveLifecycle.finishSave(firstRequest);
|
||||||
assertEquals(JigsawStudioService.SaveStart.STARTED, service.tryBeginSave(firstRequest));
|
assertEquals(JigsawStudioSaveLifecycle.SaveStart.STARTED, service.saveLifecycle.tryBeginSave(firstRequest));
|
||||||
service.finishSave(firstRequest);
|
service.saveLifecycle.finishSave(firstRequest);
|
||||||
service.finishSave(secondRequest);
|
service.saveLifecycle.finishSave(secondRequest);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IrisJigsawConnector connector() {
|
private static IrisJigsawConnector connector() {
|
||||||
@@ -1496,7 +1496,7 @@ public class JigsawStudioServiceCaptureTest {
|
|||||||
return directions;
|
return directions;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static JigsawStudioService.CapturedConnector displayedConnector(
|
private static JigsawStudioCapture.CapturedConnector displayedConnector(
|
||||||
IrisJigsawConnector source,
|
IrisJigsawConnector source,
|
||||||
JigsawStudioCellDimensions displayDimensions,
|
JigsawStudioCellDimensions displayDimensions,
|
||||||
int displayRotationQuarterTurns,
|
int displayRotationQuarterTurns,
|
||||||
@@ -1507,7 +1507,7 @@ public class JigsawStudioServiceCaptureTest {
|
|||||||
source.getPosition(),
|
source.getPosition(),
|
||||||
displayDimensions,
|
displayDimensions,
|
||||||
displayRotationQuarterTurns);
|
displayRotationQuarterTurns);
|
||||||
return new JigsawStudioService.CapturedConnector(
|
return new JigsawStudioCapture.CapturedConnector(
|
||||||
displayedPosition.getX(),
|
displayedPosition.getX(),
|
||||||
displayedPosition.getY(),
|
displayedPosition.getY(),
|
||||||
displayedPosition.getZ(),
|
displayedPosition.getZ(),
|
||||||
@@ -1629,7 +1629,7 @@ public class JigsawStudioServiceCaptureTest {
|
|||||||
EventPriority priority,
|
EventPriority priority,
|
||||||
boolean ignoreCancelled
|
boolean ignoreCancelled
|
||||||
) throws NoSuchMethodException {
|
) throws NoSuchMethodException {
|
||||||
Method method = JigsawStudioService.class.getMethod(methodName, eventType);
|
Method method = JigsawStudioProtectionListener.class.getMethod(methodName, eventType);
|
||||||
EventHandler annotation = method.getAnnotation(EventHandler.class);
|
EventHandler annotation = method.getAnnotation(EventHandler.class);
|
||||||
assertNotNull(annotation);
|
assertNotNull(annotation);
|
||||||
assertEquals(priority, annotation.priority());
|
assertEquals(priority, annotation.priority());
|
||||||
|
|||||||
+19
-15
@@ -81,10 +81,10 @@ public class JigsawStudioVisualizationBoundsTest {
|
|||||||
List<Emission> preview = emissions(fixture.player());
|
List<Emission> preview = emissions(fixture.player());
|
||||||
clearInvocations(fixture.player());
|
clearInvocations(fixture.player());
|
||||||
|
|
||||||
Method draw = JigsawStudioService.class.getDeclaredMethod("drawBounds", Player.class, Location.class,
|
Method draw = JigsawStudioVisualization.class.getDeclaredMethod("drawBounds", Player.class, Location.class,
|
||||||
JigsawStudioBounds.class, Color.class, float.class, double.class, nested("ParticleBudget"));
|
JigsawStudioBounds.class, Color.class, float.class, double.class, visualizationNested("ParticleBudget"));
|
||||||
draw.setAccessible(true);
|
draw.setAccessible(true);
|
||||||
Field color = JigsawStudioService.class.getDeclaredField("ASSEMBLY_PREVIEW_COLOR");
|
Field color = JigsawStudioVisualization.class.getDeclaredField("ASSEMBLY_PREVIEW_COLOR");
|
||||||
color.setAccessible(true);
|
color.setAccessible(true);
|
||||||
invoke(draw, null, fixture.player(), viewer, bounds, color.get(null), 0.85F, 2.0D, budget(384));
|
invoke(draw, null, fixture.player(), viewer, bounds, color.get(null), 0.85F, 2.0D, budget(384));
|
||||||
|
|
||||||
@@ -122,10 +122,10 @@ public class JigsawStudioVisualizationBoundsTest {
|
|||||||
assertTrue(fixture.service().showAssemblyPreview(fixture.player(), List.of(piece)));
|
assertTrue(fixture.service().showAssemblyPreview(fixture.player(), List.of(piece)));
|
||||||
}
|
}
|
||||||
|
|
||||||
Method draw = JigsawStudioService.class.getDeclaredMethod("drawAssemblyPreview", Player.class,
|
Method draw = JigsawStudioVisualization.class.getDeclaredMethod("drawAssemblyPreview", Player.class,
|
||||||
Location.class, nested("ParticleBudget"));
|
Location.class, visualizationNested("ParticleBudget"));
|
||||||
draw.setAccessible(true);
|
draw.setAccessible(true);
|
||||||
invoke(draw, fixture.service(), fixture.player(), new Location(fixture.world(), 201, 65, 201), budget(384));
|
invoke(draw, fixture.service().visualization, fixture.player(), new Location(fixture.world(), 201, 65, 201), budget(384));
|
||||||
|
|
||||||
assertTrue(emissions(fixture.player()).stream().anyMatch(emission ->
|
assertTrue(emissions(fixture.player()).stream().anyMatch(emission ->
|
||||||
emission.x() == 201 && emission.y() == 65 && emission.z() == 201));
|
emission.x() == 201 && emission.y() == 65 && emission.z() == 201));
|
||||||
@@ -157,24 +157,24 @@ public class JigsawStudioVisualizationBoundsTest {
|
|||||||
Object studio = constructor.newInstance(worldId, world, mock(Engine.class), generator,
|
Object studio = constructor.newInstance(worldId, world, mock(Engine.class), generator,
|
||||||
new ConcurrentHashMap<>(), ConcurrentHashMap.newKeySet(), new AtomicLong());
|
new ConcurrentHashMap<>(), ConcurrentHashMap.newKeySet(), new AtomicLong());
|
||||||
JigsawStudioService service = new JigsawStudioService();
|
JigsawStudioService service = new JigsawStudioService();
|
||||||
map(service, "studios").put(worldId, studio);
|
map(service, JigsawStudioService.class, "studios").put(worldId, studio);
|
||||||
return new Fixture(service, player, world, requestId, studio);
|
return new Fixture(service, player, world, requestId, studio);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void evaluate(Fixture fixture, JigsawStudioPreviewRenderer.PreviewBounds bounds) throws Exception {
|
private static void evaluate(Fixture fixture, JigsawStudioPreviewRenderer.PreviewBounds bounds) throws Exception {
|
||||||
map(fixture.service(), "evaluations").put(fixture.requestId(), new JigsawStudioGraphEvaluation(
|
map(fixture.service().evaluator, JigsawStudioEvaluator.class, "evaluations").put(fixture.requestId(), new JigsawStudioGraphEvaluation(
|
||||||
fixture.requestId(), 1, 1337, JigsawStudioEvaluationState.VALID, "", 2, "", bounds));
|
fixture.requestId(), 1, 1337, JigsawStudioEvaluationState.VALID, "", 2, "", bounds));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void drawLive(Fixture fixture, Location viewer, int remaining) throws Exception {
|
private static void drawLive(Fixture fixture, Location viewer, int remaining) throws Exception {
|
||||||
Method draw = JigsawStudioService.class.getDeclaredMethod("drawLivePreview", Player.class, Location.class,
|
Method draw = JigsawStudioVisualization.class.getDeclaredMethod("drawLivePreview", Player.class, Location.class,
|
||||||
nested("ActiveStudio"), nested("ParticleBudget"));
|
nested("ActiveStudio"), visualizationNested("ParticleBudget"));
|
||||||
draw.setAccessible(true);
|
draw.setAccessible(true);
|
||||||
invoke(draw, fixture.service(), fixture.player(), viewer, fixture.studio(), budget(remaining));
|
invoke(draw, fixture.service().visualization, fixture.player(), viewer, fixture.studio(), budget(remaining));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Object budget(int remaining) throws Exception {
|
private static Object budget(int remaining) throws Exception {
|
||||||
Constructor<?> constructor = nested("ParticleBudget").getDeclaredConstructor(int.class);
|
Constructor<?> constructor = visualizationNested("ParticleBudget").getDeclaredConstructor(int.class);
|
||||||
constructor.setAccessible(true);
|
constructor.setAccessible(true);
|
||||||
return constructor.newInstance(remaining);
|
return constructor.newInstance(remaining);
|
||||||
}
|
}
|
||||||
@@ -184,10 +184,14 @@ public class JigsawStudioVisualizationBoundsTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
private static Map<UUID, Object> map(JigsawStudioService service, String name) throws Exception {
|
private static Map<UUID, Object> map(Object target, Class<?> owner, String name) throws Exception {
|
||||||
Field field = JigsawStudioService.class.getDeclaredField(name);
|
Field field = owner.getDeclaredField(name);
|
||||||
field.setAccessible(true);
|
field.setAccessible(true);
|
||||||
return (Map<UUID, Object>) field.get(service);
|
return (Map<UUID, Object>) field.get(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Class<?> visualizationNested(String name) throws ClassNotFoundException {
|
||||||
|
return Class.forName(JigsawStudioVisualization.class.getName() + "$" + name);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void invoke(Method method, Object target, Object... arguments) throws Exception {
|
private static void invoke(Method method, Object target, Object... arguments) throws Exception {
|
||||||
|
|||||||
Reference in new issue
Block a user