From d5b129bda06191491567604d31a8521da3b20e3c Mon Sep 17 00:00:00 2001 From: Ryan Barlow <7389646+ryanbarlow97@users.noreply.github.com> Date: Fri, 25 Sep 2026 21:42:25 +0000 Subject: [PATCH 1/2] feat: let characters opt out of mail recipients --- README.md | 1 + .../rpcharacters/command/CharCommand.java | 27 ++++- .../rpcharacters/database/Database.java | 2 + .../mail/MailRecipientDirectory.java | 21 +++- .../rpcharacters/objects/RPCharacter.java | 9 ++ .../utils/CommandTabCompleter.java | 4 +- .../command/CharacterMailCommandTest.java | 60 +++++++++++ .../mail/MailRecipientDirectoryTest.java | 102 ++++++++++++++++++ 8 files changed, 219 insertions(+), 7 deletions(-) create mode 100644 src/test/java/net/tfminecraft/rpcharacters/command/CharacterMailCommandTest.java create mode 100644 src/test/java/net/tfminecraft/rpcharacters/mail/MailRecipientDirectoryTest.java diff --git a/README.md b/README.md index 49c7138..55571d1 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ Beyond a name and appearance, characters have traits, professions, injuries, and - **Character profiles** — create and switch between characters, with race, traits, descriptions, and website-connected creation. - **Roleplay conversation** — use local speech, whispers, shouts, actions, and out-of-character channels, with speech bubbles and channel preferences. - **Identity and disguise** — show character identities in social interactions and support masks and alternate personas. +- **Mail recipient visibility** — `/rpcharacter mail` toggles whether your active character appears in BirdMessenger’s recipient list; `/rpcharacter mail off` hides it and `/rpcharacter mail on` restores it. Characters are listed by default, and the setting persists across logouts and restarts. Already-sent mail still arrives. - **Character focus** — a shared, regenerating per-character resource used by Research and Magic. - **Progression and rolls** — bring professions, attributes, and dice rolls into character gameplay. - **Injuries and recovery** — represent injuries and prosthetics, with related treatment and progression systems. diff --git a/src/main/java/net/tfminecraft/rpcharacters/command/CharCommand.java b/src/main/java/net/tfminecraft/rpcharacters/command/CharCommand.java index 02123fe..ecdadc1 100644 --- a/src/main/java/net/tfminecraft/rpcharacters/command/CharCommand.java +++ b/src/main/java/net/tfminecraft/rpcharacters/command/CharCommand.java @@ -32,7 +32,7 @@ public final class CharCommand { private static final Set SUBCOMMANDS = Set.of( - "alias", "namecolour", "gender", "description", "profile", "override", "birthday"); + "alias", "namecolour", "gender", "description", "profile", "override", "birthday", "mail"); private static final String FIELD_ALIAS = "alias"; private static final String FIELD_GENDER = "gender"; @@ -47,7 +47,7 @@ public static boolean isPersonaSubcommand(String subcommand) { public static boolean handle(CommandSender sender, String label, String[] args) { if (args.length == 0) { RPTexts.send(sender, RPTexts.WARN + "Usage: /" + label - + " "); + + " "); return true; } @@ -69,6 +69,8 @@ public static boolean handle(CommandSender sender, String label, String[] args) } switch (sub) { + case "mail": + return handleMail(player, label, args); case "alias": return handleAlias(player, label, args); case "namecolour": @@ -81,11 +83,30 @@ public static boolean handle(CommandSender sender, String label, String[] args) return handleBirthday(player, label, args); default: RPTexts.send(player, RPTexts.ERROR - + "Unknown subcommand. Use alias, namecolour, gender, description, profile, or birthday."); + + "Unknown subcommand. Use alias, namecolour, gender, description, profile, birthday, or mail."); return true; } } + private static boolean handleMail(Player player, String label, String[] args) { + if (args.length > 2 || (args.length == 2 + && !args[1].equalsIgnoreCase("on") && !args[1].equalsIgnoreCase("off"))) { + RPTexts.send(player, RPTexts.WARN + "Usage: /" + label + " mail [on|off]"); + return true; + } + RPCharacter character = requireActiveCharacter(player); + if (character == null) { + return true; + } + boolean listed = args.length == 1 ? !character.isMailListed() : args[1].equalsIgnoreCase("on"); + character.setMailListed(listed); + RPCharacters.getPlayerManager().savePlayer(player); + RPTexts.send(player, RPTexts.SUCCESS + (listed + ? "Your character is now listed as a mail recipient." + : "Your character is no longer listed as a mail recipient.")); + return true; + } + private static boolean handleAlias(Player player, String label, String[] args) { if (!player.hasPermission(Cache.personaSetPermission)) { RPTexts.send(player, RPTexts.ERROR + "You do not have permission to change your alias."); diff --git a/src/main/java/net/tfminecraft/rpcharacters/database/Database.java b/src/main/java/net/tfminecraft/rpcharacters/database/Database.java index f8761a0..100dfad 100644 --- a/src/main/java/net/tfminecraft/rpcharacters/database/Database.java +++ b/src/main/java/net/tfminecraft/rpcharacters/database/Database.java @@ -682,6 +682,7 @@ private void loadPersonaFields(RPCharacter character, JSONObject characterJson) if (characterJson.containsKey("slug")) { character.setSlug((String) characterJson.get("slug")); } + character.setMailListed(!Boolean.FALSE.equals(characterJson.get("mail-listed"))); if (characterJson.containsKey("hidden")) { character.setHidden(Boolean.parseBoolean(characterJson.get("hidden").toString())); } @@ -786,6 +787,7 @@ private void savePersonaFields(HashMap defaults, RPCharacter cha if (character.getSlug() != null && !character.getSlug().isBlank()) { defaults.put("slug", character.getSlug()); } + defaults.put("mail-listed", character.isMailListed()); if (character.isHidden()) { defaults.put("hidden", "true"); } diff --git a/src/main/java/net/tfminecraft/rpcharacters/mail/MailRecipientDirectory.java b/src/main/java/net/tfminecraft/rpcharacters/mail/MailRecipientDirectory.java index 1709207..25bfc4f 100644 --- a/src/main/java/net/tfminecraft/rpcharacters/mail/MailRecipientDirectory.java +++ b/src/main/java/net/tfminecraft/rpcharacters/mail/MailRecipientDirectory.java @@ -81,7 +81,7 @@ public static void upsert(UUID ownerUuid, RPCharacter character) { if (ownerUuid == null || character == null || character.getId() == null) { return; } - if (character.getStatus() != Status.ALIVE) { + if (character.getStatus() != Status.ALIVE || !character.isMailListed()) { ENTRIES.remove(character.getId()); return; } @@ -89,6 +89,7 @@ public static void upsert(UUID ownerUuid, RPCharacter character) { Entry entry = new Entry(); entry.ownerUuid = ownerUuid; entry.characterId = character.getId(); + entry.mailListed = character.isMailListed(); entry.displayPlain = character.getEffectiveDisplayPlain(); entry.displayTab = DisplayIdentityService.resolveDisplayTab(character); if (character.hasLastLocation()) { @@ -213,6 +214,10 @@ public static List listMailTargets() { if (character == null || character.getId() == null) { continue; } + if (!character.isMailListed()) { + targets.remove(character.getId()); + continue; + } if (targets.containsKey(character.getId())) { continue; } @@ -263,7 +268,7 @@ private static void upsertFromJson(UUID ownerUuid, JSONObject json) { return; } } - if (status != Status.ALIVE) { + if (status != Status.ALIVE || Boolean.FALSE.equals(json.get("mail-listed"))) { ENTRIES.remove(characterId); return; } @@ -279,6 +284,7 @@ private static void upsertFromJson(UUID ownerUuid, JSONObject json) { Entry entry = new Entry(); entry.ownerUuid = ownerUuid; entry.characterId = characterId; + entry.mailListed = true; entry.displayPlain = displayPlain; entry.displayTab = DisplayIdentityService.colourPlain(displayPlain, colour); Object locRaw = json.get("last-location"); @@ -303,6 +309,7 @@ private static Entry fromLive(UUID ownerUuid, RPCharacter character) { Entry entry = new Entry(); entry.ownerUuid = ownerUuid; entry.characterId = character.getId(); + entry.mailListed = character.isMailListed(); entry.displayPlain = character.getEffectiveDisplayPlain(); entry.displayTab = DisplayIdentityService.resolveDisplayTab(character); if (character.hasLastLocation()) { @@ -316,10 +323,16 @@ private static Entry fromLive(UUID ownerUuid, RPCharacter character) { } private static CharacterMailTarget toTarget(Entry entry) { - if (entry == null || entry.characterId == null) { + if (entry == null || entry.characterId == null || !entry.mailListed) { return null; } PlayerData pd = PlayerManager.get(entry.ownerUuid); + if (pd != null) { + RPCharacter character = pd.getCharacterById(entry.characterId); + if (character != null && (character.getStatus() != Status.ALIVE || !character.isMailListed())) { + return null; + } + } Player owner = resolveOwner(entry.ownerUuid, pd); boolean liveActive = false; RPCharacter liveCharacter = null; @@ -390,6 +403,8 @@ private static Location toBukkitLocation(Entry entry) { private static final class Entry { private UUID ownerUuid; private String characterId; + // Texture-only cache entries must not become recipients. + private boolean mailListed; private String displayTab = ""; private String displayPlain = ""; private boolean hasStoredLocation; diff --git a/src/main/java/net/tfminecraft/rpcharacters/objects/RPCharacter.java b/src/main/java/net/tfminecraft/rpcharacters/objects/RPCharacter.java index 871ebf8..4b5e82e 100644 --- a/src/main/java/net/tfminecraft/rpcharacters/objects/RPCharacter.java +++ b/src/main/java/net/tfminecraft/rpcharacters/objects/RPCharacter.java @@ -69,6 +69,7 @@ public class RPCharacter { private String alias; private String slug; private boolean hidden; + private boolean mailListed = true; /** Throwaway character made in-game while dev-characters is on. */ private boolean dev; /** Per grant-kit id. Empty/missing kit = legacy never claim for that kit. */ @@ -507,6 +508,14 @@ public void setSlug(String slug) { this.slug = slug; } + public boolean isMailListed() { + return mailListed; + } + + public void setMailListed(boolean mailListed) { + this.mailListed = mailListed; + } + public boolean isHidden() { return hidden; } diff --git a/src/main/java/net/tfminecraft/rpcharacters/utils/CommandTabCompleter.java b/src/main/java/net/tfminecraft/rpcharacters/utils/CommandTabCompleter.java index 4f265be..82268e4 100644 --- a/src/main/java/net/tfminecraft/rpcharacters/utils/CommandTabCompleter.java +++ b/src/main/java/net/tfminecraft/rpcharacters/utils/CommandTabCompleter.java @@ -27,7 +27,7 @@ public class CommandTabCompleter implements TabCompleter { private static final List PERSONA_SUBCOMMANDS = List.of( - "alias", "namecolour", "gender", "description", "profile", "override", "birthday"); + "alias", "namecolour", "gender", "description", "profile", "override", "birthday", "mail"); private static final List CLEAR = List.of("clear"); private static final List OVERRIDE_FIELDS = List.of( "alias", "tempalias", "gender", "description", "namecolour", "birthday", "playtime"); @@ -361,6 +361,8 @@ private List completePersona(CommandSender sender, String sub, String[] } switch (sub) { + case "mail": + return args.length == 2 ? filter(List.of("on", "off"), args[1]) : Collections.emptyList(); case "alias": case "description": if (args.length == 2) { diff --git a/src/test/java/net/tfminecraft/rpcharacters/command/CharacterMailCommandTest.java b/src/test/java/net/tfminecraft/rpcharacters/command/CharacterMailCommandTest.java new file mode 100644 index 0000000..d727c94 --- /dev/null +++ b/src/test/java/net/tfminecraft/rpcharacters/command/CharacterMailCommandTest.java @@ -0,0 +1,60 @@ +package net.tfminecraft.rpcharacters.command; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.List; + +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.junit.jupiter.api.Test; + +import net.tfminecraft.rpcharacters.RPCharacters; +import net.tfminecraft.rpcharacters.managers.PlayerManager; +import net.tfminecraft.rpcharacters.objects.PlayerData; +import net.tfminecraft.rpcharacters.objects.RPCharacter; +import net.tfminecraft.rpcharacters.utils.CommandTabCompleter; + +class CharacterMailCommandTest { + @Test void togglesOnlyOwnActiveCharacterAndSavesExplicitOnOff() { + Player player = mock(Player.class); + PlayerData data = mock(PlayerData.class); + PlayerManager manager = mock(PlayerManager.class); + RPCharacter character = new RPCharacter(null); + when(data.hasActiveCharacter()).thenReturn(true); + when(data.getActiveCharacter()).thenReturn(character); + try (var players = mockStatic(PlayerManager.class); var rpc = mockStatic(RPCharacters.class)) { + players.when(() -> PlayerManager.get(player)).thenReturn(data); + rpc.when(RPCharacters::getPlayerManager).thenReturn(manager); + assertTrue(CharCommand.isPersonaSubcommand("mail")); + CharCommand.handle(player, "rpcharacter", new String[]{"mail"}); + assertFalse(character.isMailListed()); + CharCommand.handle(player, "rpcharacter", new String[]{"mail", "ON"}); + assertTrue(character.isMailListed()); + CharCommand.handle(player, "rpcharacter", new String[]{"mail", "off"}); + assertFalse(character.isMailListed()); + CharCommand.handle(player, "rpcharacter", new String[]{"mail", "off"}); + assertFalse(character.isMailListed()); + verify(manager, times(4)).savePlayer(player); + CharCommand.handle(player, "rpcharacter", new String[]{"mail", "invalid"}); + CharCommand.handle(player, "rpcharacter", new String[]{"mail", "on", "someone-else"}); + assertFalse(character.isMailListed()); + when(data.hasActiveCharacter()).thenReturn(false); + CharCommand.handle(player, "rpcharacter", new String[]{"mail", "on"}); + verifyNoMoreInteractions(manager); + } + } + + @Test void consoleIsRejectedAndCompletionsExposeOnOff() { + CommandSender console = mock(CommandSender.class); + CharCommand.handle(console, "rpcharacter", new String[]{"mail"}); + verify(console).sendMessage(contains("Only players")); + Command command = mock(Command.class); + when(command.getName()).thenReturn("rpcharacter"); + Player player = mock(Player.class); + CommandTabCompleter completer = new CommandTabCompleter(); + assertEquals(List.of("mail"), completer.onTabComplete(player, command, "rpcharacter", new String[]{"mai"})); + assertEquals(List.of("on", "off"), completer.onTabComplete(player, command, "rpcharacter", new String[]{"mail", ""})); + } +} diff --git a/src/test/java/net/tfminecraft/rpcharacters/mail/MailRecipientDirectoryTest.java b/src/test/java/net/tfminecraft/rpcharacters/mail/MailRecipientDirectoryTest.java new file mode 100644 index 0000000..7e9d8f3 --- /dev/null +++ b/src/test/java/net/tfminecraft/rpcharacters/mail/MailRecipientDirectoryTest.java @@ -0,0 +1,102 @@ +package net.tfminecraft.rpcharacters.mail; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import org.bukkit.Bukkit; +import org.json.simple.JSONObject; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import net.tfminecraft.rpcharacters.database.Database; +import net.tfminecraft.rpcharacters.enums.Status; +import net.tfminecraft.rpcharacters.managers.PlayerManager; +import net.tfminecraft.rpcharacters.objects.PlayerData; +import net.tfminecraft.rpcharacters.objects.RPCharacter; + +class MailRecipientDirectoryTest { + private final UUID owner = UUID.randomUUID(); + private final String id = UUID.randomUUID().toString(); + + @AfterEach void cleanup() { + MailRecipientDirectory.remove(id); + } + + private void loadDirectory(JSONObject json) throws Exception { + Method load = MailRecipientDirectory.class.getDeclaredMethod("upsertFromJson", UUID.class, JSONObject.class); + load.setAccessible(true); + load.invoke(null, owner, json); + } + + @Test void savedOptOutIsRespectedAndWardrobeRefreshCannotReintroduceIt() throws Exception { + JSONObject json = new JSONObject(Map.of("id", id, "name", "Recipient", "status", "ALIVE")); + try (var bukkit = mockStatic(Bukkit.class); var manager = mockStatic(PlayerManager.class)) { + loadDirectory(json); + assertEquals(1, MailRecipientDirectory.listMailTargets().size()); + json.put("mail-listed", false); + loadDirectory(json); + MailRecipientDirectory.updateWardrobeTexture(owner, id, "texture", "signature"); + assertTrue(MailRecipientDirectory.listMailTargets().isEmpty()); + json.put("mail-listed", true); + loadDirectory(json); + assertEquals(1, MailRecipientDirectory.listMailTargets().size()); + json.put("status", "DEAD"); + loadDirectory(json); + MailRecipientDirectory.updateWardrobeTexture(owner, id, "texture", "signature"); + assertTrue(MailRecipientDirectory.listMailTargets().isEmpty()); + } + } + + @Test void liveOptOutOverridesCachedEntryAndOnlineFallback() { + RPCharacter character = new RPCharacter(null); + character.setId(id); + character.setName("Recipient"); + PlayerData data = mock(PlayerData.class); + when(data.getUniqueId()).thenReturn(owner); + when(data.getCharacters(Status.ALIVE)).thenReturn(List.of(character)); + when(data.getCharacterById(id)).thenReturn(character); + try (var bukkit = mockStatic(Bukkit.class); var manager = mockStatic(PlayerManager.class)) { + manager.when(PlayerManager::getOnlineData).thenReturn(List.of(data)); + manager.when(() -> PlayerManager.get(owner)).thenReturn(data); + MailRecipientDirectory.upsert(owner, character); + assertEquals(1, MailRecipientDirectory.listMailTargets().size()); + character.setMailListed(false); + assertTrue(MailRecipientDirectory.listMailTargets().isEmpty()); + MailRecipientDirectory.upsert(owner, character); + assertTrue(MailRecipientDirectory.listMailTargets().isEmpty()); + character.setMailListed(true); + assertEquals(1, MailRecipientDirectory.listMailTargets().size()); + MailRecipientDirectory.upsert(owner, character); + manager.when(PlayerManager::getOnlineData).thenReturn(List.of()); + manager.when(() -> PlayerManager.get(owner)).thenReturn(null); + assertEquals(1, MailRecipientDirectory.listMailTargets().size()); + } + } + + @Test void preferenceRoundTripsThroughCharacterPersistenceAndLegacyDefaultsToListed() throws Exception { + RPCharacter original = new RPCharacter(null); + assertTrue(original.isMailListed()); + Method save = Database.class.getDeclaredMethod("savePersonaFields", HashMap.class, RPCharacter.class); + Method load = Database.class.getDeclaredMethod("loadPersonaFields", RPCharacter.class, JSONObject.class); + save.setAccessible(true); + load.setAccessible(true); + Database database = new Database(); + for (boolean listed : List.of(false, true)) { + original.setMailListed(listed); + HashMap fields = new HashMap<>(); + save.invoke(database, fields, original); + RPCharacter restored = new RPCharacter(null); + load.invoke(database, restored, new JSONObject(fields)); + assertEquals(listed, restored.isMailListed()); + } + original.setMailListed(false); + load.invoke(database, original, new JSONObject()); + assertTrue(original.isMailListed()); + } +} From bc8a5dcefe434a5d475f58a09bb2f75ec7b04ff4 Mon Sep 17 00:00:00 2001 From: Ryan Barlow <7389646+ryanbarlow97@users.noreply.github.com> Date: Fri, 25 Sep 2026 21:51:28 +0000 Subject: [PATCH 2/2] fix: persist mail visibility through character serializer --- .../rpcharacters/database/Database.java | 4 ++-- .../mail/MailRecipientDirectory.java | 2 +- .../mail/MailRecipientDirectoryTest.java | 23 ++++++++++++++++++- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/main/java/net/tfminecraft/rpcharacters/database/Database.java b/src/main/java/net/tfminecraft/rpcharacters/database/Database.java index 100dfad..60e9c63 100644 --- a/src/main/java/net/tfminecraft/rpcharacters/database/Database.java +++ b/src/main/java/net/tfminecraft/rpcharacters/database/Database.java @@ -682,7 +682,7 @@ private void loadPersonaFields(RPCharacter character, JSONObject characterJson) if (characterJson.containsKey("slug")) { character.setSlug((String) characterJson.get("slug")); } - character.setMailListed(!Boolean.FALSE.equals(characterJson.get("mail-listed"))); + character.setMailListed(!"false".equalsIgnoreCase(String.valueOf(characterJson.get("mail-listed")))); if (characterJson.containsKey("hidden")) { character.setHidden(Boolean.parseBoolean(characterJson.get("hidden").toString())); } @@ -787,7 +787,7 @@ private void savePersonaFields(HashMap defaults, RPCharacter cha if (character.getSlug() != null && !character.getSlug().isBlank()) { defaults.put("slug", character.getSlug()); } - defaults.put("mail-listed", character.isMailListed()); + defaults.put("mail-listed", String.valueOf(character.isMailListed())); if (character.isHidden()) { defaults.put("hidden", "true"); } diff --git a/src/main/java/net/tfminecraft/rpcharacters/mail/MailRecipientDirectory.java b/src/main/java/net/tfminecraft/rpcharacters/mail/MailRecipientDirectory.java index 25bfc4f..6058ba2 100644 --- a/src/main/java/net/tfminecraft/rpcharacters/mail/MailRecipientDirectory.java +++ b/src/main/java/net/tfminecraft/rpcharacters/mail/MailRecipientDirectory.java @@ -268,7 +268,7 @@ private static void upsertFromJson(UUID ownerUuid, JSONObject json) { return; } } - if (status != Status.ALIVE || Boolean.FALSE.equals(json.get("mail-listed"))) { + if (status != Status.ALIVE || "false".equalsIgnoreCase(String.valueOf(json.get("mail-listed")))) { ENTRIES.remove(characterId); return; } diff --git a/src/test/java/net/tfminecraft/rpcharacters/mail/MailRecipientDirectoryTest.java b/src/test/java/net/tfminecraft/rpcharacters/mail/MailRecipientDirectoryTest.java index 7e9d8f3..e6e2b43 100644 --- a/src/test/java/net/tfminecraft/rpcharacters/mail/MailRecipientDirectoryTest.java +++ b/src/test/java/net/tfminecraft/rpcharacters/mail/MailRecipientDirectoryTest.java @@ -4,6 +4,8 @@ import static org.mockito.Mockito.*; import java.lang.reflect.Method; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -11,8 +13,10 @@ import org.bukkit.Bukkit; import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import net.tfminecraft.rpcharacters.database.Database; import net.tfminecraft.rpcharacters.enums.Status; @@ -21,6 +25,7 @@ import net.tfminecraft.rpcharacters.objects.RPCharacter; class MailRecipientDirectoryTest { + @TempDir Path folder; private final UUID owner = UUID.randomUUID(); private final String id = UUID.randomUUID().toString(); @@ -87,13 +92,29 @@ private void loadDirectory(JSONObject json) throws Exception { save.setAccessible(true); load.setAccessible(true); Database database = new Database(); + // saveCharacter initializes the serializer from an empty character document. + var jsonField = Database.class.getDeclaredField("json"); + jsonField.setAccessible(true); + jsonField.set(database, new JSONObject()); for (boolean listed : List.of(false, true)) { original.setMailListed(listed); HashMap fields = new HashMap<>(); save.invoke(database, fields, original); RPCharacter restored = new RPCharacter(null); - load.invoke(database, restored, new JSONObject(fields)); + Path file = folder.resolve("character.json"); + assertTrue(database.save(file.toFile(), fields)); + JSONObject persisted; + try (var reader = Files.newBufferedReader(file)) { + persisted = (JSONObject) new JSONParser().parse(reader); + } + load.invoke(database, restored, persisted); assertEquals(listed, restored.isMailListed()); + persisted.put("id", id); + persisted.put("name", "Recipient"); + try (var bukkit = mockStatic(Bukkit.class); var manager = mockStatic(PlayerManager.class)) { + loadDirectory(persisted); + assertEquals(listed ? 1 : 0, MailRecipientDirectory.listMailTargets().size()); + } } original.setMailListed(false); load.invoke(database, original, new JSONObject());