diff --git a/src/main/java/net/tfminecraft/simplefactions/diplomacy/coalition/CoalitionService.java b/src/main/java/net/tfminecraft/simplefactions/diplomacy/coalition/CoalitionService.java index 43bb78d8..a2eec63b 100644 --- a/src/main/java/net/tfminecraft/simplefactions/diplomacy/coalition/CoalitionService.java +++ b/src/main/java/net/tfminecraft/simplefactions/diplomacy/coalition/CoalitionService.java @@ -273,6 +273,22 @@ public static Result join(Faction joiner, Coalition coalition) { if (!coalition.isInvited(joiner.getId(), CoalitionRegistry.now())) { return Result.fail("§cYou have not been invited to that coalition."); } + String block = admit(joiner, coalition); + return block != null ? Result.fail(block) : Result.ok("§aYou joined the " + displayName(coalition) + " §acoalition."); + } + + /** Staff add a faction as if it had accepted an invite; every other joining rule applies. */ + public static Result adminAdd(Coalition coalition, Faction joiner) { + if (!CoalitionSettings.enabled) return Result.fail(DISABLED); + if (joiner == null) return Result.fail(NO_FACTION); + String block = admit(joiner, coalition); + if (block != null) return Result.fail(block); + notifyFaction(joiner, "§aStaff added you to the " + displayName(coalition) + " §acoalition."); + return Result.ok("§aAdded " + joiner.getName() + " §ato the " + displayName(coalition) + " §acoalition."); + } + + /** Adds the faction once the join rules allow it; returns why not otherwise. */ + private static String admit(Faction joiner, Coalition coalition) { String block = membershipBlock(joiner); Faction target = target(coalition); if (block == null && (target == null || targetLost(coalition))) block = "§cThat coalition has lost its target."; @@ -292,12 +308,12 @@ public static Result join(Faction joiner, Coalition coalition) { } } if (block == null) block = capacityBlock(joiner, false); - if (block != null) return Result.fail(block); + if (block != null) return block; notifyMembers(coalition, "§a" + joiner.getName() + " §ajoined the " + displayName(coalition) + " §acoalition."); coalition.addMember(joiner.getId(), CoalitionRegistry.now()); coalition.setSingleMemberSince(null); CoalitionRegistry.save(); - return Result.ok("§aYou joined the " + displayName(coalition) + " §acoalition."); + return null; } public static Result leave(Faction member) { diff --git a/src/main/java/net/tfminecraft/simplefactions/managers/CoalitionCommandManager.java b/src/main/java/net/tfminecraft/simplefactions/managers/CoalitionCommandManager.java index 81b2fd9a..487c7cff 100644 --- a/src/main/java/net/tfminecraft/simplefactions/managers/CoalitionCommandManager.java +++ b/src/main/java/net/tfminecraft/simplefactions/managers/CoalitionCommandManager.java @@ -26,7 +26,7 @@ public final class CoalitionCommandManager implements CommandExecutor { public static final List SUBCOMMANDS = List.of( "menu", "list", "create", "invite", "join", "leave", "kick", "leader", "target", "setbanner", "resetbanner", "offensive", "disband"); - public static final List ADMIN_SUBCOMMANDS = List.of("disband", "offensive", "kick", "setleader", "cost"); + public static final List ADMIN_SUBCOMMANDS = List.of("disband", "offensive", "add", "kick", "setleader", "cost"); static final String NOT_A_LEADER = "§cOnly a faction leader can do that."; @Override @@ -164,7 +164,7 @@ private void admin(CommandSender sender, String[] args) { sender.sendMessage("§7" + CoalitionService.displayName(coalition) + " §7is now " + (coalition.isOffensive() ? "offensive" : "defensive") + "."); } - case "kick", "setleader" -> { + case "add", "kick", "setleader" -> { if (args.length < 4) { sender.sendMessage("§cUsage: /coalition admin " + sub + " "); return; @@ -172,12 +172,14 @@ private void admin(CommandSender sender, String[] args) { Coalition coalition = coalitionArg(sender, args, 3, sub + " "); if (coalition == null) return; Faction faction = MercenaryCommandManager.resolveFaction(args[2]); - CoalitionService.Result result = sub.equals("kick") - ? CoalitionService.adminKick(coalition, faction) - : CoalitionService.adminSetLeader(coalition, faction); + CoalitionService.Result result = switch (sub) { + case "add" -> CoalitionService.adminAdd(coalition, faction); + case "kick" -> CoalitionService.adminKick(coalition, faction); + default -> CoalitionService.adminSetLeader(coalition, faction); + }; sender.sendMessage(result.message()); } - default -> sender.sendMessage("§cUsage: /coalition admin "); + default -> sender.sendMessage("§cUsage: /coalition admin "); } } diff --git a/src/main/java/net/tfminecraft/simplefactions/managers/CoalitionTabCompletion.java b/src/main/java/net/tfminecraft/simplefactions/managers/CoalitionTabCompletion.java index 917f0c90..4aff3cb7 100644 --- a/src/main/java/net/tfminecraft/simplefactions/managers/CoalitionTabCompletion.java +++ b/src/main/java/net/tfminecraft/simplefactions/managers/CoalitionTabCompletion.java @@ -31,7 +31,7 @@ public List onTabComplete(CommandSender sender, Command command, String String action = args[1].toLowerCase(Locale.ROOT); if (action.equals("cost")) return prefix(args[args.length - 1], factionIds()); if (action.equals("offensive") && args.length == 3) return prefix(args[2], List.of("on", "off")); - if ((action.equals("kick") || action.equals("setleader")) && args.length == 3) { + if ((action.equals("add") || action.equals("kick") || action.equals("setleader")) && args.length == 3) { return prefix(args[2], factionIds()); } return args.length <= 4 ? prefix(args[args.length - 1], coalitionNames()) : List.of(); diff --git a/src/main/java/net/tfminecraft/simplefactions/managers/CommandManager.java b/src/main/java/net/tfminecraft/simplefactions/managers/CommandManager.java index 91742959..652c3d0b 100644 --- a/src/main/java/net/tfminecraft/simplefactions/managers/CommandManager.java +++ b/src/main/java/net/tfminecraft/simplefactions/managers/CommandManager.java @@ -547,6 +547,14 @@ private boolean dispatchCommand(CommandSender sender, Command cmd, String label, } AdminLandService.forceRemoveCapital(g).forEach(p::sendMessage); return true; + } else if(cmd.getName().equalsIgnoreCase(cmd2) && args[0].equalsIgnoreCase("forcejoin") && args.length >= 3) { + if(!Permissions.isAdmin(sender)) { + p.sendMessage("§a[SimpleFactions]§c You do not have access to this command"); + return true; + } + Guild g = FactionManager.getGuildByString(args[1]); + p.sendMessage(g == null ? "§cNo guild by the id "+args[1] : StaffJoins.guild(g, rest(args, 2))); + return true; } else if(cmd.getName().equalsIgnoreCase(cmd2) && args[0].equalsIgnoreCase("forceleader") && args.length >= 3) { if(!Permissions.isAdmin(sender)) { p.sendMessage("§a[SimpleFactions]§c You do not have access to this command"); diff --git a/src/main/java/net/tfminecraft/simplefactions/managers/MercenaryCommandManager.java b/src/main/java/net/tfminecraft/simplefactions/managers/MercenaryCommandManager.java index 3b367719..8333d344 100644 --- a/src/main/java/net/tfminecraft/simplefactions/managers/MercenaryCommandManager.java +++ b/src/main/java/net/tfminecraft/simplefactions/managers/MercenaryCommandManager.java @@ -219,8 +219,18 @@ private void admin(Player p, String[] args) { p.sendMessage("§a[SimpleFactions]§c You do not have access to this command"); return; } + if (args.length >= 2 && args[1].equalsIgnoreCase("add")) { + if (args.length < 4) { + p.sendMessage("§cUsage: §e/company admin add "); + return; + } + MercenaryCompany company = resolveCompany(args[2]); + p.sendMessage(company == null ? "§cNo company by that name." + : StaffJoins.company(company, String.join(" ", java.util.Arrays.copyOfRange(args, 3, args.length)))); + return; + } if (args.length < 3) { - p.sendMessage("§cUsage: §e/company admin give|take [amount]"); + p.sendMessage("§cUsage: §e/company admin give|take|add [amount|character]"); return; } boolean give; diff --git a/src/main/java/net/tfminecraft/simplefactions/managers/StaffJoins.java b/src/main/java/net/tfminecraft/simplefactions/managers/StaffJoins.java new file mode 100644 index 00000000..0116291d --- /dev/null +++ b/src/main/java/net/tfminecraft/simplefactions/managers/StaffJoins.java @@ -0,0 +1,108 @@ +package net.tfminecraft.simplefactions.managers; + +import java.util.List; +import java.util.UUID; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import net.tfminecraft.simplefactions.guild.Guild; +import net.tfminecraft.simplefactions.guild.HeldRealms; +import net.tfminecraft.simplefactions.identity.Members; +import net.tfminecraft.simplefactions.mercenary.MercenaryResult; +import net.tfminecraft.simplefactions.mercenary.company.MercenaryCompanies; +import net.tfminecraft.simplefactions.mercenary.company.MercenaryCompany; +import net.tfminecraft.simplefactions.mercenary.company.MercenaryCompanyService; +import net.tfminecraft.simplefactions.mercenary.company.MercenaryEligibility; +import net.tfminecraft.simplefactions.objects.Faction; +import net.tfminecraft.simplefactions.war.battle.campaign.warband.CampaignWarbandSignupService; +import net.tfminecraft.simplefactions.war.battle.warband.Warband; +import net.tfminecraft.tlibs.character.CharacterInfo; + +/** + * Staff tools that put a character straight into a guild, mercenary company or + * warband. They skip the invite and nothing else: every other joining rule + * still applies. Each returns the line to show the staff member. + */ +public final class StaffJoins { + private StaffJoins() {} + + static String noMatch(String input) { + return "§cNo living character matches " + input + "§c; use an online character's name or a character ID"; + } + + /** Typed character IDs reach offline and dead characters too, so only the living pass. */ + private static String living(String input) { + String key = Members.findOnline(input); + return key != null && Members.info(key).map(CharacterInfo::alive).orElse(false) ? key : null; + } + + /** {@code /guild forcejoin}: a stranger or a member of the base guild joins a guild of the same realm. */ + public static String guild(Guild guild, String input) { + if (guild.isBase()) return "§cThis is the base guild, use /faction forcejoin instead"; + Faction realm = guild.getFaction(); + if (HeldRealms.isClosing(realm)) return HeldRealms.CLOSING; + String key = living(input); + if (key == null) return noMatch(input); + if (guild.isMember(key)) return "§c" + Members.display(key) + " §cis already in " + guild.getName(); + Guild current = FactionManager.getGuildByMember(key); + if (current != null && current.getFaction() != realm) { + return "§c" + Members.display(key) + " §cbelongs to " + current.getFaction().getName() + + "§c, another faction"; + } + if (current != null && !current.isBase()) { + return "§c" + Members.display(key) + " §cis already in the guild " + current.getName(); + } + if (current != null && current.isLeader(key)) { + return "§c" + Members.display(key) + " §cleads the faction and cannot join a guild"; + } + guild.addMember(key); + realm.updatePrestige(); + Members.online(key).ifPresent(joiner -> joiner.sendMessage("§aStaff added you to the guild " + guild.getName())); + // The whole realm hears of it, as with an accepted invite. + List realmMembers = realm.getMembers(); + for (Player online : Bukkit.getOnlinePlayers()) { + String member = Members.key(online); + if (member != null && !member.equals(key) && realmMembers.contains(member)) { + online.sendMessage("§a" + Members.display(key) + " joined the guild " + guild.getName() + "§a!"); + } + } + return "§aAdded " + Members.display(key) + " §ato " + guild.getName(); + } + + /** {@code /company admin add}: enlists a character in a free slot. */ + public static String company(MercenaryCompany company, String input) { + String key = living(input); + if (key == null) return noMatch(input); + // The service words these two refusals for the joining player, so staff get their own. + if (!MercenaryEligibility.canJoin(key)) { + return "§c" + Members.display(key) + " §cdoes not have the mercenary trait, so cannot enlist"; + } + MercenaryCompany current = MercenaryCompanies.findByMember(key); + if (current != null) return "§c" + Members.display(key) + " §calready serves in " + current.getName(); + MercenaryResult result = MercenaryCompanyService.join(company, key); + if (!result.ok()) return "§cCannot enlist " + Members.display(key) + ": " + result.message(); + Members.online(key).ifPresent(joiner -> joiner.sendMessage("§aStaff enlisted you in " + company.getName() + ".")); + Members.online(company.getLeader()).ifPresent(leader -> + leader.sendMessage("§a" + Members.display(key) + " signed on with the company.")); + return "§aEnlisted " + Members.display(key) + " §ain " + company.getName(); + } + + /** {@code /warband forcejoin}: signs a character up as if the warband were open to them. */ + public static String warband(Warband warband, String input) { + String key = living(input); + if (key == null) return noMatch(input); + UUID account = Members.accountId(key).orElse(null); + if (account == null) return noMatch(input); + if (warband.hasMember(account)) return "§c" + Members.display(key) + " §cis already in " + warband.getId(); + Faction faction = FactionManager.getByMember(key); + if (warband.isFaction() && faction == null) { + return "§cThis is a faction warband, " + Members.display(key) + " §cneeds a faction to join"; + } + Player online = Members.player(key); + String failure = CampaignWarbandSignupService.signupMember(account, key, warband, faction, online); + if (failure != null) return "§cCannot add " + Members.display(key) + ": " + failure; + if (online != null) online.sendMessage("§aStaff added you to the warband " + warband.getId()); + return "§aAdded " + Members.display(key) + " §ato " + warband.getId(); + } +} diff --git a/src/main/java/net/tfminecraft/simplefactions/utils/TabCompletion.java b/src/main/java/net/tfminecraft/simplefactions/utils/TabCompletion.java index e4caf1fa..16bc2eb4 100644 --- a/src/main/java/net/tfminecraft/simplefactions/utils/TabCompletion.java +++ b/src/main/java/net/tfminecraft/simplefactions/utils/TabCompletion.java @@ -69,10 +69,11 @@ private List completeCompany(Player p, String[] args) { if(args.length == 2 && args[0].equalsIgnoreCase("admin") && Permissions.isAdmin(p)) { completions.add("give"); completions.add("take"); + completions.add("add"); return filtered(completions, args[1]); } if(args.length == 3 && args[0].equalsIgnoreCase("admin") && Permissions.isAdmin(p) - && (args[1].equalsIgnoreCase("give") || args[1].equalsIgnoreCase("take"))) { + && (args[1].equalsIgnoreCase("give") || args[1].equalsIgnoreCase("take") || args[1].equalsIgnoreCase("add"))) { for(Guild g : FactionManager.getAllGuilds()) { if(g == null || g.getCompany() == null) continue; if(g.getCompany().getName() != null && !g.getCompany().getName().contains(" ")) { @@ -82,6 +83,9 @@ private List completeCompany(Player p, String[] args) { } return filtered(completions, args[2]); } + if(args.length >= 4 && args[0].equalsIgnoreCase("admin") && args[1].equalsIgnoreCase("add") && Permissions.isAdmin(p)) { + return Members.completions(onlineCharacters(), java.util.Arrays.copyOfRange(args, 2, args.length)); + } if(args.length == 4 && args[0].equalsIgnoreCase("admin") && Permissions.isAdmin(p)) { return completions; @@ -130,6 +134,16 @@ private List completeMercenaries(String[] args) { return completions; } + /** Names of the characters being played right now. */ + static List onlineCharacters() { + List names = new ArrayList<>(); + for(Player online : Bukkit.getOnlinePlayers()) { + String key = Members.key(online); + if(key != null) names.add(Members.display(key)); + } + return names; + } + /** Drops the subcommand, so a member name that starts at the third word completes like one at the second. */ private static String[] afterFirst(String[] args) { return java.util.Arrays.copyOfRange(args, 1, args.length); @@ -276,6 +290,7 @@ else if(cmd.getName().equalsIgnoreCase("guild") && args.length >= 0 && args.leng completions.add("dummify"); completions.add("dummyleader"); completions.add("forceleader"); + completions.add("forcejoin"); if(Cache.provincesEnabled) completions.add("forceremovecapital"); } return completions; @@ -651,6 +666,21 @@ else if(cmd.getName().equalsIgnoreCase("faction") && args.length >= 0 && args.le if(g != null && !g.isBase() && g.hasCapital() && g.getId().toLowerCase().startsWith(args[1].toLowerCase())) completions.add(g.getId()); } return completions; + } else if(cmd.getName().equalsIgnoreCase("guild") && args.length >= 2 && args[0].equalsIgnoreCase("forcejoin")){ + if(args.length > 2) { + Guild g = FactionManager.getGuildByString(args[1]); + List names = new ArrayList(); + for(Player online : Bukkit.getOnlinePlayers()) { + String key = Members.key(online); + if(g != null && key != null && !g.isMember(key)) names.add(Members.display(key)); + } + return Members.completions(names, afterFirst(args)); + } + List completions = new ArrayList(); + for(Guild g : FactionManager.getAllGuilds()) { + if(g != null && !g.isBase() && g.getId().toLowerCase().startsWith(args[1].toLowerCase())) completions.add(g.getId()); + } + return completions; } else if(cmd.getName().equalsIgnoreCase("guild") && args.length == 2 && args[0].equalsIgnoreCase("forceleader")){ List completions = new ArrayList(); for(Faction f : FactionManager.factions) { diff --git a/src/main/java/net/tfminecraft/simplefactions/war/battle/ui/BattleCommandManager.java b/src/main/java/net/tfminecraft/simplefactions/war/battle/ui/BattleCommandManager.java index 1c7e63b9..ce8f484e 100644 --- a/src/main/java/net/tfminecraft/simplefactions/war/battle/ui/BattleCommandManager.java +++ b/src/main/java/net/tfminecraft/simplefactions/war/battle/ui/BattleCommandManager.java @@ -135,6 +135,16 @@ public boolean onCommand(CommandSender sender, Command cmd, String label, String BattlePersistenceService.deleteWarband(w); p.sendMessage("§aWarband "+w.getId()+" §adeleted!"); return true; + } else if(cmd.getName().equalsIgnoreCase(cmd1) && args[0].equalsIgnoreCase("forcejoin") && args.length >= 3) { + if(!BattlePermissions.isAdmin(sender)) { + p.sendMessage("§a[Battle]§4You do not have access to this command"); + return true; + } + Warband w = WarbandManager.getByString(args[1]); + p.sendMessage(w == null ? "§cWarband not found" + : net.tfminecraft.simplefactions.managers.StaffJoins.warband(w, + String.join(" ", java.util.Arrays.copyOfRange(args, 2, args.length)))); + return true; } else if(cmd.getName().equalsIgnoreCase(cmd1) && args[0].equalsIgnoreCase("toggleopen") && args.length == 1) { Warband w = WarbandManager.getByLeader(p); if(w == null) { 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 d2e177a7..f906f543 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 @@ -55,6 +55,7 @@ private List complete(CommandSender sender, Command cmd, String[] args) completions.add("toggleopen"); completions.add("leave"); completions.add("retreat"); + if(BattlePermissions.isAdmin(sender)) completions.add("forcejoin"); return completions; } } else if(cmd.getName().equalsIgnoreCase("warband") && args.length == 2 && args[0].equalsIgnoreCase("create")){ @@ -84,6 +85,16 @@ private List complete(CommandSender sender, Command cmd, String[] args) band, faction, player, CampaignClock.now()) == null) ids.add(band.getId()); } return ids; + } else if(cmd.getName().equalsIgnoreCase("warband") && args.length >= 2 && args[0].equalsIgnoreCase("forcejoin")) { + if(!BattlePermissions.isAdmin(sender)) return List.of(); + if(args.length == 2) return WarbandManager.get().stream().map(Warband::getId).toList(); + List names = new ArrayList<>(); + for(Player online : Bukkit.getOnlinePlayers()) { + if(Members.key(online) != null && WarbandManager.getByMemberId(online.getUniqueId()) == null) { + names.add(Members.name(online)); + } + } + return Members.completions(names, java.util.Arrays.copyOfRange(args, 1, args.length)); } else if(cmd.getName().equalsIgnoreCase("warband") && args.length == 2 && args[0].equalsIgnoreCase("invite")){ if(sender instanceof Player){ List completions = new ArrayList(); diff --git a/src/test/java/net/tfminecraft/simplefactions/diplomacy/coalition/CoalitionCommandTest.java b/src/test/java/net/tfminecraft/simplefactions/diplomacy/coalition/CoalitionCommandTest.java index a2fe5101..c3d85756 100644 --- a/src/test/java/net/tfminecraft/simplefactions/diplomacy/coalition/CoalitionCommandTest.java +++ b/src/test/java/net/tfminecraft/simplefactions/diplomacy/coalition/CoalitionCommandTest.java @@ -217,7 +217,7 @@ void staffToolsNeedThePermission() { Coalition league = league(); run(staff, "admin"); - verify(staff).sendMessage("§cUsage: /coalition admin "); + verify(staff).sendMessage("§cUsage: /coalition admin "); run(staff, "admin", "offensive", "maybe", "Grand League"); verify(staff).sendMessage("§cUsage: /coalition admin offensive "); run(staff, "admin", "offensive", "on"); @@ -234,6 +234,9 @@ void staffToolsNeedThePermission() { assertFalse(league.isMember("bog")); run(staff, "admin", "setleader", "fig", "Grand League"); assertTrue(league.isLeader("fig")); + run(staff, "admin", "add", "bog", "Grand League"); + assertTrue(league.isMember("bog")); + verify(staff).sendMessage("§aAdded bog §ato the Grand League §acoalition."); run(staff, "admin", "disband"); verify(staff).sendMessage("§cUsage: /coalition admin disband "); @@ -276,6 +279,7 @@ void tabCompletionOffersWhatEachStepTakes() { assertTrue(tabs.onTabComplete(tom, command, "coalition", new String[] {"invite", "bog", ""}).isEmpty()); assertTrue(tabs.onTabComplete(tom, command, "coalition", new String[] {"admin", ""}).isEmpty()); assertEquals(List.of("cost"), tabs.onTabComplete(staff, command, "coalition", new String[] {"admin", "co"})); + assertEquals(List.of("fig"), tabs.onTabComplete(staff, command, "coalition", new String[] {"admin", "add", "f"})); assertEquals(List.of("fig"), tabs.onTabComplete(staff, command, "coalition", new String[] {"admin", "cost", "bog", "f"})); assertEquals(List.of("on", "off"), tabs.onTabComplete(staff, command, "coalition", new String[] {"admin", "offensive", ""})); assertEquals(List.of("Grand League"), tabs.onTabComplete(staff, command, "coalition", new String[] {"admin", "offensive", "on", "G"})); diff --git a/src/test/java/net/tfminecraft/simplefactions/diplomacy/coalition/CoalitionServiceTest.java b/src/test/java/net/tfminecraft/simplefactions/diplomacy/coalition/CoalitionServiceTest.java index 80da716a..8bee091c 100644 --- a/src/test/java/net/tfminecraft/simplefactions/diplomacy/coalition/CoalitionServiceTest.java +++ b/src/test/java/net/tfminecraft/simplefactions/diplomacy/coalition/CoalitionServiceTest.java @@ -332,6 +332,24 @@ void staffCanDisbandRemoveAndAppoint() { assertNull(CoalitionRegistry.cooldownUntil("fig")); } + @Test + void staffAddWithoutAnInviteButUnderEveryOtherRule() { + CoalitionService.create(thalendor, "League", holy); + Coalition league = CoalitionService.of(thalendor); + assertEquals(CoalitionService.NO_FACTION, CoalitionService.adminAdd(league, null).message()); + CoalitionSettings.enabled = false; + assertEquals(CoalitionService.DISABLED, CoalitionService.adminAdd(league, bog).message()); + CoalitionSettings.enabled = true; + assertEquals("§cOnly independent factions can be in a coalition.", CoalitionService.adminAdd(league, sanctuary).message()); + assertEquals("§cthalendor §cis already in a coalition.", CoalitionService.adminAdd(league, thalendor).message()); + + CoalitionService.Result added = CoalitionService.adminAdd(league, bog); + assertEquals("§aAdded bog §ato the League §acoalition.", added.message()); + assertTrue(league.isMember("bog")); + assertNull(league.getSingleMemberSince()); + verify(bea).sendMessage("§aStaff added you to the League §acoalition."); + } + @Test void alliancesAcrossACoalitionAndItsTargetAreRefused() { Coalition league = league(); diff --git a/src/test/java/net/tfminecraft/simplefactions/managers/CommandManagerCoverageTest.java b/src/test/java/net/tfminecraft/simplefactions/managers/CommandManagerCoverageTest.java index e4935f28..39a67cd7 100644 --- a/src/test/java/net/tfminecraft/simplefactions/managers/CommandManagerCoverageTest.java +++ b/src/test/java/net/tfminecraft/simplefactions/managers/CommandManagerCoverageTest.java @@ -1010,6 +1010,20 @@ void decliningRemovesTheInviteAndTellsAnOnlineLeader(String name) { verify(guild, never()).addMember(anyString()); } + @Test + void forceJoinPutsACharacterInAGuildForAdminsOnly() { + try (MockedStatic joins = mockStatic(StaffJoins.class)) { + joins.when(() -> StaffJoins.guild(guild, "Big Bob")).thenReturn("joined"); + assertTrue(run("guild", "forcejoin", "merchants", "Big", "Bob")); + verify(player).sendMessage(contains("do not have access")); + admin(); + assertTrue(run("guild", "forcejoin", "missing", "Bob")); + verify(player).sendMessage("§cNo guild by the id missing"); + assertTrue(run("guild", "forcejoin", "merchants", "Big", "Bob")); + verify(player).sendMessage("joined"); + } + } + @Test void forceLeaderHandsAGuildToAnExistingMemberForAdminsOnly() { assertTrue(run("guild", "forceleader", "merchants", "alice")); diff --git a/src/test/java/net/tfminecraft/simplefactions/managers/MercenaryCommandsCoverageTest.java b/src/test/java/net/tfminecraft/simplefactions/managers/MercenaryCommandsCoverageTest.java index 45da1e52..7f3f0139 100644 --- a/src/test/java/net/tfminecraft/simplefactions/managers/MercenaryCommandsCoverageTest.java +++ b/src/test/java/net/tfminecraft/simplefactions/managers/MercenaryCommandsCoverageTest.java @@ -446,6 +446,20 @@ void administrativeSlotChangesValidatePermissionTargetAndPositiveAmount(String s verify(captain, atLeastOnce()).sendMessage(anyString()); } + @Test + void staffEnlistACharacterNamedAfterTheCompany() { + when(captain.hasPermission(Permissions.Permission_Admin)).thenReturn(true); + try (MockedStatic joins = mockStatic(StaffJoins.class)) { + joins.when(() -> StaffJoins.company(company, "Big Bob")).thenReturn("enlisted"); + run(captain, "admin", "add", guild.getId()); + verify(captain).sendMessage("§cUsage: §e/company admin add "); + run(captain, "admin", "add", "missing", "Bob"); + verify(captain).sendMessage("§cNo company by that name."); + run(captain, "admin", "ADD", guild.getId(), "Big", "Bob"); + verify(captain).sendMessage("enlisted"); + } + } + @Test void marketCommandOpensTheHallListsAvailabilityAndChecksCompanyLookup() { when(command.getName()).thenReturn("mercenaries"); diff --git a/src/test/java/net/tfminecraft/simplefactions/managers/StaffJoinsTest.java b/src/test/java/net/tfminecraft/simplefactions/managers/StaffJoinsTest.java new file mode 100644 index 00000000..7e94eda6 --- /dev/null +++ b/src/test/java/net/tfminecraft/simplefactions/managers/StaffJoinsTest.java @@ -0,0 +1,208 @@ +package net.tfminecraft.simplefactions.managers; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.CALLS_REAL_METHODS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.time.Instant; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +import net.tfminecraft.simplefactions.guild.Guild; +import net.tfminecraft.simplefactions.guild.HeldRealms; +import net.tfminecraft.simplefactions.identity.Members; +import net.tfminecraft.simplefactions.mercenary.MercenaryResult; +import net.tfminecraft.simplefactions.mercenary.company.MercenaryCompanies; +import net.tfminecraft.simplefactions.mercenary.company.MercenaryCompany; +import net.tfminecraft.simplefactions.mercenary.company.MercenaryCompanyService; +import net.tfminecraft.simplefactions.mercenary.company.MercenaryEligibility; +import net.tfminecraft.simplefactions.objects.Faction; +import net.tfminecraft.simplefactions.objects.handler.GuildHandler; +import net.tfminecraft.simplefactions.war.battle.campaign.warband.CampaignWarbandSignupService; +import net.tfminecraft.simplefactions.war.battle.warband.Warband; +import net.tfminecraft.tlibs.character.CharacterInfo; +import net.tfminecraft.tlibs.character.CharacterRef; +import net.tfminecraft.tlibs.character.CharacterStatus; + +class StaffJoinsTest { + private MockedStatic members; + private MockedStatic factions; + private MockedStatic bukkit; + private Faction realm; + private Guild guild; + private Player bob; + private Player carl; + private Player stranger; + + @BeforeEach + void setUp() { + members = mockStatic(Members.class, CALLS_REAL_METHODS); + factions = mockStatic(FactionManager.class); + bukkit = mockStatic(Bukkit.class); + realm = mock(Faction.class); + when(realm.getName()).thenReturn("Realm"); + guild = mock(Guild.class); + when(guild.getName()).thenReturn("Merchants"); + when(guild.getFaction()).thenReturn(realm); + bob = online("Bob"); + carl = online("Carl"); + stranger = mock(Player.class); + bukkit.when(Bukkit::getOnlinePlayers).thenReturn(List.of(bob, carl, stranger)); + members.when(() -> Members.findOnline(anyString())).thenReturn(null); + members.when(() -> Members.findOnline("Bob")).thenReturn("Bob"); + members.when(() -> Members.findOnline("Ghost")).thenReturn("Ghost"); + members.when(() -> Members.info("Bob")).thenReturn(Optional.of(info("Bob", CharacterStatus.ALIVE))); + members.when(() -> Members.info("Ghost")).thenReturn(Optional.of(info("Ghost", CharacterStatus.DEAD))); + members.when(() -> Members.display(anyString())).thenAnswer(call -> call.getArgument(0)); + } + + @AfterEach + void tearDown() { + MercenaryEligibility.reset(); + bukkit.close(); + factions.close(); + members.close(); + } + + private static CharacterInfo info(String name, CharacterStatus status) { + return new CharacterInfo(new CharacterRef(UUID.randomUUID(), UUID.randomUUID()), name, status, "human", + Set.of(), 0L, Instant.EPOCH); + } + + private Player online(String key) { + Player player = mock(Player.class); + members.when(() -> Members.key(player)).thenReturn(key); + members.when(() -> Members.online(key)).thenReturn(Optional.of(player)); + return player; + } + + @Test + void guildJoinsRefuseTheBaseGuildAClosingRealmAndUnknownCharacters() { + when(guild.isBase()).thenReturn(true); + assertEquals("§cThis is the base guild, use /faction forcejoin instead", StaffJoins.guild(guild, "Bob")); + when(guild.isBase()).thenReturn(false); + GuildHandler handler = mock(GuildHandler.class); + Guild main = mock(Guild.class); + when(realm.getId()).thenReturn("realm"); + when(realm.getGuildHandler()).thenReturn(handler); + when(handler.getGuild("realm")).thenReturn(main); + when(main.getClosingSince()).thenReturn(5L); + assertEquals(HeldRealms.CLOSING, StaffJoins.guild(guild, "Bob")); + when(main.getClosingSince()).thenReturn(0L); + assertEquals(StaffJoins.noMatch("Nobody"), StaffJoins.guild(guild, "Nobody")); + assertEquals(StaffJoins.noMatch("Ghost"), StaffJoins.guild(guild, "Ghost")); + members.when(() -> Members.findOnline("Unknown")).thenReturn("Unknown"); + members.when(() -> Members.info("Unknown")).thenReturn(Optional.empty()); + assertEquals(StaffJoins.noMatch("Unknown"), StaffJoins.guild(guild, "Unknown")); + when(guild.isMember("Bob")).thenReturn(true); + assertEquals("§cBob §cis already in Merchants", StaffJoins.guild(guild, "Bob")); + verify(guild, never()).addMember(anyString()); + } + + @Test + void guildJoinsRefuseOtherRealmsOtherGuildsAndTheRealmLeader() { + Guild current = mock(Guild.class); + factions.when(() -> FactionManager.getGuildByMember("Bob")).thenReturn(current); + Faction other = mock(Faction.class); + when(other.getName()).thenReturn("Other"); + when(current.getFaction()).thenReturn(other); + assertEquals("§cBob §cbelongs to Other§c, another faction", StaffJoins.guild(guild, "Bob")); + when(current.getFaction()).thenReturn(realm); + when(current.getName()).thenReturn("Smiths"); + assertEquals("§cBob §cis already in the guild Smiths", StaffJoins.guild(guild, "Bob")); + when(current.isBase()).thenReturn(true); + when(current.isLeader("Bob")).thenReturn(true); + assertEquals("§cBob §cleads the faction and cannot join a guild", StaffJoins.guild(guild, "Bob")); + verify(guild, never()).addMember(anyString()); + } + + @Test + void guildJoinsAddTheCharacterAndTellTheGuild() { + assertEquals("§aAdded Bob §ato Merchants", StaffJoins.guild(guild, "Bob")); + verify(guild).addMember("Bob"); + verify(realm).updatePrestige(); + verify(bob).sendMessage("§aStaff added you to the guild Merchants"); + verify(carl, never()).sendMessage(anyString()); + + Guild base = mock(Guild.class); + when(base.getFaction()).thenReturn(realm); + when(base.isBase()).thenReturn(true); + factions.when(() -> FactionManager.getGuildByMember("Bob")).thenReturn(base); + // Carl is in the realm's base guild, not in Merchants, and still hears of it. + when(realm.getMembers()).thenReturn(List.of("Bob", "Carl")); + assertEquals("§aAdded Bob §ato Merchants", StaffJoins.guild(guild, "Bob")); + verify(carl).sendMessage("§aBob joined the guild Merchants§a!"); + verify(bob, never()).sendMessage("§aBob joined the guild Merchants§a!"); + verify(stranger, never()).sendMessage(anyString()); + } + + @Test + void companyJoinsUseTheCompanyRules() { + try (var service = mockStatic(MercenaryCompanyService.class); var companies = mockStatic(MercenaryCompanies.class)) { + MercenaryCompany company = mock(MercenaryCompany.class); + when(company.getName()).thenReturn("Black Lances"); + when(company.getLeader()).thenReturn("Carl"); + assertEquals(StaffJoins.noMatch("Nobody"), StaffJoins.company(company, "Nobody")); + MercenaryEligibility.setProbe(member -> MercenaryEligibility.Status.INELIGIBLE); + assertEquals("§cBob §cdoes not have the mercenary trait, so cannot enlist", StaffJoins.company(company, "Bob")); + MercenaryEligibility.setProbe(MercenaryEligibility.Probe.OPEN); + MercenaryCompany other = mock(MercenaryCompany.class); + when(other.getName()).thenReturn("Red Hand"); + companies.when(() -> MercenaryCompanies.findByMember("Bob")).thenReturn(other); + assertEquals("§cBob §calready serves in Red Hand", StaffJoins.company(company, "Bob")); + companies.when(() -> MercenaryCompanies.findByMember("Bob")).thenReturn(null); + service.when(() -> MercenaryCompanyService.join(company, "Bob")) + .thenReturn(MercenaryResult.deny("Every slot is already filled.")); + assertEquals("§cCannot enlist Bob: Every slot is already filled.", StaffJoins.company(company, "Bob")); + service.when(() -> MercenaryCompanyService.join(company, "Bob")).thenReturn(MercenaryResult.ok("Joined.")); + assertEquals("§aEnlisted Bob §ain Black Lances", StaffJoins.company(company, "Bob")); + verify(bob).sendMessage("§aStaff enlisted you in Black Lances."); + verify(carl).sendMessage("§aBob signed on with the company."); + } + } + + @Test + void warbandJoinsSignTheCharacterUp() { + try (var signup = mockStatic(CampaignWarbandSignupService.class)) { + Warband warband = mock(Warband.class); + when(warband.getId()).thenReturn("vanguard"); + UUID account = UUID.randomUUID(); + assertEquals(StaffJoins.noMatch("Nobody"), StaffJoins.warband(warband, "Nobody")); + members.when(() -> Members.accountId("Bob")).thenReturn(Optional.empty()); + assertEquals(StaffJoins.noMatch("Bob"), StaffJoins.warband(warband, "Bob")); + members.when(() -> Members.accountId("Bob")).thenReturn(Optional.of(account)); + when(warband.hasMember(account)).thenReturn(true); + assertEquals("§cBob §cis already in vanguard", StaffJoins.warband(warband, "Bob")); + when(warband.hasMember(account)).thenReturn(false); + when(warband.isFaction()).thenReturn(true); + assertEquals("§cThis is a faction warband, Bob §cneeds a faction to join", StaffJoins.warband(warband, "Bob")); + factions.when(() -> FactionManager.getByMember("Bob")).thenReturn(realm); + signup.when(() -> CampaignWarbandSignupService.signupMember(account, "Bob", warband, realm, bob)) + .thenReturn("Warband identity is held for staff review"); + assertEquals("§cCannot add Bob: Warband identity is held for staff review", StaffJoins.warband(warband, "Bob")); + signup.when(() -> CampaignWarbandSignupService.signupMember(account, "Bob", warband, realm, bob)).thenReturn(null); + assertEquals("§aAdded Bob §ato vanguard", StaffJoins.warband(warband, "Bob")); + verify(bob).sendMessage("§aStaff added you to the warband vanguard"); + + when(warband.isFaction()).thenReturn(false); + members.when(() -> Members.online("Bob")).thenReturn(Optional.empty()); + signup.when(() -> CampaignWarbandSignupService.signupMember(any(), any(), any(), any(), any())).thenReturn(null); + assertEquals("§aAdded Bob §ato vanguard", StaffJoins.warband(warband, "Bob")); + } + } +} diff --git a/src/test/java/net/tfminecraft/simplefactions/managers/TabCompletionCoverageTest.java b/src/test/java/net/tfminecraft/simplefactions/managers/TabCompletionCoverageTest.java index 7e7d139b..b5303a2a 100644 --- a/src/test/java/net/tfminecraft/simplefactions/managers/TabCompletionCoverageTest.java +++ b/src/test/java/net/tfminecraft/simplefactions/managers/TabCompletionCoverageTest.java @@ -17,6 +17,7 @@ import net.tfminecraft.simplefactions.espionage.EspionageCommands; import net.tfminecraft.simplefactions.espionage.EspionageConfig; import net.tfminecraft.simplefactions.guild.Guild; +import net.tfminecraft.simplefactions.identity.Members; import net.tfminecraft.simplefactions.installation.Installation; import net.tfminecraft.simplefactions.installation.InstallationConstruction; import net.tfminecraft.simplefactions.installation.handler.InstallationHandler; @@ -95,7 +96,7 @@ void close() { } @ParameterizedTest - @ValueSource(strings = {"guild", "unrelated"}) + @ValueSource(strings = {"unrelated"}) void factionForceJoinSuggestionsAreNotReturnedForOtherCommands(String command) { admin(); assertTrue(complete(command, "forcejoin", "").isEmpty()); @@ -339,7 +340,7 @@ void companyAdminCompletesOnlyExistingCompaniesAndRecognizedOperations() { factions .when(FactionManager::getAllGuilds) .thenReturn(Arrays.asList(null, mock(Guild.class), guild, unnamed)); - assertEquals(List.of("give", "take"), complete("company", "admin", "")); + assertEquals(List.of("give", "take", "add"), complete("company", "admin", "")); assertEquals( List.of("merchants", "unnamed"), complete("company", "admin", "give", "")); @@ -964,6 +965,44 @@ void guildForceLeaderSuggestsSubGuildsThenTheirOtherMembersForAdmins() { assertEquals(List.of(), complete("guild", "forceleader", "missing", "")); } + @Test + void guildForceJoinSuggestsNonBaseGuildsThenOnlineCharactersOutsideIt() { + Guild base = mock(Guild.class); + when(base.getId()).thenReturn("mainguild"); + when(base.isBase()).thenReturn(true); + factions.when(FactionManager::getAllGuilds).thenReturn(Arrays.asList(guild, null, base)); + factions.when(() -> FactionManager.getGuildByString("merchants")).thenReturn(guild); + Player alice = gui.player("Alice"), bob = gui.player("Bob"), nobody = gui.player("Nobody"); + when(Bukkit.getOnlinePlayers()).thenAnswer(call -> List.of(alice, bob, nobody)); + CharacterKeyFixture keys = CharacterKeyFixture.install(); + keys.play(alice, "Alice Smith"); + keys.play(bob, "Bob Smith"); + when(guild.isMember(Members.key(alice))).thenReturn(true); + assertFalse(complete("guild").contains("forcejoin")); + admin(); + assertTrue(complete("guild").contains("forcejoin")); + assertEquals(List.of("merchants"), complete("guild", "forcejoin", "M")); + assertEquals(List.of("Bob Smith"), complete("guild", "forcejoin", "merchants", "")); + assertEquals(List.of("Smith"), complete("guild", "forcejoin", "merchants", "Bob", "")); + assertEquals(List.of(), complete("guild", "forcejoin", "missing", "")); + } + + @Test + void companyAdminAddSuggestsCompaniesThenOnlineCharacters() { + admin(); + MercenaryCompany company = mock(MercenaryCompany.class); + when(company.getName()).thenReturn("Lances"); + when(guild.getCompany()).thenReturn(company); + factions.when(FactionManager::getAllGuilds).thenReturn(Arrays.asList(guild)); + Player alice = gui.player("Alice"), nobody = gui.player("Nobody"); + when(Bukkit.getOnlinePlayers()).thenAnswer(call -> List.of(alice, nobody)); + CharacterKeyFixture.install().play(alice, "Alice Smith"); + assertEquals(List.of("add"), complete("company", "admin", "a")); + assertEquals(List.of("Lances", "merchants"), complete("company", "admin", "add", "")); + assertEquals(List.of("Alice Smith"), complete("company", "admin", "add", "Lances", "al")); + assertEquals(List.of("Smith"), complete("company", "admin", "add", "Lances", "Alice", "S")); + } + private Faction realm(String id, String name) { Faction faction = mock(Faction.class); when(faction.getId()).thenReturn(id); 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 9a9be8b2..5991a8dc 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 @@ -51,6 +51,7 @@ import org.junit.jupiter.params.provider.ValueSource; import org.mockito.MockedConstruction; import org.mockito.MockedStatic; +import net.tfminecraft.simplefactions.managers.StaffJoins; class BattleCommandsCoverageTest { private GuiTestFixture ui; @@ -214,6 +215,35 @@ void battleIdsAreOfferedForEveryAdministrationCommandThatTakesOne() { } } + @Test + void staffForceACharacterIntoAWarband() { + try (MockedStatic joins = mockStatic(StaffJoins.class)) { + Warband band = band("scouts", player("Bob")); + joins.when(() -> StaffJoins.warband(band, "Big Carl")).thenReturn("added"); + assertTrue(run("warband", "forcejoin", "scouts", "Big", "Carl")); + verify(alice).sendMessage("§a[Battle]§4You do not have access to this command"); + admin(); + assertTrue(run("warband", "forcejoin", "missing", "Carl")); + verify(alice).sendMessage("§cWarband not found"); + assertTrue(run("warband", "forcejoin", "scouts", "Big", "Carl")); + verify(alice).sendMessage("added"); + } + } + + @Test + void staffForceJoinSuggestsWarbandsThenCharactersOutsideAnyWarband() { + Player bob = player("Bob"); + player("Carl"); + band("scouts", bob); + assertFalse(complete(alice, "warband", "").contains("forcejoin")); + assertTrue(complete(alice, "warband", "forcejoin", "").isEmpty()); + admin(); + assertTrue(complete(alice, "warband", "").contains("forcejoin")); + assertEquals(List.of("scouts"), complete(alice, "warband", "forcejoin", "s")); + assertEquals(List.of("Carl"), complete(alice, "warband", "forcejoin", "scouts", "C")); + assertFalse(complete(alice, "warband", "forcejoin", "scouts", "").contains("Bob")); + } + @ParameterizedTest @ValueSource(strings = {"delete", "toggleopen"}) void successfulWarbandCommandsDoNotFallThroughIntoUsageErrors(String operation) {