Port the v3 map as iris std map

- iris std map opens the v3 map window; noise, palettes, objects, procedural and image maps are views in it
- Rivers view draws v4 hydrology, with an empty state when the pack has rivers off
- 3D views fall back to software when LWJGL isn't on the classpath; modded jars use the game's LWJGL
- FlatLaf and LWJGL join core; v4's separate studio windows are removed
This commit is contained in:
piexel committed 2026-10-25 16:00:17 +01:00
1 parent 2d211cf106
commit 29cca99947
214 files changed
+27582 -4368

No files matched your search

@@ -60,6 +60,7 @@ import art.arcane.iris.integration.MultiverseCoreLink;
import art.arcane.iris.platform.bukkit.nms.INMS;
import art.arcane.volmlib.nativelib.terrain.ServerShutdownBoundary;
import art.arcane.iris.studio.view.BukkitGuiHost;
import art.arcane.iris.studio.view.GuiHost;
import art.arcane.iris.studio.view.PregeneratorJob;
import art.arcane.iris.platform.bukkit.BoardSVC;
import art.arcane.iris.command.CommandSVC;
@@ -988,6 +989,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
}
public void onDisable() {
GuiHost.shutdown();
preservePendingBlockDrops();
papiLifecycleActive = false;
if (updateNotifier != null) {
@@ -21,10 +21,10 @@ package art.arcane.iris.command;
import art.arcane.iris.Iris;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.configuration.IrisSettings;
import art.arcane.iris.studio.map.MapWindow;
import art.arcane.iris.studio.map.NoiseExplorerGUI;
import art.arcane.iris.studio.map.PaletteExplorerStart;
import art.arcane.iris.studio.view.GuiHost;
import art.arcane.iris.studio.view.ImageMapStudioGUI;
import art.arcane.iris.studio.view.NoiseExplorerGUI;
import art.arcane.iris.studio.view.VisionGUI;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.studio.workspace.IrisProject;
import art.arcane.iris.studio.workspace.IrisCodeWorkspace;
@@ -255,15 +255,19 @@ public class CommandStudio implements DirectorExecutor {
long seed
) {
if (noGUI()) return;
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_OPENING_NOISE_EXPLORER));
if (generator == null) {
NoiseExplorerGUI.launch();
Engine playerEngine = engine();
Engine engine = playerEngine != null ? playerEngine : GuiHost.get().findActiveEngine();
if (generator != null) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_OPENING_NOISE_EXPLORER));
NoiseExplorerGUI.launchGeneratorKey(engine, generator.getLoadKey(), generator, seed);
return;
}
String generatorKey = generator.getLoadKey();
NoiseExplorerGUI.launchGeneratorKey(generatorKey, generator, seed);
if (engine == null) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_YOU_NEED_BE_SPECIFY_IRIS_GENERATED_WORLD));
return;
}
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_OPENING_NOISE_EXPLORER));
MapWindow.openNoise(playerEngine != null ? mapStartIn(player().getWorld()) : mapStartWithoutWorld(engine));
}
@Director(description = "Open the image-map studio (External GUI)", descriptionKey = "iris.director.commandstudio.director.open_image_map_studio_external_gui", aliases = {"imap"})
@@ -279,8 +283,7 @@ public class CommandStudio implements DirectorExecutor {
return;
}
Engine activeEngine = IrisToolbelt.access(world).getEngine();
ImageMapStudioGUI.launch(activeEngine);
MapWindow.openImageMaps(mapStartIn(world));
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_OPENING_IMAGE_MAP_STUDIO));
}
@@ -420,11 +423,27 @@ public class CommandStudio implements DirectorExecutor {
return;
}
UUID openerId = sender().isPlayer() ? player().getUniqueId() : null;
VisionGUI.launch(IrisToolbelt.access(world).getEngine(), openerId);
MapWindow.openMap(mapStartIn(world));
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_OPENING_MAP));
}
// The map opens where the player stands when they are in that world, otherwise at the world origin.
private PaletteExplorerStart mapStartIn(World world) {
Location center = sender().isPlayer() && player().getWorld().equals(world) ? player().getLocation() : null;
return new PaletteExplorerStart(
IrisToolbelt.access(world).getEngine(),
world.getUID(),
sender().isPlayer() ? player().getUniqueId() : null,
null,
center == null ? 0D : center.getX(),
center == null ? 0D : center.getZ()
);
}
private PaletteExplorerStart mapStartWithoutWorld(Engine engine) {
return new PaletteExplorerStart(engine, null, sender().isPlayer() ? player().getUniqueId() : null, null, 0D, 0D);
}
@Director(description = "Profiles the performance of a dimension", descriptionKey = "iris.director.commandstudio.director.profiles_performance_dimension", origin = DirectorOrigin.PLAYER)
public void profile(
@Param(description = "The dimension to profile", descriptionKey = "iris.director.commandstudio.param.dimension_profile", contextual = true, contextualOverride = true, defaultValue = "default", customHandler = DimensionHandler.class)
@@ -23,6 +23,8 @@ import art.arcane.iris.world.event.IrisEngineHotloadEvent;
import art.arcane.iris.world.IrisToolbelt;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.platform.generation.PlatformChunkGenerator;
import art.arcane.iris.studio.StudioSVC;
import art.arcane.iris.studio.workspace.IrisProject;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.event.EventHandler;
@@ -40,16 +42,21 @@ public final class BukkitGuiHost implements GuiHost.Provider {
GuiHost.set(new BukkitGuiHost());
}
/**
* The engine of the open studio project, which reads the pack folder being edited, or else the first
* Iris world. A normal Iris world runs on its own copy of the pack, made when the world was created.
*/
@Override
public Engine findActiveEngine() {
Engine studioEngine = findStudioEngine();
if (studioEngine != null) {
return studioEngine;
}
try {
for (World world : new ArrayList<>(Bukkit.getWorlds())) {
try {
PlatformChunkGenerator access = IrisToolbelt.access(world);
if (access != null && access.getEngine() != null && !access.getEngine().isClosed()) {
return access.getEngine();
}
} catch (Throwable ignored) {
Engine engine = engineOf(world);
if (engine != null) {
return engine;
}
}
} catch (Throwable ignored) {
@@ -57,6 +64,35 @@ public final class BukkitGuiHost implements GuiHost.Provider {
return null;
}
@Override
public Engine findEngine(UUID worldId) {
World world = worldId == null ? null : Bukkit.getWorld(worldId);
return world == null ? null : engineOf(world);
}
private static Engine findStudioEngine() {
try {
StudioSVC studio = Iris.service(StudioSVC.class);
IrisProject project = studio == null || !studio.isProjectOpen() ? null : studio.getActiveProject();
PlatformChunkGenerator provider = project == null ? null : project.getActiveProvider();
Engine engine = provider == null ? null : provider.getEngine();
return engine == null || engine.isClosed() ? null : engine;
} catch (Throwable ignored) {
return null;
}
}
private static Engine engineOf(World world) {
try {
PlatformChunkGenerator access = IrisToolbelt.access(world);
if (access != null && access.getEngine() != null && !access.getEngine().isClosed()) {
return access.getEngine();
}
} catch (Throwable ignored) {
}
return null;
}
@Override
public void registerHotloadHook(Runnable onHotload) {
Listener listener = new HotloadListener(onHotload);
@@ -18,23 +18,19 @@
package art.arcane.iris.studio.view;
import art.arcane.volmlib.nativelib.view.WorldMarker;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.studio.render.RenderType;
import art.arcane.iris.world.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.world.task.J;
import art.arcane.volmlib.util.format.Form;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -56,7 +52,7 @@ public final class BukkitVisionOverlay implements GuiOverlay {
private final AtomicBoolean playerRefreshQueued = new AtomicBoolean();
private final AtomicLong teleportSequence = new AtomicLong();
private final AtomicReference<VisionTeleportRequest> latestTeleport = new AtomicReference<>();
private volatile List<WorldMarker> playerMarkers = List.of();
private volatile List<GuiMarker> playerMarkers = List.of();
public BukkitVisionOverlay(Engine engine, UUID openerId) {
this.engine = engine;
@@ -68,7 +64,7 @@ public final class BukkitVisionOverlay implements GuiOverlay {
* built by a server thread.
*/
@Override
public List<WorldMarker> players() {
public List<GuiMarker> players() {
queuePlayerRefresh();
return playerMarkers;
}
@@ -80,10 +76,10 @@ public final class BukkitVisionOverlay implements GuiOverlay {
boolean scheduled = J.runGlobal(() -> {
try {
List<WorldMarker> markers = new ArrayList<>();
List<GuiMarker> markers = new ArrayList<>();
for (Player player : BukkitWorldBinding.players(engine.getWorld())) {
Location at = player.getLocation();
markers.add(WorldMarker.player(player.getName(), at.getX(), at.getZ()));
markers.add(GuiMarker.player(player.getName(), at.getX(), at.getZ(), at.getYaw()));
}
playerMarkers = List.copyOf(markers);
} finally {
@@ -97,7 +93,7 @@ public final class BukkitVisionOverlay implements GuiOverlay {
}
@Override
public void requestEntities(Consumer<List<WorldMarker>> sink) {
public void requestEntities(Consumer<List<GuiMarker>> sink) {
J.runGlobal(() -> {
IrisWorld target = engine.getWorld();
World world = BukkitWorldBinding.world(target);
@@ -118,7 +114,7 @@ public final class BukkitVisionOverlay implements GuiOverlay {
return;
}
List<WorldMarker> collected = Collections.synchronizedList(new ArrayList<>(living.size()));
List<GuiMarker> collected = Collections.synchronizedList(new ArrayList<>(living.size()));
AtomicInteger pending = new AtomicInteger(living.size());
Runnable complete = () -> {
if (pending.decrementAndGet() == 0) {
@@ -144,22 +140,22 @@ public final class BukkitVisionOverlay implements GuiOverlay {
});
}
private WorldMarker marker(LivingEntity entity, Location at) {
private GuiMarker marker(LivingEntity entity, Location at) {
String label = Form.capitalizeWords(entity.getType().name().toLowerCase(Locale.ROOT).replaceAll("\\Q_\\E", " "));
double maxHealth = 0;
try {
maxHealth = entity.getAttribute(MAX_HEALTH).getValue();
} catch (Throwable ignored) {
}
return WorldMarker.entity(label, at.getX(), at.getY(), at.getZ(), entity.getHealth(), maxHealth);
return GuiMarker.entity(label, at.getX(), at.getY(), at.getZ(), entity.getHealth(), maxHealth);
}
@Override
public void teleport(double worldX, double worldZ) {
VisionTeleportRequest request = new VisionTeleportRequest(
teleportSequence.incrementAndGet(),
VisionGUI.floorWorldCoordinate(worldX),
VisionGUI.floorWorldCoordinate(worldZ));
(int) StrictMath.floor(worldX),
(int) StrictMath.floor(worldZ));
latestTeleport.set(request);
startTeleport(request);
}
@@ -212,6 +208,36 @@ public final class BukkitVisionOverlay implements GuiOverlay {
}
}
@Override
public void requestOpenerPosition(Consumer<OpenerPosition> sink) {
if (openerId == null) {
sink.accept(OpenerPosition.OFFLINE);
return;
}
boolean scheduled = J.runGlobal(() -> {
Player player = Bukkit.getPlayer(openerId);
if (player == null) {
sink.accept(OpenerPosition.OFFLINE);
return;
}
World world = BukkitWorldBinding.world(engine.getWorld());
if (!J.runEntity(player, () -> sink.accept(positionOf(player, world)))) {
sink.accept(OpenerPosition.OFFLINE);
}
});
if (!scheduled) {
sink.accept(OpenerPosition.OFFLINE);
}
}
private static OpenerPosition positionOf(Player player, World world) {
if (world == null || player.getWorld() != world) {
return OpenerPosition.IN_OTHER_WORLD;
}
Location at = player.getLocation();
return OpenerPosition.inWorld(at.getX(), at.getY(), at.getZ());
}
private Player selectPlayer(List<Player> players) {
if (openerId == null) {
return players.isEmpty() ? null : players.get(0);
@@ -316,21 +342,6 @@ public final class BukkitVisionOverlay implements GuiOverlay {
+ blockX + "," + blockZ + ".", failure);
}
@Override
public String openInEditor(double worldX, double worldZ, RenderType type) {
IrisComplex complex = engine.getComplex();
File file = switch (type) {
case BIOME, LAYER_LOAD, DECORATOR_LOAD, OBJECT_LOAD, HEIGHT, RIVER ->
complex.getTrueBiomeStream().get(worldX, worldZ).openInVSCode();
case BIOME_LAND -> complex.getLandBiomeStream().get(worldX, worldZ).openInVSCode();
case BIOME_SEA -> complex.getSeaBiomeStream().get(worldX, worldZ).openInVSCode();
case REGION -> complex.getRegionStream().get(worldX, worldZ).openInVSCode();
case CAVE_LAND -> complex.getCaveBiomeStream().get(worldX, worldZ).openInVSCode();
default -> null;
};
return file == null ? null : file.getName();
}
private static final class VisionTeleportRequest {
private final long sequence;
private final int blockX;