This commit is contained in:
Brian Neumann-Fopiano
2026-08-24 02:27:26 -04:00
parent fe5651f854
commit e20bb86f40
228 changed files with 5716 additions and 3577 deletions
@@ -1,130 +0,0 @@
package art.arcane.iris.core.nms.v26_2_R1;
import art.arcane.iris.core.lifecycle.WorldReplacementSeed;
import art.arcane.iris.util.common.scheduling.J;
import io.papermc.paper.world.saveddata.PaperLevelOverrides;
import io.papermc.paper.world.saveddata.PaperWorldMetadata;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.level.storage.PrimaryLevelData;
import net.minecraft.world.level.storage.SavedDataStorage;
import org.bukkit.Bukkit;
import org.bukkit.craftbukkit.CraftServer;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
final class CurrentPaperWorldDataWriter {
private static final long SNAPSHOT_TIMEOUT_SECONDS = 30L;
private CurrentPaperWorldDataWriter() {
}
static void write(
Path sourceWorldDirectory,
Path targetWorldDirectory,
long seed
) throws IOException {
CraftServer craftServer = (CraftServer) Bukkit.getServer();
MinecraftServer server = craftServer.getHandle().getServer();
PaperLevelOverrides levelOverrides = captureLevelOverrides(craftServer, server);
Path targetWorld = targetWorldDirectory.toAbsolutePath().normalize();
UUID metadataUuid = UUID.randomUUID();
WorldReplacementSeed.copyWithAuthoritativeSeed(sourceWorldDirectory, targetWorld, seed);
try (SavedDataStorage savedDataStorage = new SavedDataStorage(
targetWorld.resolve("data"),
server.getFixerUpper(),
server.registryAccess()
)) {
savedDataStorage.set(PaperWorldMetadata.TYPE, new PaperWorldMetadata(metadataUuid));
savedDataStorage.set(PaperLevelOverrides.TYPE, levelOverrides);
}
List<Path> requiredDataFiles = List.of(
targetWorld.resolve("data/minecraft/world_gen_settings.dat"),
targetWorld.resolve("data/paper/metadata.dat"),
targetWorld.resolve("data/paper/level_overrides.dat")
);
for (Path requiredDataFile : requiredDataFiles) {
if (!Files.isRegularFile(requiredDataFile, LinkOption.NOFOLLOW_LINKS)) {
throw new IOException("Current Paper world data was not written: " + requiredDataFile);
}
}
long writtenSeed = WorldReplacementSeed.readAuthoritativeSeed(targetWorld);
if (writtenSeed != seed) {
throw new IOException("Current Paper world data did not retain the requested seed.");
}
try (SavedDataStorage verificationStorage = new SavedDataStorage(
targetWorld.resolve("data"),
server.getFixerUpper(),
server.registryAccess()
)) {
PaperWorldMetadata metadata = verificationStorage.get(PaperWorldMetadata.TYPE);
if (metadata == null || !metadataUuid.equals(metadata.uuid())) {
throw new IOException("Current Paper world metadata could not be verified.");
}
PaperLevelOverrides overrides = verificationStorage.get(PaperLevelOverrides.TYPE);
if (overrides == null || overrides.isInitialized()) {
throw new IOException("Current Paper level overrides could not be verified.");
}
}
}
private static PaperLevelOverrides captureLevelOverrides(
CraftServer craftServer,
MinecraftServer server
) throws IOException {
if (craftServer.isGlobalTickThread()) {
return createLevelOverrides(craftServer, server);
}
if (J.isFolia() && J.isPrimaryThread()) {
throw new IOException("Current Paper world data cannot be staged from a Folia region tick thread.");
}
CompletableFuture<PaperLevelOverrides> captured = new CompletableFuture<>();
boolean scheduled = J.runGlobal(() -> {
try {
captured.complete(createLevelOverrides(craftServer, server));
} catch (Throwable failure) {
captured.completeExceptionally(failure);
}
});
if (!scheduled) {
throw new IOException("Could not schedule the current Paper level-data snapshot on the global thread.");
}
try {
return captured.get(SNAPSHOT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
} catch (InterruptedException failure) {
Thread.currentThread().interrupt();
throw new IOException("Interrupted while capturing current Paper level data.", failure);
} catch (ExecutionException failure) {
throw new IOException("Could not capture current Paper level data.", failure.getCause());
} catch (TimeoutException failure) {
throw new IOException("Timed out while capturing current Paper level data.", failure);
}
}
private static PaperLevelOverrides createLevelOverrides(
CraftServer craftServer,
MinecraftServer server
) throws IOException {
if (!craftServer.isGlobalTickThread()) {
throw new IOException("Current Paper level data must be captured on the global tick thread.");
}
if (!(server.getWorldData().overworldData() instanceof PrimaryLevelData primaryLevelData)) {
throw new IOException("Paper primary level data is unavailable for current world data staging.");
}
return PaperLevelOverrides.createFromLiveLevelData(primaryLevelData);
}
}
@@ -154,7 +154,6 @@ import org.jetbrains.annotations.NotNull;
import java.awt.Color;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
@@ -1572,8 +1571,7 @@ public class NMSBinding implements INMSBinding {
injected.set(true);
return true;
} catch (Throwable e) {
IrisLogging.error(C.RED + "Failed to inject Bukkit");
e.printStackTrace();
IrisLogging.reportError(C.RED + "Failed to inject Bukkit", e);
ResettableClassFileTransformer partialServerLevel = serverLevelTransformer;
ResettableClassFileTransformer partialStorageAccess = levelStorageAccessTransformer;
serverLevelTransformer = null;
@@ -1627,22 +1625,12 @@ public class NMSBinding implements INMSBinding {
try {
transformer.reset(Agent.getInstrumentation(), AgentBuilder.RedefinitionStrategy.RETRANSFORMATION);
} catch (Throwable e) {
IrisLogging.error(C.RED + "Failed to remove Bukkit world lifecycle injection");
e.printStackTrace();
IrisLogging.reportError(C.RED + "Failed to remove Bukkit world lifecycle injection", e);
}
}
}
}
@Override
public void writeCurrentPaperWorldData(
Path sourceWorldDirectory,
Path targetWorldDirectory,
long seed
) throws IOException {
CurrentPaperWorldDataWriter.write(sourceWorldDirectory, targetWorldDirectory, seed);
}
@Override
public boolean awaitServerShutdownBoundary(long timeout, TimeUnit unit) {
MinecraftServer server = ((CraftServer) Bukkit.getServer()).getHandle().getServer();
@@ -1,132 +0,0 @@
package art.arcane.iris.core.nms.v26_2_R1;
import art.arcane.iris.core.nms.INMSBinding;
import org.junit.Test;
import java.io.InputStream;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class NMSBindingCurrentPaperWorldDataContractTest {
@Test
public void bindingDelegatesWithoutLinkingPaperSavedDataClasses() throws Exception {
String bindingSource = Files.readString(bindingSourcePath()).replace("\r\n", "\n");
String writer = section(
bindingSource,
"public void writeCurrentPaperWorldData(",
"public boolean awaitServerShutdownBoundary("
);
assertTrue(writer.contains("CurrentPaperWorldDataWriter.write("));
assertFalse(bindingSource.contains("PaperWorldMetadata"));
assertFalse(bindingSource.contains("PaperLevelOverrides"));
assertFalse(bindingSource.contains("io.papermc.paper.world.saveddata"));
InputStream classResource = NMSBindingCurrentPaperWorldDataContractTest.class
.getResourceAsStream("NMSBinding.class");
assertNotNull(classResource);
try (InputStream input = classResource) {
String classFile = new String(input.readAllBytes(), StandardCharsets.ISO_8859_1);
assertFalse(classFile.contains("PaperWorldMetadata"));
assertFalse(classFile.contains("PaperLevelOverrides"));
assertFalse(classFile.contains("io/papermc/paper/world/saveddata"));
}
}
@Test
public void stagesAllCurrentPaperWorldDataFromLiveServerState() throws Exception {
String writer = Files.readString(writerSourcePath()).replace("\r\n", "\n");
assertTrue(writer.contains("WorldReplacementSeed.copyWithAuthoritativeSeed("));
assertTrue(writer.contains("UUID metadataUuid = UUID.randomUUID()"));
assertTrue(writer.contains("new PaperWorldMetadata(metadataUuid)"));
assertTrue(writer.contains("captureLevelOverrides(craftServer, server)"));
assertTrue(writer.indexOf("captureLevelOverrides(craftServer, server)")
< writer.indexOf("WorldReplacementSeed.copyWithAuthoritativeSeed("));
assertTrue(writer.contains("new SavedDataStorage("));
assertTrue(writer.contains("server.getFixerUpper()"));
assertTrue(writer.contains("server.registryAccess()"));
assertTrue(writer.contains("data/minecraft/world_gen_settings.dat"));
assertTrue(writer.contains("data/paper/metadata.dat"));
assertTrue(writer.contains("data/paper/level_overrides.dat"));
assertTrue(writer.contains("WorldReplacementSeed.readAuthoritativeSeed(targetWorld)"));
assertTrue(writer.contains("verificationStorage.get(PaperWorldMetadata.TYPE)"));
assertTrue(writer.contains("metadataUuid.equals(metadata.uuid())"));
assertTrue(writer.contains("verificationStorage.get(PaperLevelOverrides.TYPE)"));
assertTrue(writer.contains("overrides == null || overrides.isInitialized()"));
assertTrue(writer.contains("Files.isRegularFile(requiredDataFile, LinkOption.NOFOLLOW_LINKS)"));
assertFalse(writer.toLowerCase().contains("migrat"));
assertFalse(writer.toLowerCase().contains("fallback"));
}
@Test
public void capturesOnlyLiveLevelOverridesOnTheGlobalThread() throws Exception {
String source = Files.readString(writerSourcePath()).replace("\r\n", "\n");
String capture = section(
source,
"private static PaperLevelOverrides captureLevelOverrides(",
"private static PaperLevelOverrides createLevelOverrides("
);
String create = section(
source,
"private static PaperLevelOverrides createLevelOverrides(",
"\n }\n}"
);
assertTrue(capture.contains("craftServer.isGlobalTickThread()"));
assertTrue(capture.contains("J.isFolia() && J.isPrimaryThread()"));
assertTrue(capture.contains("J.runGlobal("));
assertTrue(capture.contains("createLevelOverrides(craftServer, server)"));
assertTrue(capture.contains("captured.get(SNAPSHOT_TIMEOUT_SECONDS"));
assertTrue(capture.contains("Thread.currentThread().interrupt()"));
assertTrue(create.contains("if (!craftServer.isGlobalTickThread())"));
assertTrue(create.indexOf("if (!craftServer.isGlobalTickThread())")
< create.indexOf("server.getWorldData().overworldData()"));
assertTrue(create.contains("PaperLevelOverrides.createFromLiveLevelData(primaryLevelData)"));
assertFalse(capture.contains("WorldReplacementSeed"));
assertFalse(capture.contains("SavedDataStorage"));
assertFalse(capture.contains("Files."));
assertFalse(create.contains("WorldReplacementSeed"));
assertFalse(create.contains("SavedDataStorage"));
assertFalse(create.contains("Files."));
}
@Test
public void unsupportedBindingsRejectCurrentPaperWorldDataStaging() {
INMSBinding binding = (INMSBinding) Proxy.newProxyInstance(
INMSBinding.class.getClassLoader(),
new Class<?>[]{INMSBinding.class},
(proxy, method, arguments) -> InvocationHandler.invokeDefault(proxy, method, arguments)
);
UnsupportedOperationException error = assertThrows(
UnsupportedOperationException.class,
() -> binding.writeCurrentPaperWorldData(Path.of("source"), Path.of("target"), 1L)
);
assertTrue(error.getMessage().contains("does not support current Paper world data staging"));
}
private static Path bindingSourcePath() {
return Path.of(System.getProperty("iris.nmsBindingSource"));
}
private static Path writerSourcePath() {
return bindingSourcePath().resolveSibling("CurrentPaperWorldDataWriter.java");
}
private static String section(String source, String startMarker, String endMarker) {
int start = source.indexOf(startMarker);
int end = source.indexOf(endMarker, start);
assertTrue("Missing source section starting with " + startMarker, start >= 0);
assertTrue("Missing source section ending with " + endMarker, end > start);
return source.substring(start, end);
}
}
@@ -172,9 +172,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
try {
InstanceState.updateInstanceId();
} catch (Throwable ex) {
System.err.println("[Iris] Failed to update instance id: " + ex.getClass().getSimpleName()
+ (ex.getMessage() == null ? "" : " - " + ex.getMessage()));
ex.printStackTrace();
IrisLogging.reportError("Failed to update the Iris instance id.", ex);
}
}
@@ -350,7 +348,6 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
try {
object.run();
} catch (Throwable e) {
e.printStackTrace();
Iris.reportError(e);
}
}, RNG.r.i(100, 1200));
@@ -490,7 +487,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
pw.close();
Iris.info("DUMPED! See " + fi.getAbsolutePath());
} catch (Throwable e) {
e.printStackTrace();
Iris.reportError("Failed to write the Iris thread dump.", e);
}
}
@@ -661,7 +658,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
(EngineWorldManagerProvider) IrisWorldManager::new);
IrisServices.register(WorldDeletionQueue.class, pendingWorldDeletes);
IrisServices.register(ManagedWorldLoader.class, (ManagedWorldLoader) this::loadManagedWorld);
SettingsHotloadWatch watch = new SettingsHotloadWatch(getDataFile("settings.json"));
SettingsHotloadWatch watch = new SettingsHotloadWatch(getDataFile("iris.json"));
settingsHotloadWatch = watch;
// Stale-temp cleanup must complete before services enable: StudioSVC.onEnable downloads
// packs through cache/temp on an async thread, and a concurrent delete of that folder
@@ -774,7 +771,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
private void autoStartStudio() {
if (IrisSettings.get().getStudio().isAutoStartDefaultStudio()) {
Iris.info("Starting up auto Studio!");
Iris.debug("Starting up auto Studio!");
try {
Player r = new KList<>(getServer().getOnlinePlayers()).getRandom();
Iris.service(StudioSVC.class).open(r != null ? new VolmitSender(r) : getSender(), 1337, IrisSettings.get().getGenerator().getDefaultWorldType(), (w) -> {
@@ -782,7 +779,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
final Location spawn = w.getSpawnLocation();
for (Player i : getServer().getOnlinePlayers()) {
final Runnable playerTask = () -> {
i.setGameMode(GameMode.CREATIVE);
i.setGameMode(GameMode.SPECTATOR);
BukkitPlatform.teleportAsync(i, spawn);
};
if (!J.runEntity(i, playerTask)) {
@@ -802,10 +799,9 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
audiences = new Bindings.Adventure(this);
BukkitPlatform.hostAudiences(audiences);
} catch (Throwable e) {
e.printStackTrace();
IrisSettings.get().getGeneral().setUseConsoleCustomColors(false);
IrisSettings.get().getGeneral().setUseCustomColorsIngame(false);
Iris.error("Failed to setup Adventure API... No custom colors :(");
Iris.reportError("Failed to set up Adventure; custom colors are disabled.", e);
}
}
@@ -903,14 +899,14 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
if (IrisToolbelt.isServerStopping()) {
quiesceRuntimeForServerShutdown("pre-unload:" + reason);
startPostStopFinisher();
Iris.info("Pre-unload hook deferred generator teardown until Paper closes its chunk schedulers.");
Iris.debug("Pre-unload hook deferred generator teardown until Paper closes its chunk schedulers.");
return;
}
if (alreadyDrained.get()) {
Iris.info("Pre-unload hook skipped; Iris already drained.");
Iris.debug("Pre-unload hook skipped; Iris already drained.");
return;
}
Iris.info("BileTools pre-unload hook fired (" + reason + "). Freezing all Iris worlds.");
Iris.debug("BileTools pre-unload hook fired (" + reason + "). Freezing all Iris worlds.");
drainOnce("pre-unload:" + reason, 45L);
}
@@ -1088,7 +1084,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
}
}
if (generators.isEmpty()) {
Iris.info("No Iris worlds to freeze.");
Iris.debug("No Iris worlds to freeze.");
return;
}
@@ -1112,7 +1108,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
try {
CompletableFuture.allOf(closes.toArray(new CompletableFuture<?>[0]))
.get(timeoutSeconds, TimeUnit.SECONDS);
Iris.info("All Iris chunk generators parked. Safe to unload.");
Iris.debug("All Iris chunk generators parked. Safe to unload.");
} catch (TimeoutException e) {
Iris.warn("Iris generator drain timed out after " + timeoutSeconds + "s; unload proceeding anyway.");
} catch (InterruptedException e) {
@@ -1201,7 +1197,6 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
try {
Iris.syncJobs.next().run();
} catch (Throwable e) {
e.printStackTrace();
Iris.reportError(e);
}
}
@@ -1209,7 +1204,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
}
private void bstats() {
if (IrisSettings.get().getGeneral().isPluginMetrics()) {
if (IrisSettings.get().getGeneral().isMetrics()) {
Bindings.setupBstats(this);
}
}
@@ -242,7 +242,7 @@ public class CommandDeveloper implements DirectorExecutor {
try (CountingDataInputStream in = CountingDataInputStream.wrap(new BufferedInputStream(new FileInputStream(base)))) {
TectonicPlate.read(1088, in, true, IrisEngineMantle.createRuntimeDataAdapter(activeEngine.getData()), IrisEngineMantle.createRuntimeHooks());
} catch (Throwable e) {
e.printStackTrace();
Iris.reportError("Failed to inspect the Iris tectonic plate.", e);
}
} else {
Matter.read(section);
@@ -282,7 +282,7 @@ public class CommandDeveloper implements DirectorExecutor {
MCAFile MCARegion = MCAUtil.read(mca);
}
} catch (Exception e) {
e.printStackTrace();
Iris.reportError("Failed to inspect Minecraft region files.", e);
}
}
@@ -339,7 +339,7 @@ public class CommandDeveloper implements DirectorExecutor {
}
}
} catch (Exception e) {
e.printStackTrace();
Iris.reportError("Failed to inspect server network interfaces.", e);
}
}
@@ -22,22 +22,16 @@ import art.arcane.iris.Iris;
import art.arcane.iris.core.BukkitWorldReconciler;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.IrisStartupValidation;
import art.arcane.iris.core.DatapackInstallResult;
import art.arcane.iris.core.IrisWorldStorage;
import art.arcane.iris.core.IrisWorlds;
import art.arcane.iris.core.PendingWorldReplacementManager;
import art.arcane.iris.core.ServerConfigurator;
import art.arcane.iris.core.lifecycle.BukkitWorldConfiguration;
import art.arcane.iris.core.lifecycle.IrisWorldRemovalService;
import art.arcane.iris.core.lifecycle.LifecycleOperationCoordinator;
import art.arcane.iris.core.lifecycle.WorldLifecycleService;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.core.pack.AtomicDirectoryPublisher;
import art.arcane.iris.core.pack.PackDownloader;
import art.arcane.iris.core.pack.PackDirectoryResolver;
import art.arcane.iris.core.pack.PackValidationRegistry;
import art.arcane.iris.core.service.StudioSVC;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
@@ -62,9 +56,7 @@ import org.bukkit.World;
import org.bukkit.entity.Player;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
@@ -160,16 +152,6 @@ public class CommandIris implements DirectorExecutor {
return;
}
if (J.isFolia()) {
boolean staged = stageFoliaWorldCreation(worldName, dimension, seed);
if (!staged) {
return;
}
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_WORLD_STAGING_COMPLETED_RESTARTING_SERVER_GENERATE_LOAD, MessageArgument.untrusted("worldName", worldName)));
ServerConfigurator.restart("Iris staged Folia world \"" + worldName + "\" for startup.");
return;
}
try {
IrisToolbelt.createWorld()
.dimension(resolvedType)
@@ -263,116 +245,6 @@ public class CommandIris implements DirectorExecutor {
}
}
private boolean stageFoliaWorldCreation(String name, IrisDimension dimension, long seed) {
try {
IrisStartupValidation.requireWorldCreationReady();
PackValidationRegistry.requireLoadable(
dimension.getLoader().getDataFolder().getName());
} catch (RuntimeException exception) {
sender().sendMessage(C.RED + exception.getMessage());
return false;
}
NamespacedKey worldKey = IrisWorldStorage.managedKeyFromName(name);
LifecycleOperationCoordinator.Lease worldLease = null;
File worldFolder = IrisWorldStorage.requireSafeManagedDimensionRoot(worldKey);
Path stagedWorld = null;
try {
LifecycleOperationCoordinator coordinator = LifecycleOperationCoordinator.get();
worldLease = coordinator.acquire(
LifecycleOperationCoordinator.Domain.WORLD_MUTATION,
LifecycleOperationCoordinator.OperationKind.WORLD_CREATE,
worldKey.toString());
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_RUNTIME_WORLD_CREATION_IS_DISABLED_ON_FOLIA));
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_PREPARING_WORLD_FILES_BUKKIT_YML_NEXT_STARTUP));
if (worldFolder.exists()) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_THAT_FOLDER_ALREADY_EXISTS));
return false;
}
DatapackInstallResult datapackResult = ServerConfigurator.installDataPacksIfChanged(true);
if (!datapackResult.succeeded()) {
sender().sendMessage(C.RED + "Failed to compile the Iris datapack. No world files were staged.");
return false;
}
Path targetWorld = worldFolder.toPath().toAbsolutePath().normalize();
Path namespaceRoot = targetWorld.getParent();
if (namespaceRoot == null) {
throw new IOException("Iris world target has no namespace directory: " + targetWorld);
}
Files.createDirectories(namespaceRoot);
stagedWorld = Files.createTempDirectory(namespaceRoot, ".iris-create-" + worldKey.getKey() + "-");
Path sourceOverworld = IrisWorldStorage.dimensionRoot(
IrisWorldStorage.levelRoot(),
NamespacedKey.minecraft("overworld")
).toPath();
INMS.get().writeCurrentPaperWorldData(sourceOverworld, stagedWorld, seed);
IrisDimension installed = Iris.service(StudioSVC.class).installIntoWorld(
sender(),
dimension,
stagedWorld.toFile()
);
if (installed == null) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_FAILED_STAGE_WORLD_FILES_DIMENSION, MessageArgument.untrusted("value", dimension.getLoadKey())));
return false;
}
try (AtomicDirectoryPublisher.Publication publication = AtomicDirectoryPublisher.publishAbsent(
stagedWorld,
targetWorld
)) {
stagedWorld = null;
if (!registerWorldInBukkitYml(worldKey, dimension.getLoadKey(), seed)) {
return false;
}
publication.commit();
}
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_STAGED_IRIS_WORLD_WITH_GENERATOR_IRIS_SEED, MessageArgument.untrusted("name", name), MessageArgument.untrusted("value", dimension.getLoadKey()), MessageArgument.untrusted("seed", seed)));
return true;
} catch (LifecycleOperationCoordinator.BusyException e) {
sender().sendMessage(C.YELLOW + e.getMessage());
return false;
} catch (Throwable e) {
sender().sendMessage(C.RED + "Failed to stage the complete Iris world: " + e.getMessage());
Iris.reportError("Failed to stage complete Folia world \"" + worldKey + "\".", e);
return false;
} finally {
if (stagedWorld != null) {
deleteDirectorySafely(stagedWorld.toFile());
}
if (worldLease != null) {
worldLease.close();
}
}
}
private boolean registerWorldInBukkitYml(NamespacedKey worldKey, String dimension, Long seed) {
String configuredWorldName = IrisWorldStorage.configuredWorldName(
worldKey,
IrisWorldStorage.levelRoot().getName()
);
try {
BukkitWorldConfiguration.register(BUKKIT_YML, configuredWorldName, dimension, seed);
Iris.info("Registered \"" + configuredWorldName + "\" in bukkit.yml");
return true;
} catch (IOException e) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_FAILED_UPDATE_BUKKIT_YML, MessageArgument.untrusted("value", String.valueOf(e.getMessage()))));
Iris.error("Failed to update bukkit.yml!");
Iris.reportError(e);
return false;
}
}
private void deleteDirectorySafely(File directory) {
try {
AtomicDirectoryPublisher.deleteTree(directory.toPath());
} catch (IOException e) {
Iris.reportError("Failed to roll back staged world folder \"" + directory.getAbsolutePath() + "\".", e);
}
}
private boolean reportExpectedCreationInterruption(Throwable failure) {
Throwable current = failure;
while (current != null) {
@@ -24,6 +24,7 @@ import art.arcane.iris.core.link.WorldEditLink;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.pack.PackDirectoryResolver;
import art.arcane.iris.core.runtime.ObjectStudioActivation;
import art.arcane.iris.core.runtime.StudioOpenCoordinator;
import art.arcane.iris.core.runtime.WorldRuntimeControlService;
import art.arcane.iris.core.service.ObjectSVC;
import art.arcane.iris.core.service.StudioSVC;
@@ -171,24 +172,29 @@ public class CommandObject implements DirectorExecutor {
IrisDimension finalHost = hostDimension;
try {
Iris.service(StudioSVC.class).open(commandSender, seed, hostDimension.getLoadKey(), world -> {
if (world == null) return;
try {
WorldRuntimeControlService.get().applyObjectStudioWorldRules(world);
} catch (Throwable e) {
Iris.reportError("Failed to apply object studio world rules for " + world.getName(), e);
}
Iris.service(StudioSVC.class).open(
commandSender,
seed,
hostDimension.getLoadKey(),
StudioOpenCoordinator.StudioOpenKind.OBJECT,
world -> {
if (world == null) return;
try {
WorldRuntimeControlService.get().applyObjectStudioWorldRules(world);
} catch (Throwable e) {
Iris.reportError("Failed to apply object studio world rules for " + world.getName(), e);
}
if (commandSender.isPlayer()) {
Player p = commandSender.player();
if (p != null) {
Location target = new Location(world, 0.5D, 66D, 0.5D);
J.runEntity(p, () -> {
BukkitPlatform.teleportAsync(p, target).thenRun(() -> p.setGameMode(GameMode.CREATIVE));
});
}
}
});
if (commandSender.isPlayer()) {
Player p = commandSender.player();
if (p != null) {
Location target = new Location(world, 0.5D, 66D, 0.5D);
J.runEntity(p, () -> {
BukkitPlatform.teleportAsync(p, target).thenRun(() -> p.setGameMode(GameMode.CREATIVE));
});
}
}
});
} catch (Throwable e) {
Iris.reportError("Failed to open object studio world \"" + finalHost.getLoadKey() + "\".", e);
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_OBJECT_FAILED_OPEN_OBJECT_STUDIO, MessageArgument.untrusted("value", String.valueOf(e.getMessage()))));
@@ -360,7 +366,7 @@ public class CommandObject implements DirectorExecutor {
o.write(o.getLoadFile());
} catch (IOException e) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_FAILED_SAVE_OBJECT, MessageArgument.untrusted("value", o.getLoadFile()), MessageArgument.untrusted("value2", String.valueOf(e.getMessage()))));
e.printStackTrace();
Iris.reportError("Failed to save object " + o.getLoadFile() + ".", e);
}
}
@@ -426,7 +432,7 @@ public class CommandObject implements DirectorExecutor {
try {
IrisConverter.convertSchematics(sender());
} catch (Exception e) {
e.printStackTrace();
Iris.reportError("Failed to convert schematics to Iris objects.", e);
}
}
@@ -89,7 +89,6 @@ public class CommandPregen implements DirectorExecutor {
} catch (Throwable e) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_PREGEN_FAILED_START_PREGENERATION_SEE_CONSOLE_DETAILS));
Iris.reportError(e);
e.printStackTrace();
}
}
@@ -77,7 +77,6 @@ import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import org.bukkit.Bukkit;
import org.bukkit.Chunk;
import org.bukkit.FluidCollisionMode;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Player;
@@ -573,7 +572,6 @@ public class CommandStudio implements DirectorExecutor {
IO.writeAll(report, fileText.toString("\n"));
} catch (IOException e) {
Iris.reportError(e);
e.printStackTrace();
}
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_DONE, MessageArgument.untrusted("value", report.getPath())));
@@ -663,9 +661,6 @@ public class CommandStudio implements DirectorExecutor {
Iris.reportError("Studio teleport failed for player \"" + player.getName() + "\".", failure);
return;
}
if (Boolean.TRUE.equals(teleported)) {
J.runEntity(player, () -> player.setGameMode(GameMode.CREATIVE));
}
});
}
@@ -831,7 +826,6 @@ public class CommandStudio implements DirectorExecutor {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_REPORTED, MessageArgument.untrusted("value", ff.getPath())));
} catch (Throwable e) {
e.printStackTrace();
Iris.reportError(e);
}
}
@@ -18,14 +18,17 @@
package art.arcane.iris.core.gui;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
import art.arcane.iris.core.runtime.WorldRuntimeControlService;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.render.RenderType;
import art.arcane.iris.engine.object.IrisWorld;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.format.Form;
import org.bukkit.Chunk;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.LivingEntity;
@@ -36,15 +39,21 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import static art.arcane.iris.util.common.data.registry.Attributes.MAX_HEALTH;
public final class BukkitVisionOverlay implements GuiOverlay {
private final Engine engine;
private final AtomicBoolean nativeTeleportActive = new AtomicBoolean();
private final AtomicBoolean playerRefreshQueued = new AtomicBoolean();
private final AtomicLong teleportSequence = new AtomicLong();
private final AtomicReference<VisionTeleportRequest> latestTeleport = new AtomicReference<>();
private volatile List<GuiMarker> playerMarkers = List.of();
public BukkitVisionOverlay(Engine engine) {
@@ -144,25 +153,198 @@ public final class BukkitVisionOverlay implements GuiOverlay {
@Override
public void teleport(double worldX, double worldZ) {
IrisWorld target = engine.getWorld();
if (!target.hasPlatformWorld()) {
VisionTeleportRequest request = new VisionTeleportRequest(
teleportSequence.incrementAndGet(),
VisionGUI.floorWorldCoordinate(worldX),
VisionGUI.floorWorldCoordinate(worldZ));
latestTeleport.set(request);
startTeleport(request);
}
private void startTeleport(VisionTeleportRequest request) {
if (!request.processing.compareAndSet(false, true)) {
return;
}
J.runGlobal(() -> {
boolean scheduled = J.runGlobal(() -> {
IrisWorld target = engine.getWorld();
if (!isCurrent(request, target)) {
finish(request);
return;
}
World world = BukkitWorldBinding.world(target);
if (world == null) {
finish(request);
return;
}
List<Player> players = BukkitWorldBinding.players(target);
if (players.isEmpty()) {
finish(request);
return;
}
Player player = players.get(0);
World world = player.getWorld();
int xx = (int) worldX;
int zz = (int) worldZ;
J.runRegion(world, xx >> 4, zz >> 4, () -> {
int yy = world.getHighestBlockYAt(xx, zz) + 1;
Location destination = new Location(world, xx, yy, zz);
J.runEntity(player, () -> BukkitPlatform.teleportAsync(player, destination));
});
requestTeleportChunk(request, target, player, world);
});
if (!scheduled) {
finish(request);
}
}
private void requestTeleportChunk(
VisionTeleportRequest request,
IrisWorld target,
Player player,
World world
) {
int blockX = request.blockX;
int blockZ = request.blockZ;
int chunkX = blockX >> 4;
int chunkZ = blockZ >> 4;
CompletableFuture<Chunk> requested;
try {
requested = WorldRuntimeControlService.get().requestChunkAsync(
world,
chunkX,
chunkZ,
true,
true
);
} catch (Throwable failure) {
fail(request, target, world, failure);
return;
}
if (requested == null) {
fail(request, target, world, new IllegalStateException(
"Vision destination chunk request returned no future."));
return;
}
requested.whenComplete((chunk, failure) -> {
if (!isCurrent(request, target)) {
finish(request);
return;
}
if (failure != null) {
fail(request, target, world, failure);
return;
}
if (chunk == null || chunk.getWorld() != world) {
fail(request, target, world, new IllegalStateException(
"Vision destination chunk request returned no chunk."));
return;
}
boolean scheduled = J.runRegion(world, chunkX, chunkZ, () -> {
if (!isCurrent(request, target)) {
finish(request);
return;
}
int yy = world.getHighestBlockYAt(blockX, blockZ) + 1;
Location destination = new Location(world, blockX, yy, blockZ);
if (!J.runEntity(player, () -> delegateTeleport(
request,
target,
player,
world,
destination))) {
fail(request, target, world, new IllegalStateException(
"Failed to schedule the Vision teleport on the player entity."));
}
});
if (!scheduled) {
fail(request, target, world, new IllegalStateException(
"Failed to schedule the Vision surface lookup on its owning region."));
}
});
}
private void delegateTeleport(
VisionTeleportRequest request,
IrisWorld target,
Player player,
World world,
Location destination
) {
if (!isCurrent(request, target) || !player.isOnline() || player.getWorld() != world) {
finish(request);
return;
}
if (!nativeTeleportActive.compareAndSet(false, true)) {
finish(request);
return;
}
if (!isCurrent(request, target)) {
nativeTeleportActive.set(false);
finish(request);
restartLatest(request);
return;
}
CompletableFuture<Boolean> teleport;
try {
teleport = BukkitPlatform.teleportAsync(player, destination);
} catch (Throwable failure) {
nativeTeleportActive.set(false);
fail(request, target, world, failure);
restartLatest(request);
return;
}
if (teleport == null) {
nativeTeleportActive.set(false);
fail(request, target, world, new IllegalStateException(
"Vision teleport returned no completion future."));
restartLatest(request);
return;
}
teleport.whenComplete((success, failure) -> {
nativeTeleportActive.set(false);
finish(request);
if (isCurrent(request, target)) {
if (failure != null) {
reportTeleportFailure(world, request.blockX, request.blockZ, failure);
} else if (!Boolean.TRUE.equals(success)) {
reportTeleportFailure(world, request.blockX, request.blockZ, new IllegalStateException(
"Vision teleport did not complete successfully."));
}
}
restartLatest(request);
});
}
private boolean isCurrent(VisionTeleportRequest request, IrisWorld target) {
VisionTeleportRequest current = latestTeleport.get();
return current != null
&& current.sequence == request.sequence
&& target != null
&& engine.getWorld() == target
&& target.hasPlatformWorld()
&& !engine.isClosing()
&& !engine.isClosed();
}
private void fail(
VisionTeleportRequest request,
IrisWorld target,
World world,
Throwable failure
) {
finish(request);
if (isCurrent(request, target)) {
reportTeleportFailure(world, request.blockX, request.blockZ, failure);
}
}
private void finish(VisionTeleportRequest request) {
request.processing.set(false);
}
private void restartLatest(VisionTeleportRequest completed) {
VisionTeleportRequest current = latestTeleport.get();
if (current != null && current != completed) {
startTeleport(current);
}
}
private void reportTeleportFailure(World world, int blockX, int blockZ, Throwable failure) {
IrisLogging.reportError("Vision could not teleport to " + world.getName() + "@"
+ blockX + "," + blockZ + ".", failure);
}
@Override
@@ -179,4 +361,18 @@ public final class BukkitVisionOverlay implements GuiOverlay {
};
return file == null ? null : file.getName();
}
private static final class VisionTeleportRequest {
private final long sequence;
private final int blockX;
private final int blockZ;
private final AtomicBoolean processing;
private VisionTeleportRequest(long sequence, int blockX, int blockZ) {
this.sequence = sequence;
this.blockX = blockX;
this.blockZ = blockZ;
processing = new AtomicBoolean(false);
}
}
}
@@ -605,7 +605,6 @@ public final class IrisEngineSVC implements IrisService {
private static void reportFailure(String message, Throwable exception) {
IrisLogging.reportError(exception);
IrisLogging.error("EngineSVC: " + message);
exception.printStackTrace();
}
private final class Registered {
@@ -20,6 +20,7 @@ package art.arcane.iris.core.service;
import art.arcane.iris.Iris;
import art.arcane.iris.core.protocol.EngineResolver;
import art.arcane.iris.core.protocol.IrisCursorRequestService;
import art.arcane.iris.core.protocol.IrisProtocolServer;
import art.arcane.iris.core.protocol.IrisServerTransport;
import art.arcane.iris.core.protocol.IrisSession;
@@ -55,6 +56,7 @@ public class IrisProtocolService implements IrisService, PluginMessageListener,
private IrisSessionRegistry registry;
private IrisProtocolServer protocolServer;
private IrisCursorRequestService cursorService;
private IrisVisionRequestService visionService;
@Override
@@ -66,6 +68,8 @@ public class IrisProtocolService implements IrisService, PluginMessageListener,
protocolServer = new IrisProtocolServer(registry, SERVER_CAPABILITIES, brand(), true);
EngineResolver engineResolver = IrisProtocolService::resolveEngine;
protocolServer.setEngineResolver(engineResolver);
cursorService = IrisCursorRequestService.create(engineResolver, registry);
protocolServer.setCursorInfoHandler(cursorService);
visionService = IrisVisionRequestService.create(engineResolver, registry);
protocolServer.setVisionTileHandler(visionService);
IrisServices.register(IrisProtocolServer.class, protocolServer);
@@ -81,13 +85,22 @@ public class IrisProtocolService implements IrisService, PluginMessageListener,
messenger.unregisterIncomingPluginChannel(Iris.instance, IrisProtocol.CHANNEL, this);
messenger.unregisterOutgoingPluginChannel(Iris.instance, IrisProtocol.CHANNEL);
IrisSessionRegistry current = registry;
IrisCursorRequestService cursor = cursorService;
IrisVisionRequestService vision = visionService;
if (current != null) {
for (IrisSession session : current.all()) {
current.unregister(session.id());
if (cursor != null) {
cursor.clearSession(session.id());
}
if (vision != null) {
vision.clearSession(session.id());
}
}
}
registry = null;
protocolServer = null;
cursorService = null;
visionService = null;
}
@@ -117,6 +130,10 @@ public class IrisProtocolService implements IrisService, PluginMessageListener,
}
String sessionId = event.getPlayer().getUniqueId().toString();
current.unregister(sessionId);
IrisCursorRequestService cursor = cursorService;
if (cursor != null) {
cursor.clearSession(sessionId);
}
IrisVisionRequestService vision = visionService;
if (vision != null) {
vision.clearSession(sessionId);
@@ -51,6 +51,7 @@ import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.block.Action;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemFlag;
@@ -61,18 +62,30 @@ import org.bukkit.util.BlockVector;
import org.bukkit.util.Vector;
import java.awt.Color;
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.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
import static art.arcane.iris.util.common.data.registry.Particles.CRIT_MAGIC;
import static art.arcane.iris.util.common.data.registry.Particles.REDSTONE;
public class WandSVC implements IrisService {
private static final int MS_PER_TICK = Integer.parseInt(System.getProperty("iris.ms_per_tick", "30"));
private static final int PLAYER_RESCAN_INTERVAL_TICKS = 100;
private static ItemStack dust;
private static ItemStack wand;
private final Map<UUID, Player> activePlayers = new ConcurrentHashMap<>();
private final AtomicBoolean playerRescanScheduled = new AtomicBoolean(false);
private volatile boolean enabled;
private int taskId = -1;
private int ticksUntilPlayerRescan = 0;
public static void pasteSchematic(IrisObject s, Location at) {
s.place(at);
}
@@ -174,7 +187,6 @@ public class WandSVC implements IrisService {
return s;
} catch (Throwable e) {
e.printStackTrace();
Iris.reportError(e);
}
@@ -196,7 +208,6 @@ public class WandSVC implements IrisService {
return WorldMatter.createMatter(p.getName(), f[0], f[1]);
} catch (Throwable e) {
e.printStackTrace();
Iris.reportError(e);
}
@@ -307,8 +318,18 @@ public class WandSVC implements IrisService {
}
public static Location[] getCuboidFromItem(ItemStack is) {
if (is == null) {
return new Location[]{null, null};
}
ItemMeta im = is.getItemMeta();
return new Location[]{stringToLocation(im.getLore().get(0)), stringToLocation(im.getLore().get(1))};
if (im == null) {
return new Location[]{null, null};
}
List<String> lore = im.getLore();
if (lore == null || lore.size() < 2) {
return new Location[]{null, null};
}
return new Location[]{stringToLocation(lore.get(0)), stringToLocation(lore.get(1))};
}
public static Location[] getCuboid(Player p) {
@@ -343,30 +364,47 @@ public class WandSVC implements IrisService {
* @return True if it is
*/
public static boolean isWand(ItemStack is) {
if (is == null || is.getItemMeta() == null) {
if (is == null) {
return false;
}
Byte marker = is.getItemMeta().getPersistentDataContainer().get(wandKey(), PersistentDataType.BYTE);
if (marker != null && marker == (byte) 1) {
ItemMeta meta = is.getItemMeta();
if (meta == null) {
return false;
}
Byte marker = meta.getPersistentDataContainer().get(wandKey(), PersistentDataType.BYTE);
if (marker != null && marker.byteValue() == 1) {
return true;
}
return is.getType().equals(wand.getType()) &&
is.getItemMeta().getDisplayName().equals(wand.getItemMeta().getDisplayName()) &&
is.getItemMeta().getEnchants().equals(wand.getItemMeta().getEnchants()) &&
is.getItemMeta().getItemFlags().equals(wand.getItemMeta().getItemFlags());
ItemStack template = wand;
if (template == null || !is.getType().equals(template.getType())) {
return false;
}
ItemMeta templateMeta = template.getItemMeta();
return templateMeta != null
&& Objects.equals(meta.getDisplayName(), templateMeta.getDisplayName())
&& meta.getEnchants().equals(templateMeta.getEnchants())
&& meta.getItemFlags().equals(templateMeta.getItemFlags());
}
@Override
public void onEnable() {
wand = createWand();
dust = createDust();
J.ar(this::tickAll, 0);
enabled = true;
activePlayers.clear();
ticksUntilPlayerRescan = 0;
taskId = J.ar(this::tickAll, 1);
}
@Override
public void onDisable() {
enabled = false;
if (taskId != -1) {
J.car(taskId);
taskId = -1;
}
activePlayers.clear();
playerRescanScheduled.set(false);
}
/**
@@ -375,11 +413,16 @@ public class WandSVC implements IrisService {
*/
private void tickAll() {
try {
J.runGlobal(() -> {
for (Player p : Bukkit.getOnlinePlayers()) {
J.runEntity(p, () -> tick(p));
}
});
if (!enabled) {
return;
}
if (ticksUntilPlayerRescan-- <= 0) {
ticksUntilPlayerRescan = PLAYER_RESCAN_INTERVAL_TICKS;
rescanPlayers();
}
for (Player player : activePlayers.values()) {
J.runEntity(player, () -> tick(player));
}
} catch (Throwable e) {
Iris.reportError(e);
}
@@ -387,20 +430,53 @@ public class WandSVC implements IrisService {
public void tick(Player p) {
try {
try {
if ((IrisSettings.get().getWorld().worldEditWandCUI && isHoldingWand(p)) || isWand(p.getInventory().getItemInMainHand())) {
Location[] d = getCuboid(p);
if (d == null || d[0] == null || d[1] == null) return;
new WandSelection(new Cuboid(d[0], d[1]), p).draw();
}
} catch (Throwable e) {
Iris.reportError(e);
if (!p.isOnline()) {
activePlayers.remove(p.getUniqueId(), p);
return;
}
Location[] selection = getCuboid(p);
if (!hasCompleteSelection(selection)) {
activePlayers.remove(p.getUniqueId(), p);
return;
}
new WandSelection(new Cuboid(selection[0], selection[1]), p).draw();
} catch (Throwable e) {
e.printStackTrace();
Iris.reportError(e);
}
}
private void rescanPlayers() {
if (!playerRescanScheduled.compareAndSet(false, true)) {
return;
}
if (!J.runGlobal(() -> {
try {
if (!enabled) {
return;
}
for (Player player : Bukkit.getOnlinePlayers()) {
J.runEntity(player, () -> refreshPlayer(player));
}
} finally {
playerRescanScheduled.set(false);
}
})) {
playerRescanScheduled.set(false);
}
}
private void refreshPlayer(Player player) {
if (enabled && player.isOnline() && hasCompleteSelection(getCuboid(player))) {
activePlayers.put(player.getUniqueId(), player);
return;
}
activePlayers.remove(player.getUniqueId(), player);
}
private static boolean hasCompleteSelection(Location[] selection) {
return selection != null && selection.length >= 2 && selection[0] != null && selection[1] != null;
}
/**
* Draw the outline of a selected region
*
@@ -492,6 +568,7 @@ public class WandSVC implements IrisService {
return;
try {
if (isHoldingIrisWand(e.getPlayer())) {
activePlayers.put(e.getPlayer().getUniqueId(), e.getPlayer());
if (e.getAction().equals(Action.LEFT_CLICK_BLOCK)) {
e.setCancelled(true);
e.getPlayer().getInventory().setItemInMainHand(update(true, Objects.requireNonNull(e.getClickedBlock()).getLocation(), e.getPlayer().getInventory().getItemInMainHand()));
@@ -517,6 +594,11 @@ public class WandSVC implements IrisService {
}
}
@EventHandler
public void on(PlayerQuitEvent event) {
activePlayers.remove(event.getPlayer().getUniqueId());
}
/**
* Is the player holding Dust?
*
@@ -23,16 +23,18 @@ import art.arcane.volmlib.util.math.M;
import org.bukkit.Location;
import org.bukkit.Particle;
import org.bukkit.entity.Player;
import org.bukkit.util.Vector;
import java.awt.Color;
import static art.arcane.iris.util.common.data.registry.Particles.REDSTONE;
public class WandSelection {
private static final double STEP = 0.10;
private static final double MAX_DISTANCE = 256D;
private static final double MAX_DISTANCE_SQUARED = MAX_DISTANCE * MAX_DISTANCE;
private final Cuboid c;
private final Player p;
private static final double STEP = 0.10;
public WandSelection(Cuboid c, Player p) {
this.c = c;
@@ -45,57 +47,91 @@ public class WandSelection {
return;
}
double maxDistanceSquared = 256 * 256;
int particleCount = 0;
// cube!
Location[][] edges = {
{c.getLowerNE(), new Location(c.getWorld(), c.getUpperX() + 1, c.getLowerY(), c.getLowerZ())},
{c.getLowerNE(), new Location(c.getWorld(), c.getLowerX(), c.getUpperY() + 1, c.getLowerZ())},
{c.getLowerNE(), new Location(c.getWorld(), c.getLowerX(), c.getLowerY(), c.getUpperZ() + 1)},
{new Location(c.getWorld(), c.getUpperX() + 1, c.getLowerY(), c.getLowerZ()), new Location(c.getWorld(), c.getUpperX() + 1, c.getUpperY() + 1, c.getLowerZ())},
{new Location(c.getWorld(), c.getUpperX() + 1, c.getLowerY(), c.getLowerZ()), new Location(c.getWorld(), c.getUpperX() + 1, c.getLowerY(), c.getUpperZ() + 1)},
{new Location(c.getWorld(), c.getLowerX(), c.getUpperY() + 1, c.getLowerZ()), new Location(c.getWorld(), c.getUpperX() + 1, c.getUpperY() + 1, c.getLowerZ())},
{new Location(c.getWorld(), c.getLowerX(), c.getUpperY() + 1, c.getLowerZ()), new Location(c.getWorld(), c.getLowerX(), c.getUpperY() + 1, c.getUpperZ() + 1)},
{new Location(c.getWorld(), c.getLowerX(), c.getLowerY(), c.getUpperZ() + 1), new Location(c.getWorld(), c.getUpperX() + 1, c.getLowerY(), c.getUpperZ() + 1)},
{new Location(c.getWorld(), c.getLowerX(), c.getLowerY(), c.getUpperZ() + 1), new Location(c.getWorld(), c.getLowerX(), c.getUpperY() + 1, c.getUpperZ() + 1)},
{new Location(c.getWorld(), c.getUpperX() + 1, c.getUpperY() + 1, c.getLowerZ()), new Location(c.getWorld(), c.getUpperX() + 1, c.getUpperY() + 1, c.getUpperZ() + 1)},
{new Location(c.getWorld(), c.getLowerX(), c.getUpperY() + 1, c.getUpperZ() + 1), new Location(c.getWorld(), c.getUpperX() + 1, c.getUpperY() + 1, c.getUpperZ() + 1)},
{new Location(c.getWorld(), c.getUpperX() + 1, c.getLowerY(), c.getUpperZ() + 1), new Location(c.getWorld(), c.getUpperX() + 1, c.getUpperY() + 1, c.getUpperZ() + 1)}
};
double minX = c.getLowerX();
double minY = c.getLowerY();
double minZ = c.getLowerZ();
double maxX = c.getUpperX() + 1D;
double maxY = c.getUpperY() + 1D;
double maxZ = c.getUpperZ() + 1D;
double playerX = playerLoc.getX();
double playerY = playerLoc.getY();
double playerZ = playerLoc.getZ();
for (Location[] edge : edges) {
Vector direction = edge[1].toVector().subtract(edge[0].toVector());
double length = direction.length();
direction.normalize();
drawX(minX, maxX, minY, minZ, playerX, playerY, playerZ);
drawX(minX, maxX, maxY, minZ, playerX, playerY, playerZ);
drawX(minX, maxX, minY, maxZ, playerX, playerY, playerZ);
drawX(minX, maxX, maxY, maxZ, playerX, playerY, playerZ);
drawY(minY, maxY, minX, minZ, playerX, playerY, playerZ);
drawY(minY, maxY, maxX, minZ, playerX, playerY, playerZ);
drawY(minY, maxY, minX, maxZ, playerX, playerY, playerZ);
drawY(minY, maxY, maxX, maxZ, playerX, playerY, playerZ);
drawZ(minZ, maxZ, minX, minY, playerX, playerY, playerZ);
drawZ(minZ, maxZ, maxX, minY, playerX, playerY, playerZ);
drawZ(minZ, maxZ, minX, maxY, playerX, playerY, playerZ);
drawZ(minZ, maxZ, maxX, maxY, playerX, playerY, playerZ);
}
for (double d = 0; d <= length; d += STEP) {
Location particleLoc = edge[0].clone().add(direction.clone().multiply(d));
private void drawX(double start, double end, double y, double z, double playerX, double playerY, double playerZ) {
double fixedDistanceSquared = square(playerY - y) + square(playerZ - z);
drawAxis(start, end, playerX, fixedDistanceSquared, (double coordinate, double distanceSquared) ->
spawnParticle(coordinate, y, z, distanceSquared));
}
if (playerLoc.distanceSquared(particleLoc) > maxDistanceSquared) {
continue;
}
private void drawY(double start, double end, double x, double z, double playerX, double playerY, double playerZ) {
double fixedDistanceSquared = square(playerX - x) + square(playerZ - z);
drawAxis(start, end, playerY, fixedDistanceSquared, (double coordinate, double distanceSquared) ->
spawnParticle(x, coordinate, z, distanceSquared));
}
spawnParticle(particleLoc, playerLoc);
particleCount++;
}
private void drawZ(double start, double end, double x, double y, double playerX, double playerY, double playerZ) {
double fixedDistanceSquared = square(playerX - x) + square(playerY - y);
drawAxis(start, end, playerZ, fixedDistanceSquared, (double coordinate, double distanceSquared) ->
spawnParticle(x, y, coordinate, distanceSquared));
}
private void drawAxis(double start, double end, double playerCoordinate, double fixedDistanceSquared, AxisParticle particle) {
if (fixedDistanceSquared > MAX_DISTANCE_SQUARED) {
return;
}
double visibleRadius = Math.sqrt(MAX_DISTANCE_SQUARED - fixedDistanceSquared);
double visibleStart = Math.max(start, playerCoordinate - visibleRadius);
double visibleEnd = Math.min(end, playerCoordinate + visibleRadius);
if (visibleStart > visibleEnd) {
return;
}
int firstSample = (int) Math.max(0D, Math.ceil((visibleStart - start) / STEP));
int lastSample = (int) Math.floor((visibleEnd - start) / STEP);
for (int index = firstSample; index <= lastSample; index++) {
double coordinate = start + index * STEP;
double distanceSquared = fixedDistanceSquared + square(playerCoordinate - coordinate);
particle.spawn(coordinate, distanceSquared);
}
}
private void spawnParticle(Location particleLoc, Location playerLoc) {
double accuracy = M.lerpInverse(0, 64 * 64, playerLoc.distanceSquared(particleLoc));
private void spawnParticle(double x, double y, double z, double distanceSquared) {
double accuracy = M.lerpInverse(0, 64 * 64, distanceSquared);
double dist = M.lerp(0.125, 3.5, accuracy);
if (M.r(Math.min(dist * 5, 0.9D) * 0.995)) {
return;
}
float hue = (float) (0.5f + (Math.sin((particleLoc.getX() + particleLoc.getY() + particleLoc.getZ() + (p.getTicksLived() / 2f)) / 20f) / 2));
float hue = (float) (0.5f + (Math.sin((x + y + z + (p.getTicksLived() / 2f)) / 20f) / 2));
Color color = Color.getHSBColor(hue, 1, 1);
p.spawnParticle(REDSTONE, particleLoc,
p.spawnParticle(REDSTONE, x, y, z,
0, 0, 0, 0, 1,
new Particle.DustOptions(org.bukkit.Color.fromRGB(color.getRed(), color.getGreen(), color.getBlue()),
(float) dist * 3f));
}
private static double square(double value) {
return value * value;
}
@FunctionalInterface
private interface AxisParticle {
void spawn(double coordinate, double distanceSquared);
}
}
@@ -5,44 +5,31 @@ import org.junit.Test;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class CommandIrisFoliaCreateContractTest {
@Test
public void ordinaryFoliaCreateRestartsOnlyAfterSuccessfulStagingAndFeedback() throws Exception {
public void ordinaryFoliaCreateUsesTheSharedRuntimeCreationPath() throws Exception {
String source = Files.readString(Path.of(System.getProperty("iris.commandIrisSource")));
String foliaCreate = source.substring(
source.indexOf("if (J.isFolia()) {"),
source.indexOf(" try {", source.indexOf("if (J.isFolia()) {"))
String create = source.substring(
source.indexOf(" public void create("),
source.indexOf(" @Director(", source.indexOf(" public void create("))
);
int stage = foliaCreate.indexOf("stageFoliaWorldCreation(worldName, dimension, seed)");
int failureExit = foliaCreate.indexOf("if (!staged)");
int feedback = foliaCreate.indexOf("COMMAND_IRIS_WORLD_STAGING_COMPLETED_RESTARTING_SERVER_GENERATE_LOAD");
int restart = foliaCreate.indexOf("ServerConfigurator.restart(\"Iris staged Folia world");
assertTrue(stage >= 0);
assertTrue(stage < failureExit);
assertTrue(failureExit < feedback);
assertTrue(feedback < restart);
assertTrue(create.contains("IrisToolbelt.createWorld()"));
assertTrue(create.contains(".studio(false)"));
assertTrue(create.contains(".create();"));
assertFalse(create.contains("J.isFolia()"));
assertFalse(create.contains("ServerConfigurator.restart("));
}
@Test
public void foliaStagePublishesCurrentPaperDataBeforeRegisteringStartupAlias() throws Exception {
public void obsoleteFoliaStagingSurfaceIsRemoved() throws Exception {
String source = Files.readString(Path.of(System.getProperty("iris.commandIrisSource")));
int methodStart = source.indexOf("private boolean stageFoliaWorldCreation(");
int methodEnd = source.indexOf("private boolean registerWorldInBukkitYml(", methodStart);
String staging = source.substring(methodStart, methodEnd);
int currentPaperData = staging.indexOf("INMS.get().writeCurrentPaperWorldData(");
int pack = staging.indexOf("installIntoWorld(");
int publication = staging.indexOf("AtomicDirectoryPublisher.publishAbsent(");
int registration = staging.indexOf("registerWorldInBukkitYml(worldKey");
assertTrue(currentPaperData >= 0);
assertTrue(currentPaperData < pack);
assertTrue(pack < publication);
assertTrue(publication < registration);
assertTrue(source.contains("IrisWorldStorage.configuredWorldName("));
assertFalse(source.contains("stageFoliaWorldCreation"));
assertFalse(source.contains("writeCurrentPaperWorldData"));
assertFalse(source.contains("COMMAND_IRIS_RUNTIME_WORLD_CREATION_IS_DISABLED_ON_FOLIA"));
}
}
@@ -0,0 +1,255 @@
package art.arcane.iris.core.gui;
import art.arcane.iris.core.runtime.WorldRuntimeControlService;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisWorld;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
import art.arcane.iris.util.common.scheduling.J;
import org.bukkit.Chunk;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
public class BukkitVisionOverlayFoliaContractTest {
@Test
public void teleportLoadsTheDestinationChunkBeforeItsOwningRegionReadsTheSurface() throws Exception {
String source = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/gui/BukkitVisionOverlay.java"
)).replace("\r\n", "\n");
String request = method(source, "private void requestTeleportChunk(");
assertBefore(request, "requestChunkAsync(", "requested.whenComplete(");
assertBefore(request, "requested.whenComplete(", "J.runRegion(");
assertBefore(request, "J.runRegion(", "world.getHighestBlockYAt(");
assertBefore(request, "world.getHighestBlockYAt(", "J.runEntity(");
assertTrue(request.contains("chunkX,\n chunkZ,\n true,\n true"));
assertEquals(1, occurrences(request, "world.getHighestBlockYAt("));
}
@Test
public void teleportReportsAsyncAndSchedulingFailuresWithDestinationContext() throws Exception {
String source = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/gui/BukkitVisionOverlay.java"
)).replace("\r\n", "\n");
String request = method(source, "private void requestTeleportChunk(");
String reporter = method(source, "private void reportTeleportFailure(");
assertTrue(request.contains("if (requested == null)"));
assertTrue(request.contains("if (failure != null)"));
assertTrue(request.contains("if (chunk == null ||"));
assertTrue(request.contains("if (!J.runEntity("));
assertTrue(request.contains("if (!scheduled)"));
assertTrue(reporter.contains("IrisLogging.reportError("));
assertTrue(reporter.contains("world.getName()"));
assertTrue(reporter.contains("blockX + \",\" + blockZ"));
}
@Test
public void teleportObservesNativeCompletionAndRejectsFalseSettlement() throws Exception {
String source = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/gui/BukkitVisionOverlay.java"
)).replace("\r\n", "\n");
String delegate = method(source, "private void delegateTeleport(");
assertTrue(delegate.contains("teleport.whenComplete("));
assertTrue(delegate.contains("!Boolean.TRUE.equals(success)"));
assertTrue(delegate.contains("restartLatest(request)"));
}
@Test
public void staleChunkCompletionCannotTeleportOverTheLatestRequest() {
VisionHarness harness = new VisionHarness();
CompletableFuture<Chunk> firstChunk = new CompletableFuture<>();
CompletableFuture<Chunk> secondChunk = new CompletableFuture<>();
harness.stubChunk(0, 0, firstChunk);
harness.stubChunk(2, 2, secondChunk);
try (harness) {
harness.overlay.teleport(1.5D, 1.5D);
harness.overlay.teleport(33.5D, 33.5D);
firstChunk.complete(harness.chunk);
assertEquals(0, harness.destinations.size());
secondChunk.complete(harness.chunk);
assertEquals(1, harness.destinations.size());
assertEquals(33, harness.destinations.get(0).getBlockX());
assertEquals(33, harness.destinations.get(0).getBlockZ());
}
}
@Test
public void latestRequestRunsAfterAnOlderNativeTeleportSettles() {
VisionHarness harness = new VisionHarness();
harness.stubChunk(0, 0, CompletableFuture.completedFuture(harness.chunk));
harness.stubChunk(2, 2, CompletableFuture.completedFuture(harness.chunk));
CompletableFuture<Boolean> firstTeleport = new CompletableFuture<>();
CompletableFuture<Boolean> secondTeleport = new CompletableFuture<>();
harness.nativeTeleports.add(firstTeleport);
harness.nativeTeleports.add(secondTeleport);
try (harness) {
harness.overlay.teleport(1.5D, 1.5D);
harness.overlay.teleport(33.5D, 33.5D);
assertEquals(1, harness.destinations.size());
firstTeleport.complete(true);
assertEquals(2, harness.destinations.size());
assertEquals(33, harness.destinations.get(1).getBlockX());
assertEquals(33, harness.destinations.get(1).getBlockZ());
secondTeleport.complete(true);
}
}
private static void assertBefore(String source, String first, String second) {
int firstIndex = source.indexOf(first);
int secondIndex = source.indexOf(second);
assertTrue("Missing source contract token: " + first, firstIndex >= 0);
assertTrue("Missing source contract token: " + second, secondIndex >= 0);
assertTrue(first + " must occur before " + second, firstIndex < secondIndex);
}
private static int occurrences(String source, String match) {
int count = 0;
int offset = 0;
while ((offset = source.indexOf(match, offset)) >= 0) {
count++;
offset += match.length();
}
return count;
}
private static String method(String source, String signature) {
int start = source.indexOf(signature);
assertTrue("Missing source contract signature: " + signature, start >= 0);
int openBrace = source.indexOf('{', start);
assertTrue("Missing source contract method body: " + signature, openBrace >= 0);
int depth = 0;
for (int index = openBrace; index < source.length(); index++) {
char current = source.charAt(index);
if (current == '{') {
depth++;
} else if (current == '}') {
depth--;
if (depth == 0) {
return source.substring(start, index + 1);
}
}
}
throw new IllegalArgumentException("Unclosed source contract method: " + signature);
}
private static final class VisionHarness implements AutoCloseable {
private final Engine engine;
private final IrisWorld target;
private final World world;
private final Player player;
private final Chunk chunk;
private final WorldRuntimeControlService runtime;
private final MockedStatic<J> scheduling;
private final MockedStatic<BukkitWorldBinding> binding;
private final MockedStatic<WorldRuntimeControlService> runtimeAccess;
private final MockedStatic<BukkitPlatform> platform;
private final List<Location> destinations;
private final List<CompletableFuture<Boolean>> nativeTeleports;
private final AtomicInteger nativeTeleportIndex;
private final BukkitVisionOverlay overlay;
private VisionHarness() {
engine = mock(Engine.class);
target = mock(IrisWorld.class);
world = mock(World.class);
player = mock(Player.class);
chunk = mock(Chunk.class);
runtime = mock(WorldRuntimeControlService.class);
destinations = new ArrayList<>();
nativeTeleports = new ArrayList<>();
nativeTeleportIndex = new AtomicInteger();
when(engine.getWorld()).thenReturn(target);
when(target.hasPlatformWorld()).thenReturn(true);
when(player.isOnline()).thenReturn(true);
when(player.getWorld()).thenReturn(world);
when(chunk.getWorld()).thenReturn(world);
when(world.getHighestBlockYAt(anyInt(), anyInt())).thenReturn(70);
scheduling = mockStatic(J.class);
scheduling.when(() -> J.runGlobal(any(Runnable.class))).thenAnswer(invocation -> {
invocation.getArgument(0, Runnable.class).run();
return true;
});
scheduling.when(() -> J.runRegion(
same(world),
anyInt(),
anyInt(),
any(Runnable.class)))
.thenAnswer(invocation -> {
invocation.getArgument(3, Runnable.class).run();
return true;
});
scheduling.when(() -> J.runEntity(same(player), any(Runnable.class))).thenAnswer(invocation -> {
invocation.getArgument(1, Runnable.class).run();
return true;
});
binding = mockStatic(BukkitWorldBinding.class);
binding.when(() -> BukkitWorldBinding.world(target)).thenReturn(world);
binding.when(() -> BukkitWorldBinding.players(target)).thenReturn(List.of(player));
runtimeAccess = mockStatic(WorldRuntimeControlService.class);
runtimeAccess.when(WorldRuntimeControlService::get).thenReturn(runtime);
platform = mockStatic(BukkitPlatform.class);
platform.when(() -> BukkitPlatform.teleportAsync(same(player), any(Location.class)))
.thenAnswer(invocation -> {
destinations.add(invocation.getArgument(1, Location.class));
int index = nativeTeleportIndex.getAndIncrement();
return index < nativeTeleports.size()
? nativeTeleports.get(index)
: CompletableFuture.completedFuture(true);
});
overlay = new BukkitVisionOverlay(engine);
}
private void stubChunk(
int chunkX,
int chunkZ,
CompletableFuture<Chunk> requested
) {
when(runtime.requestChunkAsync(
same(world),
eq(chunkX),
eq(chunkZ),
eq(true),
eq(true))).thenReturn(requested);
}
@Override
public void close() {
platform.close();
runtimeAccess.close();
binding.close();
scheduling.close();
}
}
}
@@ -11,15 +11,14 @@ import static org.junit.Assert.assertTrue;
public class StudioPlayerModeContractTest {
@Test
public void studioEntryKeepsPlayersEligibleForNaturalSpawning() throws IOException {
public void autoStartedStudioUsesSpectatorMode() throws IOException {
String plugin = Files.readString(Path.of("src/main/java/art/arcane/iris/Iris.java")).replace("\r\n", "\n");
String commands = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/commands/CommandStudio.java")).replace("\r\n", "\n");
assertFalse(plugin.contains("GameMode.SPECTATOR"));
assertFalse(commands.contains("GameMode.SPECTATOR"));
assertTrue(plugin.contains("GameMode.CREATIVE"));
assertTrue(commands.contains("GameMode.CREATIVE"));
assertFalse(plugin.contains("GameMode.CREATIVE"));
assertFalse(commands.contains("GameMode.CREATIVE"));
assertTrue(plugin.contains("GameMode.SPECTATOR"));
}
@Test
@@ -32,8 +31,23 @@ public class StudioPlayerModeContractTest {
assertTrue(method.contains("StudioSVC studioService = Iris.service(StudioSVC.class)"));
assertTrue(method.contains("studioService.teleportToActiveProject(player)"));
assertFalse(method.contains("setGameMode("));
assertFalse(method.contains("getActiveProject()"));
assertFalse(method.contains("BukkitPlatform.teleportAsync"));
assertFalse(method.contains("BukkitWorldBinding.spawnLocation"));
}
@Test
public void editingStudiosRemainCreative() throws IOException {
String objectCommands = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/commands/CommandObject.java")).replace("\r\n", "\n");
String jigsawCommands = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/commands/CommandJigsaw.java")).replace("\r\n", "\n");
assertTrue(objectCommands.contains("GameMode.CREATIVE"));
assertTrue(objectCommands.contains("StudioOpenCoordinator.StudioOpenKind.OBJECT"));
assertTrue(jigsawCommands.contains("GameMode.CREATIVE"));
assertFalse(objectCommands.contains("GameMode.SPECTATOR"));
assertFalse(jigsawCommands.contains("GameMode.SPECTATOR"));
}
}