Command Corrections.

This commit is contained in:
Brian Neumann-Fopiano committed 2026-09-17 04:48:35 -04:00
1 parent 1e1eaa96aa
commit b18681a3e2
43 files changed
+1227 -251

No files matched your search

@@ -60,10 +60,11 @@ final class EngineHotloader {
hotloadSilently();
return;
}
engine.awaitNativeStructureBootstrap("complex hotload");
synchronized (engine.lifecycleLock) {
engine.requireRunning("rebuild the biome complex");
requireMutableGenerationRuntime("rebuild the biome complex");
engine.awaitNativeStructureBootstrap("complex hotload");
engine.requireNativeStructureBootstrapComplete("complex hotload");
engine.lifecycleState = LifecycleState.HOTLOADING;
EngineRuntime previous = engine.runtime;
GenerationRuntime previousGeneration = previous.generation();
@@ -133,10 +134,11 @@ final class EngineHotloader {
hotloadStudio(router);
return;
}
engine.awaitNativeStructureBootstrap("hotload");
synchronized (engine.lifecycleLock) {
engine.requireRunning("hotload");
requireMutableGenerationRuntime("hotload");
engine.awaitNativeStructureBootstrap("hotload");
engine.requireNativeStructureBootstrapComplete("hotload");
engine.lifecycleState = LifecycleState.HOTLOADING;
EngineRuntime previousRuntime = engine.runtime;
IrisDimension previousDimension = engine.getDimension();
@@ -70,7 +70,10 @@ final class EngineShutdownSequence {
if (!engine.beginShutdown()) {
return;
}
Throwable routerFailure = stopWorldManager();
Throwable routerFailure = runCleanup(null, engine::drainNativeStructureBootstrap);
if (routerFailure == null) {
routerFailure = stopWorldManager();
}
if (routerFailure == null) {
routerFailure = runCleanup(null, engine::closeAttachedGenerationHistoryRuntimeRouter);
}
@@ -86,7 +89,6 @@ final class EngineShutdownSequence {
if (engine.closed) {
return;
}
engine.awaitNativeStructureBootstrap("close");
engine.lifecycleState = LifecycleState.CLOSING;
engine.getClosing().set(true);
engine.backgroundTasks.closeBackgroundTaskAdmission();
@@ -515,6 +515,21 @@ public class IrisEngine implements Engine {
nativeStructureBootstrapBarrier.await(transition);
}
void requireNativeStructureBootstrapComplete(String transition) {
if (nativeStructureBootstrapBarrier.isActive()) {
throw new IllegalStateException("Native structure bootstrap started while preparing "
+ transition + "; retry after structure initialization completes.");
}
}
void drainNativeStructureBootstrap() {
Throwable failure = nativeStructureBootstrapBarrier.awaitForClose();
if (failure != null) {
IrisLogging.reportError("Native structure bootstrap failed for " + getWorld().name()
+ "; its workers have stopped and engine cleanup will continue.", failure);
}
}
@Override
public void generateMatter(int x, int z, boolean multicore, ChunkContext context) {
awaitGenerationCacheWarm();
@@ -31,7 +31,7 @@ final class NativeStructureBootstrapBarrier {
starter.get(),
"Native structure bootstrap completion");
completion.whenComplete((ignored, failure) -> completeBridge(bridge, failure));
return bridge;
return bridge.copy();
} catch (Throwable failure) {
poisoned = bridge;
bridge.completeExceptionally(failure);
@@ -45,32 +45,18 @@ final class NativeStructureBootstrapBarrier {
}
}
synchronized void await(String transition) {
CompletableFuture<Void> completion = active;
if (completion == null) {
return;
}
try {
completion.get(TRANSITION_TIMEOUT_SECONDS, TimeUnit.SECONDS);
active = null;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
void await(String transition) {
Throwable failure = awaitCompletion(transition);
if (failure != null) {
throw new IllegalStateException(
"Interrupted while waiting for native structure bootstrap before " + transition + ".", e);
} catch (ExecutionException e) {
if (poisoned != completion) {
active = null;
}
Throwable cause = e.getCause() == null ? e : e.getCause();
throw new IllegalStateException(
"Native structure bootstrap failed before " + transition + ".", cause);
} catch (TimeoutException e) {
throw new IllegalStateException(
"Native structure bootstrap did not finish before " + transition + " within "
+ TRANSITION_TIMEOUT_SECONDS + " seconds.", e);
"Native structure bootstrap failed before " + transition + ".", failure);
}
}
Throwable awaitForClose() {
return awaitCompletion("close");
}
synchronized boolean isActive() {
return active != null;
}
@@ -79,6 +65,45 @@ final class NativeStructureBootstrapBarrier {
return poisoned != null;
}
private Throwable awaitCompletion(String transition) {
CompletableFuture<Void> completion;
synchronized (this) {
completion = active;
}
if (completion == null) {
return null;
}
try {
completion.get(TRANSITION_TIMEOUT_SECONDS, TimeUnit.SECONDS);
clearCompleted(completion);
return null;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException(
"Interrupted while waiting for native structure bootstrap before " + transition + ".", e);
} catch (ExecutionException e) {
Throwable cause = e.getCause() == null ? e : e.getCause();
synchronized (this) {
if (poisoned == completion) {
throw new IllegalStateException(
"Native structure bootstrap could not be tracked before " + transition + ".", cause);
}
clearCompleted(completion);
}
return cause;
} catch (TimeoutException e) {
throw new IllegalStateException(
"Native structure bootstrap did not finish before " + transition + " within "
+ TRANSITION_TIMEOUT_SECONDS + " seconds.", e);
}
}
private synchronized void clearCompleted(CompletableFuture<Void> completion) {
if (active == completion) {
active = null;
}
}
private void completeBridge(CompletableFuture<Void> bridge, Throwable failure) {
if (failure == null) {
bridge.complete(null);
@@ -699,6 +699,10 @@ public final class BukkitCommandMessagesExtended {
"iris.bukkit.commandstudio.you_must_have_server_launched_guis_enabled_settings",
C.RED + "You must have server launched GUIs enabled in the settings!"
);
public static final TextKey COMMAND_STUDIO_DESKTOP_UNAVAILABLE = TextKey.of(
"iris.bukkit.commandstudio.desktop_unavailable",
C.RED + "This server has no available desktop display. Studio GUIs open on the server, so run Iris in a graphical desktop session."
);
public static final TextKey COMMAND_STUDIO_PLAYERS_ONLY = TextKey.of(
"iris.bukkit.commandstudio.players_only",
C.RED + "Players only!"
@@ -973,6 +977,7 @@ public final class BukkitCommandMessagesExtended {
COMMAND_STUDIO_CAPTURING_IGENDATA_FROM_NEARBY_CHUNKS,
COMMAND_STUDIO_REPORTED,
COMMAND_STUDIO_YOU_MUST_HAVE_SERVER_LAUNCHED_GUIS_ENABLED_SETTINGS,
COMMAND_STUDIO_DESKTOP_UNAVAILABLE,
COMMAND_STUDIO_PLAYERS_ONLY,
COMMAND_STUDIO_NO_STUDIO_WORLD_IS_OPEN_2,
COMMAND_STUDIO_YOU_MUST_BE_STUDIO_WORLD,
@@ -2,10 +2,12 @@ package art.arcane.iris.studio.jigsaw;
import art.arcane.iris.studio.jigsaw.JigsawStudioService.ActiveStudio;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.util.event.ProtectionProbe;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.block.BlockState;
import org.bukkit.entity.Player;
import org.bukkit.event.Event;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
@@ -161,8 +163,13 @@ final class JigsawStudioProtectionListener implements Listener {
}
}
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
@EventHandler(priority = EventPriority.LOWEST)
public void onUseTool(PlayerInteractEvent event) {
if (ProtectionProbe.isProbe(event)
|| event.useItemInHand() == Event.Result.DENY
|| (event.getClickedBlock() != null && event.useInteractedBlock() == Event.Result.DENY)) {
return;
}
if (event.getHand() != EquipmentSlot.HAND
|| (event.getAction() != Action.RIGHT_CLICK_AIR
&& event.getAction() != Action.RIGHT_CLICK_BLOCK)) {
@@ -299,6 +306,9 @@ final class JigsawStudioProtectionListener implements Listener {
return;
}
event.setCancelled(true);
if (ProtectionProbe.isProbe(event)) {
return;
}
ActiveStudio studio = service.studios.get(event.getPlayer().getWorld().getUID());
if (!authorizeOwner(event.getPlayer(), studio)) {
return;
@@ -501,6 +511,9 @@ final class JigsawStudioProtectionListener implements Listener {
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onPlayerInteract(PlayerInteractEvent event) {
if (ProtectionProbe.isProbe(event)) {
return;
}
if (event.getAction() != Action.RIGHT_CLICK_BLOCK && event.getAction() != Action.PHYSICAL) {
return;
}
@@ -18,6 +18,7 @@
package art.arcane.iris.studio.object;
import art.arcane.volmlib.util.event.ProtectionProbe;
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisServices;
@@ -180,6 +181,9 @@ public class ObjectStudioSaveService implements IrisService {
@EventHandler(ignoreCancelled = true)
public void onPlayerInteract(PlayerInteractEvent event) {
if (ProtectionProbe.isProbe(event)) {
return;
}
if (event.getHand() != EquipmentSlot.HAND) return;
Action action = event.getAction();
if (action != Action.RIGHT_CLICK_BLOCK && action != Action.LEFT_CLICK_BLOCK) return;
@@ -47,16 +47,14 @@ import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
import java.util.function.Supplier;
public final class StudioOpenCoordinator {
private static final long STUDIO_CLOSE_TIMEOUT_SECONDS = 120L;
private static final long STUDIO_STRUCTURE_ACTIVATION_TIMEOUT_SECONDS = 30L;
private static final long STUDIO_CLOSE_WARNING_SECONDS = 120L;
private static volatile StudioOpenCoordinator instance;
private StudioOpenCoordinator() {
@@ -136,6 +134,7 @@ public final class StudioOpenCoordinator {
private void executeOpen(StudioOpenRequest request, CompletableFuture<StudioOpenResult> future) {
World world = null;
PlatformChunkGenerator provider = null;
CompletableFuture<Void> entryBootstrap = null;
try {
long openStart = System.nanoTime();
long t = openStart;
@@ -166,15 +165,14 @@ public final class StudioOpenCoordinator {
}
World entryWorld = world;
PlatformChunkGenerator entryProvider = provider;
CompletableFuture<Void> entryBootstrap = J.afut(
() -> endStudioEntryBootstrap(entryWorld, entryProvider));
entryBootstrap = endStudioEntryBootstrap(entryWorld, entryProvider);
updateStage(request, "apply_world_rules", 0.72D);
final World rulesWorld = world;
CompletableFuture<Boolean> rulesApplied =
J.sfut(() -> WorldRuntimeControlService.get().applyStudioWorldRules(rulesWorld));
if (rulesApplied != null) {
rulesApplied.get(15L, TimeUnit.SECONDS);
rulesApplied.get();
}
t = logStudioPhase(request, "apply_world_rules", t, openStart);
@@ -191,7 +189,7 @@ public final class StudioOpenCoordinator {
t = logStudioPhase(request, "resolve_entry_anchor", t, openStart);
updateStage(request, "prepare_structure_rings", 0.79D);
entryBootstrap.get(STUDIO_STRUCTURE_ACTIVATION_TIMEOUT_SECONDS, TimeUnit.SECONDS);
entryBootstrap.get();
t = logStudioPhase(request, "prepare_structure_rings", t, openStart);
updateStage(request, "prepare_generation_caches", 0.88D);
@@ -251,6 +249,9 @@ public final class StudioOpenCoordinator {
+ elapsedMillis(openStart) + "ms");
future.complete(new StudioOpenResult(world, entryLocation));
} catch (Throwable e) {
if (entryBootstrap != null) {
entryBootstrap.handle((ignored, failure) -> null).join();
}
abandonStudioEntryBootstrap(world, e);
IrisLogging.reportError("Studio open failed for world \"" + request.worldName() + "\".", e);
if (!request.retainOnFailure()) {
@@ -269,7 +270,7 @@ public final class StudioOpenCoordinator {
request.worldName(),
world,
request.project());
cleanup.get(45L, TimeUnit.SECONDS);
cleanup.get();
} catch (Throwable cleanupError) {
IrisLogging.reportError("Studio cleanup failed for world \""
+ request.worldName() + "\".", unwrapFailure(cleanupError));
@@ -317,7 +318,7 @@ public final class StudioOpenCoordinator {
}
private void runOpenFinalizer(Consumer<World> finalizer, World world)
throws InterruptedException, ExecutionException, TimeoutException {
throws InterruptedException, ExecutionException {
if (finalizer == null) {
return;
}
@@ -327,7 +328,7 @@ public final class StudioOpenCoordinator {
}
CompletableFuture<Void> completion = J.sfut(() -> finalizer.accept(world));
completion.get(STUDIO_STRUCTURE_ACTIVATION_TIMEOUT_SECONDS, TimeUnit.SECONDS);
completion.get();
}
private long elapsedMillis(long startedAtNanos) {
@@ -342,60 +343,23 @@ public final class StudioOpenCoordinator {
duration);
}
private void endStudioEntryBootstrap(World world, PlatformChunkGenerator provider) {
private CompletableFuture<Void> endStudioEntryBootstrap(World world, PlatformChunkGenerator provider) {
if (!(provider instanceof BukkitChunkGenerator bukkitGenerator)) {
throw new IllegalStateException("Studio runtime provider cannot finish its entry bootstrap.");
return CompletableFuture.failedFuture(new IllegalStateException(
"Studio runtime provider cannot finish its entry bootstrap."));
}
AtomicBoolean activationClaim = new AtomicBoolean(true);
CompletableFuture<CompletableFuture<Void>> scheduledActivation = J.sfut(() -> {
if (!activationClaim.compareAndSet(true, false)) {
INMS.get().abandonStudioStructureBootstrap(world);
return CompletableFuture.failedFuture(new IllegalStateException(
"Studio native structure activation was cancelled before it began."));
}
try {
CompletableFuture<Void> nativeActivation =
INMS.get().completeStudioStructureBootstrap(world);
if (nativeActivation == null) {
return CompletableFuture.failedFuture(new IllegalStateException(
"Studio native structure activation returned no completion future."));
}
return nativeActivation;
return Objects.requireNonNull(INMS.get().completeStudioStructureBootstrap(world),
"Studio native structure activation completion");
} catch (ReflectiveOperationException e) {
return CompletableFuture.failedFuture(new IllegalStateException(
"Studio native structure state could not be activated after entry bootstrap.", e));
}
});
CompletableFuture<Void> activation = scheduledActivation
return scheduledActivation
.thenCompose(nativeActivation -> nativeActivation)
.thenCompose(ignored -> J.sfut(bukkitGenerator::endStudioEntryBootstrap));
try {
activation.get(STUDIO_STRUCTURE_ACTIVATION_TIMEOUT_SECONDS, TimeUnit.SECONDS);
} catch (InterruptedException e) {
activationClaim.compareAndSet(true, false);
Thread.currentThread().interrupt();
throw new IllegalStateException("Studio native structure activation was interrupted.", e);
} catch (ExecutionException e) {
activationClaim.compareAndSet(true, false);
throw new IllegalStateException("Studio native structure activation did not complete.",
unwrapFailure(e));
} catch (TimeoutException e) {
if (!activationClaim.compareAndSet(true, false)) {
try {
activation.get(5L, TimeUnit.SECONDS);
return;
} catch (InterruptedException interrupted) {
Thread.currentThread().interrupt();
throw new IllegalStateException(
"Studio native structure activation was interrupted.", interrupted);
} catch (ExecutionException | TimeoutException settlementFailure) {
throw new IllegalStateException(
"Studio native structure activation did not settle after claiming completion.",
unwrapFailure(settlementFailure));
}
}
throw new IllegalStateException("Studio native structure activation did not complete.", e);
}
}
private void abandonStudioEntryBootstrap(World world, Throwable failure) {
@@ -413,12 +377,12 @@ public final class StudioOpenCoordinator {
CompletableFuture<Void> abandonment = J.sfut(
() -> INMS.get().abandonStudioStructureBootstrap(world));
try {
abandonment.get(STUDIO_STRUCTURE_ACTIVATION_TIMEOUT_SECONDS, TimeUnit.SECONDS);
abandonment.get();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
failure.addSuppressed(new IllegalStateException(
"Studio native structure abandonment was interrupted.", e));
} catch (ExecutionException | TimeoutException e) {
} catch (ExecutionException e) {
failure.addSuppressed(new IllegalStateException(
"Studio native structure abandonment did not complete.", unwrapFailure(e)));
}
@@ -475,7 +439,15 @@ public final class StudioOpenCoordinator {
failure
));
}
return closeFuture.whenComplete((result, throwable) -> lease.close());
return closeFuture.whenComplete((result, throwable) -> {
if (throwable == null && result.failureCause() == null) {
lease.close();
} else {
IrisLogging.error("Studio cleanup remains incomplete for \"" + operationTarget
+ "\". Further Iris world operations are blocked. Resolve the reported failure "
+ "and restart the server manually before opening another world.");
}
}).copy();
}
private CompletableFuture<StudioCloseResult> closeWorldReserved(
@@ -486,69 +458,37 @@ public final class StudioOpenCoordinator {
IrisProject project
) {
AtomicBoolean unloadConfirmed = new AtomicBoolean(false);
AtomicBoolean unloadStarted = new AtomicBoolean(false);
AtomicBoolean folderDeleted = new AtomicBoolean(!deleteFolder);
AtomicBoolean terminalTimeout = new AtomicBoolean(false);
if (world != null) {
IrisToolbelt.beginWorldMaintenance(world, "studio-close", true);
}
CompletableFuture<Void> sequence = sequenceStudioClose(
() -> evacuateWorldFamily(worldName, world),
() -> {
unloadStarted.set(true);
return unloadWorldFamily(worldName, world).thenRun(() -> {
if (terminalTimeout.get()) {
throw new CompletionException(new TimeoutException(
"Studio close stopped after its terminal timeout."));
}
unloadConfirmed.set(true);
if (project != null) {
project.setActiveProvider(null);
project.setActiveOpenKind(null);
}
});
},
() -> unloadWorldFamily(worldName, world).thenRun(() -> unloadConfirmed.set(true)),
() -> provider == null ? CompletableFuture.completedFuture(null) : provider.closeAsync(),
() -> deleteFolder
? deleteWorldFamily(worldName).thenRun(() -> folderDeleted.set(true))
: CompletableFuture.completedFuture(null),
terminalTimeout::get
: CompletableFuture.completedFuture(null)
);
CompletableFuture<StudioCloseResult> operation = guardCloseCompletion(
sequence,
terminalTimeout,
worldName)
.thenApply(ignored -> new StudioCloseResult(
worldName,
true,
folderDeleted.get(),
false,
null
))
warnOnSlowClose(sequence, worldName,
CompletableFuture.delayedExecutor(STUDIO_CLOSE_WARNING_SECONDS, TimeUnit.SECONDS));
return sequence.thenApply(ignored -> {
if (project != null) {
project.setActiveProvider(null);
project.setActiveOpenKind(null);
}
if (world != null) {
IrisToolbelt.endWorldMaintenance(world, "studio-close", true);
}
return new StudioCloseResult(worldName, true, folderDeleted.get(), false, null);
})
.exceptionally(throwable -> {
Throwable failure = unwrapFailure(throwable);
requestRestartAfterPartialClose(worldName, unloadStarted.get());
boolean queued = deleteFolder && queueStartupCleanup(worldName, failure);
return new StudioCloseResult(
worldName,
unloadConfirmed.get(),
folderDeleted.get(),
queued,
failure
);
worldName, unloadConfirmed.get(), folderDeleted.get(), queued, failure);
});
return operation.whenComplete((result, throwable) -> {
if (world != null) {
IrisToolbelt.endWorldMaintenance(world, "studio-close", true);
}
});
}
static void requestRestartAfterPartialClose(String worldName, boolean unloadStarted) {
if (unloadStarted) {
ServerConfigurator.restart("Studio close failed after world unload began for \"" + worldName + "\".");
}
}
static CompletableFuture<Void> sequenceStudioClose(
@@ -556,75 +496,28 @@ public final class StudioOpenCoordinator {
Supplier<CompletableFuture<Void>> unload,
Supplier<CompletableFuture<Void>> closeGenerator,
Supplier<CompletableFuture<Void>> deleteFolders
) {
return sequenceStudioClose(evacuate, unload, closeGenerator, deleteFolders, () -> false);
}
static CompletableFuture<Void> sequenceStudioClose(
Supplier<CompletableFuture<Void>> evacuate,
Supplier<CompletableFuture<Void>> unload,
Supplier<CompletableFuture<Void>> closeGenerator,
Supplier<CompletableFuture<Void>> deleteFolders,
BooleanSupplier terminalTimeout
) {
return invokePhase(evacuate)
.thenCompose(ignored -> invokePhaseUnlessTimedOut(unload, terminalTimeout))
.thenCompose(ignored -> invokePhaseUnlessTimedOut(closeGenerator, terminalTimeout))
.thenCompose(ignored -> invokePhaseUnlessTimedOut(deleteFolders, terminalTimeout));
.thenCompose(ignored -> invokePhase(unload))
.thenCompose(ignored -> invokePhase(closeGenerator))
.thenCompose(ignored -> invokePhase(deleteFolders));
}
private static CompletableFuture<Void> invokePhase(Supplier<CompletableFuture<Void>> phase) {
try {
CompletableFuture<Void> future = phase.get();
if (future == null) {
return CompletableFuture.failedFuture(new IllegalStateException("Studio close phase returned no completion future."));
}
return future;
return Objects.requireNonNull(phase.get(), "Studio close phase completion");
} catch (Throwable failure) {
return CompletableFuture.failedFuture(failure);
}
}
private static CompletableFuture<Void> invokePhaseUnlessTimedOut(
Supplier<CompletableFuture<Void>> phase,
BooleanSupplier terminalTimeout
) {
if (terminalTimeout.getAsBoolean()) {
return CompletableFuture.failedFuture(new TimeoutException(
"Studio close stopped after its terminal timeout."));
}
return invokePhase(phase);
}
private CompletableFuture<Void> guardCloseCompletion(
CompletableFuture<Void> source,
AtomicBoolean terminalTimeout,
String worldName
) {
CompletableFuture<Void> guarded = new CompletableFuture<>();
AtomicBoolean settled = new AtomicBoolean(false);
source.whenComplete((ignored, throwable) -> {
if (!settled.compareAndSet(false, true)) {
return;
}
if (throwable == null) {
guarded.complete(null);
} else {
guarded.completeExceptionally(throwable);
static void warnOnSlowClose(CompletableFuture<Void> completion, String worldName, Executor warningExecutor) {
warningExecutor.execute(() -> {
if (!completion.isDone()) {
IrisLogging.warn("Studio cleanup is still running for \"" + worldName
+ "\". Iris is waiting for active work before releasing world resources.");
}
});
CompletableFuture.delayedExecutor(STUDIO_CLOSE_TIMEOUT_SECONDS, TimeUnit.SECONDS).execute(() -> {
if (!settled.compareAndSet(false, true)) {
return;
}
terminalTimeout.set(true);
TimeoutException timeout = new TimeoutException(
"Studio close did not settle within " + STUDIO_CLOSE_TIMEOUT_SECONDS
+ " seconds for \"" + worldName + "\".");
ServerConfigurator.restart("Studio close timed out for \"" + worldName + "\".");
guarded.completeExceptionally(timeout);
});
return guarded;
}
private CompletableFuture<Void> evacuateWorldFamily(String worldName, World primaryWorld) {
@@ -653,7 +546,7 @@ public final class StudioOpenCoordinator {
CompletableFuture<Boolean> unload = J.sfut(() ->
IrisServices.get(MultiverseCoreLink.class)
.removeIfPresent(loadedWorld))
.thenCompose(ignored -> WorldLifecycleService.get().unloadAsync(loadedWorld, false));
.thenCompose(ignored -> WorldLifecycleService.get().unloadSettledAsync(loadedWorld, false));
unloads.add(unload);
}
@@ -743,7 +636,7 @@ public final class StudioOpenCoordinator {
null,
true,
null
).get(30L, TimeUnit.SECONDS);
).get();
if (cleanupResult.failureCause() != null) {
IrisLogging.reportError("Stale studio world cleanup failed for \"" + staleWorldName + "\".", cleanupResult.failureCause());
}
@@ -159,7 +159,14 @@ public final class ImageMapStudioGUI {
}
public static void launch(Engine engine) {
EventQueue.invokeLater(() -> new ImageMapStudioGUI(engine).show());
if (!GuiHost.isAvailable()) {
return;
}
EventQueue.invokeLater(() -> {
if (GuiHost.isAvailable()) {
new ImageMapStudioGUI(engine).show();
}
});
}
static void reloadActiveEngine(Engine engine) {
@@ -216,17 +216,29 @@ public final class NoiseExplorerGUI extends JPanel {
}
public static void launch() {
if (!GuiHost.isAvailable()) {
return;
}
Engine engine = GuiHost.get().findActiveEngine();
EventQueue.invokeLater(() -> {
if (!GuiHost.isAvailable()) {
return;
}
NoiseExplorerGUI explorer = new NoiseExplorerGUI(engine, null, null, null, DEFAULT_SEED, null, null);
buildFrame(IrisLanguage.plain(DesktopUiMessages.NOISE_TITLE), explorer);
});
}
public static void launchGeneratorKey(String generatorKey, IrisGenerator fallbackGenerator, long initialSeed) {
if (!GuiHost.isAvailable()) {
return;
}
Engine engine = GuiHost.get().findActiveEngine();
String displayName = generatorKey == null || generatorKey.isBlank() ? "Custom Generator" : generatorKey;
EventQueue.invokeLater(() -> {
if (!GuiHost.isAvailable()) {
return;
}
NoiseExplorerGUI explorer = new NoiseExplorerGUI(
engine,
generatorKey,
@@ -209,10 +209,16 @@ public final class VisionGUI extends JPanel implements MouseWheelListener, KeyLi
}
public static void launch(Engine engine, UUID openerId) {
if (!GuiHost.isAvailable()) {
return;
}
EventQueue.invokeLater(() -> createAndShowGUI(engine, openerId));
}
private static void createAndShowGUI(Engine engine, UUID openerId) {
if (!GuiHost.isAvailable()) {
return;
}
JFrame frame = new JFrame(IrisLanguage.plain(DesktopUiMessages.VISION_TITLE));
GuiHost.prepareFrame(frame);
VisionGUI vision = new VisionGUI(frame, engine, openerId);
@@ -123,6 +123,11 @@ public final class WorldLifecycleService {
}
public CompletableFuture<Boolean> unloadAsync(World world, boolean save) {
World requiredWorld = Objects.requireNonNull(world, "world");
return guardUnloadCompletion(requiredWorld.getName(), unloadSettledAsync(requiredWorld, save));
}
public CompletableFuture<Boolean> unloadSettledAsync(World world, boolean save) {
World requiredWorld = Objects.requireNonNull(world, "world");
String worldIdentity = WorldIdentity.serialize(requiredWorld);
String worldName = requiredWorld.getName();
@@ -150,8 +155,7 @@ public final class WorldLifecycleService {
unloadFuture.whenComplete((unloaded, throwable) ->
WorldUnloadBoundaryRegistry.complete(rawBoundary, unloaded, throwable));
CompletableFuture<Boolean> guardedFuture = guardUnloadCompletion(worldName, unloadFuture);
return guardedFuture.whenComplete((unloaded, throwable) -> {
return unloadFuture.whenComplete((unloaded, throwable) -> {
if (throwable != null) {
Throwable cause = WorldLifecycleSupport.unwrap(throwable);
IrisLogging.reportError("WorldLifecycle unload failed: world=\"" + worldName
@@ -162,7 +166,7 @@ public final class WorldLifecycleService {
if (Boolean.TRUE.equals(unloaded)) {
worldBackendByKey.remove(worldIdentity, backend.backendName());
}
});
}).copy();
}
private CompletableFuture<Boolean> guardUnloadCompletion(
@@ -664,6 +664,7 @@ you_must_be_iris_world_2 = "Du musst in einer Iris-Welt sein."
capturing_igendata_from_nearby_chunks = "IGenData wird aus {value} nahe gelegenen Chunks erfasst."
reported = "Gemeldet an: {value}"
you_must_have_server_launched_guis_enabled_settings = "§cIn den Einstellungen müssen vom Server gestartete GUIs aktiviert sein!"
desktop_unavailable = "§cDieser Server hat keine verfügbare Desktop-Anzeige. Die Studio-Fenster öffnen sich auf dem Server. Starte Iris in einer grafischen Desktop-Sitzung."
players_only = "§cNur Spieler!"
no_studio_world_is_open_2 = "§cKeine Studio-Welt ist offen!"
you_must_be_studio_world = "§cDu musst in einer Studiowelt sein!"
@@ -664,6 +664,7 @@ you_must_be_iris_world_2 = "Debes estar en un mundo de Iris."
capturing_igendata_from_nearby_chunks = "Capturando IGenData de {value} chunks cercanos."
reported = "Informe escrito en: {value}"
you_must_have_server_launched_guis_enabled_settings = "§c¡Debes habilitar en los ajustes las GUI iniciadas por el servidor!"
desktop_unavailable = "§cEste servidor no dispone de una pantalla de escritorio. Las ventanas de Studio se abren en el servidor. Ejecuta Iris en una sesión de escritorio gráfica."
players_only = "§c¡Solo jugadores!"
no_studio_world_is_open_2 = "§c¡No hay ningún mundo de Studio abierto!"
you_must_be_studio_world = "§c¡Debes estar en un mundo de Studio!"
@@ -669,6 +669,7 @@ you_must_be_iris_world_2 = "Olet varmaan iris-maailmassa."
capturing_igendata_from_nearby_chunks = "Kaappaus IGenData alkaen {value} Lähellä on chunkia."
reported = "Raportoitu: {value}"
you_must_have_server_launched_guis_enabled_settings = "§cSinulla täytyy olla palvelin käynnistetty GUIs käytössä asetuksissa!"
desktop_unavailable = "§cTällä palvelimella ei ole käytettävissä työpöytänäyttöä. Studion ikkunat avautuvat palvelimella. Käynnistä Iris graafisessa työpöytäistunnossa."
players_only = "§cVain pelaajille!"
no_studio_world_is_open_2 = "§cYksikään studiomaailma ei ole auki!"
you_must_be_studio_world = "§cOlet varmaan studiomaailmassa!"
@@ -664,6 +664,7 @@ you_must_be_iris_world_2 = "Vous devez être dans un monde Iris."
capturing_igendata_from_nearby_chunks = "Capture d'IGenData depuis {value} chunks à proximité."
reported = "Rapport écrit dans : {value}"
you_must_have_server_launched_guis_enabled_settings = "§cVous devez activer les GUI lancées par le serveur dans les paramètres !"
desktop_unavailable = "§cCe serveur ne dispose pas d’un affichage de bureau. Les fenêtres de Studio s’ouvrent sur le serveur. Lancez Iris dans une session de bureau graphique."
players_only = "§cJoueurs uniquement !"
no_studio_world_is_open_2 = "§cAucun monde Studio n'est ouvert !"
you_must_be_studio_world = "§cVous devez être dans un monde Studio !"
@@ -669,6 +669,7 @@ you_must_be_iris_world_2 = "אתה חייב להיות בעולם חג המול
capturing_igendata_from_nearby_chunks = "איסוף הנתונים של {value} הסמוך."
reported = "דווח על: {value}"
you_must_have_server_launched_guis_enabled_settings = "§cאתה חייב לפתוח את השרת GUIs אפשרו בהגדרות!"
desktop_unavailable = "§cאין לשרת הזה תצוגת שולחן עבודה זמינה. חלונות Studio נפתחים בשרת. יש להפעיל את Iris בסביבת שולחן עבודה גרפית."
players_only = "§cשחקנים בלבד!"
no_studio_world_is_open_2 = "§cאין עולם סטודיו פתוח!"
you_must_be_studio_world = "§cאתה חייב להיות בעולם סטודיו!"
@@ -664,6 +664,7 @@ you_must_be_iris_world_2 = "Devi essere in un mondo iris."
capturing_igendata_from_nearby_chunks = "Acquisizione di IGenData da {value} chunk vicini."
reported = "Segnalato a: {value}"
you_must_have_server_launched_guis_enabled_settings = "§cDevi abilitare nelle impostazioni le GUI avviate dal server!"
desktop_unavailable = "§cQuesto server non dispone di uno schermo desktop. Le finestre di Studio si aprono sul server. Avvia Iris in una sessione desktop grafica."
players_only = "§cSolo i giocatori!"
no_studio_world_is_open_2 = "§cNessun mondo di studio è aperto!"
you_must_be_studio_world = "§cDevi essere in uno studio!"
@@ -667,6 +667,7 @@ you_must_be_iris_world_2 = "Iris ワールド内にいる必要があります
capturing_igendata_from_nearby_chunks = "周辺の {value} チャンクから IGenData をキャプチャしています。"
reported = "報告: {value}"
you_must_have_server_launched_guis_enabled_settings = "§c設定でサーバー起動 GUI を有効にする必要があります!"
desktop_unavailable = "§cこのサーバーには利用可能なデスクトップ画面がありません。Studio のウィンドウはサーバー上で開くため、グラフィカルなデスクトップセッションで Iris を実行してください。"
players_only = "§cプレイヤーのみ!"
no_studio_world_is_open_2 = "§c開いているスタジオワールドはありません!"
you_must_be_studio_world = "§cスタジオワールドでなければいけません!"
@@ -667,6 +667,7 @@ you_must_be_iris_world_2 = "당신은 아이리스 세계에 있어야합니다.
capturing_igendata_from_nearby_chunks = "주변 {value}개 청크에서 IGenData를 캡처하는 중입니다."
reported = "보고 위치: {value}"
you_must_have_server_launched_guis_enabled_settings = "§c서버가 시작되어야 합니다. GUIs 설정에서 활성화!"
desktop_unavailable = "§c이 서버에는 사용 가능한 데스크톱 화면이 없습니다. Studio 창은 서버에서 열리므로 그래픽 데스크톱 세션에서 Iris를 실행하세요."
players_only = "§c플레이어 만!"
no_studio_world_is_open_2 = "§c스튜디오 세계가 오픈!"
you_must_be_studio_world = "§c스튜디오 세계에 있어야합니다!"
@@ -669,6 +669,7 @@ you_must_be_iris_world_2 = "Turbūt esi rainelės pasaulyje."
capturing_igendata_from_nearby_chunks = "Gaudymas IGenData nuo {value} Šalia šakutės."
reported = "Pranešama apie: {value}"
you_must_have_server_launched_guis_enabled_settings = "§cJūs turite paleisti serverį GUIs įjungta nustatymuose!"
desktop_unavailable = "§cŠiame serveryje nėra prieinamo darbalaukio ekrano. Studio langai atidaromi serveryje. Paleiskite Iris grafinėje darbalaukio sesijoje."
players_only = "§cTik žaidėjai!"
no_studio_world_is_open_2 = "§cJokio studijos pasaulio nėra!"
you_must_be_studio_world = "§cJūs turbūt studijuojat!"
@@ -664,6 +664,7 @@ you_must_be_iris_world_2 = "Je moet in een iriswereld zijn."
capturing_igendata_from_nearby_chunks = "Vangen IGenData van {value} In de buurt."
reported = "Gerapporteerd aan: {value}"
you_must_have_server_launched_guis_enabled_settings = "§cU moet de server gestart hebben GUIs ingeschakeld in de instellingen!"
desktop_unavailable = "§cDeze server heeft geen beschikbaar bureaubladscherm. Studio-vensters openen op de server. Start Iris in een grafische bureaubladsessie."
players_only = "§cAlleen spelers!"
no_studio_world_is_open_2 = "§cGeen enkele studiowereld is open!"
you_must_be_studio_world = "§cJe moet in een studio wereld zijn!"
@@ -669,6 +669,7 @@ you_must_be_iris_world_2 = "Musisz być w świecie tęczówki."
capturing_igendata_from_nearby_chunks = "Chwytanie IGenData od {value} Pobliskie chunki."
reported = "Zgłoszone do: {value}"
you_must_have_server_launched_guis_enabled_settings = "§cMusisz mieć serwer uruchomiony GUIs włączone w ustawieniach!"
desktop_unavailable = "§cTen serwer nie ma dostępnego ekranu pulpitu. Okna Studio otwierają się na serwerze. Uruchom Iris w graficznej sesji pulpitu."
players_only = "§cTylko gracze!"
no_studio_world_is_open_2 = "§cŻadne studio nie jest otwarte!"
you_must_be_studio_world = "§cMusisz być w świecie studia!"
@@ -664,6 +664,7 @@ you_must_be_iris_world_2 = "Deves estar num mundo íris."
capturing_igendata_from_nearby_chunks = "Captura IGenData de {value} Pedaços próximos."
reported = "Comunicado em: {value}"
you_must_have_server_launched_guis_enabled_settings = "§cVocê deve ter lançado o servidor GUIs habilitado nas configurações!"
desktop_unavailable = "§cEste servidor não dispõe de um ecrã de ambiente de trabalho. As janelas do Studio abrem no servidor. Execute o Iris numa sessão gráfica de ambiente de trabalho."
players_only = "§cSó jogadores!"
no_studio_world_is_open_2 = "§cNenhum mundo de estúdio está aberto!"
you_must_be_studio_world = "§cDeves estar num mundo de estúdio!"
@@ -669,6 +669,7 @@ you_must_be_iris_world_2 = "Вы должны быть в мире радужн
capturing_igendata_from_nearby_chunks = "захватывать IGenData из {value} близлежащих кусков."
reported = "Сообщалось: {value}"
you_must_have_server_launched_guis_enabled_settings = "§cВы должны запустить сервер GUIs Включено в настройках!"
desktop_unavailable = "§cНа этом сервере недоступен графический рабочий стол. Окна Studio открываются на сервере. Запустите Iris в графическом сеансе рабочего стола."
players_only = "§cТолько игроки!"
no_studio_world_is_open_2 = "§cНи один студийный мир не открыт!"
you_must_be_studio_world = "§cВы должны быть в студии!"
@@ -669,6 +669,7 @@ you_must_be_iris_world_2 = "Bir iris dünyasında olmalısınız."
capturing_igendata_from_nearby_chunks = "Sheturing IGenData -den {value} Yakındaki chunks."
reported = "Rapor: {value}"
you_must_have_server_launched_guis_enabled_settings = "§cSunucu başlatmış olmalısın GUIs ayarlarda etkinleştirin!"
desktop_unavailable = "§cBu sunucuda kullanılabilir bir masaüstü ekranı yok. Studio pencereleri sunucuda açılır. Iris’i grafik masaüstü oturumunda çalıştırın."
players_only = "§cOyuncular sadece!"
no_studio_world_is_open_2 = "§cStüdyo dünyası açık değil!"
you_must_be_studio_world = "§cBir stüdyo dünyasında olmalısın!"
@@ -667,6 +667,7 @@ you_must_be_iris_world_2 = "Chắc cô đang ở thế giới của Iris."
capturing_igendata_from_nearby_chunks = "Bắt buộc IGenData từ {value} Gần đây thôi."
reported = "Báo cáo: {value}"
you_must_have_server_launched_guis_enabled_settings = "§cBạn phải có máy chủ đã chạy GUIs đã bật trong thiết lập."
desktop_unavailable = "§cMáy chủ này không có màn hình đồ họa khả dụng. Cửa sổ Studio mở trên máy chủ. Hãy chạy Iris trong phiên làm việc có giao diện đồ họa."
players_only = "§cChỉ người chơi thôi!"
no_studio_world_is_open_2 = "§cKhông phòng thu nào mở cửa cả!"
you_must_be_studio_world = "§cCậu chắc đang ở trong phòng thu!"
@@ -667,6 +667,7 @@ you_must_be_iris_world_2 = "你一定在Iris世界里"
capturing_igendata_from_nearby_chunks = "隐藏 IGenData 从 {value} 附近块。"
reported = "向: {value}"
you_must_have_server_launched_guis_enabled_settings = "§c您必须已启动服务器 GUIs 在设置中启用 !"
desktop_unavailable = "§c此服务器没有可用的桌面显示环境。Studio 窗口会在服务器上打开,请在图形桌面会话中运行 Iris。"
players_only = "§c只有球员!"
no_studio_world_is_open_2 = "§c没有工作室的世界是开放的!"
you_must_be_studio_world = "§c你一定在一个工作室的世界!"
@@ -667,6 +667,7 @@ you_must_be_iris_world_2 = "你一定在Iris世界裡"
capturing_igendata_from_nearby_chunks = "隱藏 IGenData 從 {value} 附近塊。"
reported = "向: {value}"
you_must_have_server_launched_guis_enabled_settings = "§c您必須已啟動伺服器 GUIs 在設定中啟用 !"
desktop_unavailable = "§c此伺服器沒有可用的桌面顯示環境。Studio 視窗會在伺服器上開啟,請在圖形桌面工作階段中執行 Iris。"
players_only = "§c只有球員!"
no_studio_world_is_open_2 = "§c沒有工作室的世界是開放的!"
you_must_be_studio_world = "§c你一定在一個工作室的世界!"