From 49d51f9dbdee27b8130171816185bb220faee910 Mon Sep 17 00:00:00 2001 From: Ryan Barlow <7389646+ryanbarlow97@users.noreply.github.com> Date: Thu, 15 Oct 2026 18:09:34 +0000 Subject: [PATCH] fix: complete resolvable owner tokens and unassigned pets --- .../companionpets/staff/StaffCommands.java | 29 ++++++- .../staff/StaffCommandsTest.java | 77 ++++++++++++++++++- 2 files changed, 101 insertions(+), 5 deletions(-) diff --git a/src/main/java/net/tfminecraft/companionpets/staff/StaffCommands.java b/src/main/java/net/tfminecraft/companionpets/staff/StaffCommands.java index 71b6b4d..2956fc0 100644 --- a/src/main/java/net/tfminecraft/companionpets/staff/StaffCommands.java +++ b/src/main/java/net/tfminecraft/companionpets/staff/StaffCommands.java @@ -71,6 +71,18 @@ public List owners() { .sorted(Comparator.comparing(this::ownerName, String.CASE_INSENSITIVE_ORDER)).toList(); } + private String ownerToken(UUID id, boolean creating) { + var access = net.tfminecraft.companionpets.ownership.CharacterAccess.resolve(id.toString()); + if (!id.equals(access) || (creating && !net.tfminecraft.companionpets.ownership.CharacterAccess.alive(id))) return null; + String name = ownerName(id); + // Owner arguments consume one token. Confirm names select exactly this directory identity. + if (name != null && !name.isBlank() && name.chars().noneMatch(Character::isWhitespace) + && !name.equalsIgnoreCase("unassigned") + && id.equals(net.tfminecraft.companionpets.ownership.CharacterAccess.resolve(name)) + && net.tfminecraft.tlibs.character.Characters.findByName(name).size() == 1) return name; + return id.toString(); + } + public List owned(UUID owner) { return runtime.store().ofCharacter(owner).stream().sorted(Comparator.comparing((Pet p) -> p.name(), String.CASE_INSENSITIVE_ORDER).thenComparing(Pet::id)).toList(); } @@ -209,9 +221,20 @@ public List complete(CommandSender sender, String[] args) { if (args.length == 2) { if (action.equals("egg")) { choices.add("all"); choices.addAll(runtime.config().types().keySet()); } else if (List.of("list", "find", "create").contains(action)) { - owners().forEach(id -> { if (!ownerName(id).equals("Unknown player")) choices.add(ownerName(id)); }); - if (action.equals("create")) { - Bukkit.getOnlinePlayers().forEach(p -> choices.add(p.getName())); + boolean creating = action.equals("create"); + owners().stream().map(id -> ownerToken(id, creating)).filter(Objects::nonNull).forEach(choices::add); + if (action.equals("list")) choices.add("unassigned"); + if (creating) { + for (Player player : Bukkit.getOnlinePlayers()) { + var session = net.tfminecraft.companionpets.ownership.CharacterAccess.capture(player); + if (session == null) continue; + String token = ownerToken(session.character(), true); + if (token != null) choices.add(token); + if (session.character().equals( + net.tfminecraft.companionpets.ownership.CharacterAccess.resolve(player.getName())) + && net.tfminecraft.tlibs.character.Characters.findByName(player.getName()).size() < 2) + choices.add(player.getName()); + } } } } else if (action.equals("egg")) { diff --git a/src/test/java/net/tfminecraft/companionpets/staff/StaffCommandsTest.java b/src/test/java/net/tfminecraft/companionpets/staff/StaffCommandsTest.java index bdeffa2..bdedc6d 100644 --- a/src/test/java/net/tfminecraft/companionpets/staff/StaffCommandsTest.java +++ b/src/test/java/net/tfminecraft/companionpets/staff/StaffCommandsTest.java @@ -357,7 +357,7 @@ private Pet add(UUID owner, String type, String name, boolean stored) { add(staff.getUniqueId(), "wolf", "Staff pet", true); add(owner.getUniqueId(), "wolf", "Toby Junior", true); for (String action : List.of("list", "find")) { - assertEquals(List.of("Owner", "Staff"), commands.complete(staff, new String[]{action, ""})); + assertEquals(action.equals("list") ? List.of("Owner", "Staff", "unassigned") : List.of("Owner", "Staff"), commands.complete(staff, new String[]{action, ""})); assertEquals(List.of("Toby", "Toby Junior"), commands.complete(staff, new String[]{action, "Owner", ""})); assertEquals(List.of("Junior"), commands.complete(staff, new String[]{action, "Owner", "Toby", "J"})); assertTrue(commands.complete(staff, new String[]{action, "Staff", "Toby"}).isEmpty()); @@ -376,7 +376,7 @@ private Pet add(UUID owner, String type, String name, boolean stored) { assertThrows(IllegalArgumentException.class, () -> commands.resolveOwner("NotCached")); assertEquals(List.of("Owner", "Staff"), commands.complete(staff, new String[]{"create", ""})); for (String action : List.of("list", "find")) { - assertEquals(List.of("Owner"), commands.complete(staff, new String[]{action, ""})); + assertEquals(action.equals("list") ? List.of("Owner", "unassigned") : List.of("Owner"), commands.complete(staff, new String[]{action, ""})); assertEquals(List.of("Toby"), commands.complete(staff, new String[]{action, "Owner", ""})); } command("list", "Owner"); @@ -513,6 +513,79 @@ private Pet add(UUID owner, String type, String name, boolean stored) { assertEquals(Set.of("Toby", "Retry"), new HashSet<>(afterRetry.of(owner.getUniqueId()).stream().map(Pet::name).toList())); } + @Test void ownerCandidatesRoundTripThroughTheDirectoryForMultiwordDuplicateAndUnresolvedOwners() { + var infos = new LinkedHashMap(); + var duplicateAccount = server.addPlayer("Duplicate"); + for (String name : List.of("Old Owner", "Duplicate", "Duplicate", "Solo", "unassigned", "Dead Owner")) { + UUID id = UUID.randomUUID(); + UUID account = name.equals("Duplicate") && infos.values().stream().noneMatch(info -> info.name().equals("Duplicate")) + ? duplicateAccount.getUniqueId() : UUID.randomUUID(); + var ref = new net.tfminecraft.tlibs.character.CharacterRef(account, id); + infos.put(id, new net.tfminecraft.tlibs.character.CharacterInfo(ref, name, + name.equals("Dead Owner") ? net.tfminecraft.tlibs.character.CharacterStatus.DEAD + : net.tfminecraft.tlibs.character.CharacterStatus.ALIVE, null, Set.of(), 0, null)); + Pet saved = add(account, "wolf", "Pet " + name, true); saved.ownership(account, id); + } + UUID unresolved = UUID.randomUUID(); pet.ownership(owner.getUniqueId(), unresolved); + var identity = new net.tfminecraft.tlibs.character.CharacterIdentity() { + public Optional currentSession(org.bukkit.entity.Player player) { + return player == null ? Optional.empty() : infos.values().stream() + .filter(info -> info.ref().ownerId().equals(player.getUniqueId())) + .map(info -> new net.tfminecraft.tlibs.character.CharacterSession(info.ref(), 1)).findFirst(); + } + public boolean isCurrent(net.tfminecraft.tlibs.character.CharacterSession session) { + return currentSession(server.getPlayer(session.ref().ownerId())).filter(session::equals).isPresent(); + } + public Optional info(UUID id) { return Optional.ofNullable(infos.get(id)); } + public Optional lastPlayed(UUID account) { return Optional.empty(); } + public Optional> characters(UUID account) { return Optional.of(infos.values().stream().filter(info -> info.ref().ownerId().equals(account)).toList()); } + public List findByName(String name) { return infos.values().stream().filter(info -> info.name().equalsIgnoreCase(name)).toList(); } + public String displayName(org.bukkit.entity.Player player) { return player.getName(); } + }; + server.getServicesManager().unregister(net.tfminecraft.tlibs.character.CharacterIdentity.class, + server.getServicesManager().load(net.tfminecraft.tlibs.character.CharacterIdentity.class)); + server.getServicesManager().register(net.tfminecraft.tlibs.character.CharacterIdentity.class, identity, + plugin, org.bukkit.plugin.ServicePriority.Highest); + forbidOfflineScan = true; + assertEquals("Unknown character", net.tfminecraft.companionpets.ownership.CharacterAccess.name(unresolved)); + assertEquals("Unassigned character", net.tfminecraft.companionpets.ownership.CharacterAccess.name(null)); + for (String action : List.of("list", "find", "create")) { + var choices = commands.complete(staff, new String[]{action, ""}); + assertFalse(choices.contains("Old Owner")); assertFalse(choices.contains("Duplicate")); + assertFalse(choices.contains("Unknown character")); assertFalse(choices.contains("Unassigned character")); + assertFalse(choices.contains(unresolved.toString())); + for (var entry : infos.entrySet()) { + UUID id = entry.getKey(); var info = entry.getValue(); + String token = info.name().equals("Solo") ? "Solo" : id.toString(); + if (action.equals("create") && !info.alive()) { assertFalse(choices.contains(token)); continue; } + assertTrue(choices.contains(token), action + " missing " + token + " from " + choices); + assertEquals(id, commands.resolveOwner(token)); + assertEquals(List.of(token), commands.complete(staff, new String[]{action, token.substring(0, Math.min(8, token.length())).toUpperCase(Locale.ROOT)})); + } + } + var first = infos.values().iterator().next(); + var console = server.getConsoleSender(); + commands.execute(console, "list", first.ref().characterId().toString()); + assertEquals("Pets belonging to Old Owner:", console.nextMessage()); + assertTrue(console.nextMessage().contains("Pet Old Owner")); + // A missing provider must not turn the fallback label into an owner argument. + server.getServicesManager().unregister(net.tfminecraft.tlibs.character.CharacterIdentity.class, + server.getServicesManager().load(net.tfminecraft.tlibs.character.CharacterIdentity.class)); + assertEquals(List.of("unassigned"), commands.complete(staff, new String[]{"list", ""})); + } + + @Test void listUnassignedCompletesWithPrefixesAndOnlyTheListPermission() { + assertEquals(List.of("unassigned"), commands.complete(staff, new String[]{"list", "un"})); + assertEquals(List.of("unassigned"), commands.complete(staff, new String[]{"LIST", "UnA"})); + assertTrue(commands.complete(staff, new String[]{"list", ""}).contains("unassigned")); + assertTrue(commands.complete(staff, new String[]{"find", "un"}).isEmpty()); + assertTrue(commands.complete(staff, new String[]{"create", "un"}).isEmpty()); + commands.execute(server.getConsoleSender(), "list", "unassigned"); + assertTrue(server.getConsoleSender().nextMessage().contains("Unassigned or held pet records")); + staff.setOp(false); staff.addAttachment(plugin, "companionpets.admin.list", false); + assertTrue(commands.complete(staff, new String[]{"list", "un"}).isEmpty()); + } + @Test void modelledEggDeliveryPreservesConfiguredIdentityAndCompletesOnlineTargetsAndAmounts() throws Exception { configure(""" pets: