diff --git a/src/main/java/net/tfminecraft/geigercounters/commands/GeigerCommand.java b/src/main/java/net/tfminecraft/geigercounters/commands/GeigerCommand.java index fad7d9b..2fe2e6c 100644 --- a/src/main/java/net/tfminecraft/geigercounters/commands/GeigerCommand.java +++ b/src/main/java/net/tfminecraft/geigercounters/commands/GeigerCommand.java @@ -8,6 +8,9 @@ import org.bukkit.command.CommandExecutor; import org.bukkit.command.TabCompleter; import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; import net.tfminecraft.geigercounters.config.GeigerConfiguration; import net.tfminecraft.geigercounters.config.Messages; import net.tfminecraft.geigercounters.managers.GeigerManager; @@ -17,6 +20,10 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Locale; +import java.util.Map; import java.util.Optional; import java.util.UUID; @@ -24,14 +31,26 @@ // Admin command handler: // /geiger [x z]|limits |resetlimits |droplist [name]|reload> // ==================================== -public class GeigerCommand implements CommandExecutor, TabCompleter { +public class GeigerCommand implements CommandExecutor, TabCompleter, Listener { private static final List SUBCOMMANDS = Arrays.asList("locate", "move", "limits", "resetlimits", "droplist", "reload"); private final GeigerManager manager; + private final Map knownNames = new LinkedHashMap<>(); - public GeigerCommand(GeigerManager manager) { + public GeigerCommand(GeigerManager manager, OfflinePlayer[] knownPlayers) { this.manager = manager; + for (OfflinePlayer player : knownPlayers) { + if (player.getName() != null && player.hasPlayedBefore()) { + knownNames.put(player.getUniqueId(), player.getName()); + } + } + } + + @EventHandler + public void onJoin(PlayerJoinEvent event) { + Player player = event.getPlayer(); + knownNames.put(player.getUniqueId(), player.getName()); } // Fetched per use rather than cached - /geiger reload swaps the file @@ -322,6 +341,7 @@ private static boolean takesPlayer(String argument) { @Override public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) { + if (args.length == 0) return List.of(); if (args.length == 1) { List matches = new ArrayList<>(); for (String sub : SUBCOMMANDS) { @@ -350,13 +370,17 @@ public List onTabComplete(CommandSender sender, Command command, String // locate, move and both limit subcommands (either spelling) take a player name if (args.length == 2 && takesPlayer(args[0])) { - List matches = new ArrayList<>(); + Map names = new LinkedHashMap<>(); + if (args[0].equalsIgnoreCase("limit") || args[0].equalsIgnoreCase("limits") + || args[0].equalsIgnoreCase("resetlimit") || args[0].equalsIgnoreCase("resetlimits")) { + names.putAll(knownNames); + } for (Player player : Bukkit.getOnlinePlayers()) { - if (player.getName().toLowerCase().startsWith(args[1].toLowerCase())) { - matches.add(player.getName()); - } + names.put(player.getUniqueId(), player.getName()); } - return matches; + return new LinkedHashSet<>(names.values()).stream() + .filter(name -> name.toLowerCase(Locale.ROOT).startsWith(args[1].toLowerCase(Locale.ROOT))) + .toList(); } return new ArrayList<>(); diff --git a/src/main/java/net/tfminecraft/geigercounters/geiger_counter.java b/src/main/java/net/tfminecraft/geigercounters/geiger_counter.java index 8ef37cc..f212559 100644 --- a/src/main/java/net/tfminecraft/geigercounters/geiger_counter.java +++ b/src/main/java/net/tfminecraft/geigercounters/geiger_counter.java @@ -26,9 +26,10 @@ public void onEnable() { GeigerManager.getInstance(this).initialize(); - GeigerCommand geigerCommand = new GeigerCommand(GeigerManager.getInstance()); + GeigerCommand geigerCommand = new GeigerCommand(GeigerManager.getInstance(), getServer().getOfflinePlayers()); getCommand("geiger").setExecutor(geigerCommand); getCommand("geiger").setTabCompleter(geigerCommand); + getServer().getPluginManager().registerEvents(geigerCommand, this); setupMetrics(); @@ -81,4 +82,4 @@ private static String bucketDistance(double distance) { return "5000+"; } -} \ No newline at end of file +} diff --git a/src/test/java/net/tfminecraft/geigercounters/LifecycleUtilitiesTest.java b/src/test/java/net/tfminecraft/geigercounters/LifecycleUtilitiesTest.java index 9ab05b6..d1646dd 100644 --- a/src/test/java/net/tfminecraft/geigercounters/LifecycleUtilitiesTest.java +++ b/src/test/java/net/tfminecraft/geigercounters/LifecycleUtilitiesTest.java @@ -49,6 +49,11 @@ void pluginLifecycleRegistersCommandsMetricsAndShutsDownResources() throws Excep bukkit.when(Bukkit::getUnsafe).thenReturn(mock(UnsafeValues.class)); assertNotNull(new TestPluginLoader().definePluginSubclass().getConstructor().newInstance()); geiger_counter plugin = mock(geiger_counter.class, CALLS_REAL_METHODS); + Server server = mock(Server.class); + var pluginManager = mock(org.bukkit.plugin.PluginManager.class); + doReturn(server).when(plugin).getServer(); + when(server.getOfflinePlayers()).thenReturn(new OfflinePlayer[0]); + when(server.getPluginManager()).thenReturn(pluginManager); doReturn(Logger.getAnonymousLogger()).when(plugin).getLogger(); doNothing().when(plugin).saveDefaultConfig(); PluginCommand command = mock(PluginCommand.class); @@ -61,6 +66,7 @@ void pluginLifecycleRegistersCommandsMetricsAndShutsDownResources() throws Excep verify(manager).initialize(); verify(command).setExecutor(any()); verify(command).setTabCompleter(any()); + verify(pluginManager).registerEvents(any(net.tfminecraft.geigercounters.commands.GeigerCommand.class), eq(plugin)); Metrics created = metrics.constructed().getFirst(); ArgumentCaptor charts = ArgumentCaptor.forClass(CustomChart.class); verify(created, times(4)).addCustomChart(charts.capture()); diff --git a/src/test/java/net/tfminecraft/geigercounters/commands/GeigerCommandTest.java b/src/test/java/net/tfminecraft/geigercounters/commands/GeigerCommandTest.java index 96fb8b6..73c15c9 100644 --- a/src/test/java/net/tfminecraft/geigercounters/commands/GeigerCommandTest.java +++ b/src/test/java/net/tfminecraft/geigercounters/commands/GeigerCommandTest.java @@ -52,7 +52,7 @@ void setUp() { when(config.isLimitEnabled()).thenReturn(true); when(plugin.getConfig()).thenReturn(new YamlConfiguration()); when(world.getName()).thenReturn("world"); - command = new GeigerCommand(manager); + command = new GeigerCommand(manager, new OfflinePlayer[0]); } void run(String... args) { @@ -265,7 +265,9 @@ void coordinateCompletionFloorsNegativeNumbersDeduplicatesAndUsesCorrectAxis() { void playerArgumentsCompleteOnlinePlayerNamesCaseInsensitively() { Player first = mock(Player.class), second = mock(Player.class); when(first.getName()).thenReturn("Hunter"); + when(first.getUniqueId()).thenReturn(UUID.randomUUID()); when(second.getName()).thenReturn("Other"); + when(second.getUniqueId()).thenReturn(UUID.randomUUID()); try (var bukkit = mockStatic(Bukkit.class)) { bukkit.when(Bukkit::getOnlinePlayers).thenReturn(List.of(first, second)); for (String alias : List.of("locate", "move", "limit", "limits", "resetlimit", "resetlimits")) { @@ -273,4 +275,44 @@ void playerArgumentsCompleteOnlinePlayerNamesCaseInsensitively() { } } } + + @Test + void limitCompletionUsesKnownOfflineNamesAndJoinUpdatesWithoutProfileLookups() { + UUID knownId = UUID.randomUUID(); + OfflinePlayer known = mock(OfflinePlayer.class), unnamed = mock(OfflinePlayer.class), + unknown = mock(OfflinePlayer.class); + when(known.getUniqueId()).thenReturn(knownId); + when(known.getName()).thenReturn("OldName"); + when(known.hasPlayedBefore()).thenReturn(true); + when(unknown.getName()).thenReturn("NeverPlayed"); + command = new GeigerCommand(manager, new OfflinePlayer[] {known, unnamed, unknown}); + Player joined = mock(Player.class), online = mock(Player.class); + when(joined.getUniqueId()).thenReturn(knownId); + when(joined.getName()).thenReturn("RenamedHunter"); + when(online.getUniqueId()).thenReturn(UUID.randomUUID()); + when(online.getName()).thenReturn("Online"); + var join = mock(org.bukkit.event.player.PlayerJoinEvent.class); + when(join.getPlayer()).thenReturn(joined); + try (var bukkit = mockStatic(Bukkit.class)) { + bukkit.when(Bukkit::getOnlinePlayers).thenReturn(List.of(online)); + for (String action : List.of("limit", "limits", "resetlimit", "resetlimits")) { + assertEquals(List.of("OldName", "Online"), complete(sender, action, "")); + assertEquals(List.of("OldName"), complete(sender, action.toUpperCase(java.util.Locale.ROOT), "oLD")); + assertTrue(complete(sender, action, "absent").isEmpty()); + assertTrue(complete(sender, action, "OldName", "").isEmpty()); + } + for (String action : List.of("locate", "move")) { + assertEquals(List.of("Online"), complete(sender, action, "")); + assertTrue(complete(sender, action, "Old").isEmpty()); + } + command.onJoin(join); + assertEquals(List.of("RenamedHunter"), complete(sender, "limits", "rEN")); + assertTrue(complete(sender, "resetlimits", "Old").isEmpty()); + bukkit.when(Bukkit::getOnlinePlayers).thenReturn(List.of(joined, online)); + assertEquals(List.of("RenamedHunter", "Online"), complete(sender, "limits", "")); + assertTrue(complete(sender).isEmpty()); + bukkit.verify(Bukkit::getOfflinePlayers, never()); + bukkit.verify(() -> Bukkit.getOfflinePlayer(anyString()), never()); + } + } }