diff --git a/src/main/java/net/tfminecraft/simplefactions/managers/CommandManager.java b/src/main/java/net/tfminecraft/simplefactions/managers/CommandManager.java index da157527..a8f90ac9 100644 --- a/src/main/java/net/tfminecraft/simplefactions/managers/CommandManager.java +++ b/src/main/java/net/tfminecraft/simplefactions/managers/CommandManager.java @@ -1125,6 +1125,10 @@ private boolean dispatchCommand(CommandSender sender, Command cmd, String label, return true; } String s = args[1].replace("_", " "); + if(s.equalsIgnoreCase("")) { + p.sendMessage("§cEnter a culture name instead of ."); + return true; + } f.setCulture(s); p.sendMessage("§aFaction culture changed to "+f.getCulture()); return true; @@ -1135,6 +1139,10 @@ private boolean dispatchCommand(CommandSender sender, Command cmd, String label, return true; } String s = args[1].replace("_", " "); + if(s.equalsIgnoreCase("")) { + p.sendMessage("§cEnter a religion name instead of ."); + return true; + } f.setReligion(s); p.sendMessage("§aFaction religion changed to "+f.getReligion()); return true; diff --git a/src/main/java/net/tfminecraft/simplefactions/managers/MercenaryCommandManager.java b/src/main/java/net/tfminecraft/simplefactions/managers/MercenaryCommandManager.java index 912097eb..3b367719 100644 --- a/src/main/java/net/tfminecraft/simplefactions/managers/MercenaryCommandManager.java +++ b/src/main/java/net/tfminecraft/simplefactions/managers/MercenaryCommandManager.java @@ -258,7 +258,8 @@ static Faction resolveFaction(String nameOrId) { Faction byId = FactionManager.getByString(nameOrId); if (byId != null) return byId; for (Faction f : FactionManager.factions) { - if (f.getName() != null && f.getName().equalsIgnoreCase(nameOrId)) return f; + if (f.getName() != null && (f.getName().equalsIgnoreCase(nameOrId) + || org.bukkit.ChatColor.stripColor(f.getName()).equalsIgnoreCase(nameOrId))) return f; } return null; } diff --git a/src/main/java/net/tfminecraft/simplefactions/migration/Quarantine.java b/src/main/java/net/tfminecraft/simplefactions/migration/Quarantine.java index 94c9353f..e783aa13 100644 --- a/src/main/java/net/tfminecraft/simplefactions/migration/Quarantine.java +++ b/src/main/java/net/tfminecraft/simplefactions/migration/Quarantine.java @@ -74,12 +74,14 @@ private Quarantine(Path file) { public static Quarantine load(Path file) throws IOException { Quarantine quarantine = new Quarantine(file); + QuarantineCommands.snapshot(file, List.of()); if (!Files.exists(file)) return quarantine; try { List loaded = GSON.fromJson(Files.readString(file, StandardCharsets.UTF_8), new TypeToken>() {}.getType()); if (loaded == null) throw new JsonParseException("empty quarantine"); for (Record record : loaded) quarantine.records.put(record.id, record); + QuarantineCommands.snapshot(file, quarantine.records()); return quarantine; } catch (JsonParseException malformed) { throw new IOException("Unreadable quarantine " + file, malformed); @@ -88,6 +90,7 @@ public static Quarantine load(Path file) throws IOException { public void save() throws IOException { MigrationFiles.write(file, GSON.toJson(new ArrayList<>(records.values()))); + QuarantineCommands.snapshot(file, records()); } public Collection records() { diff --git a/src/main/java/net/tfminecraft/simplefactions/migration/QuarantineCommands.java b/src/main/java/net/tfminecraft/simplefactions/migration/QuarantineCommands.java index fdf9d308..1c520445 100644 --- a/src/main/java/net/tfminecraft/simplefactions/migration/QuarantineCommands.java +++ b/src/main/java/net/tfminecraft/simplefactions/migration/QuarantineCommands.java @@ -2,6 +2,8 @@ import java.io.IOException; import java.nio.file.Path; +import java.util.Collection; +import java.util.List; import java.util.logging.Level; import org.bukkit.command.CommandSender; @@ -14,8 +16,35 @@ /** {@code /f admin quarantine list} and {@code /f admin quarantine assign }. */ public final class QuarantineCommands { + private record Snapshot(Path file, List unresolved) {} + private static volatile Snapshot snapshot = new Snapshot(null, List.of()); + private QuarantineCommands() {} + /** Refreshed by migration and staff reads/writes, never by a tab request. */ + static void snapshot(Path file, Collection records) { + snapshot = new Snapshot(file.toAbsolutePath().normalize(), records.stream() + .filter(record -> !record.applied && record.assigned == null) + .map(record -> record.id).toList()); + } + + public static List complete(CommandSender sender, String[] args) { + if (!Permissions.isAdmin(sender) || args.length < 2 || !args[0].equalsIgnoreCase("admin")) return List.of(); + List candidates = List.of(); + if (args.length == 2) candidates = List.of("quarantine"); + else if (args[1].equalsIgnoreCase("quarantine")) { + if (args.length == 3) candidates = List.of("list", "assign"); + else if (args.length == 4 && args[2].equalsIgnoreCase("assign")) { + SimpleFactions plugin = SimpleFactions.getInstance(); + Snapshot current = snapshot; + if (plugin != null && plugin.getDataFolder().toPath().resolve(CharacterMigration.QUARANTINE) + .toAbsolutePath().normalize().equals(current.file())) candidates = current.unresolved(); + } + } + String prefix = args[args.length - 1]; + return candidates.stream().filter(value -> value.regionMatches(true, 0, prefix, 0, prefix.length())).toList(); + } + public static boolean matches(String[] args) { return args.length >= 2 && args[0].equalsIgnoreCase("admin") && args[1].equalsIgnoreCase("quarantine"); } diff --git a/src/main/java/net/tfminecraft/simplefactions/tiers/admin/TitleAdminCommand.java b/src/main/java/net/tfminecraft/simplefactions/tiers/admin/TitleAdminCommand.java index f28102f7..5319c3af 100644 --- a/src/main/java/net/tfminecraft/simplefactions/tiers/admin/TitleAdminCommand.java +++ b/src/main/java/net/tfminecraft/simplefactions/tiers/admin/TitleAdminCommand.java @@ -224,7 +224,7 @@ public static List complete(String[] args) { if (!TitleLoader.getByTier(tier).isEmpty()) completions.add(tier.getId()); } } else if (sub.equals("where")) { - completions.add(""); + return List.of(); } else if (SUBCOMMANDS.contains(sub) || sub.equals("setcolor")) { for (Title title : TitleLoader.getTitles()) { if (sub.equals("addprovince") && title.isComposite()) continue; @@ -237,13 +237,9 @@ public static List complete(String[] args) { Title title = TitleLoader.getById(args[2]); String last = args[args.length - 1]; switch (sub) { - case "rename" -> { - if (args.length == 4) completions.add(""); - } case "setcolour", "setcolor" -> { - if (args.length == 4) completions.add(title != null && title.getRgb() != null ? title.getRgb() : "R,G,B"); + if (args.length == 4 && title != null && title.getRgb() != null) completions.add(title.getRgb()); } - case "addprovince" -> completions.add(""); case "removeprovince" -> { if (title != null) { for (int province : title.getProvinces()) completions.add(String.valueOf(province)); diff --git a/src/main/java/net/tfminecraft/simplefactions/utils/TabCompletion.java b/src/main/java/net/tfminecraft/simplefactions/utils/TabCompletion.java index 14dc001f..419173fd 100644 --- a/src/main/java/net/tfminecraft/simplefactions/utils/TabCompletion.java +++ b/src/main/java/net/tfminecraft/simplefactions/utils/TabCompletion.java @@ -1,6 +1,7 @@ package net.tfminecraft.simplefactions.utils; import net.tfminecraft.simplefactions.identity.Members; +import net.tfminecraft.simplefactions.migration.QuarantineCommands; import net.tfminecraft.simplefactions.espionage.EspionageCommands; @@ -53,8 +54,9 @@ private List completeCompany(Player p, String[] args) { completions.add("expand"); completions.add("draft"); completions.add("offer"); - completions.add("contracts"); } + Guild memberGuild = guild != null ? guild : FactionManager.getGuildByMember(Members.key(p)); + if(memberGuild != null && memberGuild.hasCompany()) completions.add("contracts"); if(RequestManager.getRequest(p) instanceof MercenaryInviteRequest) { completions.add("accept"); completions.add("decline"); @@ -69,10 +71,11 @@ private List completeCompany(Player p, String[] args) { completions.add("take"); return filtered(completions, args[1]); } - if(args.length == 3 && args[0].equalsIgnoreCase("admin") && Permissions.isAdmin(p)) { + if(args.length == 3 && args[0].equalsIgnoreCase("admin") && Permissions.isAdmin(p) + && (args[1].equalsIgnoreCase("give") || args[1].equalsIgnoreCase("take"))) { for(Guild g : FactionManager.getAllGuilds()) { if(g == null || g.getCompany() == null) continue; - if(g.getCompany().getName() != null) { + if(g.getCompany().getName() != null && !g.getCompany().getName().contains(" ")) { completions.add(g.getCompany().getName()); } completions.add(g.getId()); @@ -80,7 +83,7 @@ private List completeCompany(Player p, String[] args) { return filtered(completions, args[2]); } if(args.length == 4 && args[0].equalsIgnoreCase("admin") && Permissions.isAdmin(p)) { - completions.add("1"); + return completions; } if(args.length >= 2 && args[0].equalsIgnoreCase("kick")) { @@ -89,17 +92,23 @@ private List completeCompany(Player p, String[] args) { } return Members.completions(completions, args); } - if(args.length == 2 && args[0].equalsIgnoreCase("invite")) { - for(Player online : Bukkit.getOnlinePlayers()) { - completions.add(online.getName()); + if(args.length >= 2 && args[0].equalsIgnoreCase("invite")) { + if(guild != null && guild.hasCompany()) { + for(Player online : Bukkit.getOnlinePlayers()) { + if(Members.key(online) != null && net.tfminecraft.simplefactions.mercenary.company.MercenaryCompanyService + .canInvite(guild, Members.key(p), Members.key(online)).ok()) { + completions.add(online.getName()); + completions.add(Members.name(online)); + } + } } - return filtered(completions, args[1]); + return Members.completions(completions, args); } - if(args.length == 2 && args[0].equalsIgnoreCase("offer")) { + if(args.length >= 2 && args[0].equalsIgnoreCase("offer") && guild != null && guild.hasCompany()) { for(net.tfminecraft.simplefactions.objects.Faction f : FactionManager.factions) { if(f.getName() != null) completions.add(f.getName()); } - return filtered(completions, args[1]); + return Members.completions(completions, args); } return completions; } @@ -111,12 +120,12 @@ private List completeMercenaries(String[] args) { completions.add("hire"); return filtered(completions, args.length == 1 ? args[0] : ""); } - if(args.length == 2 && args[0].equalsIgnoreCase("hire")) { + if(args.length >= 2 && args[0].equalsIgnoreCase("hire")) { for(net.tfminecraft.simplefactions.mercenary.company.MercenaryCompany company : net.tfminecraft.simplefactions.mercenary.contract.MercenaryMarket.listing()) { if(company.getName() != null) completions.add(company.getName()); } - return filtered(completions, args[1]); + return Members.completions(completions, args); } return completions; } @@ -127,9 +136,9 @@ private static String[] afterFirst(String[] args) { } private List filtered(List completions, String prefix) { - String lower = prefix == null ? "" : prefix.toLowerCase(); - completions.removeIf(s -> !s.toLowerCase().startsWith(lower)); - return completions; + String lower = prefix == null ? "" : prefix.toLowerCase(java.util.Locale.ROOT); + return completions.stream().filter(java.util.Objects::nonNull) + .filter(s -> s.toLowerCase(java.util.Locale.ROOT).startsWith(lower)).distinct().toList(); } private List completeConstructKinds(String[] args) { @@ -171,12 +180,40 @@ private List completeInstallationIds(Player p, String prefix, boolean in return net.tfminecraft.simplefactions.vehicles.VehicleFactionCommands.VehicleTabCompletions.filter(completions, prefix); } + private List leaderlessCandidates(Guild guild) { + List names = new ArrayList<>(); + for(String member : guild.getFaction().getMembers()) { + if(Members.info(member).map(net.tfminecraft.tlibs.character.CharacterInfo::alive).orElse(false) + && FactionManager.getGuildByLeader(member) == null) names.add(Members.display(member)); + } + return names; + } + @Override public List onTabComplete (CommandSender sender, Command cmd, String label, String[] args){ - if (cmd.getName().equalsIgnoreCase("faction") && args.length == 1 - && sender.hasPermission(net.tfminecraft.simplefactions.espionage.EspionageConfig.reloadPermission()) - && "reloadespionage".regionMatches(true, 0, args[0], 0, args[0].length()) && !args[0].isEmpty()) - return List.of("reloadespionage"); + return filtered(complete(sender, cmd, args), args.length == 0 ? "" : args[args.length - 1]); + } + + private List complete(CommandSender sender, Command cmd, String[] args) { + if(cmd.getName().equalsIgnoreCase("faction") && args.length >= 2 && args[0].equalsIgnoreCase("admin")) { + return QuarantineCommands.complete(sender, args); + } + if(cmd.getName().equalsIgnoreCase("faction") && args.length < 2 && !(sender instanceof Player)) { + List completions = new ArrayList<>(); + if(sender.hasPermission(net.tfminecraft.simplefactions.espionage.EspionageConfig.reloadPermission())) + completions.add("reloadespionage"); + if(Permissions.isAdmin(sender)) { + completions.addAll(List.of("admin", "addguildbank")); + if(Cache.provincesEnabled) completions.addAll(List.of(TitleAdminCommand.SUBCOMMAND, "tradegraph")); + } + return completions; + } + if(cmd.getName().equalsIgnoreCase("faction") && sender instanceof Player + && Permissions.isAdmin(sender) && args.length == 2 + && (args[0].equalsIgnoreCase("delbank") || args[0].equalsIgnoreCase("startelection") + || args[0].equalsIgnoreCase("endelection") || (Cache.provincesEnabled && args[0].equalsIgnoreCase("unclaim")))) { + return FactionManager.factions.stream().map(Faction::getId).toList(); + } if (cmd.getName().equalsIgnoreCase("faction") && args.length >= 2 && EspionageCommands.matches(args[0])) { return sender instanceof Player player @@ -210,6 +247,11 @@ else if(cmd.getName().equalsIgnoreCase("guild") && args.length >= 0 && args.leng Player p = (Player) sender; List completions = new ArrayList<>(); completions.add("create"); + completions.add("list"); + Guild ownGuild = FactionManager.getGuildByMember(Members.key(p)); + Guild ledGuild = FactionManager.getGuildByLeader(Members.key(p)); + if(ownGuild != null && ledGuild == null) completions.add("leave"); + if(Permissions.isAdmin(sender) || (ledGuild != null && !ledGuild.isBase())) completions.add("delete"); completions.add("join"); completions.add("decline"); completions.add("menu"); @@ -238,8 +280,7 @@ else if(cmd.getName().equalsIgnoreCase("guild") && args.length >= 0 && args.leng } } else if(cmd.getName().equalsIgnoreCase("faction") - && args.length >= 1 - && args.length <= 2 + && args.length == 2 && args[0].equalsIgnoreCase("construct")) { if(!Cache.provincesEnabled) { return new ArrayList<>(); @@ -253,8 +294,7 @@ else if(cmd.getName().equalsIgnoreCase("faction") } } else if (cmd.getName().equalsIgnoreCase("faction") - && args.length >= 1 - && args.length <= 2 + && args.length == 2 && args[0].equalsIgnoreCase("upgrade")) { if (!Cache.provincesEnabled || !(sender instanceof Player p) @@ -264,8 +304,7 @@ else if (cmd.getName().equalsIgnoreCase("faction") return completeInstallationIds(p, args.length >= 2 ? args[1] : "", false); } else if(cmd.getName().equalsIgnoreCase("faction") - && args.length >= 1 - && args.length <= 2 + && args.length == 2 && args[0].equalsIgnoreCase("deconstruct")) { if(!Cache.provincesEnabled) { return new ArrayList<>(); @@ -276,7 +315,7 @@ else if(cmd.getName().equalsIgnoreCase("faction") } } else if(cmd.getName().equalsIgnoreCase("faction") - && args.length >= 1 + && args.length >= 2 && args[0].equalsIgnoreCase("vehicle")) { if(sender instanceof Player) { Player p = (Player) sender; @@ -284,12 +323,15 @@ else if(cmd.getName().equalsIgnoreCase("faction") return net.tfminecraft.simplefactions.vehicles.VehicleFactionCommands.VehicleTabCompletions.subcommands( args.length >= 2 ? args[1] : ""); } - if(args.length == 3 && (args[1].equalsIgnoreCase("give") || args[1].equalsIgnoreCase("handover"))) { + if(args.length >= 3 && (args[1].equalsIgnoreCase("give") || args[1].equalsIgnoreCase("handover"))) { List names = new ArrayList<>(); + if(args[1].equalsIgnoreCase("give") && FactionManager.getByLeader(Members.key(p)) == null) return names; for(Player online : Bukkit.getOnlinePlayers()) { + if(online.getUniqueId().equals(p.getUniqueId())) continue; names.add(online.getName()); + names.add(Members.name(online)); } - return VehicleTabCompletions.filter(names, args[2]); + return Members.completions(names, afterFirst(args)); } if(args.length == 3 && args[1].equalsIgnoreCase("transfer")) { List ids = completeInstallationIds(p, args[2], false); @@ -308,8 +350,7 @@ else if(cmd.getName().equalsIgnoreCase("faction") } } else if(cmd.getName().equalsIgnoreCase("faction") - && args.length >= 1 - && args.length <= 2 + && args.length == 2 && args[0].equalsIgnoreCase("transfervehicle")) { if(sender instanceof Player) { Player p = (Player) sender; @@ -317,8 +358,7 @@ else if(cmd.getName().equalsIgnoreCase("faction") } } else if(cmd.getName().equalsIgnoreCase("faction") - && args.length >= 1 - && args.length <= 2 + && args.length == 2 && args[0].equalsIgnoreCase("findvehicles")) { if(sender instanceof Player) { Player p = (Player) sender; @@ -330,6 +370,11 @@ else if(cmd.getName().equalsIgnoreCase("faction") && args.length >= 0 && args.le Player p = (Player) sender; List completions = new ArrayList<>(); completions.add("list"); + Faction ownFaction = FactionManager.getByMember(Members.key(p)); + if(ownFaction != null) { + if(ownFaction.getBank() != null) completions.add("deposit"); + if(FactionManager.getByLeader(Members.key(p)) == null && !ownFaction.isInGuild(Members.key(p))) completions.add("leave"); + } if(FactionManager.getByMember(Members.key(p)) != null) completions.add("menu"); if(FactionManager.getByMember(Members.key(p)) != null) completions.addAll(List.of("positions", "spymaster", "espionage")); if (p.hasPermission(net.tfminecraft.simplefactions.espionage.EspionageConfig.reloadPermission())) completions.add("reloadespionage"); @@ -365,6 +410,7 @@ else if(cmd.getName().equalsIgnoreCase("faction") && args.length >= 0 && args.le completions.add("setcolour"); } if(Permissions.isAdmin(sender)) { + completions.addAll(List.of("admin", "delete", "forcedelete", "refresh", "delbank")); completions.add("addguildbank"); completions.add("forceleader"); completions.add("forcejoin"); @@ -374,6 +420,7 @@ else if(cmd.getName().equalsIgnoreCase("faction") && args.length >= 0 && args.le //completions.add("addwealthmodifier"); completions.add("getglobalwealth"); if(Cache.provincesEnabled) { + completions.add("provincecap"); completions.add("forceconstruct"); completions.add("forceupgrade"); completions.add("queueallnations"); @@ -399,10 +446,7 @@ else if(cmd.getName().equalsIgnoreCase("faction") && args.length >= 0 && args.le } } else if(eitherCommand(cmd) && args.length == 2 && args[0].equalsIgnoreCase("create")){ if(sender instanceof Player){ - List completions = new ArrayList(); - completions.add(""); - - return completions; + return List.of(); } } else if(eitherCommand(cmd) && args.length == 2 && args[0].equalsIgnoreCase("setcapital")) { if(!Cache.provincesEnabled) { @@ -431,9 +475,11 @@ else if(cmd.getName().equalsIgnoreCase("faction") && args.length >= 0 && args.le } else if(cmd.getName().equalsIgnoreCase("faction") && args.length == 2 && args[0].equalsIgnoreCase("delete")){ if(sender instanceof Player){ Player p = (Player) sender; - Faction f = FactionManager.getByMember(Members.key(p)); + Faction f = FactionManager.getByLeader(Members.key(p)); List completions = new ArrayList(); - if(f != null) { + if(Permissions.isAdmin(sender)) { + for(Faction target : FactionManager.factions) completions.add(target.getId()); + } else if(f != null) { completions.add(f.getId()); } return completions; @@ -441,7 +487,7 @@ else if(cmd.getName().equalsIgnoreCase("faction") && args.length >= 0 && args.le } else if(cmd.getName().equalsIgnoreCase("guild") && args.length == 2 && args[0].equalsIgnoreCase("delete")){ if(sender instanceof Player){ Player p = (Player) sender; - Guild g = FactionManager.getGuildByMember(Members.key(p)); + Guild g = FactionManager.getGuildByLeader(Members.key(p)); List completions = new ArrayList(); if(g != null && !g.isBase()) { completions.add(g.getId()); @@ -481,11 +527,11 @@ else if(cmd.getName().equalsIgnoreCase("faction") && args.length >= 0 && args.le } else if(cmd.getName().equalsIgnoreCase("faction") && args.length >= 2 && args[0].equalsIgnoreCase("kick")){ if(sender instanceof Player){ Player p = (Player) sender; - Faction f = FactionManager.getByMember(Members.key(p)); + Faction f = FactionManager.getByLeader(Members.key(p)); List completions = new ArrayList(); if(f != null) { for(String member : f.getMembers()) { - if(!member.equals(f.getLeader())) completions.add(Members.display(member)); + if(!member.equals(f.getLeader()) && !f.isInGuild(member)) completions.add(Members.display(member)); } } return Members.completions(completions, args); @@ -502,64 +548,57 @@ else if(cmd.getName().equalsIgnoreCase("faction") && args.length >= 0 && args.le } return Members.completions(completions, args); } + } else if(cmd.getName().equalsIgnoreCase("guild") && args.length >= 2 && args[0].equalsIgnoreCase("setleader")) { + if(sender instanceof Player p) { + Guild g = FactionManager.getGuildByLeader(Members.key(p)); + List names = new ArrayList<>(); + if(g != null) for(String member : g.getMembers()) { + if(!g.isLeader(member)) names.add(Members.display(member)); + } + return Members.completions(names, args); + } + } else if(cmd.getName().equalsIgnoreCase("guild") && args.length >= 3 && args[0].equalsIgnoreCase("appoint")) { + if(sender instanceof Player p) { + Guild g = FactionManager.getGuildByString(args[1]); + if(g != null && LeaderlessGuilds.appointable(Members.key(p)).contains(g)) { + return Members.completions(leaderlessCandidates(g), afterFirst(args)); + } + } } else if(cmd.getName().equalsIgnoreCase("faction") && args.length == 2 && args[0].equalsIgnoreCase("rename")){ if(sender instanceof Player){ - List completions = new ArrayList(); - completions.add(""); - - return completions; + return List.of(); } } else if(cmd.getName().equalsIgnoreCase("faction") && args.length == 2 && args[0].equalsIgnoreCase("setculture")){ if(sender instanceof Player){ - List completions = new ArrayList(); - completions.add(""); - - return completions; + return List.of(); } } else if(cmd.getName().equalsIgnoreCase("faction") && args.length == 2 && args[0].equalsIgnoreCase("setreligion")){ if(sender instanceof Player){ - List completions = new ArrayList(); - completions.add(""); - - return completions; + return List.of(); } } else if(cmd.getName().equalsIgnoreCase("faction") && args.length == 2 && args[0].equalsIgnoreCase("setrulingsystem")){ if(sender instanceof Player){ - List completions = new ArrayList(); - completions.add(""); - - return completions; + return List.of(); } } else if(cmd.getName().equalsIgnoreCase("faction") && args.length == 2 && args[0].equalsIgnoreCase("setrulertitle")){ if(sender instanceof Player){ - List completions = new ArrayList(); - completions.add(""); - - return completions; + return List.of(); } } else if(eitherCommand(cmd) && args.length == 2 && args[0].equalsIgnoreCase("setbanner")){ if(sender instanceof Player){ - List completions = new ArrayList(); - - return completions; + return List.of(); } } else if(cmd.getName().equalsIgnoreCase("faction") && args.length == 2 && args[0].equalsIgnoreCase("claim")){ if(sender instanceof Player){ - List completions = new ArrayList(); - - return completions; + return List.of(); } } else if(cmd.getName().equalsIgnoreCase("faction") && args.length >= 3 && args[0].equalsIgnoreCase("construct")){ if(sender instanceof Player){ - List completions = new ArrayList<>(); - completions.add(""); - return completions; + return List.of(); } } else if(cmd.getName().equalsIgnoreCase("faction") && args.length == 2 && args[0].equalsIgnoreCase("unclaim")){ if(sender instanceof Player){ - List completions = new ArrayList(); - - return completions; + return List.of(); } } else if(cmd.getName().equalsIgnoreCase("guild") && args.length == 2 && args[0].equalsIgnoreCase("appoint")){ if(sender instanceof Player p){ @@ -576,20 +615,21 @@ else if(cmd.getName().equalsIgnoreCase("faction") && args.length >= 0 && args.le } else if(eitherCommand(cmd) && args.length == 2 && (args[0].equalsIgnoreCase("join") || args[0].equalsIgnoreCase("decline"))){ if(sender instanceof Player p){ - return InviteCommands.inviteIds(cmd.getName(), Members.key(p), args[1]); + List ids = new ArrayList<>(InviteCommands.inviteIds(cmd.getName(), Members.key(p), args[1])); + if(cmd.getName().equalsIgnoreCase("guild") && args[0].equalsIgnoreCase("decline")) { + Guild offered = LeaderlessGuilds.offeredTo(Members.key(p)); + if(offered != null) ids.add(offered.getId()); + } + return ids; } } else if(eitherCommand(cmd) && args.length == 2 && args[0].equalsIgnoreCase("accept")){ if(sender instanceof Player){ - List completions = new ArrayList(); - - return completions; + return List.of(); } } else if((cmd.getName().equalsIgnoreCase("faction") || cmd.getName().equalsIgnoreCase("guild")) && args.length == 2 && args[0].equalsIgnoreCase("setcolour")){ if(sender instanceof Player){ - List completions = new ArrayList(); - completions.add("R,G,B"); - return completions; + return List.of(); } } if(Permissions.isAdmin(sender)) { @@ -613,7 +653,8 @@ else if(cmd.getName().equalsIgnoreCase("faction") && args.length >= 0 && args.le } else if(cmd.getName().equalsIgnoreCase("guild") && args.length >= 3 && args[0].equalsIgnoreCase("forceleader")){ List completions = new ArrayList(); Guild g = FactionManager.getGuildByString(args[1]); - if(g != null) { + if(g != null && !g.isBase()) { + if(g.isLeaderless()) return Members.completions(leaderlessCandidates(g), afterFirst(args)); for(String member : g.getMembers()) { if(!g.isLeader(member)) completions.add(Members.display(member)); } @@ -672,9 +713,7 @@ else if(cmd.getName().equalsIgnoreCase("faction") && args.length >= 0 && args.le && Cache.provincesEnabled && args.length >= 4 && args[0].equalsIgnoreCase("forceconstruct")) { - List completions = new ArrayList<>(); - completions.add(""); - return completions; + return List.of(); } else if(cmd.getName().equalsIgnoreCase("faction") && args.length == 2 && args[0].equalsIgnoreCase("forceregiment")) { @@ -697,7 +736,7 @@ else if(cmd.getName().equalsIgnoreCase("faction") && args.length >= 0 && args.le Faction f = FactionManager.getByString(args[1]); if(f != null) { for(net.tfminecraft.simplefactions.army.Regiment r : f.getMilitary().getRegiments()) { - if(!r.isLevy()) { + if(!r.isLevy() && !r.isMercenary()) { completions.add(r.getId()); } } @@ -706,9 +745,7 @@ else if(cmd.getName().equalsIgnoreCase("faction") && args.length >= 0 && args.le } else if(cmd.getName().equalsIgnoreCase("faction") && args.length == 5 && args[0].equalsIgnoreCase("forceregiment")) { - List completions = new ArrayList<>(); - completions.add("1"); - return completions; + return List.of(); } else if (cmd.getName().equalsIgnoreCase("faction") && args.length == 2 && args[0].equalsIgnoreCase("forcejoin")) { List completions = new ArrayList(); // Suggest faction names @@ -746,9 +783,7 @@ else if (cmd.getName().equalsIgnoreCase("faction") && args.length == 3 && args[0 } } else if(cmd.getName().equalsIgnoreCase("faction") && args.length == 3 && args[0].equalsIgnoreCase("forcewithdraw")){ if(sender instanceof Player){ - List completions = new ArrayList(); - completions.add("1.0"); - return completions; + return List.of(); } } else if(cmd.getName().equalsIgnoreCase("faction") && args.length == 2 && args[0].equalsIgnoreCase("addprestigemodifier")){ if(sender instanceof Player){ @@ -760,15 +795,11 @@ else if (cmd.getName().equalsIgnoreCase("faction") && args.length == 3 && args[0 } } else if(cmd.getName().equalsIgnoreCase("faction") && args.length == 3 && args[0].equalsIgnoreCase("addprestigemodifier")){ if(sender instanceof Player){ - List completions = new ArrayList(); - completions.add(""); - return completions; + return List.of(); } } else if(cmd.getName().equalsIgnoreCase("faction") && args.length == 4 && args[0].equalsIgnoreCase("addprestigemodifier")){ if(sender instanceof Player){ - List completions = new ArrayList(); - completions.add("1.0"); - return completions; + return List.of(); } } else if(cmd.getName().equalsIgnoreCase("faction") && args.length == 2 && args[0].equalsIgnoreCase("forcedelete")){ if(sender instanceof Player){ @@ -950,6 +981,6 @@ else if (cmd.getName().equalsIgnoreCase("faction") && args.length == 3 && args[0 } } } - return null; + return List.of(); } } diff --git a/src/main/java/net/tfminecraft/simplefactions/war/battle/campaign/warband/CampaignWarbandSignupService.java b/src/main/java/net/tfminecraft/simplefactions/war/battle/campaign/warband/CampaignWarbandSignupService.java index a5edbd16..e1b015ca 100644 --- a/src/main/java/net/tfminecraft/simplefactions/war/battle/campaign/warband/CampaignWarbandSignupService.java +++ b/src/main/java/net/tfminecraft/simplefactions/war/battle/campaign/warband/CampaignWarbandSignupService.java @@ -70,6 +70,33 @@ public static String signupMember( Faction playerFaction, Player onlinePlayer, Instant now) { + String failure = signupFailure(playerId, memberKey, warband, playerFaction, onlinePlayer, now); + if(failure != null) return failure; + if(warband.hasMember(playerId)) return null; + var character = Warband.participant(playerId, memberKey, onlinePlayer).orElseThrow(); + CampaignBattleContext ctx = CampaignBattleJoinService.findCampaignBattleForWarband(warband); + CampaignWarbandSideLock.Tie tie = CampaignWarbandSideLock.tieOf(warband); + boolean firstSignup = warband.getRealMemberCount() == 0; + applyLeaderRules(ctx, warband, playerId, memberKey); + warband.addMember(playerId, character); + if (tie != null && warband.getMemberCharacter(playerId).isPresent()) { + CampaignWarbandSideLock.bind(tie.war(), tie.coalition(), playerId); + } + + if (ctx != null && firstSignup) { + WarDevMode.seedDummyMembersOnFirstSignupIfEnabled( + warband, ctx.war(), ctx.battle(), ctx.sideId()); + } + if (ctx != null && ctx.battle().hasStarted() && onlinePlayer != null) { + CampaignWarbandBattleService.onMemberJoined(onlinePlayer, warband, ctx); + } + BattlePersistenceService.persistWarband(warband); + return null; + } + + /** The same eligibility checks as signup, without changing the roster or binding a side. */ + public static String signupFailure(UUID playerId, String memberKey, Warband warband, + Faction playerFaction, Player onlinePlayer, Instant now) { if (playerId == null || memberKey == null || warband == null) { return "Invalid warband signup"; } @@ -110,26 +137,8 @@ public static String signupMember( return sideError; } - var character = Warband.participant(playerId, memberKey, onlinePlayer).orElse(null); - if (character == null) { - return "Character identity is not ready for warband signup"; - } - boolean firstSignup = warband.getRealMemberCount() == 0; - applyLeaderRules(ctx, warband, playerId, memberKey); - warband.addMember(playerId, character); - if (tie != null && warband.getMemberCharacter(playerId).isPresent()) { - CampaignWarbandSideLock.bind(tie.war(), tie.coalition(), playerId); - } - - if (ctx != null && firstSignup) { - WarDevMode.seedDummyMembersOnFirstSignupIfEnabled( - warband, ctx.war(), ctx.battle(), ctx.sideId()); - } - if (ctx != null && ctx.battle().hasStarted() && onlinePlayer != null) { - CampaignWarbandBattleService.onMemberJoined(onlinePlayer, warband, ctx); - } - BattlePersistenceService.persistWarband(warband); - return null; + return Warband.participant(playerId, memberKey, onlinePlayer).isPresent() + ? null : "Character identity is not ready for warband signup"; } private static void applyLeaderRules( diff --git a/src/main/java/net/tfminecraft/simplefactions/war/battle/ui/BattleTabCompletion.java b/src/main/java/net/tfminecraft/simplefactions/war/battle/ui/BattleTabCompletion.java index 13493630..d2e177a7 100644 --- a/src/main/java/net/tfminecraft/simplefactions/war/battle/ui/BattleTabCompletion.java +++ b/src/main/java/net/tfminecraft/simplefactions/war/battle/ui/BattleTabCompletion.java @@ -9,6 +9,10 @@ import org.bukkit.command.TabCompleter; import org.bukkit.entity.Player; +import net.tfminecraft.simplefactions.identity.Members; +import net.tfminecraft.simplefactions.managers.FactionManager; +import net.tfminecraft.simplefactions.war.battle.campaign.warband.CampaignWarbandSignupService; +import net.tfminecraft.simplefactions.war.campaign.runtime.CampaignClock; import net.tfminecraft.simplefactions.war.battle.engine.core.BattleManager; import net.tfminecraft.simplefactions.war.battle.warband.WarbandManager; import net.tfminecraft.simplefactions.war.battle.engine.core.Battle; @@ -18,12 +22,9 @@ public class BattleTabCompletion implements TabCompleter{ - private boolean isKnownSubcommand(String[] args, String... names) { - if (args.length == 0) { - return false; - } + private boolean isKnownSubcommand(String action, String... names) { for (String name : names) { - if (args[0].equalsIgnoreCase(name)) { + if (action.equalsIgnoreCase(name)) { return true; } } @@ -32,11 +33,19 @@ private boolean isKnownSubcommand(String[] args, String... names) { @Override public List onTabComplete (CommandSender sender, Command cmd, String label, String[] args){ - if(cmd.getName().equalsIgnoreCase("warband") && args.length < 2 && - !isKnownSubcommand(args, "create", "delete", "invite", "kick", "setleader", "list", "toggleopen", "leave", "retreat")){ + String prefix = args.length == 0 ? "" : args[args.length - 1]; + return complete(sender, cmd, args).stream() + .filter(value -> value.regionMatches(true, 0, prefix, 0, prefix.length())).distinct().toList(); + } + + private List complete(CommandSender sender, Command cmd, String[] args) { + if (!(sender instanceof Player)) return List.of(); + if(cmd.getName().equalsIgnoreCase("warband") && args.length < 2) { if(sender instanceof Player){ List completions = new ArrayList<>(); + if(WarbandManager.getByMemberId(((Player) sender).getUniqueId()) == null && Members.key((Player) sender) != null) + completions.add("join"); completions.add("create"); completions.add("delete"); completions.add("invite"); @@ -50,21 +59,31 @@ public List onTabComplete (CommandSender sender, Command cmd, String lab } } else if(cmd.getName().equalsIgnoreCase("warband") && args.length == 2 && args[0].equalsIgnoreCase("create")){ if(sender instanceof Player){ - List completions = new ArrayList(); - completions.add(""); - - return completions; + return List.of(); } } else if(cmd.getName().equalsIgnoreCase("warband") && args.length == 2 && args[0].equalsIgnoreCase("delete")){ if(sender instanceof Player){ Player p = (Player) sender; Warband w = WarbandManager.getByLeader(p); List completions = new ArrayList(); - if(w != null) { + if(BattlePermissions.isAdmin(sender)) { + for(Warband target : WarbandManager.get()) completions.add(target.getId()); + } else if(w != null && !w.isFaction()) { completions.add(w.getId()); } return completions; } + } else if(cmd.getName().equalsIgnoreCase("warband") && args.length == 2 && args[0].equalsIgnoreCase("join")) { + Player player = (Player) sender; + if(WarbandManager.getByMemberId(player.getUniqueId()) != null || Members.key(player) == null) return List.of(); + var faction = FactionManager.getByMember(Members.key(player)); + List ids = new ArrayList<>(); + for(Warband band : WarbandManager.get()) { + if(band.isFaction() ? faction == null : band.isLocked() && !band.isInvited(player)) continue; + if(CampaignWarbandSignupService.signupFailure(player.getUniqueId(), Members.key(player), + band, faction, player, CampaignClock.now()) == null) ids.add(band.getId()); + } + return ids; } else if(cmd.getName().equalsIgnoreCase("warband") && args.length == 2 && args[0].equalsIgnoreCase("invite")){ if(sender instanceof Player){ List completions = new ArrayList(); @@ -100,10 +119,7 @@ public List onTabComplete (CommandSender sender, Command cmd, String lab return completions; } } - if(cmd.getName().equalsIgnoreCase("battle") && args.length < 2 && - !isKnownSubcommand(args, "create", "edit", "delete", "addside", "addpoint", "setlives", "setspawn", - "setjail", "setcontestmin", "setcontestmax", "setcontestduration", "setraidtarget", - "setdefenderlives", "list", "join")){ + if(cmd.getName().equalsIgnoreCase("battle") && args.length < 2) { if(sender instanceof Player){ List completions = new ArrayList<>(); completions.add("list"); @@ -134,7 +150,7 @@ public List onTabComplete (CommandSender sender, Command cmd, String lab } else if(cmd.getName().equalsIgnoreCase("battle") && args.length == 3 && args[0].equalsIgnoreCase("join")) { Battle b = BattleManager.getByString(args[1]); if (b == null) { - return null; + return List.of(); } List completions = new ArrayList<>(); for(BattleSide s : b.getSides()) { @@ -142,7 +158,7 @@ public List onTabComplete (CommandSender sender, Command cmd, String lab } return completions; } - if(BattlePermissions.isAdmin(sender)) { + if(BattlePermissions.isAdmin(sender)) { if(cmd.getName().equalsIgnoreCase("battle") && args.length == 2 && args[0].equalsIgnoreCase("create")) { List completions = new ArrayList<>(); completions.add("field"); @@ -150,11 +166,9 @@ public List onTabComplete (CommandSender sender, Command cmd, String lab completions.add("raid"); return completions; } else if(cmd.getName().equalsIgnoreCase("battle") && args.length == 3 && args[0].equalsIgnoreCase("create")) { - List completions = new ArrayList<>(); - completions.add(""); - return completions; + return List.of(); } else if(cmd.getName().equalsIgnoreCase("battle") && args.length == 2 && - isKnownSubcommand(args, "edit", "delete", "addside", "addpoint", "setlives", + isKnownSubcommand(args[0], "edit", "delete", "addside", "addpoint", "setlives", "setspawn", "setjail", "setcontestmin", "setcontestmax", "setcontestduration", "setraidtarget", "setdefenderlives")) { if(sender instanceof Player){ @@ -165,15 +179,10 @@ public List onTabComplete (CommandSender sender, Command cmd, String lab return completions; } } else if(cmd.getName().equalsIgnoreCase("battle") && args.length == 3 && args[0].equalsIgnoreCase("addside")) { - return List.of(""); + return List.of(); } else if(cmd.getName().equalsIgnoreCase("battle") && args.length == 3 && args[0].equalsIgnoreCase("setlives")){ if(sender instanceof Player){ - List completions = new ArrayList(); - completions.add("5"); - completions.add("10"); - completions.add("20"); - completions.add("50"); - return completions; + return List.of(); } } else if(cmd.getName().equalsIgnoreCase("battle") && args.length == 3 && args[0].equalsIgnoreCase("addpoint")){ if(sender instanceof Player){ @@ -190,7 +199,7 @@ public List onTabComplete (CommandSender sender, Command cmd, String lab (args[0].equalsIgnoreCase("setspawn") || args[0].equalsIgnoreCase("setjail"))) { Battle b = BattleManager.getByString(args[1]); if (b == null) { - return null; + return List.of(); } List completions = new ArrayList<>(); for(BattleSide s : b.getSides()) { @@ -198,23 +207,11 @@ public List onTabComplete (CommandSender sender, Command cmd, String lab } return completions; } else if(cmd.getName().equalsIgnoreCase("battle") && args.length == 3 && args[0].equalsIgnoreCase("setcontestduration")){ - List completions = new ArrayList<>(); - completions.add("60"); - completions.add("120"); - completions.add("180"); - completions.add("240"); - completions.add("300"); - return completions; + return List.of(); } else if(cmd.getName().equalsIgnoreCase("battle") && args.length == 3 && args[0].equalsIgnoreCase("setdefenderlives")){ - List completions = new ArrayList<>(); - completions.add("5"); - completions.add("10"); - completions.add("20"); - completions.add("25"); - completions.add("50"); - return completions; + return List.of(); } } - return null; + return List.of(); } } diff --git a/src/test/java/net/tfminecraft/simplefactions/managers/CommandManagerCoverageTest.java b/src/test/java/net/tfminecraft/simplefactions/managers/CommandManagerCoverageTest.java index 63a6c35b..84730336 100644 --- a/src/test/java/net/tfminecraft/simplefactions/managers/CommandManagerCoverageTest.java +++ b/src/test/java/net/tfminecraft/simplefactions/managers/CommandManagerCoverageTest.java @@ -516,6 +516,15 @@ void leaderIdentityCommandsConvertUnderscoresAndRefuseOutsiders(String action) { verify(player).sendMessage(contains("must be the leader")); } + @ParameterizedTest + @ValueSource(strings = {"culture", "religion"}) + void oldTabHintsCannotBeSavedAsFactionIdentity(String field) { + assertTrue(run("faction", "set" + field, "<" + field.toUpperCase() + ">")); + verify(faction, never()).setCulture(anyString()); + verify(faction, never()).setReligion(anyString()); + verify(player).sendMessage(contains("name instead")); + } + private void admin() { when(player.hasPermission(Permissions.Permission_Admin)).thenReturn(true); } diff --git a/src/test/java/net/tfminecraft/simplefactions/managers/MercenaryCommandsCoverageTest.java b/src/test/java/net/tfminecraft/simplefactions/managers/MercenaryCommandsCoverageTest.java index 56262e5f..45da1e52 100644 --- a/src/test/java/net/tfminecraft/simplefactions/managers/MercenaryCommandsCoverageTest.java +++ b/src/test/java/net/tfminecraft/simplefactions/managers/MercenaryCommandsCoverageTest.java @@ -324,6 +324,19 @@ void aCompanyMemberCanReadContractsButCannotDraftOrOfferOnTheLeadersBehalf() { assertTrue(company.getContractHandler().getAll().isEmpty()); } + @Test + void factionOfferCompletionExecutesFormattedMultiwordTargets() { + hirer.setName("§6Eastern Realm"); + captain.getInventory().setItem(0, ContractBook.reviewBook(company, ContractTerms.defaults())); + var completions = new net.tfminecraft.simplefactions.utils.TabCompletion().onTabComplete( + captain, command, "company", new String[] {"offer", "Eastern", "rE"}); + assertEquals(List.of("Realm"), completions); + run(captain, "offer", "Eastern", completions.getFirst()); + assertEquals(1, company.getContractHandler().getAll().size()); + assertSame(hirer, company.getContractHandler().getAll().getFirst().getHirer()); + assertSame(hirer, MercenaryCommandManager.resolveFaction("§6Eastern Realm")); + } + @Test void draftCommandCreatesExactlyOneCompanyBoundBookInAvailableInventorySpace() { run(captain, "draft"); diff --git a/src/test/java/net/tfminecraft/simplefactions/managers/TabCompletionCoverageTest.java b/src/test/java/net/tfminecraft/simplefactions/managers/TabCompletionCoverageTest.java index b0f9efd0..e6a8baf9 100644 --- a/src/test/java/net/tfminecraft/simplefactions/managers/TabCompletionCoverageTest.java +++ b/src/test/java/net/tfminecraft/simplefactions/managers/TabCompletionCoverageTest.java @@ -297,6 +297,9 @@ void companyArgumentsSuggestMatchingEnlistedPlayersOnlinePlayersAndFactionNames( MercenaryCompany company = mock(MercenaryCompany.class); when(company.getEnlisted()).thenReturn(List.of("Alice", "Bob")); when(guild.getCompany()).thenReturn(company); + when(guild.hasCompany()).thenReturn(true); + when(company.isLeader("Leader")).thenReturn(true); + when(company.hasFreeSlot()).thenReturn(true); when(Bukkit.getOnlinePlayers()) .thenAnswer(call -> List.of(gui.player("Alice"), gui.player("Bob"))); assertEquals(List.of("Alice"), complete("company", "kick", "al")); @@ -323,10 +326,10 @@ void companyAdminCompletesOnlyExistingCompaniesAndRecognizedOperations() { .thenReturn(Arrays.asList(null, mock(Guild.class), guild, unnamed)); assertEquals(List.of("give", "take"), complete("company", "admin", "")); assertEquals( - List.of("Silver Shields", "merchants", "unnamed"), + List.of("merchants", "unnamed"), complete("company", "admin", "give", "")); assertEquals(List.of("merchants"), complete("company", "admin", "take", "mer")); - assertEquals(List.of("1"), complete("company", "admin", "give", "merchants", "")); + assertEquals(List.of(), complete("company", "admin", "give", "merchants", "")); when(player.hasPermission(Permissions.Permission_Admin)).thenReturn(false); assertTrue(complete("company", "admin", "").isEmpty()); } @@ -352,9 +355,9 @@ void constructionKindsAreFilteredAndRequireAnEligibleLeader() { assertTrue(complete("faction", "construct", "").isEmpty()); factions.when(() -> FactionManager.getByLeader("Leader")).thenReturn(realm); assertEquals( - List.of("fort", "port", "airport", "train_station"), complete("faction", "construct")); + List.of("fort", "port", "airport", "train_station"), complete("faction", "construct", "")); assertEquals(List.of("train_station"), complete("faction", "construct", "TR")); - assertEquals(List.of(""), complete("faction", "construct", "fort", "")); + assertEquals(List.of(), complete("faction", "construct", "fort", "")); Cache.provincesEnabled = false; assertTrue(complete("faction", "construct", "").isEmpty()); assertTrue(complete(mock(CommandSender.class), "faction", "construct", "").isEmpty()); @@ -365,7 +368,8 @@ void constructionKindsAreFilteredAndRequireAnEligibleLeader() { void installationCompletionsUseTheLeadersCurrentInstallations(String action) { assertTrue(complete("faction", action, "").isEmpty()); installationChoices(); - List all = complete("faction", action); + assertEquals(List.of(action), complete("faction", action)); + List all = complete("faction", action, ""); assertTrue(all.containsAll(List.of("fort", "port"))); assertEquals(action.equals("deconstruct"), all.contains("fort_building")); assertEquals(List.of("port"), complete("faction", action, "P")); @@ -381,7 +385,7 @@ void vehicleArgumentsSuggestPlayersPoolInstallationsAndBankPaymentOnlyAtTheirPos when(Bukkit.getOnlinePlayers()) .thenAnswer(call -> List.of(gui.player("Alice"), gui.player("Bob"))); assertTrue( - complete("faction", "vehicle") + complete("faction", "vehicle", "") .containsAll(List.of("give", "handover", "transfer", "maintenance"))); assertEquals(List.of("transfer"), complete("faction", "vehicle", "tr")); assertEquals(List.of("Alice"), complete("faction", "vehicle", "give", "A")); @@ -406,8 +410,9 @@ void vehicleArgumentsSuggestPlayersPoolInstallationsAndBankPaymentOnlyAtTheirPos "faction,setcolour,'R,G,B'", "guild,setcolour,'R,G,B'" }) - void scalarArgumentsHaveSpecificPlaceholders(String command, String action, String placeholder) { - assertEquals(List.of(placeholder), complete(command, action, "")); + void freeTextArgumentsDoNotInsertUsageHints(String command, String action, String placeholder) { + assertEquals(List.of(), complete(command, action, "")); + assertEquals(List.of(), complete(command, action, "MIXED")); } @ParameterizedTest @@ -444,7 +449,9 @@ void deletionSuggestionsNeverIncludeTheBaseGuildAndDoNotRepeatThePlayersGuild() assertTrue(complete("faction", "delete", "").isEmpty()); assertTrue(complete("guild", "delete", "").isEmpty()); factions.when(() -> FactionManager.getByMember("Leader")).thenReturn(realm); + factions.when(() -> FactionManager.getByLeader("Leader")).thenReturn(realm); factions.when(() -> FactionManager.getGuildByMember("Leader")).thenReturn(guild); + factions.when(() -> FactionManager.getGuildByLeader("Leader")).thenReturn(guild); assertEquals(List.of("realm"), complete("faction", "delete", "")); assertEquals(List.of("merchants"), complete("guild", "delete", "")); Guild base = mock(Guild.class); @@ -467,6 +474,7 @@ void memberAndLeadershipSuggestionsExcludeTheCurrentLeaderAndIneligibleSuccessor assertTrue(complete("guild", "kick", "").isEmpty()); assertTrue(complete("faction", "setleader", "").isEmpty()); factions.when(() -> FactionManager.getByMember("Leader")).thenReturn(realm); + factions.when(() -> FactionManager.getByLeader("Leader")).thenReturn(realm); factions.when(() -> FactionManager.getGuildByLeader("Leader")).thenReturn(guild); when(realm.canBecomeLeader("Alice")).thenReturn(true); assertEquals(List.of("Alice", "Bob"), complete("guild", "kick", "")); @@ -523,7 +531,7 @@ void administrativeInstallationSuggestionsUseSelectedFactionKindAndPrefix() { assertEquals(List.of("fort"), complete("faction", "forceupgrade", "realm", "F")); assertTrue(complete("faction", "forceupgrade", "missing", "").isEmpty()); assertEquals(List.of("airport"), complete("faction", "forceconstruct", "realm", "air")); - assertEquals(List.of(""), complete("faction", "forceconstruct", "realm", "fort", "")); + assertEquals(List.of(), complete("faction", "forceconstruct", "realm", "fort", "")); Cache.provincesEnabled = false; assertTrue(complete("faction", "forceconstruct", "realm", "").isEmpty()); assertTrue(complete("faction", "forceupgrade", "realm", "").isEmpty()); @@ -538,13 +546,17 @@ void administrativeRegimentChoicesExcludeLeviesAndOfferValidActionsAndAmount() { Regiment levy = mock(Regiment.class); when(levy.getId()).thenReturn("levy"); when(levy.isLevy()).thenReturn(true); - when(military.getRegiments()).thenReturn(List.of(professional, levy)); + Regiment mercenary = mock(Regiment.class); + when(mercenary.isMercenary()).thenReturn(true); + when(mercenary.getId()).thenReturn("company_owned"); + when(military.getRegiments()).thenReturn(List.of(professional, levy, mercenary)); when(realm.getMilitary()).thenReturn(military); assertEquals(List.of("give", "take"), complete("faction", "forceregiment", "realm", "")); assertEquals(List.of("archers"), complete("faction", "forceregiment", "realm", "give", "")); + assertEquals(List.of("archers"), complete("faction", "forceregiment", "realm", "take", "AR")); assertTrue(complete("faction", "forceregiment", "missing", "give", "").isEmpty()); assertEquals( - List.of("1"), complete("faction", "forceregiment", "realm", "give", "archers", "")); + List.of(), complete("faction", "forceregiment", "realm", "give", "archers", "")); } @Test @@ -562,12 +574,12 @@ void forceJoinSuggestsOnlyMatchingFactionIdsAndPlayersWithoutMembership() { } @Test - void administrativeNumbersAndModifiersHaveClearArgumentPlaceholders() { + void administrativeFreeTextAndNumbersHaveNoInventedValues() { admin(); - assertEquals(List.of("1.0"), complete("faction", "forcewithdraw", "realm", "")); - assertEquals(List.of(""), complete("faction", "addprestigemodifier", "realm", "")); + assertEquals(List.of(), complete("faction", "forcewithdraw", "realm", "")); + assertEquals(List.of(), complete("faction", "addprestigemodifier", "realm", "")); assertEquals( - List.of("1.0"), complete("faction", "addprestigemodifier", "realm", "festival", "")); + List.of(), complete("faction", "addprestigemodifier", "realm", "festival", "")); } @Test @@ -609,6 +621,8 @@ void diplomacySuggestionsFilterSubjectsExcludeTheSourceAndSeparateRelationTypes( types.when(RelationLoader::getTreatyTypes).thenReturn(List.of(trade)); types.when(RelationLoader::getPoliticalTreatyTypes).thenReturn(List.of(political)); assertEquals(List.of("alliance"), complete("faction", "setrelation", "realm", "other", "")); + assertEquals(List.of("alliance"), complete("faction", "setrelation", "realm", "other", "ALL")); + assertTrue(complete("faction", "setrelation", "realm", "other", "tr").isEmpty()); assertEquals( List.of("trade", "non_aggression"), complete("faction", "settreaty", "realm", "other", "")); @@ -644,6 +658,207 @@ void playerOnlyAndUnknownCommandsLeaveConsoleCompletionEmpty() { assertTrue(complete("unrelated", "unknown").isEmpty()); } + @Test + void missingRootActionsFollowMemberLeaderAdminAndProvinceContext() { + assertTrue(complete("guild", "li").contains("list")); + factions.when(() -> FactionManager.getByMember("Leader")).thenReturn(realm); + when(realm.getBank()).thenReturn(mock(net.tfminecraft.simplefactions.objects.Bank.class)); + assertTrue(complete("faction", "").containsAll(List.of("deposit", "leave"))); + when(realm.isInGuild("Leader")).thenReturn(true); + assertFalse(complete("faction", "").contains("leave")); + factions.when(() -> FactionManager.getGuildByMember("Leader")).thenReturn(guild); + assertEquals(List.of("leave"), complete("guild", "lea")); + factions.when(() -> FactionManager.getGuildByLeader("Leader")).thenReturn(guild); + assertFalse(complete("guild", "").contains("leave")); + assertEquals(List.of("delete"), complete("guild", "del")); + admin(); + assertTrue(complete("faction", "").containsAll(List.of("admin", "forcedelete", "refresh", "delbank", "provincecap"))); + assertEquals(List.of("quarantine"), complete("faction", "admin", "QU")); + assertEquals(List.of("assign"), complete("faction", "admin", "quarantine", "AS")); + Cache.provincesEnabled = false; + assertFalse(complete("faction", "").contains("provincecap")); + } + + @Test + void rootPrefixFilteringKeepsEveryAuthorizedMatchAndExactCommandToken() { + admin(); + factions.when(() -> FactionManager.getByLeader("Leader")).thenReturn(realm); + factions.when(() -> FactionManager.getGuildByLeader("Leader")).thenReturn(guild); + when(player.hasPermission(EspionageConfig.reloadPermission())).thenReturn(true); + assertEquals(List.of("setleader"), complete("guild", "SeTL")); + assertEquals(List.of("invite"), complete("faction", "iNV")); + assertTrue(complete("faction", "re").containsAll(List.of("rename", "reloadconfigs", "reloadtitles", "reloadespionage", "refresh"))); + assertTrue(complete("faction", "re").stream().allMatch(value -> value.startsWith("re"))); + for (String action : List.of("construct", "upgrade", "deconstruct", "transfervehicle", "findvehicles", "vehicle")) + assertEquals(List.of(action), complete("faction", action)); + assertEquals(List.of("fort", "port", "airport", "train_station"), complete("faction", "construct", "")); + } + + @Test + void adminFactionIdsAndDeletionIncludeTargetsOutsideMembership() { + admin(); + for (String action : List.of("delete", "delbank", "startelection", "endelection", "unclaim")) { + assertEquals(List.of("realm", "other"), complete("faction", action, "")); + assertEquals(List.of("other"), complete("faction", action, "Ot")); + assertEquals(List.of(), complete("faction", action, "other", "")); + } + when(player.hasPermission(Permissions.Permission_Admin)).thenReturn(false); + assertTrue(complete("faction", "delete", "").isEmpty()); + assertTrue(complete("faction", "delbank", "").isEmpty()); + } + + @Test + void companyContractsRemainDiscoverableToMembersAndNamesCompleteAfterSpaces() { + MercenaryCompany company = mock(MercenaryCompany.class); + when(guild.getCompany()).thenReturn(company); + when(guild.hasCompany()).thenReturn(true); + factions.when(() -> FactionManager.getGuildByMember("Leader")).thenReturn(guild); + assertEquals(List.of("contracts"), complete("company", "CON")); + assertFalse(complete("company", "").contains("invite")); + factions.when(() -> FactionManager.getGuildByLeader("Leader")).thenReturn(guild); + assertEquals(List.of("Realm"), complete("company", "offer", "Other", "rE")); + assertTrue(complete("company", "offer", "Other", "Realm", "").isEmpty()); + when(company.getName()).thenReturn("Silver Shields"); + try (MockedStatic market = mockStatic(MercenaryMarket.class)) { + market.when(MercenaryMarket::listing).thenReturn(List.of(company)); + assertEquals(List.of("Shields"), complete("mercenaries", "hire", "Silver", "sH")); + assertTrue(complete("mercenaries", "hire", "Silver", "Shields", "").isEmpty()); + } + } + + @Test + void companyAdminTargetsFitTheExecutorsSingleTokenGrammar() { + admin(); + MercenaryCompany company = mock(MercenaryCompany.class); + when(company.getName()).thenReturn("Silver Shields"); + when(guild.getCompany()).thenReturn(company); + factions.when(FactionManager::getAllGuilds).thenReturn(List.of(guild)); + assertEquals(List.of("merchants"), complete("company", "admin", "give", "")); + for (String target : complete("company", "admin", "take", "")) { + assertEquals(1, target.split(" ").length); + assertEquals(guild.getId(), target); + } + when(company.getName()).thenReturn("Silver"); + assertEquals(List.of("Silver", "merchants"), complete("company", "admin", "take", "")); + } + + @Test + void guildSetLeaderAndLeaderlessAppointmentsCompleteEligibleOfflineCharacterSuffixes() { + CharacterKeyFixture characters = CharacterKeyFixture.install(); + String leader = characters.play(player, "Lord Aldric"); + String alice = characters.offline(UUID.randomUUID(), "Alice Fairweather"); + String bob = characters.offline(UUID.randomUUID(), "Bob Stone"); + when(realm.getMembers()).thenReturn(List.of(leader, alice, bob)); + when(guild.getMembers()).thenReturn(List.of(leader, alice)); + when(guild.getLeader()).thenReturn(leader); + when(guild.isLeader(leader)).thenReturn(true); + factions.when(() -> FactionManager.getGuildByLeader(leader)).thenReturn(guild); + factions.when(() -> FactionManager.getGuildByString("merchants")).thenReturn(guild); + assertEquals(List.of("Alice Fairweather"), complete("guild", "setleader", "aL")); + assertEquals(List.of("Fairweather"), complete("guild", "setleader", "Alice", "f")); + assertTrue(complete("guild", "setleader", "Bob", "").isEmpty()); + when(guild.isLeaderless()).thenReturn(true); + when(guild.getMembers()).thenReturn(List.of()); + factions.when(() -> FactionManager.getGuildByLeader(bob)).thenReturn(mock(Guild.class)); + try (MockedStatic leaderless = mockStatic(net.tfminecraft.simplefactions.guild.LeaderlessGuilds.class)) { + leaderless.when(() -> net.tfminecraft.simplefactions.guild.LeaderlessGuilds.appointable(leader)).thenReturn(List.of(guild)); + assertEquals(List.of("Alice Fairweather"), complete("guild", "appoint", "merchants", "")); + assertEquals(List.of("Fairweather"), complete("guild", "appoint", "merchants", "Alice", "")); + assertTrue(complete("guild", "appoint", "missing", "").isEmpty()); + leaderless.when(() -> net.tfminecraft.simplefactions.guild.LeaderlessGuilds.appointable(leader)).thenReturn(List.of()); + assertTrue(complete("guild", "appoint", "merchants", "").isEmpty()); + } + admin(); + assertEquals(List.of("Alice Fairweather"), complete("guild", "forceleader", "merchants", "")); + assertEquals(List.of("Fairweather"), complete("guild", "forceleader", "merchants", "Alice", "F")); + assertTrue(complete("guild", "forceleader", "merchants", "Bob", "").isEmpty()); + var livingBob = net.tfminecraft.simplefactions.identity.Members.info(bob).orElseThrow(); + factions.when(() -> FactionManager.getGuildByLeader(bob)).thenReturn(null); + var deadBob = new net.tfminecraft.tlibs.character.CharacterInfo(livingBob.ref(), livingBob.name(), + net.tfminecraft.tlibs.character.CharacterStatus.DEAD, null, java.util.Set.of(), 0L, java.time.Instant.EPOCH); + try (MockedStatic identities = mockStatic(net.tfminecraft.simplefactions.identity.Members.class, CALLS_REAL_METHODS)) { + identities.when(() -> net.tfminecraft.simplefactions.identity.Members.info(bob)).thenReturn(java.util.Optional.of(deadBob)); + assertEquals(List.of("Alice Fairweather"), complete("guild", "forceleader", "merchants", "")); + } + + } + + @Test + void companyAndVehicleRecipientsIncludeOnlineCharacterNamesAndSuffixes() { + CharacterKeyFixture characters = CharacterKeyFixture.install(); + String leader = characters.play(player, "Lord Aldric"); + Player account = gui.player("BlockCrafter"); + String alice = characters.play(account, "Alice Fairweather"); + when(Bukkit.getOnlinePlayers()).thenAnswer(call -> List.of(player, account)); + factions.when(() -> FactionManager.getByLeader(leader)).thenReturn(realm); + factions.when(() -> FactionManager.getGuildByLeader(leader)).thenReturn(guild); + MercenaryCompany company = mock(MercenaryCompany.class); + when(guild.hasCompany()).thenReturn(true); + when(guild.getCompany()).thenReturn(company); + when(company.isLeader(leader)).thenReturn(true); + when(company.hasFreeSlot()).thenReturn(true); + assertEquals(List.of("Alice Fairweather"), complete("company", "invite", "Al")); + assertEquals(List.of("Fairweather"), complete("company", "invite", "Alice", "f")); + for (String action : List.of("give", "handover")) { + assertEquals(List.of("Alice Fairweather"), complete("faction", "vehicle", action, "Al")); + assertEquals(List.of("Fairweather"), complete("faction", "vehicle", action, "Alice", "")); + assertEquals(List.of("BlockCrafter"), complete("faction", "vehicle", action, "Bl")); + } + when(company.isEnlisted(alice)).thenReturn(true); + assertTrue(complete("company", "invite", "Al").isEmpty()); + } + + @Test + void factionKickExcludesMembersWhoMustLeaveThroughTheirSubguild() { + factions.when(() -> FactionManager.getByLeader("Leader")).thenReturn(realm); + when(realm.isInGuild("Bob")).thenReturn(true); + assertEquals(List.of("Alice"), complete("faction", "kick", "")); + assertTrue(complete("faction", "kick", "B").isEmpty()); + } + + @Test + void offeredLeaderlessGuildCanBeDeclinedWithoutAnOrdinaryInvite() { + try (MockedStatic leaderless = mockStatic(net.tfminecraft.simplefactions.guild.LeaderlessGuilds.class)) { + leaderless.when(() -> net.tfminecraft.simplefactions.guild.LeaderlessGuilds.offeredTo("Leader")).thenReturn(guild); + assertEquals(List.of("merchants"), complete("guild", "decline", "MER")); + assertTrue(complete("guild", "join", "MER").isEmpty()); + assertTrue(complete("guild", "decline", "x").isEmpty()); + } + } + + @Test + void staffConsoleDiscoversOnlySupportedFactionRoots() { + CommandSender console = mock(CommandSender.class); + when(console.hasPermission(Permissions.Permission_Admin)).thenReturn(true); + assertEquals(List.of("title"), complete(console, "faction", "tiT")); + assertEquals(List.of("addguildbank"), complete(console, "faction", "addg")); + assertEquals(List.of("tradegraph"), complete(console, "faction", "trad")); + assertTrue(complete(console, "faction", "").contains("admin")); + assertFalse(complete(console, "faction", "").contains("create")); + Cache.provincesEnabled = false; + assertTrue(complete(console, "faction", "tit").isEmpty()); + } + + @Test + void pluginCommandDoesNotFallBackToPlayerNamesAfterTerminalOrUnknownArguments() throws Exception { + Player fallback = gui.player("UnrelatedAccount"); + when(player.getServer()).thenReturn(gui.server); + when(gui.server.suggestPlayerNamesWhenNullTabCompletions()).thenReturn(true); + when(gui.server.getOnlinePlayers()).thenAnswer(call -> List.of(fallback)); + when(player.canSee(fallback)).thenReturn(true); + var constructor = org.bukkit.command.PluginCommand.class.getDeclaredConstructor(String.class, org.bukkit.plugin.Plugin.class); + constructor.setAccessible(true); + for (String root : List.of("faction", "guild")) { + var command = constructor.newInstance(root, gui.plugin); + command.setExecutor((sender, cmd, label, args) -> true); + command.setTabCompleter((sender, cmd, label, args) -> null); + assertEquals(List.of("UnrelatedAccount"), command.tabComplete(player, root, new String[] {"list", ""})); + command.setTabCompleter(completion); + for (String action : List.of("list", "reloadconfigs", "menu", "leave", "unknown")) + assertEquals(List.of(), command.tabComplete(player, root, new String[] {action, ""})); + } + } + private void installationChoices() { InstallationHandler installations = mock(InstallationHandler.class); Installation fort = mock(Installation.class); @@ -672,6 +887,7 @@ void memberSuggestionsOfferCharacterNamesAndCompleteTheirLastWords() { when(guild.getMembers()).thenAnswer(call -> new ArrayList<>(members)); when(guild.isLeader(leader)).thenReturn(true); factions.when(() -> FactionManager.getByMember(leader)).thenReturn(realm); + factions.when(() -> FactionManager.getByLeader(leader)).thenReturn(realm); factions.when(() -> FactionManager.getGuildByLeader(leader)).thenReturn(guild); factions.when(() -> FactionManager.getGuildByString("merchants")).thenReturn(guild); @@ -751,7 +967,8 @@ private List complete(CommandSender sender, String commandName, String.. Command command = mock(Command.class); when(command.getName()).thenReturn(commandName); List result = completion.onTabComplete(sender, command, commandName, args); - return result == null ? List.of() : result; + assertNotNull(result); + return result; } @Test diff --git a/src/test/java/net/tfminecraft/simplefactions/managers/TitleManagementCoverageTest.java b/src/test/java/net/tfminecraft/simplefactions/managers/TitleManagementCoverageTest.java index 2800d426..d9035ae7 100644 --- a/src/test/java/net/tfminecraft/simplefactions/managers/TitleManagementCoverageTest.java +++ b/src/test/java/net/tfminecraft/simplefactions/managers/TitleManagementCoverageTest.java @@ -489,17 +489,17 @@ void adminCompletionsOfferOnlyRelevantTiersTitlesPartsAndValues() { composite("river", "duchy", "north"); assertEquals(List.of("setcolour", "setcomplete"), complete("set")); assertEquals(List.of("county", "duchy"), complete("list", "")); - assertEquals(List.of(""), complete("where", "")); + assertEquals(List.of(), complete("where", "")); assertEquals(List.of("north", "south"), complete("addprovince", "")); assertEquals(List.of("river"), complete("addtitle", "")); assertEquals(List.of("river"), complete("removetitle", "")); assertEquals(List.of("north"), complete("info", "N")); assertEquals(List.of("north", "south", "river"), complete("setcolor", "")); assertTrue(complete("unknown", "").isEmpty()); - assertEquals(List.of(""), complete("rename", "north", "")); + assertEquals(List.of(), complete("rename", "north", "")); assertEquals(List.of("4,5,6"), complete("setcolour", "north", "")); - assertEquals(List.of("R,G,B"), complete("setcolor", "missing", "")); - assertEquals(List.of(""), complete("addprovince", "north", "")); + assertEquals(List.of(), complete("setcolor", "missing", "")); + assertEquals(List.of(), complete("addprovince", "north", "")); assertEquals(List.of("10", "11"), complete("removeprovince", "north", "")); assertEquals(List.of("south"), complete("addtitle", "river", "")); assertEquals(List.of("north"), complete("removetitle", "river", "")); diff --git a/src/test/java/net/tfminecraft/simplefactions/migration/QuarantineCommandsTest.java b/src/test/java/net/tfminecraft/simplefactions/migration/QuarantineCommandsTest.java index 8303c32c..e0904692 100644 --- a/src/test/java/net/tfminecraft/simplefactions/migration/QuarantineCommandsTest.java +++ b/src/test/java/net/tfminecraft/simplefactions/migration/QuarantineCommandsTest.java @@ -59,6 +59,40 @@ private Quarantine.Record record(String id, boolean leader) throws IOException { return record; } + @Test + void completionOffersOnlyTheAuthorizedGrammarAndUnresolvedSnapshot() throws IOException { + assertEquals(List.of("quarantine"), QuarantineCommands.complete(staff, new String[] {"admin", "Qu"})); + assertEquals(List.of("list", "assign"), QuarantineCommands.complete(staff, new String[] {"admin", "quarantine", ""})); + assertEquals(List.of("assign"), QuarantineCommands.complete(staff, new String[] {"admin", "quarantine", "AS"})); + record("q1", true); + record("q2", false); + record("q3", false); + Quarantine quarantine = Quarantine.load(root.resolve(CharacterMigration.QUARANTINE)); + quarantine.get("q2").assigned = "pending"; + quarantine.get("q3").applied = true; + quarantine.save(); + assertEquals(List.of("q1"), QuarantineCommands.complete(staff, new String[] {"admin", "quarantine", "assign", "Q"})); + Files.delete(root.resolve(CharacterMigration.QUARANTINE)); + assertEquals(List.of("q1"), QuarantineCommands.complete(staff, new String[] {"admin", "quarantine", "assign", ""}), "completion must not reread disk"); + assertTrue(QuarantineCommands.complete(mock(CommandSender.class), new String[] {"admin", "quarantine", "assign", ""}).isEmpty()); + assertTrue(QuarantineCommands.complete(staff, new String[] {"admin", "quarantine", "assign", "q1", ""}).isEmpty()); + assertTrue(QuarantineCommands.complete(staff, new String[] {"admin", "quarantine", "list", ""}).isEmpty()); + assertTrue(QuarantineCommands.complete(staff, new String[] {"admin", "other", ""}).isEmpty()); + } + + @Test + void completionDropsAssignedRecordsAndDoesNotReuseAnotherDataFoldersSnapshot() throws IOException { + record("q1", true); + try (var identity = new SharedIdentity()) { + CharacterRef ada = identity.character(identity.account("Steve"), "Ada"); + QuarantineCommands.handle(staff, new String[] {"admin", "quarantine", "assign", "q1", ada.characterId().toString()}); + assertTrue(QuarantineCommands.complete(staff, new String[] {"admin", "quarantine", "assign", ""}).isEmpty()); + } + record("q4", false); + when(SimpleFactions.plugin.getDataFolder()).thenReturn(root.resolve("other").toFile()); + assertTrue(QuarantineCommands.complete(staff, new String[] {"admin", "quarantine", "assign", ""}).isEmpty()); + } + @Test void onlyStaffMayUseIt() { assertTrue(QuarantineCommands.matches(new String[] {"admin", "QUARANTINE"})); diff --git a/src/test/java/net/tfminecraft/simplefactions/war/battle/ui/BattleCommandsCoverageTest.java b/src/test/java/net/tfminecraft/simplefactions/war/battle/ui/BattleCommandsCoverageTest.java index 39d8ada0..9a9be8b2 100644 --- a/src/test/java/net/tfminecraft/simplefactions/war/battle/ui/BattleCommandsCoverageTest.java +++ b/src/test/java/net/tfminecraft/simplefactions/war/battle/ui/BattleCommandsCoverageTest.java @@ -198,11 +198,11 @@ void invitingAnOfflinePlayerGivesFeedbackWithoutPassingNullIntoTheWarband() { } @ParameterizedTest - @CsvSource({"setlives,5|10|20|50", "setcontestduration,60|120|180|240|300", "setdefenderlives,5|10|20|25|50"}) - void numericSettingsCompleteNumbersInsteadOfUnrelatedSideIds(String operation, String values) { + @ValueSource(strings = {"setlives", "setcontestduration", "setdefenderlives"}) + void numericSettingsLeaveTheValueForTheCaller(String operation) { admin(); battle("battle", BattleType.FIELD); - assertEquals(List.of(values.split("\\|")), complete(alice, "battle", operation, "battle", "")); + assertEquals(List.of(), complete(alice, "battle", operation, "battle", "")); } @Test @@ -485,11 +485,105 @@ void listingBattlesRequiresAWarbandLeaderButJoiningReportsServiceErrors() { } } + @Test + void warbandJoinCompletesOpenInvitedAndEligibleCampaignBandsWithoutEnrolling() { + Player bob = player("Bob"); + Warband open = band("open", bob); + open.setLocked(false); + Warband invited = band("invited", bob); + invited.invite(alice); + band("private", bob); + assertEquals(List.of("join"), complete(alice, "warband", "jO")); + assertEquals(List.of("open", "invited"), complete(alice, "warband", "join", "")); + assertEquals(List.of("invited"), complete(alice, "warband", "join", "INV")); + assertEquals(1, open.getMemberCount()); + assertFalse(invited.hasMember(alice)); + persistence.verifyNoInteractions(); + when(alice.isInsideVehicle()).thenReturn(true); + assertTrue(complete(alice, "warband", "join", "").isEmpty()); + when(alice.isInsideVehicle()).thenReturn(false); + open.addPlayer(alice); + assertTrue(complete(alice, "warband", "join", "").isEmpty()); + assertTrue(complete(alice, "warband", "j").isEmpty()); + } + + @Test + void campaignWarbandJoinUsesScheduleAndCampaignEligibility() { + Faction faction = mock(Faction.class); + Warband shell = mock(Warband.class); + when(shell.getId()).thenReturn("campaign"); + when(shell.isFaction()).thenReturn(true); + WarbandManager.addWarband(shell); + assertTrue(complete(alice, "warband", "join", "").isEmpty()); + factions.when(() -> FactionManager.getByMember("Alice")).thenReturn(faction); + var day = java.time.LocalDate.of(2026, 10, 15); + var now = net.tfminecraft.simplefactions.war.campaign.runtime.BattleWindowService.atScheduleHour(day, 23); + War war = mock(War.class); + when(war.getBattleDay()).thenReturn(day); + Battle battle = mock(Battle.class); + CampaignBattleContext context = new CampaignBattleContext(battle, "attacker", war); + try (MockedStatic campaigns = mockStatic(CampaignBattleJoinService.class); + MockedStatic clock = mockStatic(net.tfminecraft.simplefactions.war.campaign.runtime.CampaignClock.class)) { + clock.when(net.tfminecraft.simplefactions.war.campaign.runtime.CampaignClock::now).thenReturn(now); + campaigns.when(() -> CampaignBattleJoinService.findCampaignBattleForWarband(shell)).thenReturn(context); + assertEquals(List.of("campaign"), complete(alice, "warband", "join", "c")); + campaigns.when(() -> CampaignBattleJoinService.validateWarbandMemberJoin(war, battle, "attacker", shell, "Alice", alice.getUniqueId())).thenReturn("Roster full"); + assertTrue(complete(alice, "warband", "join", "").isEmpty()); + campaigns.when(() -> CampaignBattleJoinService.validateWarbandMemberJoin(war, battle, "attacker", shell, "Alice", alice.getUniqueId())).thenReturn(null); + when(war.getBattleDay()).thenReturn(day.plusDays(1)); + assertTrue(complete(alice, "warband", "join", "").isEmpty()); + verify(shell, never()).addMember(any(), any()); + persistence.verifyNoInteractions(); + } + } + + @Test + void adminWarbandDeletionCompletesBandsOutsideOwnership() { + band("others", player("Bob")); + assertEquals(List.of(), complete(alice, "warband", "delete", "")); + admin(); + assertEquals(List.of("others"), complete(alice, "warband", "delete", "OTH")); + } + + @Test + void battleAndWarbandPrefixesAndExactRootTokensDoNotAdvanceArguments() { + admin(); + assertEquals(List.of("create"), complete(alice, "battle", "create")); + assertEquals(List.of("create"), complete(alice, "warband", "create")); + assertEquals(List.of("setspawn"), complete(alice, "battle", "SeTSp")); + assertEquals(List.of("invite"), complete(alice, "warband", "iNV")); + battle("front", BattleType.FIELD); + assertEquals(List.of("attacker"), complete(alice, "battle", "join", "front", "AtT")); + assertEquals(List.of("front"), complete(alice, "battle", "edit", "FRO")); + assertTrue(complete(alice, "battle", "join", "front", "defender", "").isEmpty()); + assertTrue(complete(alice, "battle", "unknown", "").isEmpty()); + } + + @Test + void terminalBattleCommandsStayEmptyThroughPluginCommand() throws Exception { + Player fallback = ui.player("UnrelatedAccount"); + when(alice.getServer()).thenReturn(ui.server); + when(ui.server.suggestPlayerNamesWhenNullTabCompletions()).thenReturn(true); + when(ui.server.getOnlinePlayers()).thenAnswer(call -> List.of(fallback)); + when(alice.canSee(fallback)).thenReturn(true); + var constructor = org.bukkit.command.PluginCommand.class.getDeclaredConstructor(String.class, org.bukkit.plugin.Plugin.class); + constructor.setAccessible(true); + for (String root : List.of("battle", "warband")) { + var command = constructor.newInstance(root, ui.plugin); + command.setExecutor((sender, cmd, label, args) -> true); + command.setTabCompleter((sender, cmd, label, args) -> null); + assertEquals(List.of("UnrelatedAccount"), command.tabComplete(alice, root, new String[] {"list", ""})); + command.setTabCompleter(tabs); + for (String action : List.of("list", "leave", "unknown")) + assertEquals(List.of(), command.tabComplete(alice, root, new String[] {action, ""})); + } + } + @Test void tabCompletionReflectsPermissionsCurrentWarbandsAndSideNames() { assertEquals(List.of("list", "join"), complete(alice, "battle")); assertTrue(complete(alice, "warband").containsAll(List.of("create", "kick", "retreat"))); - assertEquals(List.of(""), complete(alice, "warband", "create", "")); + assertEquals(List.of(), complete(alice, "warband", "create", "")); assertEquals(List.of(), complete(alice, "warband", "delete", "")); assertEquals(List.of(), complete(alice, "warband", "kick", "")); assertEquals(List.of(), complete(alice, "warband", "setleader", "")); @@ -503,20 +597,20 @@ void tabCompletionReflectsPermissionsCurrentWarbandsAndSideNames() { battle("front", BattleType.FIELD); assertEquals(List.of("front"), complete(alice, "battle", "join", "")); assertEquals(List.of("attacker", "defender"), complete(alice, "battle", "join", "front", "")); - assertNull(complete(alice, "battle", "join", "missing", "")); - assertNull(complete(alice, "battle", "create", "")); + assertEquals(List.of(), complete(alice, "battle", "join", "missing", "")); + assertEquals(List.of(), complete(alice, "battle", "create", "")); admin(); assertTrue(complete(alice, "battle", "").containsAll(List.of("create", "edit", "delete", "setlives"))); assertEquals(List.of("field", "siege", "raid"), complete(alice, "battle", "create", "")); - assertEquals(List.of(""), complete(alice, "battle", "create", "field", "")); - assertEquals(List.of(""), complete(alice, "battle", "addside", "front", "")); + assertEquals(List.of(), complete(alice, "battle", "create", "field", "")); + assertEquals(List.of(), complete(alice, "battle", "addside", "front", "")); for (String operation : List.of("addpoint", "setspawn", "setjail")) { assertEquals(List.of("attacker", "defender"), complete(alice, "battle", operation, "front", "")); } assertEquals(List.of(), complete(alice, "battle", "addpoint", "missing", "")); - assertNull(complete(alice, "battle", "setspawn", "missing", "")); - assertNull(complete(alice, "unknown", "")); - assertNull(complete(mock(CommandSender.class), "warband", "")); + assertEquals(List.of(), complete(alice, "battle", "setspawn", "missing", "")); + assertEquals(List.of(), complete(alice, "unknown", "")); + assertEquals(List.of(), complete(mock(CommandSender.class), "warband", "")); } @Test void retreatReportsRejectionsAndRequiresConfirmationForAnEligibleCampaignLeader() {