From a82b8f4a9c8eed287de5db28cdd507cf7ea142ee Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Tue, 9 Jun 2026 20:14:27 -0400 Subject: [PATCH] dwa --- core/src/main/java/art/arcane/iris/Iris.java | 3 +- .../iris/core/commands/CommandIris.java | 18 ++++---- .../methods/MedievalPregenMethod.java | 15 +++++++ .../core/runtime/StudioOpenCoordinator.java | 36 +++++++++++++++ .../arcane/iris/engine/object/IrisObject.java | 13 ++++++ .../iris/engine/object/IrisObjectVacuum.java | 32 +++++++++++++- .../engine/object/IrisVacuumSettings.java | 8 ++++ .../iris/engine/object/ObjectPlaceMode.java | 4 ++ .../engine/object/IrisObjectVacuumTest.java | 44 +++++++++++++++++++ 9 files changed, 163 insertions(+), 10 deletions(-) diff --git a/core/src/main/java/art/arcane/iris/Iris.java b/core/src/main/java/art/arcane/iris/Iris.java index 1f691ea2d..c11e76b8a 100644 --- a/core/src/main/java/art/arcane/iris/Iris.java +++ b/core/src/main/java/art/arcane/iris/Iris.java @@ -71,6 +71,7 @@ import art.arcane.iris.util.common.scheduling.J; import art.arcane.iris.util.common.misc.ServerProperties; import art.arcane.volmlib.util.scheduling.Queue; import art.arcane.volmlib.util.scheduling.ShurikenQueue; +import io.papermc.lib.PaperLib; import lombok.NonNull; import org.bukkit.*; import org.bukkit.block.data.BlockData; @@ -872,7 +873,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware { for (Player i : getServer().getOnlinePlayers()) { final Runnable playerTask = () -> { i.setGameMode(GameMode.SPECTATOR); - i.teleport(spawn); + PaperLib.teleportAsync(i, spawn); }; if (!J.runEntity(i, playerTask)) { playerTask.run(); diff --git a/core/src/main/java/art/arcane/iris/core/commands/CommandIris.java b/core/src/main/java/art/arcane/iris/core/commands/CommandIris.java index 25c3527fe..e2e9765f2 100644 --- a/core/src/main/java/art/arcane/iris/core/commands/CommandIris.java +++ b/core/src/main/java/art/arcane/iris/core/commands/CommandIris.java @@ -44,7 +44,9 @@ import art.arcane.iris.util.common.parallel.SyncExecutor; import art.arcane.iris.util.common.misc.ServerProperties; import art.arcane.iris.util.common.plugin.VolmitSender; import art.arcane.iris.util.common.scheduling.J; +import io.papermc.lib.PaperLib; import org.bukkit.Bukkit; +import org.bukkit.Location; import org.bukkit.World; import org.bukkit.boss.BarColor; import org.bukkit.boss.BarStyle; @@ -52,7 +54,6 @@ import org.bukkit.boss.BossBar; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.entity.Player; -import org.bukkit.scheduler.BukkitRunnable; import java.io.*; import java.util.ArrayDeque; @@ -430,13 +431,14 @@ public class CommandIris implements DirectorExecutor { return; } - new BukkitRunnable() { - @Override - public void run() { - target.teleport(world.getSpawnLocation()); - new VolmitSender(target).sendMessage(C.GREEN + "You have been teleported to " + world.getName() + "."); - } - }.runTask(Iris.instance); + final Location spawn = world.getSpawnLocation(); + final Runnable teleportTask = () -> { + PaperLib.teleportAsync(target, spawn); + new VolmitSender(target).sendMessage(C.GREEN + "You have been teleported to " + world.getName() + "."); + }; + if (!J.runEntity(target, teleportTask)) { + teleportTask.run(); + } } @Director(description = "Print version information") diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/methods/MedievalPregenMethod.java b/core/src/main/java/art/arcane/iris/core/pregenerator/methods/MedievalPregenMethod.java index 0de08c2a1..0b2809d18 100644 --- a/core/src/main/java/art/arcane/iris/core/pregenerator/methods/MedievalPregenMethod.java +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/methods/MedievalPregenMethod.java @@ -28,6 +28,7 @@ import art.arcane.volmlib.util.mantle.runtime.Mantle; import art.arcane.volmlib.util.matter.Matter; import art.arcane.volmlib.util.math.M; import art.arcane.iris.util.common.scheduling.J; +import io.papermc.lib.PaperLib; import org.bukkit.Bukkit; import org.bukkit.Chunk; import org.bukkit.World; @@ -124,6 +125,20 @@ public class MedievalPregenMethod implements PregeneratorMethod { } listener.onChunkGenerating(x, z); + if (J.isFolia()) { + // Folia: a synchronous world.getChunkAt() runs off the owning region + // thread and fails the TickThread check. Drive the chunk system + // asynchronously instead, which is region-safe. + futures.add(PaperLib.getChunkAtAsync(world, x, z, true).thenAccept(c -> { + if (c != null) { + lastUse.put(c, M.ms()); + } + listener.onChunkGenerated(x, z); + listener.onChunkCleaned(x, z); + })); + return; + } + futures.add(J.sfut(() -> { world.getChunkAt(x, z); Chunk c = Bukkit.getWorld(world.getUID()).getChunkAt(x, z); diff --git a/core/src/main/java/art/arcane/iris/core/runtime/StudioOpenCoordinator.java b/core/src/main/java/art/arcane/iris/core/runtime/StudioOpenCoordinator.java index 7c276bcda..9d391c1cd 100644 --- a/core/src/main/java/art/arcane/iris/core/runtime/StudioOpenCoordinator.java +++ b/core/src/main/java/art/arcane/iris/core/runtime/StudioOpenCoordinator.java @@ -126,6 +126,17 @@ public final class StudioOpenCoordinator { } t = logStudioPhase("resolveEntryAnchor", t, openStart); + updateStage(request, "load_entry_chunk", 0.80D); + int entryChunkX = entryAnchor.getBlockX() >> 4; + int entryChunkZ = entryAnchor.getBlockZ() >> 4; + try { + loadEntryChunk(world, entryChunkX, entryChunkZ).get(30L, TimeUnit.SECONDS); + } catch (TimeoutException e) { + throw new IllegalStateException("Studio entry chunk did not load in time at " + + entryChunkX + "," + entryChunkZ + " — chunk system may be stalled."); + } + t = logStudioPhase("loadEntryChunk (generate spawn chunk to FULL)", t, openStart); + updateStage(request, "resolve_safe_entry", 0.84D); Location safeEntry; try { @@ -187,6 +198,31 @@ public final class StudioOpenCoordinator { return now; } + private CompletableFuture loadEntryChunk(World world, int chunkX, int chunkZ) { + // A freshly created studio world has no ticking region at the entry + // chunk. On Folia getChunkAtAsync only works from the owning region + // thread, and RegionScheduler.execute never fires for a chunk no region + // owns yet — which is why resolveSafeEntry (a region task) would stall + // and time out. A plugin chunk ticket force-loads the chunk and creates + // its ticking region; we then confirm via a region task that the region + // is live before resolving the safe entry / teleporting into it. + CompletableFuture loaded = new CompletableFuture<>(); + J.s(() -> { + try { + world.addPluginChunkTicket(chunkX, chunkZ, Iris.instance); + } catch (Throwable t) { + loaded.completeExceptionally(t); + return; + } + + if (!J.runRegion(world, chunkX, chunkZ, () -> loaded.complete(null))) { + loaded.completeExceptionally(new IllegalStateException( + "Failed to confirm entry-chunk region at " + chunkX + "," + chunkZ + ".")); + } + }); + return loaded; + } + private StudioCloseResult closeWorld( PlatformChunkGenerator provider, String worldName, diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisObject.java b/core/src/main/java/art/arcane/iris/engine/object/IrisObject.java index 62d974390..88d1f6eb9 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisObject.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisObject.java @@ -33,6 +33,7 @@ import art.arcane.iris.util.common.data.IrisCustomData; import art.arcane.iris.util.common.data.VectorMap; import art.arcane.volmlib.util.format.Form; import art.arcane.iris.util.project.interpolation.IrisInterpolation; +import art.arcane.iris.util.project.noise.SimplexNoise; import art.arcane.volmlib.util.json.JSONObject; import art.arcane.volmlib.util.math.BlockPosition; import art.arcane.volmlib.util.math.Position2; @@ -83,6 +84,7 @@ public class IrisObject extends IrisRegistrant { protected static final BlockData VAIR_DEBUG = B.get("COBWEB"); protected static final BlockData[] SNOW_LAYERS = new BlockData[]{B.get("minecraft:snow[layers=1]"), B.get("minecraft:snow[layers=2]"), B.get("minecraft:snow[layers=3]"), B.get("minecraft:snow[layers=4]"), B.get("minecraft:snow[layers=5]"), B.get("minecraft:snow[layers=6]"), B.get("minecraft:snow[layers=7]"), B.get("minecraft:snow[layers=8]")}; private static final long IMPLAUSIBLE_BEDROCK_WARN_THROTTLE_MS = 5000L; + private static final long VACUUM_WAVE_SEED = 7392113L; private static final java.util.concurrent.ConcurrentHashMap IMPLAUSIBLE_BEDROCK_WARNS = new java.util.concurrent.ConcurrentHashMap<>(); protected transient final Lock readLock; protected transient final Lock writeLock; @@ -1421,6 +1423,10 @@ public class IrisObject extends IrisRegistrant { double falloff = IrisObjectVacuum.resolveFalloff(settings); int jitter = settings != null ? Math.max(0, settings.getOrganicJitter()) : 4; boolean organicEdge = mode == ObjectPlaceMode.VACUUM_ORGANIC; + boolean wavyEdge = mode == ObjectPlaceMode.VACUUM_WAVY; + double waveAmplitude = IrisObjectVacuum.resolveWaveAmplitude(settings); + double waveScale = IrisObjectVacuum.resolveWaveScale(settings); + SimplexNoise waveNoise = (wavyEdge && waveAmplitude > 0) ? new SimplexNoise(VACUUM_WAVE_SEED) : null; int meetY = baseY - 1; IrisComplex complex = placer.getEngine().getComplex(); @@ -1439,6 +1445,13 @@ public class IrisObject extends IrisRegistrant { } int origY = placer.getHighest(cx, cz, getLoader(), true); int targetY = IrisObjectVacuum.columnTargetY(dx, dz, lowX, highX, lowZ, highZ, effRadius, falloff, origY, meetY); + if (waveNoise != null) { + int outX = IrisObjectVacuum.outset(dx, lowX, highX); + int outZ = IrisObjectVacuum.outset(dz, lowZ, highZ); + double waveDistance = Math.sqrt((double) (outX * outX) + (double) (outZ * outZ)); + double sample = waveNoise.noiseSigned(cx * waveScale, cz * waveScale); + targetY += IrisObjectVacuum.waveOffset(waveDistance, effRadius, sample, waveAmplitude); + } if (targetY == origY) { continue; } diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectVacuum.java b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectVacuum.java index 55f70e148..10abc9b28 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectVacuum.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectVacuum.java @@ -19,6 +19,9 @@ package art.arcane.iris.engine.object; public final class IrisObjectVacuum { + private static final double DEFAULT_WAVE_AMPLITUDE = 3.0; + private static final double DEFAULT_WAVE_SCALE = 5.0; + private IrisObjectVacuum() { } @@ -26,7 +29,8 @@ public final class IrisObjectVacuum { return mode == ObjectPlaceMode.VACUUM || mode == ObjectPlaceMode.VACUUM_HIGH || mode == ObjectPlaceMode.VACUUM_FAST - || mode == ObjectPlaceMode.VACUUM_ORGANIC; + || mode == ObjectPlaceMode.VACUUM_ORGANIC + || mode == ObjectPlaceMode.VACUUM_WAVY; } public static int resolveRadius(ObjectPlaceMode mode, IrisVacuumSettings settings) { @@ -49,6 +53,21 @@ public final class IrisObjectVacuum { return settings != null ? Math.max(0.25, settings.getFalloff()) : 2.0; } + public static double resolveWaveAmplitude(IrisVacuumSettings settings) { + if (settings == null) { + return DEFAULT_WAVE_AMPLITUDE; + } + return Math.max(0, settings.getWaveAmplitude()); + } + + public static double resolveWaveScale(IrisVacuumSettings settings) { + if (settings == null) { + return DEFAULT_WAVE_SCALE; + } + double scale = settings.getWaveScale(); + return scale > 0 ? scale : DEFAULT_WAVE_SCALE; + } + public static int footprintLow(int dimension) { int size = Math.abs(dimension); return -(size / 2); @@ -91,6 +110,17 @@ public final class IrisObjectVacuum { return (int) Math.round(originalY + ((meetY - originalY) * factor)); } + public static int waveOffset(double distance, double effectiveRadius, double signedNoise, double amplitude) { + if (amplitude <= 0 || effectiveRadius <= 0) { + return 0; + } + if (distance <= 0 || distance >= effectiveRadius) { + return 0; + } + double envelope = Math.sin(Math.PI * (distance / effectiveRadius)); + return (int) Math.round(signedNoise * amplitude * envelope); + } + public static int carveFloorY(int targetY, int objectTopY, boolean insideFootprint) { int floor = targetY + 1; if (insideFootprint) { diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisVacuumSettings.java b/core/src/main/java/art/arcane/iris/engine/object/IrisVacuumSettings.java index 80ab1fd18..398c562ab 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisVacuumSettings.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisVacuumSettings.java @@ -28,4 +28,12 @@ public class IrisVacuumSettings { @MaxNumber(64) @Desc("For VACUUM_ORGANIC: the maximum number of blocks the effective radius is randomly perturbed per column, giving the meeting edge an irregular organic outline instead of a clean circle.") private int organicJitter = 4; + @MinNumber(0) + @MaxNumber(64) + @Desc("For VACUUM_WAVY: the vertical amplitude (in blocks) of the rolling simplex wave applied across the terrain bend. The wave fades to zero under the object (so it stays flush) and at the outer radius (so it blends back to the surface), peaking across the mid-slope. 0 disables the wave.") + private int waveAmplitude = 3; + @MinNumber(0.1) + @MaxNumber(64) + @Desc("For VACUUM_WAVY: the frequency of the simplex wave. Higher values make tighter, choppier ripples; lower values make broad, gentle swells. The wavelength in blocks is roughly 100 / waveScale.") + private double waveScale = 5.0; } diff --git a/core/src/main/java/art/arcane/iris/engine/object/ObjectPlaceMode.java b/core/src/main/java/art/arcane/iris/engine/object/ObjectPlaceMode.java index 79fb9bc21..1160425a9 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/ObjectPlaceMode.java +++ b/core/src/main/java/art/arcane/iris/engine/object/ObjectPlaceMode.java @@ -90,6 +90,10 @@ public enum ObjectPlaceMode { VACUUM_ORGANIC, + @Desc("VACUUM whose terrain bend is bent by smooth coherent simplex noise so the surface meets the object with a gently rolling, wavy slope instead of the ragged white-noise edge of VACUUM_ORGANIC or a clean parabolic bowl. The object itself still seats flush; the waves fade to zero under the object and at the outer rim, peaking across the mid-slope. Tune with vacuum-settings (waveAmplitude, waveScale).") + + VACUUM_WAVY, + @Desc("Samples the height of the terrain at every x,z position of your object and pushes it down to the surface. It's pretty much like a melt function over the terrain.") PAINT, diff --git a/core/src/test/java/art/arcane/iris/engine/object/IrisObjectVacuumTest.java b/core/src/test/java/art/arcane/iris/engine/object/IrisObjectVacuumTest.java index 964fea72d..f7c0d7a04 100644 --- a/core/src/test/java/art/arcane/iris/engine/object/IrisObjectVacuumTest.java +++ b/core/src/test/java/art/arcane/iris/engine/object/IrisObjectVacuumTest.java @@ -101,6 +101,7 @@ public class IrisObjectVacuumTest { assertEquals(20, IrisObjectVacuum.resolveRadius(ObjectPlaceMode.VACUUM_HIGH, null)); assertEquals(8, IrisObjectVacuum.resolveRadius(ObjectPlaceMode.VACUUM_FAST, null)); assertEquals(12, IrisObjectVacuum.resolveRadius(ObjectPlaceMode.VACUUM_ORGANIC, null)); + assertEquals(12, IrisObjectVacuum.resolveRadius(ObjectPlaceMode.VACUUM_WAVY, null)); IrisVacuumSettings settings = new IrisVacuumSettings(); settings.setRadius(30); @@ -113,6 +114,7 @@ public class IrisObjectVacuumTest { assertEquals(1, IrisObjectVacuum.resolveStep(ObjectPlaceMode.VACUUM_HIGH)); assertEquals(2, IrisObjectVacuum.resolveStep(ObjectPlaceMode.VACUUM_FAST)); assertEquals(1, IrisObjectVacuum.resolveStep(ObjectPlaceMode.VACUUM_ORGANIC)); + assertEquals(1, IrisObjectVacuum.resolveStep(ObjectPlaceMode.VACUUM_WAVY)); } @Test @@ -121,6 +123,7 @@ public class IrisObjectVacuumTest { assertTrue(IrisObjectVacuum.isVacuumMode(ObjectPlaceMode.VACUUM_HIGH)); assertTrue(IrisObjectVacuum.isVacuumMode(ObjectPlaceMode.VACUUM_FAST)); assertTrue(IrisObjectVacuum.isVacuumMode(ObjectPlaceMode.VACUUM_ORGANIC)); + assertTrue(IrisObjectVacuum.isVacuumMode(ObjectPlaceMode.VACUUM_WAVY)); assertFalse(IrisObjectVacuum.isVacuumMode(ObjectPlaceMode.CENTER_HEIGHT)); assertFalse(IrisObjectVacuum.isVacuumMode(ObjectPlaceMode.STILT)); assertFalse(IrisObjectVacuum.isVacuumMode(ObjectPlaceMode.PAINT)); @@ -143,4 +146,45 @@ public class IrisObjectVacuumTest { assertEquals("the ring carves down to the deformation target (the crater floor)", meetY + 1, ringFloor); assertTrue("ring crater is deeper than the object-protected inside floor", ringFloor < insideFloor); } + + @Test + public void wavyOffsetIsZeroAtSeatAndRimSoSeatStaysFlushAndEdgeBlends() { + assertEquals("directly under the object the wave must be zero so it seats flush", 0, IrisObjectVacuum.waveOffset(0.0, 12.0, 1.0, 3.0)); + assertEquals("at the outer rim the wave must be zero so it blends to terrain", 0, IrisObjectVacuum.waveOffset(12.0, 12.0, 1.0, 3.0)); + assertEquals("beyond the radius there is no wave", 0, IrisObjectVacuum.waveOffset(20.0, 12.0, 1.0, 3.0)); + assertEquals("amplitude 0 disables the wave entirely", 0, IrisObjectVacuum.waveOffset(6.0, 12.0, 1.0, 0.0)); + } + + @Test + public void wavyOffsetIsSignedSymmetricAndPeaksMidSlope() { + int up = IrisObjectVacuum.waveOffset(6.0, 12.0, 1.0, 3.0); + int down = IrisObjectVacuum.waveOffset(6.0, 12.0, -1.0, 3.0); + assertEquals("peak noise at mid-slope lifts the full amplitude", 3, up); + assertEquals("negative noise pushes the slope down by the same amount", -3, down); + assertEquals("equal and opposite noise mirrors the offset", up, -down); + assertTrue("the wave never exceeds the configured amplitude", Math.abs(up) <= 3); + + int nearSeat = IrisObjectVacuum.waveOffset(1.0, 12.0, 1.0, 3.0); + assertTrue("waves are strongest mid-slope and gentle near the flush seat", up > nearSeat); + } + + @Test + public void resolveWaveAmplitudeAndScaleUseDefaultsAndHonorOverrides() { + assertEquals(3.0, IrisObjectVacuum.resolveWaveAmplitude(null), 0.0001); + assertEquals(5.0, IrisObjectVacuum.resolveWaveScale(null), 0.0001); + + IrisVacuumSettings settings = new IrisVacuumSettings(); + settings.setWaveAmplitude(6); + settings.setWaveScale(2.5); + assertEquals(6.0, IrisObjectVacuum.resolveWaveAmplitude(settings), 0.0001); + assertEquals(2.5, IrisObjectVacuum.resolveWaveScale(settings), 0.0001); + + IrisVacuumSettings disabled = new IrisVacuumSettings(); + disabled.setWaveAmplitude(0); + assertEquals("amplitude 0 disables the wave", 0.0, IrisObjectVacuum.resolveWaveAmplitude(disabled), 0.0001); + + IrisVacuumSettings badScale = new IrisVacuumSettings(); + badScale.setWaveScale(0); + assertEquals("non-positive scale falls back to the default", 5.0, IrisObjectVacuum.resolveWaveScale(badScale), 0.0001); + } }