mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-08-27 12:41:43 +00:00
Way too much
This commit is contained in:
+29
-4
@@ -11,6 +11,7 @@ import art.arcane.iris.engine.framework.GenerationSessionLease;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisBiomeCustom;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisDimensionCarvingResolver;
|
||||
import art.arcane.iris.util.project.context.IrisContext;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
@@ -521,6 +522,16 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
int y,
|
||||
int z,
|
||||
Climate.Sampler sampler
|
||||
) {
|
||||
return getVisibleNoiseBiomeWithActiveGenerationLease(x, y, z, sampler, null);
|
||||
}
|
||||
|
||||
Holder<Biome> getVisibleNoiseBiomeWithActiveGenerationLease(
|
||||
int x,
|
||||
int y,
|
||||
int z,
|
||||
Climate.Sampler sampler,
|
||||
IrisDimensionCarvingResolver.State resolverState
|
||||
) {
|
||||
long cacheKey = packNoiseKey(x, y, z);
|
||||
Holder<Biome> cachedHolder = noiseBiomeCache.get(cacheKey);
|
||||
@@ -528,7 +539,7 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
return cachedHolder;
|
||||
}
|
||||
|
||||
Holder<Biome> resolvedHolder = resolveVisibleBiomeHolder(x, y, z);
|
||||
Holder<Biome> resolvedHolder = resolveVisibleBiomeHolder(x, y, z, resolverState);
|
||||
Holder<Biome> existingHolder = noiseBiomeCache.putIfAbsent(cacheKey, resolvedHolder);
|
||||
if (existingHolder != null) {
|
||||
return existingHolder;
|
||||
@@ -602,8 +613,13 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
return holder;
|
||||
}
|
||||
|
||||
private Holder<Biome> resolveVisibleBiomeHolder(int x, int y, int z) {
|
||||
BiomeResolution resolution = resolveBiomeResolution(x, y, z);
|
||||
private Holder<Biome> resolveVisibleBiomeHolder(
|
||||
int x,
|
||||
int y,
|
||||
int z,
|
||||
IrisDimensionCarvingResolver.State resolverState
|
||||
) {
|
||||
BiomeResolution resolution = resolveBiomeResolution(x, y, z, resolverState);
|
||||
if (resolution == null) {
|
||||
return getFallbackBiome();
|
||||
}
|
||||
@@ -636,6 +652,15 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
}
|
||||
|
||||
private BiomeResolution resolveBiomeResolution(int x, int y, int z) {
|
||||
return resolveBiomeResolution(x, y, z, null);
|
||||
}
|
||||
|
||||
private BiomeResolution resolveBiomeResolution(
|
||||
int x,
|
||||
int y,
|
||||
int z,
|
||||
IrisDimensionCarvingResolver.State resolverState
|
||||
) {
|
||||
if (engine == null || engine.isClosed()) {
|
||||
return null;
|
||||
}
|
||||
@@ -657,7 +682,7 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
int surfaceInternalY = engine.getComplex().getHeightStream().get(blockX, blockZ).intValue();
|
||||
underground = internalY <= surfaceInternalY - 8;
|
||||
irisBiome = underground
|
||||
? engine.getCaveBiome(blockX, internalY, blockZ)
|
||||
? engine.getCaveBiome(blockX, internalY, blockZ, resolverState)
|
||||
: engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
|
||||
} else {
|
||||
irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
|
||||
|
||||
+4
-1
@@ -10,6 +10,7 @@ import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
||||
import art.arcane.iris.engine.IrisEngine;
|
||||
import art.arcane.iris.engine.platform.BukkitChunkGenerator;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisDimensionCarvingResolver;
|
||||
import art.arcane.iris.engine.object.IrisMaterialPalette;
|
||||
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
|
||||
import art.arcane.iris.nativegen.NativeStructureGenerationException;
|
||||
@@ -643,8 +644,10 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_create_biomes");
|
||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
customBiomeSource.prepareVisibleBiomeBatch();
|
||||
IrisDimensionCarvingResolver.State resolverState = new IrisDimensionCarvingResolver.State();
|
||||
ichunkaccess.fillBiomesFromNoise(
|
||||
customBiomeSource::getVisibleNoiseBiomeWithActiveGenerationLease,
|
||||
(x, y, z, sampler) -> customBiomeSource.getVisibleNoiseBiomeWithActiveGenerationLease(
|
||||
x, y, z, sampler, resolverState),
|
||||
randomstate.sampler());
|
||||
return CompletableFuture.completedFuture(ichunkaccess);
|
||||
}
|
||||
|
||||
+2
-1
@@ -258,7 +258,8 @@ public class IrisChunkGeneratorFailureContractTest {
|
||||
|
||||
assertTrue(createBiomes.contains("requireGenerationStage(\"bukkit_nms_create_biomes\")"));
|
||||
assertTrue(createBiomes.contains("customBiomeSource.prepareVisibleBiomeBatch()"));
|
||||
assertTrue(createBiomes.contains("customBiomeSource::getVisibleNoiseBiomeWithActiveGenerationLease"));
|
||||
assertTrue(createBiomes.contains("new IrisDimensionCarvingResolver.State()"));
|
||||
assertTrue(createBiomes.contains("sampler, resolverState"));
|
||||
assertTrue(buildSurface.contains("delegate.buildSurface("));
|
||||
assertTrue(carvers.contains("delegate.applyCarvers("));
|
||||
assertTrue(noise.contains("requireNoiseGenerationStage("));
|
||||
|
||||
@@ -573,7 +573,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
}
|
||||
|
||||
private static String logPrefix(Iris plugin) {
|
||||
return plugin == null ? "[Iris] " : plugin.getTag();
|
||||
return plugin == null ? ComponentLog.discriminator("Iris", "&a") : plugin.getTag();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,6 +9,7 @@ import art.arcane.iris.core.lifecycle.WorldReplacementBootstrap;
|
||||
import art.arcane.iris.core.lifecycle.WorldReplacementBootstrapMarker;
|
||||
import art.arcane.iris.core.pack.DefaultPackBootstrapProvisioner;
|
||||
import art.arcane.iris.core.pack.DefaultPackBootstrapProvisioner.ProvisionResult;
|
||||
import art.arcane.iris.util.common.misc.SlimJar;
|
||||
import io.papermc.paper.plugin.bootstrap.BootstrapContext;
|
||||
import io.papermc.paper.plugin.bootstrap.PluginBootstrap;
|
||||
import io.papermc.paper.plugin.lifecycle.event.LifecycleEvent;
|
||||
@@ -26,6 +27,7 @@ public final class IrisBootstrap implements PluginBootstrap {
|
||||
public void bootstrap(BootstrapContext context) {
|
||||
WorldReplacementBootstrapMarker.markBootstrapped();
|
||||
try {
|
||||
loadRuntimeLibraries(context);
|
||||
BukkitStartupPaths startupPaths = BukkitStartupPaths.resolveCurrent();
|
||||
reconcilePendingWorldReplacements(context, startupPaths);
|
||||
quarantineWorthlessHusks(startupPaths, message -> context.getLogger().warn(message));
|
||||
@@ -38,6 +40,27 @@ public final class IrisBootstrap implements PluginBootstrap {
|
||||
}
|
||||
}
|
||||
|
||||
private static void loadRuntimeLibraries(BootstrapContext context) {
|
||||
SlimJar.loadBootstrap(
|
||||
context.getDataDirectory().resolve("cache").resolve("libraries"),
|
||||
new SlimJar.BootstrapLogger() {
|
||||
@Override
|
||||
public void info(String message) {
|
||||
context.getLogger().info(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void error(String message) {
|
||||
context.getLogger().error(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void debug(String message) {
|
||||
context.getLogger().debug(message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void reconcilePendingWorldReplacements(
|
||||
BootstrapContext context,
|
||||
BukkitStartupPaths startupPaths
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package art.arcane.iris;
|
||||
|
||||
import art.arcane.iris.util.common.misc.SlimJar;
|
||||
import io.papermc.paper.plugin.bootstrap.PluginProviderContext;
|
||||
import io.papermc.paper.plugin.loader.PluginClasspathBuilder;
|
||||
import io.papermc.paper.plugin.loader.PluginLoader;
|
||||
import io.papermc.paper.plugin.loader.library.impl.JarLibrary;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@SuppressWarnings("UnstableApiUsage")
|
||||
public final class IrisPluginLoader implements PluginLoader {
|
||||
@Override
|
||||
public void classloader(PluginClasspathBuilder classpathBuilder) {
|
||||
PluginProviderContext context = classpathBuilder.getContext();
|
||||
Path libraryRoot = context.getDataDirectory().resolve("cache").resolve("libraries");
|
||||
SlimJar.loadBootstrap(libraryRoot, new SlimJar.BootstrapLogger() {
|
||||
@Override
|
||||
public void info(String message) {
|
||||
context.getLogger().info(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void error(String message) {
|
||||
context.getLogger().error(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void debug(String message) {
|
||||
context.getLogger().debug(message);
|
||||
}
|
||||
});
|
||||
for (Path library : relocatedLibraries(libraryRoot)) {
|
||||
classpathBuilder.addLibrary(new JarLibrary(library));
|
||||
}
|
||||
}
|
||||
|
||||
static List<Path> relocatedLibraries(Path libraryRoot) {
|
||||
try (Stream<Path> paths = Files.walk(libraryRoot)) {
|
||||
List<Path> libraries = paths
|
||||
.filter(Files::isRegularFile)
|
||||
.filter(path -> path.getFileName().toString().endsWith(".jar"))
|
||||
.filter(IrisPluginLoader::isRelocatedLibrary)
|
||||
.sorted()
|
||||
.toList();
|
||||
if (libraries.isEmpty()) {
|
||||
throw new IllegalStateException("Iris runtime library provisioning produced no relocated libraries.");
|
||||
}
|
||||
return libraries;
|
||||
} catch (IOException failure) {
|
||||
throw new IllegalStateException("Unable to enumerate provisioned Iris runtime libraries.", failure);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isRelocatedLibrary(Path path) {
|
||||
int count = path.getNameCount();
|
||||
for (int i = 0; i < count - 2; i++) {
|
||||
if ("relocated".equals(path.getName(i).toString())
|
||||
&& "Iris".equals(path.getName(i + 1).toString())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -96,6 +96,7 @@ import java.time.temporal.ChronoUnit;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
@@ -412,7 +413,8 @@ public class CommandStudio implements DirectorExecutor {
|
||||
return;
|
||||
}
|
||||
|
||||
VisionGUI.launch(IrisToolbelt.access(world).getEngine());
|
||||
UUID openerId = sender().isPlayer() ? player().getUniqueId() : null;
|
||||
VisionGUI.launch(IrisToolbelt.access(world).getEngine(), openerId);
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_OPENING_MAP));
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ import org.bukkit.event.Listener;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public final class BukkitGuiHost implements GuiHost.Provider {
|
||||
@@ -72,8 +73,8 @@ public final class BukkitGuiHost implements GuiHost.Provider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public GuiOverlay overlayFor(Engine engine) {
|
||||
return engine == null ? null : new BukkitVisionOverlay(engine);
|
||||
public GuiOverlay overlayFor(Engine engine, UUID openerId) {
|
||||
return engine == null ? null : new BukkitVisionOverlay(engine, openerId);
|
||||
}
|
||||
|
||||
private static final class HotloadListener implements Listener {
|
||||
|
||||
+32
-65
@@ -18,7 +18,6 @@
|
||||
|
||||
package art.arcane.iris.core.gui;
|
||||
|
||||
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;
|
||||
@@ -28,7 +27,6 @@ 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;
|
||||
@@ -39,6 +37,7 @@ import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
@@ -50,14 +49,16 @@ import static art.arcane.iris.util.common.data.registry.Attributes.MAX_HEALTH;
|
||||
|
||||
public final class BukkitVisionOverlay implements GuiOverlay {
|
||||
private final Engine engine;
|
||||
private final UUID openerId;
|
||||
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) {
|
||||
public BukkitVisionOverlay(Engine engine, UUID openerId) {
|
||||
this.engine = engine;
|
||||
this.openerId = openerId;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -177,67 +178,20 @@ public final class BukkitVisionOverlay implements GuiOverlay {
|
||||
return;
|
||||
}
|
||||
List<Player> players = BukkitWorldBinding.players(target);
|
||||
if (players.isEmpty()) {
|
||||
Player player = selectPlayer(players);
|
||||
if (player == null) {
|
||||
finish(request);
|
||||
return;
|
||||
}
|
||||
Player player = players.get(0);
|
||||
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);
|
||||
int blockX = request.blockX;
|
||||
int blockZ = request.blockZ;
|
||||
try {
|
||||
int blockY = engine.getMinHeight() + engine.getHeight(blockX, blockZ, false) + 2;
|
||||
Location destination = new Location(
|
||||
world,
|
||||
blockX + 0.5D,
|
||||
blockY,
|
||||
blockZ + 0.5D);
|
||||
if (!J.runEntity(player, () -> delegateTeleport(
|
||||
request,
|
||||
target,
|
||||
@@ -247,12 +201,25 @@ public final class BukkitVisionOverlay implements GuiOverlay {
|
||||
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."));
|
||||
} catch (Throwable failure) {
|
||||
fail(request, target, world, failure);
|
||||
}
|
||||
});
|
||||
if (!scheduled) {
|
||||
finish(request);
|
||||
}
|
||||
}
|
||||
|
||||
private Player selectPlayer(List<Player> players) {
|
||||
if (openerId == null) {
|
||||
return players.isEmpty() ? null : players.get(0);
|
||||
}
|
||||
for (Player player : players) {
|
||||
if (openerId.equals(player.getUniqueId())) {
|
||||
return player;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void delegateTeleport(
|
||||
|
||||
@@ -2,6 +2,7 @@ name: ${name}
|
||||
version: ${version}
|
||||
main: ${main}
|
||||
bootstrapper: ${bootstrapper}
|
||||
loader: art.arcane.iris.IrisPluginLoader
|
||||
folia-supported: true
|
||||
api-version: '${apiVersion}'
|
||||
load: STARTUP
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package art.arcane.iris;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class IrisPluginLoaderTest {
|
||||
@Rule
|
||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
|
||||
@Test
|
||||
public void exposesOnlyRelocatedRuntimeLibrariesInStableOrder() throws Exception {
|
||||
Path root = temporaryFolder.newFolder("libraries").toPath();
|
||||
Path original = Files.createDirectories(root.resolve("gson/2.14.0"))
|
||||
.resolve("gson.jar");
|
||||
Path relocatedGson = Files.createDirectories(root.resolve("gson/2.14.0/relocated/Iris"))
|
||||
.resolve("gson.jar");
|
||||
Path relocatedCaffeine = Files.createDirectories(root.resolve("caffeine/3.2.4/relocated/Iris"))
|
||||
.resolve("caffeine.jar");
|
||||
Files.createFile(original);
|
||||
Files.createFile(relocatedGson);
|
||||
Files.createFile(relocatedCaffeine);
|
||||
|
||||
List<Path> libraries = IrisPluginLoader.relocatedLibraries(root);
|
||||
|
||||
assertEquals(List.of(relocatedCaffeine, relocatedGson), libraries);
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,7 @@ public class PaperPluginMetadataTest {
|
||||
}
|
||||
|
||||
assertTrue(metadata.contains("bootstrapper: " + IrisBootstrap.class.getName()));
|
||||
assertTrue(metadata.contains("loader: " + IrisPluginLoader.class.getName()));
|
||||
assertTrue(metadata.contains("folia-supported: true"));
|
||||
assertTrue(metadata.contains("load: STARTUP"));
|
||||
assertFalse(metadata.contains("commands:"));
|
||||
|
||||
+40
-131
@@ -1,108 +1,67 @@
|
||||
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.UUID;
|
||||
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.verify;
|
||||
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() {
|
||||
public void teleportDelegatesImmediatelyToTheNativeAsyncPath() {
|
||||
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());
|
||||
Location destination = harness.destinations.get(0);
|
||||
assertEquals(33.5D, destination.getX(), 0D);
|
||||
assertEquals(76D, destination.getY(), 0D);
|
||||
assertEquals(33.5D, destination.getZ(), 0D);
|
||||
verify(harness.engine).getHeight(33, 33, false);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void teleportFloorsNegativeCoordinatesBeforeCenteringTheDestination() {
|
||||
VisionHarness harness = new VisionHarness();
|
||||
|
||||
try (harness) {
|
||||
harness.overlay.teleport(-0.25D, -16.01D);
|
||||
|
||||
assertEquals(1, harness.destinations.size());
|
||||
assertEquals(33, harness.destinations.get(0).getBlockX());
|
||||
assertEquals(33, harness.destinations.get(0).getBlockZ());
|
||||
Location destination = harness.destinations.get(0);
|
||||
assertEquals(-0.5D, destination.getX(), 0D);
|
||||
assertEquals(-16.5D, destination.getZ(), 0D);
|
||||
verify(harness.engine).getHeight(-1, -17, false);
|
||||
}
|
||||
}
|
||||
|
||||
@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);
|
||||
@@ -121,42 +80,17 @@ public class BukkitVisionOverlayFoliaContractTest {
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
@Test
|
||||
public void missingOpenerDoesNotTeleportAnotherPlayer() {
|
||||
VisionHarness harness = new VisionHarness();
|
||||
harness.binding.when(() -> BukkitWorldBinding.players(harness.target))
|
||||
.thenReturn(List.of(harness.otherPlayer));
|
||||
|
||||
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;
|
||||
}
|
||||
try (harness) {
|
||||
harness.overlay.teleport(1.5D, 1.5D);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
assertEquals(0, harness.destinations.size());
|
||||
}
|
||||
throw new IllegalArgumentException("Unclosed source contract method: " + signature);
|
||||
}
|
||||
|
||||
private static final class VisionHarness implements AutoCloseable {
|
||||
@@ -164,11 +98,10 @@ public class BukkitVisionOverlayFoliaContractTest {
|
||||
private final IrisWorld target;
|
||||
private final World world;
|
||||
private final Player player;
|
||||
private final Chunk chunk;
|
||||
private final WorldRuntimeControlService runtime;
|
||||
private final Player otherPlayer;
|
||||
private final UUID openerId;
|
||||
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;
|
||||
@@ -180,33 +113,26 @@ public class BukkitVisionOverlayFoliaContractTest {
|
||||
target = mock(IrisWorld.class);
|
||||
world = mock(World.class);
|
||||
player = mock(Player.class);
|
||||
chunk = mock(Chunk.class);
|
||||
runtime = mock(WorldRuntimeControlService.class);
|
||||
otherPlayer = mock(Player.class);
|
||||
openerId = UUID.randomUUID();
|
||||
destinations = new ArrayList<>();
|
||||
nativeTeleports = new ArrayList<>();
|
||||
nativeTeleportIndex = new AtomicInteger();
|
||||
|
||||
when(engine.getWorld()).thenReturn(target);
|
||||
when(engine.getMinHeight()).thenReturn(-64);
|
||||
when(engine.getHeight(anyInt(), anyInt(), eq(false))).thenReturn(138);
|
||||
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);
|
||||
when(player.getUniqueId()).thenReturn(openerId);
|
||||
when(otherPlayer.getUniqueId()).thenReturn(UUID.randomUUID());
|
||||
|
||||
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;
|
||||
@@ -214,10 +140,7 @@ public class BukkitVisionOverlayFoliaContractTest {
|
||||
|
||||
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);
|
||||
binding.when(() -> BukkitWorldBinding.players(target)).thenReturn(List.of(otherPlayer, player));
|
||||
|
||||
platform = mockStatic(BukkitPlatform.class);
|
||||
platform.when(() -> BukkitPlatform.teleportAsync(same(player), any(Location.class)))
|
||||
@@ -228,26 +151,12 @@ public class BukkitVisionOverlayFoliaContractTest {
|
||||
? 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);
|
||||
overlay = new BukkitVisionOverlay(engine, openerId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
platform.close();
|
||||
runtimeAccess.close();
|
||||
binding.close();
|
||||
scheduling.close();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user