Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 24 additions & 15 deletions src/main/java/net/tfminecraft/archaeo/command/ArchaeoCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -1340,7 +1340,10 @@ public List<String> onTabComplete(CommandSender sender, Command command, String
if (args.length == 3) {
if ("tool".equalsIgnoreCase(args[1]) || "pick".equalsIgnoreCase(args[1])) {
String typed = args[2].toLowerCase(Locale.ROOT);
return handPick.giveToolTokens().stream()
return java.util.stream.Stream.concat(handPick.giveToolTokens().stream(),
onlineNamesStartingWith(args[2]).stream()
.filter(name -> handPick.toolForGive(name).isEmpty()))
.distinct()
.filter(token -> token.toLowerCase(Locale.ROOT).startsWith(typed))
.toList();
}
Expand Down Expand Up @@ -1446,32 +1449,38 @@ public List<String> onTabComplete(CommandSender sender, Command command, String
}
if ("info".equalsIgnoreCase(args[1]) || "tp".equalsIgnoreCase(args[1])) {
String typed = Arrays.stream(args).skip(2).collect(Collectors.joining(" "));
List<String> names = new ArrayList<>(sites.namesStartingWith(typed));
if (args.length == 3) {
String token = args[2].toLowerCase(Locale.ROOT);
for (Site site : sites.all()) {
String serial = "#" + site.getSerial();
if (serial.startsWith(token) || String.valueOf(site.getSerial()).startsWith(token)) {
names.add(serial);
}
}
}
return names;
return suggestSiteQueries(typed, args.length == 3, site -> true);
}
if ("camps".equalsIgnoreCase(args[1])) {
if (args.length == 3) {
List<String> suggestions = new ArrayList<>(onlineNamesStartingWith(args[2]));
List<String> suggestions = new ArrayList<>(onlineNamesStartingWith(args[2]).stream()
.filter(name -> parseSerial(name).isEmpty()
&& net.tfminecraft.archaeo.ownership.CharacterActors.invited(name).isPresent())
.toList());
String token = args[2].toLowerCase(Locale.ROOT);
for (Site site : sites.all()) {
if (!site.isCampLocked()) {
continue;
}
var director = site.getDirector() == null ? null
: net.tfminecraft.tlibs.character.Characters.info(site.getDirector()).orElse(null);
if (director != null) {
String id = director.ref().characterId().toString();
if (id.startsWith(token)) suggestions.add(id);
String name = director.name();
if (name != null && !name.isBlank() && name.chars().noneMatch(Character::isWhitespace)
&& parseSerial(name).isEmpty() && name.toLowerCase(Locale.ROOT).startsWith(token)
&& net.tfminecraft.archaeo.ownership.CharacterActors.invited(name)
.filter(ref -> ref.equals(director.ref())).isPresent()) {
suggestions.add(name);
}
}
String serial = "#" + site.getSerial();
if (serial.startsWith(token) || String.valueOf(site.getSerial()).startsWith(token)) {
suggestions.add(serial);
}
}
return suggestions;
return suggestions.stream().distinct().toList();
}
if (args.length == 4) {
var director = net.tfminecraft.archaeo.ownership.CharacterActors.invited(args[2]).orElse(null);
Expand Down Expand Up @@ -1512,7 +1521,7 @@ private List<String> suggestSiteQueries(String needle, boolean includeSerials, P
}
}
if (allowed) {
names.add(name);
names.add(name.substring(needle.lastIndexOf(' ') + 1));
}
}
if (includeSerials) {
Expand Down
134 changes: 128 additions & 6 deletions src/test/java/net/tfminecraft/archaeo/command/ArchaeoCommandTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ public void completionUsesPermissionsPlayersWorldsAndEligibleSiteStates() {
assertFalse(complete("ruin", "close", "").contains(hidden.getName()));
assertTrue(complete("ruin", "delete", "con").contains("confirm"));
assertTrue(complete("ruin", "info", "#").containsAll(List.of("#1", "#2")));
assertTrue(complete("ruin", "tp", "Old", "r").contains("Old ruin"));
assertTrue(complete("ruin", "tp", "Old", "r").contains("ruin"));
assertTrue(complete("ruin", "camps", "#").contains("#2"));
assertTrue(complete("ruin", "camps", "Archaeologist", "2").contains("#2"));
assertTrue(complete("ruin", "delete", "#1", "confirm").isEmpty());
Expand Down Expand Up @@ -517,18 +517,18 @@ public void consoleCommandsExplainWorldRequirementsAndCanGiveToNamedPlayers() {
new String[]{"ruin", "camps", "Nobody", "#"}, new String[]{"ruin", "camps", "Archaeologist", "#2", "x"}))
assertTrue(String.join(" ", extra), complete(extra).isEmpty());
assertEquals(List.of("Archaeologist"), complete("workday", "reset", "arch"));
assertEquals(List.of("Old ruin"), complete("ruin", "set-interest", "high", "Old", "r"));
assertEquals(List.of("ruin"), complete("ruin", "set-interest", "high", "Old", "r"));
assertEquals(List.of("confirm"), complete("ruin", "close", "confirm"));
assertEquals(List.of("Open camp"), complete("ruin", "close", "Open"));
assertEquals(List.of("Open camp", "confirm"), complete("ruin", "close", "Open", "c"));
assertEquals(List.of("camp", "confirm"), complete("ruin", "close", "Open", "c"));
assertEquals(List.of("#2"), complete("ruin", "close", "2"));
assertEquals(List.of("confirm"), complete("ruin", "delete", "confirm"));
assertEquals(List.of("#1"), complete("ruin", "delete", "1"));
assertTrue(complete("ruin", "delete", "z").isEmpty());
assertEquals(List.of("Old ruin"), complete("ruin", "delete", "Old", "r"));
assertEquals(List.of("ruin"), complete("ruin", "delete", "Old", "r"));
assertEquals(List.of("#1"), complete("ruin", "info", "1"));
assertTrue(complete("ruin", "info", "z").isEmpty());
assertEquals(List.of("Old ruin"), complete("ruin", "info", "Old", "r"));
assertEquals(List.of("ruin"), complete("ruin", "info", "Old", "r"));
assertEquals(java.util.Set.of("#1", "#2"), new java.util.HashSet<>(complete("ruin", "tp", "#")));
assertEquals(List.of("#2"), complete("ruin", "camps", "2"));
assertTrue(complete("ruin", "camps", "z").isEmpty());
Expand All @@ -537,6 +537,128 @@ public void consoleCommandsExplainWorldRequirementsAndCanGiveToNamedPlayers() {
assertEquals(SiteStatus.HIDDEN, hidden.getStatus());
}

@Test public void acceptedNameCompletionsReplaceOnlyTheCurrentToken() {
Site hidden = dossier(1, "Old ruined city");
hidden.setSurfaceY(40);
for (int x = 0; x < 16; x++) for (int z = 0; z < 16; z++)
staff.getWorld().getBlockAt(x, 40, z).setType(Material.STONE);
Site camp = dossier(2, "Open camp");
net.tfminecraft.archaeo.CharacterTestSupport.establish(camp, staff.getUniqueId(), 1, 0, 20, 4, 4);
for (String action : List.of("info", "tp", "delete", "set-interest high")) {
for (String query : List.of("Ol", "oLD r", "Old ", "Old ruined c", "Old ruined ")) {
String input = "ruin " + action + " " + query;
List<String> suggestions = complete(input.split(" ", -1)).stream()
.filter(value -> !value.equals("confirm")).toList();
assertEquals(1, suggestions.size());
String accepted = input.substring(0, input.lastIndexOf(' ') + 1) + suggestions.getFirst();
assertEquals("ruin " + action + " " + (query.startsWith("oLD") ? "oLD ruined city" : "Old ruined city"), accepted);
assertFalse(run(accepted).stream().anyMatch(message -> message.contains("No site named")));
}
}
assertEquals(List.of("camp", "confirm"), complete("ruin", "close", "Open", ""));
assertContains(run("ruin close Open " + complete("ruin", "close", "Open", "ca").getFirst()), "Closed Open camp");
assertEquals(SiteStatus.CLOSED, camp.getStatus());
assertTrue(complete("ruin", "set-interest", "high", "Open", "").isEmpty());
assertEquals(List.of("confirm"), complete("ruin", "close", "Old", ""));
}

@Test public void optionalToolSlotIncludesExecutablePlayerShorthand() {
assertEquals(List.of("Archaeologist"), complete("give", "tool", "aRcH"));
assertEquals(List.of("Archaeologist"), complete("give", "pick", "aRcH"));
assertTrue(complete("give", "tool", "").contains("IRON_PICKAXE"));
assertTrue(complete("give", "tool", "").contains("Archaeologist"));
assertTrue(complete("give", "tool", "absent").isEmpty());
assertContains(run("give tool " + complete("give", "tool", "Arch").getFirst()), "Gave");
}

@Test public void optionalToolSlotOmitsCollidingPlayerAliasesButExplicitRecipientsStillWork() {
PlayerMock recipient = player("bRuSh");
player("BrushUser");
assertEquals(List.of("bRuSh", "BrushUser"), complete("give", "tool", "br"));
plugin.getConfig().set("excavation.tools.light.items", List.of("BRUSH"));
plugin.saveConfig();
assertContains(run("reload"), "Reloaded Archaeo");

for (String kind : List.of("tool", "pick")) {
for (String prefix : List.of("", "b", "bRu", "bRuSh")) {
List<String> suggestions = complete("give", kind, prefix);
assertTrue(suggestions.toString(), suggestions.contains("BRUSH"));
assertTrue(suggestions.toString(), suggestions.contains("BrushUser"));
assertFalse(suggestions.toString(), suggestions.contains("bRuSh"));
assertTrue(complete("give", kind, "BRUSH", prefix).contains("bRuSh"));
}
staff.getInventory().clear();
recipient.getInventory().clear();
String explicitRecipient = complete("give", kind, "BRUSH", "bRuSh").stream()
.filter(name -> name.equals("bRuSh")).findFirst().orElseThrow();
assertContains(run("give " + kind + " BRUSH " + explicitRecipient), "to bRuSh");
assertTrue(recipient.getInventory().contains(Material.BRUSH));
assertTrue(staff.getInventory().isEmpty());

recipient.getInventory().clear();
assertContains(run("give " + kind + " " + complete("give", kind, "bRuSh").getFirst()), "to Archaeologist");
assertTrue(staff.getInventory().contains(Material.BRUSH));
assertTrue(recipient.getInventory().isEmpty());
}

plugin.getConfig().set("excavation.tools.light.items", List.of("AIR"));
plugin.saveConfig();
assertContains(run("reload"), "Reloaded Archaeo");
assertEquals(List.of("bRuSh", "BrushUser"), complete("give", "tool", "br"));
}

@Test public void optionalToolSlotKeepsCanonicalToolTokensThatAreAlsoPlayerNames() {
PlayerMock recipient = player("IRON_PICKAXE");
for (String kind : List.of("tool", "pick")) {
for (String prefix : List.of("", "iRoN_", "IRON_PICKAXE")) {
assertEquals(1L, complete("give", kind, prefix).stream()
.filter(token -> token.equals("IRON_PICKAXE")).count());
}
staff.getInventory().clear();
recipient.getInventory().clear();
String explicitRecipient = complete("give", kind, "IRON_PICKAXE", "iRoN_").getFirst();
assertContains(run("give " + kind + " IRON_PICKAXE " + explicitRecipient), "to IRON_PICKAXE");
assertTrue(recipient.getInventory().contains(Material.IRON_PICKAXE));
assertTrue(staff.getInventory().isEmpty());
}
}

@Test public void campDirectorCompletionIncludesOfflineDirectoryIdsAndSafeNames() {
var identity = mock(net.tfminecraft.tlibs.character.CharacterIdentity.class);
UUID multiword = UUID.randomUUID(), single = UUID.randomUUID(), duplicate = UUID.randomUUID();
var info = new net.tfminecraft.tlibs.character.CharacterInfo(
new net.tfminecraft.tlibs.character.CharacterRef(UUID.randomUUID(), multiword), "Old Owner",
net.tfminecraft.tlibs.character.CharacterStatus.ALIVE, null, java.util.Set.of(), 0, null);
var safe = new net.tfminecraft.tlibs.character.CharacterInfo(
new net.tfminecraft.tlibs.character.CharacterRef(UUID.randomUUID(), single), "Director",
net.tfminecraft.tlibs.character.CharacterStatus.ALIVE, null, java.util.Set.of(), 0, null);
var repeated = new net.tfminecraft.tlibs.character.CharacterInfo(
new net.tfminecraft.tlibs.character.CharacterRef(UUID.randomUUID(), duplicate), "Duplicate",
net.tfminecraft.tlibs.character.CharacterStatus.ALIVE, null, java.util.Set.of(), 0, null);
when(identity.info(multiword)).thenReturn(java.util.Optional.of(info));
when(identity.info(single)).thenReturn(java.util.Optional.of(safe));
when(identity.info(duplicate)).thenReturn(java.util.Optional.of(repeated));
when(identity.findByName("Director")).thenReturn(List.of(safe));
when(identity.findByName("Duplicate")).thenReturn(List.of(repeated, safe));
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);
int serial = 1;
for (UUID id : List.of(multiword, single, duplicate, UUID.randomUUID())) {
Site camp = dossier(serial++, "Camp " + serial);
net.tfminecraft.archaeo.CharacterTestSupport.establish(camp, id, serial, 0, 20, 4, 4);
}
dossier(20, "Hidden");
var all = complete("ruin", "camps", "");
assertTrue(all.containsAll(List.of(multiword.toString(), single.toString(), duplicate.toString(), "Director")));
assertFalse(all.contains("Old Owner")); assertFalse(all.contains("Duplicate"));
assertFalse(all.contains("#20"));
assertEquals(List.of("Director"), complete("ruin", "camps", "dIr"));
assertEquals(List.of(multiword.toString()), complete("ruin", "camps", multiword.toString().substring(0, 8)));
assertContains(run("ruin camps " + multiword), "Camp");
}

@Test public void worldCompletionOffersUnloadedDossierWorldsAndSkipsDossiersWithoutAWorld() {
dossier(1, "Archived").setWorldName("archive");
dossier(2, "Worldless").setWorldName(null); // a dossier file without its world: key
Expand All @@ -549,7 +671,7 @@ public void consoleCommandsExplainWorldRequirementsAndCanGiveToNamedPlayers() {
for (String profile : List.of("hand", "light", "heavy", "super-heavy")) plugin.getConfig().set("excavation.tools." + profile + ".items", List.of("AIR"));
plugin.saveConfig();
assertContains(run("reload"), "Reloaded Archaeo");
assertTrue(complete("give", "tool", "").isEmpty());
assertEquals(List.of("Archaeologist"), complete("give", "tool", ""));
assertContains(run("give tool Missing"), "Player not online: Missing");
staff.getInventory().clear();
assertContains(run("give tool"), "an excavation tool (stone pickaxe)");
Expand Down
Loading