From 5cee4cb3697662b9360bcba641376a990d0cb724 Mon Sep 17 00:00:00 2001 From: Ryan Barlow <7389646+ryanbarlow97@users.noreply.github.com> Date: Thu, 15 Oct 2026 18:02:34 +0000 Subject: [PATCH 1/2] fix: complete known offline targets with action permissions --- .../activitytf/ActivityPlugin.java | 2 + .../activitytf/commands/ActivityCommand.java | 37 ++++++++++++-- .../activitytf/ActivityPluginTest.java | 8 ++-- .../commands/ActivityCommandTest.java | 48 +++++++++++++++++++ 4 files changed, 86 insertions(+), 9 deletions(-) diff --git a/src/main/java/net/tfminecraft/activitytf/ActivityPlugin.java b/src/main/java/net/tfminecraft/activitytf/ActivityPlugin.java index ee031e6..1dca453 100644 --- a/src/main/java/net/tfminecraft/activitytf/ActivityPlugin.java +++ b/src/main/java/net/tfminecraft/activitytf/ActivityPlugin.java @@ -40,6 +40,8 @@ public void onEnable() { ActivityGui gui = new ActivityGui(manager); ActivityCommand command = new ActivityCommand(manager, gui); + command.loadKnownPlayers(); + getServer().getPluginManager().registerEvents(command, this); getCommand("activity").setExecutor(command); getCommand("activity").setTabCompleter(command); diff --git a/src/main/java/net/tfminecraft/activitytf/commands/ActivityCommand.java b/src/main/java/net/tfminecraft/activitytf/commands/ActivityCommand.java index 1945479..4e9d6c4 100644 --- a/src/main/java/net/tfminecraft/activitytf/commands/ActivityCommand.java +++ b/src/main/java/net/tfminecraft/activitytf/commands/ActivityCommand.java @@ -7,6 +7,9 @@ import org.bukkit.command.CommandSender; 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 org.bukkit.inventory.Inventory; import net.md_5.bungee.api.ChatColor; import net.tfminecraft.activitytf.config.ActivityConfiguration; @@ -22,18 +25,18 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.LinkedHashMap; +import java.util.Map; import java.util.Locale; import java.util.UUID; -public class ActivityCommand implements CommandExecutor, TabCompleter { +public class ActivityCommand implements CommandExecutor, TabCompleter, Listener { private static final List SUBCOMMANDS = Arrays.asList("reload", "rewards", "check", "reset", "givereroll", "add", "addpoints"); private static final String APPLY = "apply"; - private static final List READ_ONLY_SUBCOMMANDS = List.of("check"); - private static final String FORCE = "--force"; private static final int MAX_ADD = 1_000_000; @@ -42,6 +45,7 @@ public class ActivityCommand implements CommandExecutor, TabCompleter { private final ActivityManager manager; private final ActivityGui gui; + private final Map knownPlayers = new LinkedHashMap<>(); public ActivityCommand(ActivityManager manager, ActivityGui gui) { this.manager = manager; @@ -440,7 +444,8 @@ public List onTabComplete(CommandSender sender, Command command, String } if (args.length == 1) { - return filter(admin ? SUBCOMMANDS : READ_ONLY_SUBCOMMANDS, args[0]); + return filter(SUBCOMMANDS.stream() + .filter(sub -> sender.hasPermission(permissionFor(sub))).toList(), args[0]); } String sub = args[0].toLowerCase(Locale.ROOT); @@ -451,7 +456,14 @@ public List onTabComplete(CommandSender sender, Command command, String if (args.length == 2 && (sub.equals("reset") || sub.equals("add") || sub.equals("addpoints") || sub.equals("check") || sub.equals("givereroll"))) { - return filter(onlineNames(), args[1]); + Map names = new LinkedHashMap<>(); + for (String name : onlineNames()) { + names.put(name.toLowerCase(Locale.ROOT), name); + } + for (String name : knownPlayers.values()) { + names.putIfAbsent(name.toLowerCase(Locale.ROOT), name); + } + return filter(new ArrayList<>(names.values()), args[1]); } if (args.length == 2 && sub.equals("rewards")) { @@ -473,6 +485,21 @@ public List onTabComplete(CommandSender sender, Command command, String return Collections.emptyList(); } + // Enumerate saved players once at enable, never while completing. Joins refresh renamed accounts. + public void loadKnownPlayers() { + for (OfflinePlayer player : Bukkit.getOfflinePlayers()) { + if (player.hasPlayedBefore() && player.getName() != null) { + knownPlayers.put(player.getUniqueId(), player.getName()); + } + } + } + + @EventHandler + public void onJoin(PlayerJoinEvent event) { + Player player = event.getPlayer(); + knownPlayers.put(player.getUniqueId(), player.getName()); + } + List onlineNames() { List names = new ArrayList<>(); for (Player player : Bukkit.getOnlinePlayers()) { diff --git a/src/test/java/net/tfminecraft/activitytf/ActivityPluginTest.java b/src/test/java/net/tfminecraft/activitytf/ActivityPluginTest.java index 0f7cac1..ce4cca2 100644 --- a/src/test/java/net/tfminecraft/activitytf/ActivityPluginTest.java +++ b/src/test/java/net/tfminecraft/activitytf/ActivityPluginTest.java @@ -111,11 +111,11 @@ void startupInstallsCommandsListenersAndTimersAndShutdownPersistsState() throws assertTrue(Files.isRegularFile(plugin.getDataFolder().toPath().resolve("messages.yml"))); assertInstanceOf(ActivityCommand.class, plugin.getCommand("activity").getExecutor()); assertSame(plugin.getCommand("activity").getExecutor(), plugin.getCommand("activity").getTabCompleter()); - assertEquals(Set.of(JoinListener.class, CraftListener.class, ActivityGui.class), listeners()); + assertEquals(Set.of(JoinListener.class, CraftListener.class, ActivityGui.class, ActivityCommand.class), listeners()); assertTrue(server.dispatchCommand(server.getConsoleSender(), "activity")); assertNotNull(server.getConsoleSender().nextMessage()); server.getScheduler().performOneTick(); - assertEquals(Set.of(JoinListener.class, CraftListener.class, ActivityGui.class), listeners()); + assertEquals(Set.of(JoinListener.class, CraftListener.class, ActivityGui.class, ActivityCommand.class), listeners()); UUID uuid = UUID.randomUUID(); manager.getStore().get(uuid).addPoints(7, manager.getConfiguration().barMax()); server.getScheduler().performTicks(manager.getConfiguration().saveIntervalMinutes() * 1200L); @@ -189,12 +189,12 @@ void optionalPluginsRegisterTheirListenersOnTheNextTickAndTheOwnedExpansionIsRem } LocalExpansionManager expansions = installPlaceholderApi(); enablePlugin(); - assertEquals(Set.of(JoinListener.class, CraftListener.class, ActivityGui.class), listeners()); + assertEquals(Set.of(JoinListener.class, CraftListener.class, ActivityGui.class, ActivityCommand.class), listeners()); assertNull(expansions.getExpansion("activity")); server.getScheduler().performOneTick(); - assertEquals(Set.of("JoinListener", "CraftListener", "ActivityGui", "VoteListener", "InstrumentListener", + assertEquals(Set.of("JoinListener", "CraftListener", "ActivityGui", "ActivityCommand", "VoteListener", "InstrumentListener", "GeigerListener", "ProfessionXpListener", "MmoItemsStationListener", "VehicleBuildListener", "CharacterChatListener", "InjuryListener", "ProfessionUpgradeListener", "FurnitureListener", "BattleListener", "AdvancedCraftListener", "ArchaeologyListener", "MarketSaleListener", "CasinoWinListener", "DishCookedListener", diff --git a/src/test/java/net/tfminecraft/activitytf/commands/ActivityCommandTest.java b/src/test/java/net/tfminecraft/activitytf/commands/ActivityCommandTest.java index 60536cc..75bffee 100644 --- a/src/test/java/net/tfminecraft/activitytf/commands/ActivityCommandTest.java +++ b/src/test/java/net/tfminecraft/activitytf/commands/ActivityCommandTest.java @@ -520,6 +520,54 @@ void completionsUseOnlineNamesLoadedActivitiesAndExactForceFlagWithoutMutatingDa } } + @Test + void cachedOfflineTargetsAndJoinedRenamesCompleteWithoutProfileLookups() throws Exception { + try (CommandFixture f = new CommandFixture(directory)) { + OfflinePlayer offline = mock(OfflinePlayer.class); + UUID id = UUID.randomUUID(); + when(offline.getUniqueId()).thenReturn(id); + when(offline.getName()).thenReturn("OldName"); + when(offline.hasPlayedBefore()).thenReturn(true); + OfflinePlayer unnamed = mock(OfflinePlayer.class); + when(unnamed.hasPlayedBefore()).thenReturn(true); + OfflinePlayer neverPlayed = mock(OfflinePlayer.class); + when(neverPlayed.getName()).thenReturn("NeverPlayed"); + f.bukkit.when(Bukkit::getOfflinePlayers).thenReturn(new OfflinePlayer[] {offline, unnamed, neverPlayed}); + f.command.loadKnownPlayers(); + Player online = f.online("Online"); + f.bukkit.when(Bukkit::getOnlinePlayers).thenReturn(List.of(online)); + for (String action : List.of("check", "reset", "givereroll", "add", "addpoints")) { + assertEquals(List.of("Online", "OldName"), f.complete(f.admin(), action, "")); + assertEquals(List.of("OldName"), f.complete(f.admin(), action.toUpperCase(), "oLd")); + assertEquals(List.of(), f.complete(f.admin(), action, "absent")); + } + Player renamed = f.online("NewName"); + when(renamed.getUniqueId()).thenReturn(id); + f.command.onJoin(new org.bukkit.event.player.PlayerJoinEvent(renamed, + (net.kyori.adventure.text.Component) null)); + f.command.onJoin(new org.bukkit.event.player.PlayerJoinEvent(online, + (net.kyori.adventure.text.Component) null)); + assertEquals(List.of("Online", "NewName"), f.complete(f.admin(), "check", "")); + assertEquals(List.of(), f.complete(f.admin(), "check", "old")); + f.bukkit.verify(Bukkit::getOfflinePlayers, org.mockito.Mockito.times(1)); + f.bukkit.verify(() -> Bukkit.getOfflinePlayerIfCached(org.mockito.ArgumentMatchers.anyString()), + org.mockito.Mockito.never()); + assertTrue(f.audit().isEmpty()); + assertNull(f.manager.getStore().peek(id)); + } + } + + @Test + void explicitlyNegatedCheckIsHiddenForAdminsAtEveryDepth() throws Exception { + try (CommandFixture f = new CommandFixture(directory)) { + Sender adminWithoutCheck = new Sender(Set.of("activity.admin")); + assertEquals(List.of("reload", "rewards", "reset", "givereroll", "add", "addpoints"), + f.complete(adminWithoutCheck, "")); + assertEquals(List.of(), f.complete(adminWithoutCheck, "C")); + assertEquals(List.of(), f.complete(adminWithoutCheck, "check", "")); + } + } + private static final class Sender { final CommandSender bukkit = mock(CommandSender.class); final List sent = new ArrayList<>(); From 80fac05cabe8a4f29584478b74823d3d8accbbf8 Mon Sep 17 00:00:00 2001 From: Ryan Barlow <7389646+ryanbarlow97@users.noreply.github.com> Date: Thu, 15 Oct 2026 18:09:26 +0000 Subject: [PATCH 2/2] fix: omit offline names missing from the execution cache --- .../tfminecraft/activitytf/commands/ActivityCommand.java | 5 ++++- .../activitytf/commands/ActivityCommandTest.java | 6 +++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/main/java/net/tfminecraft/activitytf/commands/ActivityCommand.java b/src/main/java/net/tfminecraft/activitytf/commands/ActivityCommand.java index 4e9d6c4..e447590 100644 --- a/src/main/java/net/tfminecraft/activitytf/commands/ActivityCommand.java +++ b/src/main/java/net/tfminecraft/activitytf/commands/ActivityCommand.java @@ -461,7 +461,10 @@ public List onTabComplete(CommandSender sender, Command command, String names.put(name.toLowerCase(Locale.ROOT), name); } for (String name : knownPlayers.values()) { - names.putIfAbsent(name.toLowerCase(Locale.ROOT), name); + String key = name.toLowerCase(Locale.ROOT); + if (!names.containsKey(key) && Bukkit.getOfflinePlayerIfCached(name) != null) { + names.put(key, name); + } } return filter(new ArrayList<>(names.values()), args[1]); } diff --git a/src/test/java/net/tfminecraft/activitytf/commands/ActivityCommandTest.java b/src/test/java/net/tfminecraft/activitytf/commands/ActivityCommandTest.java index 75bffee..15b76af 100644 --- a/src/test/java/net/tfminecraft/activitytf/commands/ActivityCommandTest.java +++ b/src/test/java/net/tfminecraft/activitytf/commands/ActivityCommandTest.java @@ -534,6 +534,7 @@ void cachedOfflineTargetsAndJoinedRenamesCompleteWithoutProfileLookups() throws when(neverPlayed.getName()).thenReturn("NeverPlayed"); f.bukkit.when(Bukkit::getOfflinePlayers).thenReturn(new OfflinePlayer[] {offline, unnamed, neverPlayed}); f.command.loadKnownPlayers(); + f.bukkit.when(() -> Bukkit.getOfflinePlayerIfCached("OldName")).thenReturn(offline); Player online = f.online("Online"); f.bukkit.when(Bukkit::getOnlinePlayers).thenReturn(List.of(online)); for (String action : List.of("check", "reset", "givereroll", "add", "addpoints")) { @@ -541,8 +542,11 @@ void cachedOfflineTargetsAndJoinedRenamesCompleteWithoutProfileLookups() throws assertEquals(List.of("OldName"), f.complete(f.admin(), action.toUpperCase(), "oLd")); assertEquals(List.of(), f.complete(f.admin(), action, "absent")); } + f.bukkit.when(() -> Bukkit.getOfflinePlayerIfCached("OldName")).thenReturn(null); + assertEquals(List.of(), f.complete(f.admin(), "check", "old")); Player renamed = f.online("NewName"); when(renamed.getUniqueId()).thenReturn(id); + f.bukkit.when(() -> Bukkit.getOfflinePlayerIfCached("NewName")).thenReturn(renamed); f.command.onJoin(new org.bukkit.event.player.PlayerJoinEvent(renamed, (net.kyori.adventure.text.Component) null)); f.command.onJoin(new org.bukkit.event.player.PlayerJoinEvent(online, @@ -550,7 +554,7 @@ void cachedOfflineTargetsAndJoinedRenamesCompleteWithoutProfileLookups() throws assertEquals(List.of("Online", "NewName"), f.complete(f.admin(), "check", "")); assertEquals(List.of(), f.complete(f.admin(), "check", "old")); f.bukkit.verify(Bukkit::getOfflinePlayers, org.mockito.Mockito.times(1)); - f.bukkit.verify(() -> Bukkit.getOfflinePlayerIfCached(org.mockito.ArgumentMatchers.anyString()), + f.bukkit.verify(() -> Bukkit.getOfflinePlayer(org.mockito.ArgumentMatchers.anyString()), org.mockito.Mockito.never()); assertTrue(f.audit().isEmpty()); assertNull(f.manager.getStore().peek(id));