mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-25 21:40:05 +00:00
Integrate Iris Studio
- Merge the unified Studio interface and command entry points. - Adapt water views and provisional sampling to basin hydrology. - Restore diagnostic estimates and the biome water overlay. - Fix render cancellation, preview closure, and platform position callbacks. - Fall back to software rendering when native bindings are unavailable. - Cover renderer, lifecycle, and scheduler regressions.
This commit is contained in:
commit
9f49d29e2b
225 files changed
+29666
-4395
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,9 @@ 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.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;
|
||||
@@ -98,7 +97,9 @@ import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import art.arcane.iris.localization.IrisLanguage;
|
||||
import art.arcane.iris.localization.BukkitCommandMessages;
|
||||
@@ -255,15 +256,10 @@ 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();
|
||||
return;
|
||||
}
|
||||
|
||||
String generatorKey = generator.getLoadKey();
|
||||
NoiseExplorerGUI.launchGeneratorKey(generatorKey, generator, seed);
|
||||
VolmitSender commandSender = sender();
|
||||
UUID openerId = commandSender.isPlayer() ? commandSender.player().getUniqueId() : null;
|
||||
MapWindow.NoiseSelection selection = new MapWindow.NoiseSelection(generator == null ? null : generator.getLoadKey(), generator, seed);
|
||||
withPlayerContext(opener -> openNoiseAtPlayer(commandSender, openerId, opener, selection));
|
||||
}
|
||||
|
||||
@Director(description = "Open the image-map studio (External GUI)", descriptionKey = "iris.director.commandstudio.director.open_image_map_studio_external_gui", aliases = {"imap"})
|
||||
@@ -279,8 +275,7 @@ public class CommandStudio implements DirectorExecutor {
|
||||
return;
|
||||
}
|
||||
|
||||
Engine activeEngine = IrisToolbelt.access(world).getEngine();
|
||||
ImageMapStudioGUI.launch(activeEngine);
|
||||
requestMapStart(world, MapWindow::openImageMaps);
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_OPENING_IMAGE_MAP_STUDIO));
|
||||
}
|
||||
|
||||
@@ -420,11 +415,61 @@ public class CommandStudio implements DirectorExecutor {
|
||||
return;
|
||||
}
|
||||
|
||||
UUID openerId = sender().isPlayer() ? player().getUniqueId() : null;
|
||||
VisionGUI.launch(IrisToolbelt.access(world).getEngine(), openerId);
|
||||
requestMapStart(world, MapWindow::openMap);
|
||||
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, Player opener, UUID openerId) {
|
||||
Location center = opener != null && opener.getWorld().equals(world) ? opener.getLocation() : null;
|
||||
return new PaletteExplorerStart(
|
||||
IrisToolbelt.access(world).getEngine(),
|
||||
world.getUID(),
|
||||
openerId,
|
||||
null,
|
||||
center == null ? 0D : center.getX(),
|
||||
center == null ? 0D : center.getZ()
|
||||
);
|
||||
}
|
||||
|
||||
private void requestMapStart(World world, Consumer<PaletteExplorerStart> launch) {
|
||||
UUID openerId = sender().isPlayer() ? player().getUniqueId() : null;
|
||||
withPlayerContext(opener -> launch.accept(mapStartIn(world, opener, openerId)));
|
||||
}
|
||||
|
||||
private void openNoiseAtPlayer(VolmitSender commandSender, UUID openerId, Player opener, MapWindow.NoiseSelection selection) {
|
||||
World world = opener == null ? null : opener.getWorld();
|
||||
PlatformChunkGenerator worldGenerator = world == null ? null : IrisToolbelt.access(world);
|
||||
Engine engine = worldGenerator == null ? GuiHost.get().findActiveEngine() : worldGenerator.getEngine();
|
||||
if (engine == null) {
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_YOU_NEED_BE_SPECIFY_IRIS_GENERATED_WORLD));
|
||||
return;
|
||||
}
|
||||
PaletteExplorerStart start = worldGenerator == null
|
||||
? new PaletteExplorerStart(engine, null, openerId, null, 0D, 0D)
|
||||
: mapStartIn(world, opener, openerId);
|
||||
MapWindow.openNoise(start, selection);
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_OPENING_NOISE_EXPLORER));
|
||||
}
|
||||
|
||||
private void withPlayerContext(Consumer<Player> action) {
|
||||
Player opener = sender().isPlayer() ? player() : null;
|
||||
if (opener == null) {
|
||||
action.accept(null);
|
||||
return;
|
||||
}
|
||||
AtomicBoolean completed = new AtomicBoolean();
|
||||
Consumer<Player> complete = player -> {
|
||||
if (completed.compareAndSet(false, true)) {
|
||||
action.accept(player);
|
||||
}
|
||||
};
|
||||
Runnable retired = () -> complete.accept(null);
|
||||
if (!J.runEntity(opener, () -> complete.accept(opener.isOnline() ? opener : null), 0, retired)) {
|
||||
retired.run();
|
||||
}
|
||||
}
|
||||
|
||||
@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)
|
||||
|
||||
+42
-6
@@ -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);
|
||||
|
||||
+83
-40
@@ -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;
|
||||
}
|
||||
@@ -78,26 +74,48 @@ public final class BukkitVisionOverlay implements GuiOverlay {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean scheduled = J.runGlobal(() -> {
|
||||
try {
|
||||
List<WorldMarker> markers = new ArrayList<>();
|
||||
for (Player player : BukkitWorldBinding.players(engine.getWorld())) {
|
||||
Location at = player.getLocation();
|
||||
markers.add(WorldMarker.player(player.getName(), at.getX(), at.getZ()));
|
||||
}
|
||||
playerMarkers = List.copyOf(markers);
|
||||
} finally {
|
||||
playerRefreshQueued.set(false);
|
||||
}
|
||||
});
|
||||
boolean scheduled = J.runGlobal(this::refreshPlayers);
|
||||
|
||||
if (!scheduled) {
|
||||
playerRefreshQueued.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshPlayers() {
|
||||
World world = BukkitWorldBinding.world(engine.getWorld());
|
||||
List<Player> players = BukkitWorldBinding.players(engine.getWorld());
|
||||
if (players.isEmpty() || world == null) {
|
||||
playerMarkers = List.of();
|
||||
playerRefreshQueued.set(false);
|
||||
return;
|
||||
}
|
||||
List<GuiMarker> markers = Collections.synchronizedList(new ArrayList<>(players.size()));
|
||||
AtomicInteger pending = new AtomicInteger(players.size());
|
||||
for (Player player : players) {
|
||||
AtomicBoolean completed = new AtomicBoolean();
|
||||
Runnable complete = () -> {
|
||||
if (completed.compareAndSet(false, true) && pending.decrementAndGet() == 0) {
|
||||
playerMarkers = List.copyOf(markers);
|
||||
playerRefreshQueued.set(false);
|
||||
}
|
||||
};
|
||||
if (!J.runEntity(player, () -> {
|
||||
try {
|
||||
if (player.isOnline() && player.getWorld() == world) {
|
||||
Location at = player.getLocation();
|
||||
markers.add(GuiMarker.player(player.getName(), at.getX(), at.getZ(), at.getYaw()));
|
||||
}
|
||||
} finally {
|
||||
complete.run();
|
||||
}
|
||||
}, 0, complete)) {
|
||||
complete.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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 +136,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 +162,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 +230,46 @@ public final class BukkitVisionOverlay implements GuiOverlay {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestOpenerPosition(Consumer<OpenerPosition> sink) {
|
||||
AtomicBoolean completed = new AtomicBoolean();
|
||||
Consumer<OpenerPosition> complete = position -> {
|
||||
if (completed.compareAndSet(false, true)) {
|
||||
sink.accept(position);
|
||||
}
|
||||
};
|
||||
if (openerId == null) {
|
||||
complete.accept(OpenerPosition.OFFLINE);
|
||||
return;
|
||||
}
|
||||
boolean scheduled = J.runGlobal(() -> {
|
||||
Player player = Bukkit.getPlayer(openerId);
|
||||
if (player == null) {
|
||||
complete.accept(OpenerPosition.OFFLINE);
|
||||
return;
|
||||
}
|
||||
Runnable retired = () -> complete.accept(OpenerPosition.OFFLINE);
|
||||
if (!J.runEntity(player, () -> complete.accept(positionOf(player,
|
||||
BukkitWorldBinding.world(engine.getWorld()))), 0, retired)) {
|
||||
retired.run();
|
||||
}
|
||||
});
|
||||
if (!scheduled) {
|
||||
complete.accept(OpenerPosition.OFFLINE);
|
||||
}
|
||||
}
|
||||
|
||||
private static OpenerPosition positionOf(Player player, World world) {
|
||||
if (!player.isOnline()) {
|
||||
return OpenerPosition.OFFLINE;
|
||||
}
|
||||
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 +374,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;
|
||||
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
package art.arcane.iris.command;
|
||||
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
|
||||
import art.arcane.iris.platform.generation.PlatformChunkGenerator;
|
||||
import art.arcane.iris.studio.map.MapWindow;
|
||||
import art.arcane.iris.studio.map.PaletteExplorerStart;
|
||||
import art.arcane.iris.studio.view.GuiHost;
|
||||
import art.arcane.iris.world.IrisToolbelt;
|
||||
import art.arcane.iris.world.task.J;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
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 StudioMapPositionTest {
|
||||
@Test
|
||||
public void mapCapturesItsStartingPositionOnThePlayerScheduler() {
|
||||
IrisSettings previous = IrisSettings.settings;
|
||||
IrisSettings.settings = new IrisSettings();
|
||||
IrisSettings.settings.getGui().setUseServerLaunchedGuis(true);
|
||||
World world = mock(World.class);
|
||||
Player player = mock(Player.class);
|
||||
VolmitSender sender = mock(VolmitSender.class);
|
||||
PlatformChunkGenerator generator = mock(PlatformChunkGenerator.class);
|
||||
Engine engine = mock(Engine.class);
|
||||
UUID playerId = UUID.randomUUID();
|
||||
UUID worldId = UUID.randomUUID();
|
||||
AtomicBoolean owned = new AtomicBoolean();
|
||||
AtomicReference<Runnable> task = new AtomicReference<>();
|
||||
AtomicReference<PaletteExplorerStart> start = new AtomicReference<>();
|
||||
when(world.getUID()).thenReturn(worldId);
|
||||
when(player.getUniqueId()).thenReturn(playerId);
|
||||
when(player.isOnline()).thenReturn(true);
|
||||
when(player.getWorld()).thenAnswer(invocation -> {
|
||||
assertTrue("world read must use the player scheduler", owned.get());
|
||||
return world;
|
||||
});
|
||||
when(player.getLocation()).thenAnswer(invocation -> {
|
||||
assertTrue("location read must use the player scheduler", owned.get());
|
||||
return new Location(world, 33.25D, 91D, -72.5D);
|
||||
});
|
||||
when(sender.isPlayer()).thenReturn(true);
|
||||
when(sender.player()).thenReturn(player);
|
||||
when(generator.getEngine()).thenReturn(engine);
|
||||
CommandStudio command = new CommandStudio() {
|
||||
@Override
|
||||
public VolmitSender sender() {
|
||||
return sender;
|
||||
}
|
||||
};
|
||||
try (MockedStatic<GuiHost> host = mockStatic(GuiHost.class);
|
||||
MockedStatic<IrisToolbelt> toolbelt = mockStatic(IrisToolbelt.class);
|
||||
MockedStatic<J> scheduling = mockStatic(J.class);
|
||||
MockedStatic<MapWindow> windows = mockStatic(MapWindow.class)) {
|
||||
host.when(GuiHost::isAvailable).thenReturn(true);
|
||||
toolbelt.when(() -> IrisToolbelt.isIrisWorld(world)).thenReturn(true);
|
||||
toolbelt.when(() -> IrisToolbelt.access(world)).thenReturn(generator);
|
||||
scheduling.when(() -> J.runEntity(same(player), any(Runnable.class), eq(0), any(Runnable.class)))
|
||||
.thenAnswer(invocation -> {
|
||||
task.set(invocation.getArgument(1));
|
||||
return true;
|
||||
});
|
||||
windows.when(() -> MapWindow.openMap(any(PaletteExplorerStart.class))).thenAnswer(invocation -> {
|
||||
start.set(invocation.getArgument(0));
|
||||
return null;
|
||||
});
|
||||
|
||||
command.map(world);
|
||||
assertNull(start.get());
|
||||
owned.set(true);
|
||||
task.get().run();
|
||||
|
||||
assertEquals(engine, start.get().engine());
|
||||
assertEquals(playerId, start.get().playerId());
|
||||
assertEquals(worldId, start.get().worldId());
|
||||
assertEquals(33.25D, start.get().centerX(), 0D);
|
||||
assertEquals(-72.5D, start.get().centerZ(), 0D);
|
||||
} finally {
|
||||
IrisSettings.settings = previous;
|
||||
}
|
||||
}
|
||||
}
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
package art.arcane.iris.studio.view;
|
||||
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
||||
import art.arcane.iris.world.IrisWorld;
|
||||
import art.arcane.iris.world.task.J;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
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 BukkitOpenerPositionTest {
|
||||
@Test
|
||||
public void playerMarkersAreCapturedOnTheirEntitySchedulers() {
|
||||
try (Harness harness = new Harness()) {
|
||||
harness.binding.when(() -> BukkitWorldBinding.players(harness.target)).thenReturn(List.of(harness.player));
|
||||
when(harness.player.getName()).thenReturn("Explorer");
|
||||
assertTrue(harness.overlay.players().isEmpty());
|
||||
|
||||
harness.runEntity();
|
||||
|
||||
List<GuiMarker> markers = harness.overlay.players();
|
||||
assertEquals(1, markers.size());
|
||||
assertEquals("Explorer", markers.getFirst().label());
|
||||
assertEquals(18.5D, markers.getFirst().worldX(), 0D);
|
||||
assertEquals(-4.25D, markers.getFirst().worldZ(), 0D);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void positionIsCapturedOnlyOnTheOwningEntityTask() {
|
||||
try (Harness harness = new Harness()) {
|
||||
harness.overlay.requestOpenerPosition(harness.results::add);
|
||||
assertTrue(harness.results.isEmpty());
|
||||
|
||||
harness.runEntity();
|
||||
|
||||
assertEquals(List.of(OpenerPosition.inWorld(18.5D, 72D, -4.25D)), harness.results);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void retiredEntityCompletesTheRequestOnce() {
|
||||
try (Harness harness = new Harness()) {
|
||||
harness.overlay.requestOpenerPosition(harness.results::add);
|
||||
assertTrue("retirement callback must be registered", harness.retired != null);
|
||||
|
||||
harness.retired.run();
|
||||
harness.retired.run();
|
||||
|
||||
assertEquals(List.of(OpenerPosition.OFFLINE), harness.results);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void disconnectBeforeCaptureCompletesAsOfflineAndReconnectCanRefresh() {
|
||||
try (Harness harness = new Harness()) {
|
||||
harness.overlay.requestOpenerPosition(harness.results::add);
|
||||
when(harness.player.isOnline()).thenReturn(false);
|
||||
harness.runEntity();
|
||||
assertEquals(List.of(OpenerPosition.OFFLINE), harness.results);
|
||||
|
||||
when(harness.player.isOnline()).thenReturn(true);
|
||||
harness.overlay.requestOpenerPosition(harness.results::add);
|
||||
harness.runEntity();
|
||||
assertEquals(List.of(OpenerPosition.OFFLINE, OpenerPosition.inWorld(18.5D, 72D, -4.25D)), harness.results);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectedEntityTaskCompletesOnceEvenIfRetirementAlreadyRan() {
|
||||
try (Harness harness = new Harness()) {
|
||||
harness.reject = true;
|
||||
harness.overlay.requestOpenerPosition(harness.results::add);
|
||||
assertEquals(List.of(OpenerPosition.OFFLINE), harness.results);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void changingWorldBeforeCaptureReportsOtherWorld() {
|
||||
try (Harness harness = new Harness()) {
|
||||
harness.overlay.requestOpenerPosition(harness.results::add);
|
||||
when(harness.player.getWorld()).thenReturn(mock(World.class));
|
||||
harness.runEntity();
|
||||
assertEquals(List.of(OpenerPosition.IN_OTHER_WORLD), harness.results);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Harness implements AutoCloseable {
|
||||
private final Player player = mock(Player.class);
|
||||
private final World world = mock(World.class);
|
||||
private final Engine engine = mock(Engine.class);
|
||||
private final IrisWorld target = mock(IrisWorld.class);
|
||||
private final UUID opener = UUID.randomUUID();
|
||||
private final List<OpenerPosition> results = new ArrayList<>();
|
||||
private final MockedStatic<J> scheduling = mockStatic(J.class);
|
||||
private final MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
|
||||
private final MockedStatic<BukkitWorldBinding> binding = mockStatic(BukkitWorldBinding.class);
|
||||
private final BukkitVisionOverlay overlay = new BukkitVisionOverlay(engine, opener);
|
||||
private Runnable task;
|
||||
private Runnable retired;
|
||||
private boolean entityOwned;
|
||||
private boolean reject;
|
||||
|
||||
private Harness() {
|
||||
when(engine.getWorld()).thenReturn(target);
|
||||
when(player.isOnline()).thenReturn(true);
|
||||
when(player.getWorld()).thenReturn(world);
|
||||
when(player.getLocation()).thenAnswer(invocation -> {
|
||||
assertTrue("location read outside owning entity task", entityOwned);
|
||||
return new Location(world, 18.5D, 72D, -4.25D);
|
||||
});
|
||||
bukkit.when(() -> Bukkit.getPlayer(opener)).thenReturn(player);
|
||||
binding.when(() -> BukkitWorldBinding.world(target)).thenReturn(world);
|
||||
scheduling.when(() -> J.runGlobal(any(Runnable.class))).thenAnswer(invocation -> {
|
||||
invocation.getArgument(0, Runnable.class).run();
|
||||
return true;
|
||||
});
|
||||
scheduling.when(() -> J.runEntity(same(player), any(Runnable.class))).thenAnswer(invocation -> {
|
||||
task = invocation.getArgument(1);
|
||||
return !reject;
|
||||
});
|
||||
scheduling.when(() -> J.runEntity(same(player), any(Runnable.class), eq(0), any(Runnable.class)))
|
||||
.thenAnswer(invocation -> {
|
||||
task = invocation.getArgument(1);
|
||||
retired = invocation.getArgument(3);
|
||||
if (reject) {
|
||||
retired.run();
|
||||
}
|
||||
return !reject;
|
||||
});
|
||||
}
|
||||
|
||||
private void runEntity() {
|
||||
entityOwned = true;
|
||||
try {
|
||||
task.run();
|
||||
} finally {
|
||||
entityOwned = false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
binding.close();
|
||||
bukkit.close();
|
||||
scheduling.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -131,6 +131,8 @@ configurations.named('bundle').configure {
|
||||
exclude(group: 'de.crazydev22.slimjar.helper')
|
||||
exclude(group: 'de.crazydev22.slimjar')
|
||||
exclude(group: 'io.github.slimjar')
|
||||
// The game ships its own LWJGL; without the modules it leaves out, the map's 3D views draw in software.
|
||||
exclude(group: 'org.lwjgl')
|
||||
}
|
||||
|
||||
configurations.compileClasspath.extendsFrom(configurations.devBundle)
|
||||
@@ -216,7 +218,9 @@ dependencies {
|
||||
libs.caffeine,
|
||||
'com.fulcrumgenomics:jlibdeflate:0.2.0',
|
||||
libs.dom4j,
|
||||
libs.jaxen
|
||||
libs.jaxen,
|
||||
libs.flatlaf,
|
||||
libs.flatlaf.extras
|
||||
]
|
||||
shared.each { Object notation ->
|
||||
['bundle', 'devBundle'].each { String configuration ->
|
||||
|
||||
@@ -152,12 +152,16 @@ configurations.named('bundle').configure {
|
||||
exclude(group: 'de.crazydev22.slimjar.helper')
|
||||
exclude(group: 'de.crazydev22.slimjar')
|
||||
exclude(group: 'io.github.slimjar')
|
||||
// The game ships its own LWJGL; without the modules it leaves out, the map's 3D views draw in software.
|
||||
exclude(group: 'org.lwjgl')
|
||||
}
|
||||
|
||||
configurations.named('runBundle').configure {
|
||||
exclude(group: 'de.crazydev22.slimjar.helper')
|
||||
exclude(group: 'de.crazydev22.slimjar')
|
||||
exclude(group: 'io.github.slimjar')
|
||||
// The game ships its own LWJGL; without the modules it leaves out, the map's 3D views draw in software.
|
||||
exclude(group: 'org.lwjgl')
|
||||
}
|
||||
|
||||
configurations.compileClasspath.extendsFrom(configurations.devBundle)
|
||||
@@ -215,7 +219,9 @@ dependencies {
|
||||
libs.caffeine,
|
||||
'com.fulcrumgenomics:jlibdeflate:0.2.0',
|
||||
libs.dom4j,
|
||||
libs.jaxen
|
||||
libs.jaxen,
|
||||
libs.flatlaf,
|
||||
libs.flatlaf.extras
|
||||
]
|
||||
shared.each { Object notation ->
|
||||
['bundle', 'devBundle', 'runBundle'].each { String configuration ->
|
||||
|
||||
@@ -227,6 +227,7 @@ public final class ModdedEngineBootstrap {
|
||||
failure = runStopStage(failure, "wand service", ModdedWandService::clearAll);
|
||||
failure = runStopStage(failure, "block break handler", ModdedBlockBreakHandler::clear);
|
||||
failure = runStopStage(failure, "studio commands", ModdedStudioCommands::clear);
|
||||
failure = runStopStage(failure, "gui windows", GuiHost::shutdown);
|
||||
failure = runStopStage(failure, "gui host", ModdedGuiHost::clear);
|
||||
failure = runStopStage(failure, "services", () -> services().disableAll());
|
||||
reportStopFailure("stopping", failure);
|
||||
|
||||
+19
-9
@@ -23,10 +23,9 @@ import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldView;
|
||||
|
||||
import art.arcane.iris.modded.ModdedIrisLog;
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
import art.arcane.iris.studio.map.MapWindow;
|
||||
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.pack.StructurePackageClosure;
|
||||
import art.arcane.iris.pack.ImageMapPackageClosure;
|
||||
@@ -38,6 +37,7 @@ import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.generation.biome.IrisBiomeGeneratorLink;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.world.entity.IrisEntitySpawn;
|
||||
import art.arcane.iris.world.IrisWorld;
|
||||
import art.arcane.iris.generation.noise.IrisGenerator;
|
||||
import art.arcane.iris.pack.PackExportClosure;
|
||||
import art.arcane.iris.world.entity.IrisEntity;
|
||||
@@ -247,8 +247,9 @@ public final class ModdedStudioCommands {
|
||||
NativeProtocolPlayer player = source.player();
|
||||
ModdedGuiHost.bindContext(worldView(source.server(), level), engine, player == null ? null : player.id());
|
||||
}
|
||||
if (generatorKey == null || generatorKey.isBlank()) {
|
||||
NoiseExplorerGUI.launch();
|
||||
Engine mapEngine = engine != null ? engine : GuiHost.get().findActiveEngine();
|
||||
if ((generatorKey == null || generatorKey.isBlank()) && mapEngine != null) {
|
||||
MapWindow.openNoise(mapStart(mapEngine, source.player()), new MapWindow.NoiseSelection(null, null, seed));
|
||||
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_OPENING_NOISE_EXPLORER_ON_SERVER_DISPLAY));
|
||||
return 1;
|
||||
}
|
||||
@@ -262,7 +263,7 @@ public final class ModdedStudioCommands {
|
||||
return 0;
|
||||
}
|
||||
String selectedGeneratorKey = generatorKey.trim();
|
||||
NoiseExplorerGUI.launchGeneratorKey(selectedGeneratorKey, generator, seed);
|
||||
MapWindow.openNoise(mapStart(engine, source.player()), new MapWindow.NoiseSelection(selectedGeneratorKey, generator, seed));
|
||||
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_OPENING_NOISE_EXPLORER_GENERATOR_SEED, MessageArgument.untrusted("value", generatorKey.trim()), MessageArgument.untrusted("seed", seed)));
|
||||
return 1;
|
||||
}
|
||||
@@ -280,11 +281,20 @@ public final class ModdedStudioCommands {
|
||||
}
|
||||
NativeProtocolPlayer player = source.player();
|
||||
ModdedGuiHost.bindContext(worldView(source.server(), level), engine, player == null ? null : player.id());
|
||||
VisionGUI.launch(engine, player == null ? null : player.id());
|
||||
MapWindow.openMap(mapStart(engine, player));
|
||||
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_OPENING_VISION_MAP_ON_SERVER_DISPLAY, MessageArgument.untrusted("value", level.name())));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Modded worlds have no Bukkit world id; the map finds the engine again through the bound GUI context.
|
||||
private static PaletteExplorerStart mapStart(Engine engine, NativeProtocolPlayer player) {
|
||||
IrisWorld world = engine.getWorld();
|
||||
NativeWorld level = world == null ? null : world.platformWorld();
|
||||
boolean inWorld = player != null && player.connected() && level != null && player.inWorld(level);
|
||||
return new PaletteExplorerStart(engine, null, player == null ? null : player.id(), null,
|
||||
inWorld ? player.x() : 0D, inWorld ? player.z() : 0D);
|
||||
}
|
||||
|
||||
private static int imageMap(NativeCommandSource source) {
|
||||
NativeWorld level = source.world();
|
||||
Engine engine = IrisModdedCommands.engineFor(level);
|
||||
@@ -294,13 +304,13 @@ public final class ModdedStudioCommands {
|
||||
));
|
||||
return 0;
|
||||
}
|
||||
if (!ImageMapStudioGUI.isAvailable()) {
|
||||
if (!GuiHost.isAvailable() || !IrisSettings.get().getGui().isUseServerLaunchedGuis()) {
|
||||
IrisModdedCommands.fail(source, guiUnavailableMessage());
|
||||
return 0;
|
||||
}
|
||||
NativeProtocolPlayer player = source.player();
|
||||
ModdedGuiHost.bindContext(worldView(source.server(), level), engine, player == null ? null : player.id());
|
||||
ImageMapStudioGUI.launch(engine);
|
||||
MapWindow.openImageMaps(mapStart(engine, player));
|
||||
IrisModdedCommands.ok(source, IrisLanguage.plain(
|
||||
ModdedCommandMessages.MODDED_STUDIO_COMMANDS_OPENING_IMAGE_MAP_STUDIO_ON_SERVER_DISPLAY,
|
||||
MessageArgument.untrusted("value", level.name())
|
||||
|
||||
+48
-26
@@ -18,21 +18,22 @@
|
||||
|
||||
package art.arcane.iris.modded.command;
|
||||
|
||||
import art.arcane.iris.studio.view.GuiHost;
|
||||
import art.arcane.volmlib.nativelib.view.WorldMarker;
|
||||
import art.arcane.iris.studio.view.GuiMarker;
|
||||
import art.arcane.iris.studio.view.GuiOverlay;
|
||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||
import art.arcane.iris.studio.view.OpenerPosition;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.studio.render.RenderType;
|
||||
import art.arcane.iris.modded.ModdedIrisLog;
|
||||
import art.arcane.iris.modded.ModdedTeleportDeadline;
|
||||
import art.arcane.iris.world.IrisWorld;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeProtocolPlayer;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
|
||||
import art.arcane.volmlib.nativelib.view.WorldView;
|
||||
|
||||
import java.awt.Desktop;
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import art.arcane.volmlib.nativelib.view.WorldView;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public final class ModdedVisionOverlay implements GuiOverlay {
|
||||
@@ -46,14 +47,33 @@ public final class ModdedVisionOverlay implements GuiOverlay {
|
||||
opener = context.opener();
|
||||
}
|
||||
|
||||
// WorldView doesn't report which way a player faces, so their markers all face south.
|
||||
@Override
|
||||
public List<WorldMarker> players() {
|
||||
return world.players();
|
||||
public List<GuiMarker> players() {
|
||||
return world.players().stream()
|
||||
.map(player -> GuiMarker.player(player.label(), player.worldX(), player.worldZ(), 0F))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestEntities(Consumer<List<WorldMarker>> sink) {
|
||||
world.requestEntities(sink);
|
||||
public void requestEntities(Consumer<List<GuiMarker>> sink) {
|
||||
world.requestEntities(entities -> sink.accept(entities.stream()
|
||||
.map(entity -> GuiMarker.entity(entity.label(), entity.worldX(), entity.worldY(), entity.worldZ(),
|
||||
entity.health(), entity.maxHealth()))
|
||||
.toList()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestOpenerPosition(Consumer<OpenerPosition> sink) {
|
||||
if (opener == null) {
|
||||
sink.accept(OpenerPosition.OFFLINE);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
world.execute(() -> sink.accept(openerPosition()));
|
||||
} catch (RejectedExecutionException failure) {
|
||||
sink.accept(OpenerPosition.OFFLINE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -80,23 +100,25 @@ public final class ModdedVisionOverlay implements GuiOverlay {
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public String openInEditor(double worldX, double worldZ, RenderType type) {
|
||||
if (!GuiHost.isAvailable() || !Desktop.isDesktopSupported()) {
|
||||
return null;
|
||||
private OpenerPosition openerPosition() {
|
||||
IrisWorld target = engine.getWorld();
|
||||
if (target == null || engine.isClosing() || engine.isClosed()) {
|
||||
return OpenerPosition.OFFLINE;
|
||||
}
|
||||
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();
|
||||
NativeWorld level = target.platformWorld();
|
||||
if (level == null) {
|
||||
return OpenerPosition.OFFLINE;
|
||||
}
|
||||
NativeProtocolPlayer player = NativeModdedServer.forWorld(level).player(opener);
|
||||
if (player == null || !player.connected()) {
|
||||
return OpenerPosition.OFFLINE;
|
||||
}
|
||||
if (!player.inWorld(level)) {
|
||||
return OpenerPosition.IN_OTHER_WORLD;
|
||||
}
|
||||
return OpenerPosition.inWorld(player.x(), player.y(), player.z());
|
||||
}
|
||||
|
||||
public record Context(WorldView world, Engine engine, UUID opener) {
|
||||
}
|
||||
|
||||
|
||||
+73
@@ -1,25 +1,98 @@
|
||||
package art.arcane.iris.modded.command;
|
||||
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.studio.view.OpenerPosition;
|
||||
import art.arcane.iris.world.IrisWorld;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeProtocolPlayer;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
|
||||
import art.arcane.volmlib.nativelib.view.WorldView;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class ModdedVisionOverlayTest {
|
||||
@Test
|
||||
public void rejectedServerTaskCompletesThePositionRequestAsOffline() {
|
||||
WorldView world = mock(WorldView.class);
|
||||
doThrow(new RejectedExecutionException("Server stopped")).when(world).execute(any(Runnable.class));
|
||||
ModdedVisionOverlay overlay = new ModdedVisionOverlay(new ModdedVisionOverlay.Context(world,
|
||||
mock(Engine.class), UUID.randomUUID()));
|
||||
AtomicReference<OpenerPosition> position = new AtomicReference<>();
|
||||
|
||||
overlay.requestOpenerPosition(position::set);
|
||||
|
||||
assertEquals(OpenerPosition.OFFLINE, position.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void openerPositionRunsOnTheServerAndTracksDisconnectAndWorldChanges() {
|
||||
WorldView world = mock(WorldView.class);
|
||||
AtomicReference<Runnable> scheduled = new AtomicReference<>();
|
||||
doAnswer(invocation -> {
|
||||
scheduled.set(invocation.getArgument(0));
|
||||
return null;
|
||||
}).when(world).execute(any(Runnable.class));
|
||||
Engine engine = mock(Engine.class);
|
||||
IrisWorld irisWorld = mock(IrisWorld.class);
|
||||
NativeWorld level = mock(NativeWorld.class);
|
||||
NativeModdedServer server = mock(NativeModdedServer.class);
|
||||
NativeProtocolPlayer player = mock(NativeProtocolPlayer.class);
|
||||
UUID opener = UUID.randomUUID();
|
||||
when(engine.getWorld()).thenReturn(irisWorld);
|
||||
when(irisWorld.platformWorld()).thenReturn(level);
|
||||
when(server.player(opener)).thenReturn(player);
|
||||
when(player.connected()).thenReturn(true);
|
||||
when(player.inWorld(level)).thenReturn(true);
|
||||
when(player.x()).thenReturn(21.5D);
|
||||
when(player.y()).thenReturn(85.25D);
|
||||
when(player.z()).thenReturn(-39.5D);
|
||||
ModdedVisionOverlay overlay = new ModdedVisionOverlay(new ModdedVisionOverlay.Context(world, engine, opener));
|
||||
AtomicReference<OpenerPosition> position = new AtomicReference<>();
|
||||
try (MockedStatic<NativeModdedServer> nativeServer = mockStatic(NativeModdedServer.class)) {
|
||||
nativeServer.when(() -> NativeModdedServer.forWorld(level)).thenReturn(server);
|
||||
|
||||
overlay.requestOpenerPosition(position::set);
|
||||
assertNull("position reads must wait for the server task", position.get());
|
||||
scheduled.get().run();
|
||||
assertEquals(OpenerPosition.inWorld(21.5D, 85.25D, -39.5D), position.get());
|
||||
|
||||
overlay.requestOpenerPosition(position::set);
|
||||
when(player.connected()).thenReturn(false);
|
||||
scheduled.get().run();
|
||||
assertEquals(OpenerPosition.OFFLINE, position.get());
|
||||
|
||||
when(player.connected()).thenReturn(true);
|
||||
when(player.inWorld(level)).thenReturn(false);
|
||||
overlay.requestOpenerPosition(position::set);
|
||||
scheduled.get().run();
|
||||
assertEquals(OpenerPosition.IN_OTHER_WORLD, position.get());
|
||||
|
||||
when(player.inWorld(level)).thenReturn(true);
|
||||
overlay.requestOpenerPosition(position::set);
|
||||
scheduled.get().run();
|
||||
assertEquals(OpenerPosition.inWorld(21.5D, 85.25D, -39.5D), position.get());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A Vision click can land anywhere in the world, so the destination chunk is usually cold and gets the same
|
||||
* bounded deadline as /iris tp.
|
||||
|
||||
@@ -132,6 +132,8 @@ configurations.named('bundle').configure {
|
||||
exclude(group: 'de.crazydev22.slimjar.helper')
|
||||
exclude(group: 'de.crazydev22.slimjar')
|
||||
exclude(group: 'io.github.slimjar')
|
||||
// The game ships its own LWJGL; without the modules it leaves out, the map's 3D views draw in software.
|
||||
exclude(group: 'org.lwjgl')
|
||||
}
|
||||
|
||||
configurations.compileClasspath.extendsFrom(configurations.devBundle)
|
||||
@@ -173,7 +175,9 @@ dependencies {
|
||||
libs.caffeine,
|
||||
'com.fulcrumgenomics:jlibdeflate:0.2.0',
|
||||
libs.dom4j,
|
||||
libs.jaxen
|
||||
libs.jaxen,
|
||||
libs.flatlaf,
|
||||
libs.flatlaf.extras
|
||||
]
|
||||
add('bundle', libs.asm)
|
||||
add('devBundle', 'org.ow2.asm:asm') {
|
||||
|
||||
Reference in new issue
Block a user