Command Corrections.

This commit is contained in:
Brian Neumann-Fopiano
2026-09-17 04:48:35 -04:00
parent 1e1eaa96aa
commit b18681a3e2
43 changed files with 1227 additions and 251 deletions
@@ -122,7 +122,7 @@ public class CommandFind implements DirectorExecutor {
EngineBukkitOps.gotoPOI(e, type, player(), teleport);
}
@Director(description = "Find an accepted hydrology feature")
@Director(description = "Find an accepted hydrology feature", sync = true)
public void river(
@Param(description = "Feature type: surface, waterfall, underground, grotto, mouth, or a deep-fluid id", customHandler = HydrologyTypeHandler.class)
String type,
@@ -147,6 +147,7 @@ public class CommandFind implements DirectorExecutor {
return;
}
Location origin = target.getLocation();
World originWorld = origin.getWorld();
int requestedDistance = runtime == null
? 8192
: Math.min(8192, runtime.settings().routing().tileSize() * 15);
@@ -179,9 +180,9 @@ public class CommandFind implements DirectorExecutor {
return;
}
Location destination = new Location(
target.getWorld(), feature.x(), feature.y(), feature.z());
originWorld, feature.x(), feature.y(), feature.z());
prepareStructureTeleport(
target, target.getWorld(), commandSender, label, destination, true);
target, originWorld, commandSender, label, destination, true);
} catch (Throwable error) {
sendStructureMessage(target, commandSender,
C.RED + "Could not locate " + type + " hydrology: " + error.getClass().getSimpleName());
@@ -737,22 +738,41 @@ public class CommandFind implements DirectorExecutor {
private void prepareStructureTeleport(Player target, World world, VolmitSender commandSender, String structure,
Location at, boolean useLocatedY) {
int chunkX = at.getBlockX() >> 4;
int chunkZ = at.getBlockZ() >> 4;
BukkitPlatform.chunkAtAsync(world, chunkX, chunkZ, true).whenComplete((chunk, error) -> {
if (error != null) {
sendStructureMessage(target, commandSender, C.RED + "Could not load the destination for " + structure + ".");
Iris.reportError("Could not load structure destination '" + structure + "'.", error);
J.runEntity(target, () -> {
if (!target.isOnline()) {
return;
}
boolean scheduled = J.runRegion(world, chunkX, chunkZ,
() -> teleportToStructure(target, world, commandSender, structure, at, useLocatedY));
if (!scheduled) {
sendStructureMessage(target, commandSender, C.RED + "Could not schedule the destination lookup for " + structure + ".");
if (!world.equals(target.getWorld())) {
commandSender.sendMessage(C.YELLOW + "Cancelled teleport to " + structure + " because your world changed.");
return;
}
loadStructureDestination(target, world, commandSender, structure, at, useLocatedY);
});
}
private void loadStructureDestination(Player target, World world, VolmitSender commandSender, String structure,
Location at, boolean useLocatedY) {
int chunkX = at.getBlockX() >> 4;
int chunkZ = at.getBlockZ() >> 4;
try {
BukkitPlatform.chunkAtAsync(world, chunkX, chunkZ, true).whenComplete((chunk, error) -> {
if (error != null) {
sendStructureMessage(target, commandSender, C.RED + "Could not load the destination for " + structure + ".");
Iris.reportError("Could not load structure destination '" + structure + "'.", error);
return;
}
boolean scheduled = J.runRegion(world, chunkX, chunkZ,
() -> teleportToStructure(target, world, commandSender, structure, at, useLocatedY));
if (!scheduled) {
sendStructureMessage(target, commandSender, C.RED + "Could not schedule the destination lookup for " + structure + ".");
}
});
} catch (Throwable error) {
sendStructureMessage(target, commandSender, C.RED + "Could not load the destination for " + structure + ".");
Iris.reportError("Could not load structure destination '" + structure + "'.", error);
}
}
private void teleportToStructure(Player target, World world, VolmitSender commandSender, String structure,
Location at, boolean useLocatedY) {
try {
@@ -760,16 +780,41 @@ public class CommandFind implements DirectorExecutor {
? Math.max(world.getMinHeight() + 1, Math.min(world.getMaxHeight() - 1, at.getBlockY() + 2))
: world.getHighestBlockYAt(at.getBlockX(), at.getBlockZ()) + 2;
Location destination = new Location(world, at.getBlockX() + 0.5, y, at.getBlockZ() + 0.5);
J.runEntity(target, () -> {
BukkitPlatform.teleportAsync(target, destination);
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_FIND_TELEPORTED, MessageArgument.untrusted("structure", structure), MessageArgument.untrusted("value", at.getBlockX()), MessageArgument.untrusted("y", y), MessageArgument.untrusted("value2", at.getBlockZ())));
});
J.runEntity(target, () -> completeStructureTeleport(target, world, commandSender, structure, destination));
} catch (Throwable t) {
sendStructureMessage(target, commandSender, C.RED + "Could not prepare the destination for " + structure + ".");
Iris.reportError("Could not prepare structure destination '" + structure + "'.", t);
}
}
private void completeStructureTeleport(Player target, World world, VolmitSender commandSender, String structure,
Location destination) {
if (!target.isOnline()) {
return;
}
if (!world.equals(target.getWorld())) {
commandSender.sendMessage(C.YELLOW + "Cancelled teleport to " + structure + " because your world changed.");
return;
}
try {
BukkitPlatform.teleportAsync(target, destination).whenComplete((teleported, error) -> {
if (error != null) {
Iris.reportError("Could not teleport to '" + structure + "'.", error);
}
sendStructureMessage(target, commandSender, error == null && Boolean.TRUE.equals(teleported)
? IrisLanguage.text(BukkitCommandMessages.COMMAND_FIND_TELEPORTED,
MessageArgument.untrusted("structure", structure),
MessageArgument.untrusted("value", destination.getBlockX()),
MessageArgument.untrusted("y", destination.getBlockY()),
MessageArgument.untrusted("value2", destination.getBlockZ()))
: C.RED + "Could not teleport to " + structure + ".");
});
} catch (Throwable error) {
commandSender.sendMessage(C.RED + "Could not teleport to " + structure + ".");
Iris.reportError("Could not teleport to '" + structure + "'.", error);
}
}
private void sendStructureMessage(Player target, VolmitSender commandSender, String message) {
J.runEntity(target, () -> commandSender.sendMessage(message));
}
@@ -21,6 +21,7 @@ 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.view.GuiHost;
import art.arcane.iris.studio.view.ImageMapStudioGUI;
import art.arcane.iris.studio.view.NoiseExplorerGUI;
import art.arcane.iris.studio.view.VisionGUI;
@@ -274,10 +275,6 @@ public class CommandStudio implements DirectorExecutor {
if (noGUI()) {
return;
}
if (!ImageMapStudioGUI.isAvailable()) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_YOU_MUST_HAVE_SERVER_LAUNCHED_GUIS_ENABLED_SETTINGS));
return;
}
if (!IrisToolbelt.isIrisWorld(world)) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_YOU_NEED_BE_SPECIFY_IRIS_GENERATED_WORLD));
return;
@@ -883,6 +880,10 @@ public class CommandStudio implements DirectorExecutor {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_YOU_MUST_HAVE_SERVER_LAUNCHED_GUIS_ENABLED_SETTINGS));
return true;
}
if (!GuiHost.isAvailable()) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_DESKTOP_UNAVAILABLE));
return true;
}
return false;
}
@@ -0,0 +1,178 @@
package art.arcane.iris.command;
import art.arcane.iris.Iris;
import art.arcane.iris.generation.hydrology.HydrologyFeatureType;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.localization.IrisLanguage;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
import art.arcane.iris.world.history.GenerationSemanticQueries;
import art.arcane.iris.world.task.J;
import art.arcane.volmlib.util.director.annotations.Director;
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.util.Optional;
import java.util.OptionalLong;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.contains;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
public class CommandFindHydrologyTest {
@Test
public void riverCapturesPlayerStateOnTheOwningThread() throws Exception {
assertTrue(CommandFind.class.getMethod("river", String.class, boolean.class)
.getAnnotation(Director.class).sync());
}
@Test
public void worldChangeDuringSearchDoesNotLoadOrTeleportIntoTheNewWorld() {
try (Fixture fixture = new Fixture()) {
fixture.command.river("surface", true);
when(fixture.player.getWorld()).thenReturn(mock(World.class));
fixture.search.get().run();
fixture.platform.verify(() -> BukkitPlatform.chunkAtAsync(any(World.class), anyInt(), anyInt(), eq(true)), never());
fixture.platform.verify(() -> BukkitPlatform.teleportAsync(any(Player.class), any(Location.class)), never());
verify(fixture.sender).sendMessage(contains("world changed"));
}
}
@Test
public void disconnectDuringSearchDoesNotLoadTheDestination() {
try (Fixture fixture = new Fixture()) {
fixture.command.river("surface", true);
when(fixture.player.isOnline()).thenReturn(false);
fixture.search.get().run();
fixture.platform.verify(() -> BukkitPlatform.chunkAtAsync(any(World.class), anyInt(), anyInt(), eq(true)), never());
}
}
@Test
public void worldChangeWhileLoadingTheDestinationCancelsTeleport() {
try (Fixture fixture = new Fixture()) {
fixture.command.river("surface", true);
fixture.search.get().run();
when(fixture.player.getWorld()).thenReturn(mock(World.class));
fixture.chunk.complete(mock(Chunk.class));
fixture.platform.verify(() -> BukkitPlatform.teleportAsync(any(Player.class), any(Location.class)), never());
verify(fixture.sender).sendMessage(contains("world changed"));
}
}
@Test
public void successIsReportedOnlyAfterTeleportCompletes() {
try (Fixture fixture = new Fixture()) {
fixture.command.river("surface", true);
fixture.search.get().run();
fixture.chunk.complete(mock(Chunk.class));
verify(fixture.sender, never()).sendMessage("teleported");
fixture.teleport.complete(true);
verify(fixture.sender).sendMessage("teleported");
}
}
@Test
public void rejectedTeleportReportsFailureWithoutSuccess() {
try (Fixture fixture = new Fixture()) {
fixture.command.river("surface", true);
fixture.search.get().run();
fixture.chunk.complete(mock(Chunk.class));
fixture.teleport.complete(false);
verify(fixture.sender, never()).sendMessage("teleported");
verify(fixture.sender).sendMessage(contains("Could not teleport"));
}
}
@Test
public void failedTeleportReportsTheFullCauseWithoutSuccess() {
try (Fixture fixture = new Fixture()) {
IllegalStateException failure = new IllegalStateException("destination unloaded");
fixture.command.river("surface", true);
fixture.search.get().run();
fixture.chunk.complete(mock(Chunk.class));
fixture.teleport.completeExceptionally(failure);
verify(fixture.sender, never()).sendMessage("teleported");
verify(fixture.sender).sendMessage(contains("Could not teleport"));
fixture.logging.verify(() -> Iris.reportError(any(String.class), eq(failure)));
}
}
private static final class Fixture implements AutoCloseable {
private final CommandFind command = mock(CommandFind.class, CALLS_REAL_METHODS);
private final Engine engine = mock(Engine.class);
private final IrisComplex complex = mock(IrisComplex.class);
private final Player player = mock(Player.class);
private final World world = mock(World.class);
private final VolmitSender sender = mock(VolmitSender.class);
private final AtomicReference<Runnable> search = new AtomicReference<>();
private final CompletableFuture<Chunk> chunk = new CompletableFuture<>();
private final CompletableFuture<Boolean> teleport = new CompletableFuture<>();
private final MockedStatic<J> scheduler = mockStatic(J.class);
private final MockedStatic<BukkitPlatform> platform = mockStatic(BukkitPlatform.class);
private final MockedStatic<GenerationSemanticQueries> queries = mockStatic(GenerationSemanticQueries.class);
private final MockedStatic<IrisLanguage> language = mockStatic(IrisLanguage.class, invocation -> "teleported");
private final MockedStatic<Iris> logging = mockStatic(Iris.class);
private Fixture() {
doReturn(engine).when(command).engine();
doReturn(player).when(command).player();
doReturn(sender).when(command).sender();
when(engine.getComplex()).thenReturn(complex);
when(player.getWorld()).thenReturn(world);
when(player.getLocation()).thenReturn(new Location(world, 0, 70, 0));
when(player.isOnline()).thenReturn(true);
when(world.getMinHeight()).thenReturn(-64);
when(world.getMaxHeight()).thenReturn(320);
scheduler.when(() -> J.a(any(Runnable.class))).thenAnswer(invocation -> {
search.set(invocation.getArgument(0));
return null;
});
scheduler.when(() -> J.runEntity(eq(player), any(Runnable.class))).thenAnswer(invocation -> {
invocation.<Runnable>getArgument(1).run();
return true;
});
scheduler.when(() -> J.runRegion(any(World.class), anyInt(), anyInt(), any(Runnable.class))).thenAnswer(invocation -> {
invocation.<Runnable>getArgument(3).run();
return true;
});
platform.when(() -> BukkitPlatform.chunkAtAsync(any(World.class), anyInt(), anyInt(), eq(true))).thenReturn(chunk);
platform.when(() -> BukkitPlatform.teleportAsync(eq(player), any(Location.class))).thenReturn(teleport);
queries.when(() -> GenerationSemanticQueries.nearestRiver(eq(engine), any(), eq(0), eq(0), anyInt(), any()))
.thenReturn(Optional.of(new GenerationSemanticQueries.RiverResult(
GenerationSemanticQueries.RiverSource.ACTIVE_PREDICTION,
HydrologyFeatureType.RIFFLE, Optional.empty(), 1L, 32, 63, 32, OptionalLong.empty())));
}
@Override
public void close() {
logging.close();
language.close();
queries.close();
platform.close();
scheduler.close();
}
}
}
@@ -0,0 +1,83 @@
package art.arcane.iris.command;
import art.arcane.iris.configuration.IrisSettings;
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
import art.arcane.iris.studio.view.GuiHost;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class CommandStudioDesktopTest {
private IrisSettings previousSettings;
private boolean previousSuppressed;
private RecordingSender sender;
private CommandStudio command;
@Before
public void setup() {
previousSettings = IrisSettings.settings;
previousSuppressed = GuiHost.isDesktopSuppressed();
IrisSettings.settings = new IrisSettings();
IrisSettings.settings.getGui().setUseServerLaunchedGuis(true);
GuiHost.suppressDesktop(true);
sender = new RecordingSender();
command = new TestCommand(sender);
}
@After
public void restore() {
IrisSettings.settings = previousSettings;
GuiHost.suppressDesktop(previousSuppressed);
}
@Test
public void mapReportsMissingDisplayBeforeAccessingWorld() {
command.map(null);
assertDisplayMessage();
}
@Test
public void imageMapReportsMissingDisplayInsteadOfDisabledSetting() {
command.imagemap(null);
assertDisplayMessage();
}
private void assertDisplayMessage() {
assertEquals(1, sender.messages.size());
assertTrue(sender.messages.getFirst(), sender.messages.getFirst().contains("display"));
}
private static final class TestCommand extends CommandStudio {
private final VolmitSender sender;
private TestCommand(VolmitSender sender) {
this.sender = sender;
}
@Override
public VolmitSender sender() {
return sender;
}
}
private static final class RecordingSender extends VolmitSender {
private final List<String> messages = new ArrayList<>();
private RecordingSender() {
super(null);
}
@Override
public void sendMessage(String message) {
messages.add(message);
}
}
}