mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-08-27 04:37:47 +00:00
f
This commit is contained in:
-130
@@ -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);
|
||||
}
|
||||
}
|
||||
+2
-14
@@ -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();
|
||||
|
||||
-132
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -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) {
|
||||
|
||||
+25
-19
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+209
-13
@@ -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 {
|
||||
|
||||
+17
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+14
-27
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
+255
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+19
-5
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user