From 91faf468e4e09fb5aa0e3ffa23ba27957a67ea4a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 7 May 2026 12:38:33 +0000 Subject: [PATCH 1/4] Add LRU-backed Discord member lookups Agent-Logs-Url: https://github.com/AsyncTI4/TI4_map_generator_bot/sessions/65c67648-7b2c-4d4e-abf6-ecebc305d9cd Co-authored-by: wholton <3317595+wholton@users.noreply.github.com> --- src/main/java/ti4/discord/JdaService.java | 115 ++++++++++++++++-- .../game/CreateGameButtonHandler.java | 6 +- .../bothelper/CreateFOWGameChannels.java | 5 +- .../commands/event/LookAtBottomEvent.java | 3 +- .../commands/event/LookAtTopEvent.java | 3 +- .../interactions/commands/game/Observer.java | 9 +- .../interactions/commands/game/Replace.java | 4 +- .../commands/player/RemoveAllianceMember.java | 4 +- .../interactions/commands/player/Stats.java | 3 +- .../listeners/AutoCompleteProvider.java | 2 +- .../listeners/UserJoinServerListener.java | 1 + .../listeners/UserLeaveServerListener.java | 1 + src/main/java/ti4/game/Game.java | 4 +- src/main/java/ti4/game/Player.java | 8 +- src/main/java/ti4/helpers/ButtonHelper.java | 5 +- .../ti4/helpers/GameLaunchThreadHelper.java | 6 +- src/main/java/ti4/helpers/Helper.java | 7 +- .../java/ti4/helpers/SearchGameHelper.java | 5 +- src/main/java/ti4/helpers/TIGLHelper.java | 4 +- src/main/java/ti4/image/DrawingUtil.java | 2 +- .../java/ti4/image/PlayerAreaGenerator.java | 2 +- src/main/java/ti4/message/MessageHelper.java | 2 +- .../ti4/service/async/BanCleanupService.java | 2 +- .../ti4/service/fow/CreateFoWGameService.java | 4 +- .../ti4/service/game/CloneGameService.java | 3 +- .../ti4/service/game/CreateGameService.java | 4 +- .../java/ti4/service/game/EndGameService.java | 4 +- .../ti4/service/game/ManagedGameService.java | 2 +- .../java/ti4/service/game/RematchService.java | 7 +- .../ti4/service/tigl/TiglReportService.java | 4 +- .../TourneyWinnerService.java | 2 +- 31 files changed, 169 insertions(+), 64 deletions(-) diff --git a/src/main/java/ti4/discord/JdaService.java b/src/main/java/ti4/discord/JdaService.java index 4b826bb994..e994c06c55 100644 --- a/src/main/java/ti4/discord/JdaService.java +++ b/src/main/java/ti4/discord/JdaService.java @@ -1,10 +1,14 @@ package ti4.discord; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; import jakarta.annotation.Nullable; +import java.time.Duration; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -19,12 +23,15 @@ import net.dv8tion.jda.api.entities.Role; import net.dv8tion.jda.api.entities.User; import net.dv8tion.jda.api.entities.channel.concrete.Category; +import net.dv8tion.jda.api.exceptions.ErrorResponseException; +import net.dv8tion.jda.api.requests.ErrorResponse; import net.dv8tion.jda.api.requests.GatewayIntent; import net.dv8tion.jda.api.requests.restaction.CommandListUpdateAction; import net.dv8tion.jda.api.utils.ChunkingFilter; import net.dv8tion.jda.api.utils.MemberCachePolicy; import org.apache.commons.lang3.function.Consumers; import ti4.AsyncTI4DiscordBot; +import ti4.cache.CacheManager; import ti4.contest.cron.CombatContestJanitorCron; import ti4.contest.cron.CombatReplayCron; import ti4.contest.cron.CombatReplayPromotionCron; @@ -109,6 +116,12 @@ public class JdaService { public static final List serversToCreateNewGamesOn = new ArrayList<>(); public static final List fowServers = new ArrayList<>(); + private static final int USER_CACHE_MAX_SIZE = 20_000; + private static final int MEMBER_CACHE_MAX_SIZE = 20_000; + private static final Duration DISCORD_ENTITY_CACHE_TTL = Duration.ofHours(6); + private static final Cache> USER_CACHE = createUserCache(); + private static final Cache> MEMBER_CACHE = createMemberCache(); + private static final ExecutorService EVENT_EXECUTOR = Executors.newFixedThreadPool( Runtime.getRuntime().availableProcessors(), Thread.ofPlatform().name("ti4-jda-event-", 0).factory()); @@ -124,11 +137,9 @@ public static void startJdaAndRegisterListeners(String[] args) { .enableIntents(GatewayIntent.MESSAGE_CONTENT) // Needed for emoji searches and validation .enableIntents(GatewayIntent.GUILD_EXPRESSIONS) - // It *appears* we need to pull all members or else the bot has trouble pinging players - // but that may be a misunderstanding, in case we want to try to use an LRU cache in the future - // and avoid loading every user at startup - .setMemberCachePolicy(MemberCachePolicy.ALL) - .setChunkingFilter(ChunkingFilter.ALL) + // Avoid loading every guild member at startup and rely on targeted lookups instead. + .setMemberCachePolicy(MemberCachePolicy.NONE) + .setChunkingFilter(ChunkingFilter.NONE) // This allows us to use our own ShutdownHook, created below .setEnableShutdownHook(false) .build(); @@ -561,13 +572,47 @@ public static boolean isValidGuild(String guildId) { @Nullable public static String getUsername(String userId) { - Member member = guildPrimary.getMemberById(userId); + Member member = getMemberById(guildPrimary, userId); if (member != null) return member.getEffectiveName(); - User user = jda.getUserById(userId); + User user = getUserById(userId); if (user != null) return user.getEffectiveName(); return null; } + @Nullable + public static Member getMemberById(@Nullable Guild guild, @Nullable String userId) { + if (guild == null || userId == null) return null; + Member member = guild.getMemberById(userId); + if (member != null) { + cacheMember(member); + return member; + } + return MEMBER_CACHE.get(new MemberCacheKey(guild.getId(), userId), _ -> retrieveMember(guild, userId)) + .orElse(null); + } + + @Nullable + public static User getUserById(@Nullable String userId) { + if (jda == null || userId == null) return null; + User user = jda.getUserById(userId); + if (user != null) { + cacheUser(user); + return user; + } + return USER_CACHE.get(userId, JdaService::retrieveUser).orElse(null); + } + + public static void cacheMember(@Nullable Member member) { + if (member == null) return; + MEMBER_CACHE.put(new MemberCacheKey(member.getGuild().getId(), member.getId()), Optional.of(member)); + cacheUser(member.getUser()); + } + + public static void invalidateMember(@Nullable Guild guild, @Nullable String userId) { + if (guild == null || userId == null) return; + MEMBER_CACHE.invalidate(new MemberCacheKey(guild.getId(), userId)); + } + public static void shutdown() { try { AsyncTI4DiscordBot.markShuttingDown(); @@ -623,4 +668,60 @@ private static boolean shutdownEventExecutor() { return false; } } + + private static Cache> createUserCache() { + Cache> cache = Caffeine.newBuilder() + .maximumSize(USER_CACHE_MAX_SIZE) + .expireAfterAccess(DISCORD_ENTITY_CACHE_TTL) + .recordStats() + .build(); + CacheManager.registerCache("discordUserCache", cache); + return cache; + } + + private static Cache> createMemberCache() { + Cache> cache = Caffeine.newBuilder() + .maximumSize(MEMBER_CACHE_MAX_SIZE) + .expireAfterAccess(DISCORD_ENTITY_CACHE_TTL) + .recordStats() + .build(); + CacheManager.registerCache("discordMemberCache", cache); + return cache; + } + + private static void cacheUser(@Nullable User user) { + if (user == null) return; + USER_CACHE.put(user.getId(), Optional.of(user)); + } + + private static Optional retrieveUser(String userId) { + try { + return Optional.of(jda.retrieveUserById(userId).complete()); + } catch (ErrorResponseException e) { + if (e.getErrorResponse() == ErrorResponse.UNKNOWN_USER) { + return Optional.empty(); + } + BotLogger.warning("Unable to retrieve Discord user `" + userId + "`.", e); + return Optional.empty(); + } + } + + private static Optional retrieveMember(Guild guild, String userId) { + try { + Member member = guild.retrieveMemberById(userId).complete(); + cacheUser(member.getUser()); + return Optional.of(member); + } catch (ErrorResponseException e) { + if (e.getErrorResponse() == ErrorResponse.UNKNOWN_MEMBER + || e.getErrorResponse() == ErrorResponse.UNKNOWN_USER) { + return Optional.empty(); + } + BotLogger.warning( + "Unable to retrieve Discord member `" + userId + "` from guild `" + guild.getId() + "`.", e); + return Optional.empty(); + } + } + + private record MemberCacheKey(String guildId, String userId) { + } } diff --git a/src/main/java/ti4/discord/interactions/buttons/handlers/game/CreateGameButtonHandler.java b/src/main/java/ti4/discord/interactions/buttons/handlers/game/CreateGameButtonHandler.java index e7ecf6c307..9e5dc8ded2 100644 --- a/src/main/java/ti4/discord/interactions/buttons/handlers/game/CreateGameButtonHandler.java +++ b/src/main/java/ti4/discord/interactions/buttons/handlers/game/CreateGameButtonHandler.java @@ -156,7 +156,7 @@ private static List fetchMembersFromMessage(ButtonInteractionEvent event for (int i = 0; i < StringUtils.countMatches(buttonMsg, "<@"); i++) { String user = buttonMsg.split("@")[i + 1]; user = StringUtils.substringBefore(user, ">"); - Member member = event.getGuild().getMemberById(user); + Member member = JdaService.getMemberById(event.getGuild(), user); if (member != null) { members.add(member); } @@ -170,7 +170,7 @@ private static List fetchMembersFromMessage(ModalInteractionEvent event) for (int i = 0; i < StringUtils.countMatches(buttonMsg, "<@"); i++) { String user = buttonMsg.split("@")[i + 1]; user = StringUtils.substringBefore(user, ">"); - Member member = event.getGuild().getMemberById(user); + Member member = JdaService.getMemberById(event.getGuild(), user); if (member != null) { members.add(member); } @@ -400,7 +400,7 @@ private static List parseMembersFromButtonMessage(ButtonInteractionEvent for (int i = 3; i < parts.length; i++) { String userId = StringUtils.substringBefore(parts[i], "."); - Member member = event.getGuild().getMemberById(userId); + Member member = JdaService.getMemberById(event.getGuild(), userId); if (member != null) { members.add(member); } diff --git a/src/main/java/ti4/discord/interactions/commands/bothelper/CreateFOWGameChannels.java b/src/main/java/ti4/discord/interactions/commands/bothelper/CreateFOWGameChannels.java index d20af18269..ef959acae1 100644 --- a/src/main/java/ti4/discord/interactions/commands/bothelper/CreateFOWGameChannels.java +++ b/src/main/java/ti4/discord/interactions/commands/bothelper/CreateFOWGameChannels.java @@ -1,5 +1,6 @@ package ti4.discord.interactions.commands.bothelper; +import ti4.discord.JdaService; import java.util.List; import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.ISnowflake; @@ -53,12 +54,10 @@ public void execute(SlashCommandInteractionEvent event) { } // CHECK IF GUILD HAS ALL PLAYERS LISTED - List guildMemberIDs = - guild.getMembers().stream().map(ISnowflake::getId).toList(); boolean sendInviteLink = false; int count = 0; for (Member member : members) { - if (!guildMemberIDs.contains(member.getId())) { + if (JdaService.getMemberById(guild, member.getId()) == null) { MessageHelper.sendMessageToEventChannel( event, member.getAsMention() + " is not a member of the server **" + guild.getName() diff --git a/src/main/java/ti4/discord/interactions/commands/event/LookAtBottomEvent.java b/src/main/java/ti4/discord/interactions/commands/event/LookAtBottomEvent.java index 93f1901a89..f1c20da2ed 100644 --- a/src/main/java/ti4/discord/interactions/commands/event/LookAtBottomEvent.java +++ b/src/main/java/ti4/discord/interactions/commands/event/LookAtBottomEvent.java @@ -1,5 +1,6 @@ package ti4.discord.interactions.commands.event; +import ti4.discord.JdaService; import net.dv8tion.jda.api.entities.User; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; import net.dv8tion.jda.api.interactions.commands.OptionMapping; @@ -44,7 +45,7 @@ public void execute(SlashCommandInteractionEvent event) { sb.append("-----------\n"); Player player = getPlayer(); - User userById = event.getJDA().getUserById(player.getUserID()); + User userById = JdaService.getUserById(player.getUserID()); if (userById != null) { if (game.isCommunityMode() && player.getPrivateChannel() != null) { MessageHelper.sendMessageToChannel(player.getPrivateChannel(), sb.toString()); diff --git a/src/main/java/ti4/discord/interactions/commands/event/LookAtTopEvent.java b/src/main/java/ti4/discord/interactions/commands/event/LookAtTopEvent.java index 28ff8b82de..7b85e22933 100644 --- a/src/main/java/ti4/discord/interactions/commands/event/LookAtTopEvent.java +++ b/src/main/java/ti4/discord/interactions/commands/event/LookAtTopEvent.java @@ -1,5 +1,6 @@ package ti4.discord.interactions.commands.event; +import ti4.discord.JdaService; import net.dv8tion.jda.api.entities.User; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; import net.dv8tion.jda.api.interactions.commands.OptionMapping; @@ -45,7 +46,7 @@ public void execute(SlashCommandInteractionEvent event) { sb.append("-----------\n"); Player player = getPlayer(); - User userById = event.getJDA().getUserById(player.getUserID()); + User userById = JdaService.getUserById(player.getUserID()); if (userById != null) { if (game.isCommunityMode() && player.getPrivateChannel() != null) { MessageHelper.sendMessageToChannel(player.getPrivateChannel(), sb.toString()); diff --git a/src/main/java/ti4/discord/interactions/commands/game/Observer.java b/src/main/java/ti4/discord/interactions/commands/game/Observer.java index 1bc48ca75e..0fe4fe1427 100644 --- a/src/main/java/ti4/discord/interactions/commands/game/Observer.java +++ b/src/main/java/ti4/discord/interactions/commands/game/Observer.java @@ -1,5 +1,6 @@ package ti4.discord.interactions.commands.game; +import ti4.discord.JdaService; import java.util.ArrayList; import java.util.List; import net.dv8tion.jda.api.Permission; @@ -52,10 +53,10 @@ public void execute(SlashCommandInteractionEvent event) { ManagedGame game = GameManager.getManagedGame(gameName); Guild guild = game.getGuild(); - Member member = guild.getMemberById(user.getId()); + Member member = JdaService.getMemberById(guild, user.getId()); if (member == null && event.getGuild() != null) { - member = event.getGuild().getMemberById(user.getId()); + member = JdaService.getMemberById(event.getGuild(), user.getId()); } // INVITE TO GAME SERVER IF MISSING @@ -101,7 +102,7 @@ private void addObserver( SlashCommandInteractionEvent event, String userID, GuildChannel channel, boolean skipMessage) { if (channel == null) return; Guild guild = channel.getGuild(); - Member user = guild.getMemberById(userID); + Member user = JdaService.getMemberById(guild, userID); channel.getPermissionContainer() .upsertPermissionOverride(user) .grant(Permission.VIEW_CHANNEL, Permission.MESSAGE_SEND) @@ -121,7 +122,7 @@ private void removeObserver( // This resets the member's perms to the default value, // -> -> -> SO IF THE USER IS IN THE GAME, THEY DON'T GET REMOVED Guild guild = channel.getGuild(); - Member user = guild.getMemberById(userID); + Member user = JdaService.getMemberById(guild, userID); channel.getPermissionContainer() .upsertPermissionOverride(user) .clear(Permission.VIEW_CHANNEL, Permission.MESSAGE_SEND) diff --git a/src/main/java/ti4/discord/interactions/commands/game/Replace.java b/src/main/java/ti4/discord/interactions/commands/game/Replace.java index 4c608d058e..ec2fe445cb 100644 --- a/src/main/java/ti4/discord/interactions/commands/game/Replace.java +++ b/src/main/java/ti4/discord/interactions/commands/game/Replace.java @@ -86,14 +86,14 @@ public void execute(SlashCommandInteractionEvent event) { if (guild == null) { guild = event.getGuild(); } - Member newMember = guild.getMemberById(replacementUser.getId()); + Member newMember = JdaService.getMemberById(guild, replacementUser.getId()); if (newMember == null) { MessageHelper.replyToMessage(event, "Added player must be on the game's server."); return; } // REMOVE ROLE - Member oldMember = guild.getMemberById(replacedPlayer.getUserID()); + Member oldMember = JdaService.getMemberById(guild, replacedPlayer.getUserID()); List roles = guild.getRolesByName(game.getName(), true); if (oldMember != null && roles.size() == 1) { guild.removeRoleFromMember(oldMember, roles.getFirst()).queue(Consumers.nop(), BotLogger::catchRestError); diff --git a/src/main/java/ti4/discord/interactions/commands/player/RemoveAllianceMember.java b/src/main/java/ti4/discord/interactions/commands/player/RemoveAllianceMember.java index 5526b5a1df..6e1941e060 100644 --- a/src/main/java/ti4/discord/interactions/commands/player/RemoveAllianceMember.java +++ b/src/main/java/ti4/discord/interactions/commands/player/RemoveAllianceMember.java @@ -44,11 +44,11 @@ public void execute(SlashCommandInteractionEvent event) { } player.getCardsInfoThread() - .removeThreadMember(JdaService.jda.getUserById(targetPlayer.getUserID())) + .removeThreadMember(JdaService.getUserById(targetPlayer.getUserID())) .queue(Consumers.nop(), BotLogger::catchRestError); targetPlayer .getCardsInfoThread() - .removeThreadMember(JdaService.jda.getUserById(player.getUserID())) + .removeThreadMember(JdaService.getUserById(player.getUserID())) .queue(Consumers.nop(), BotLogger::catchRestError); MessageHelper.sendMessageToEventChannel( diff --git a/src/main/java/ti4/discord/interactions/commands/player/Stats.java b/src/main/java/ti4/discord/interactions/commands/player/Stats.java index 7f4782d04e..ebcdc0b6ba 100644 --- a/src/main/java/ti4/discord/interactions/commands/player/Stats.java +++ b/src/main/java/ti4/discord/interactions/commands/player/Stats.java @@ -1,5 +1,6 @@ package ti4.discord.interactions.commands.player; +import ti4.discord.JdaService; import java.util.ArrayList; import java.util.List; import java.util.Set; @@ -343,7 +344,7 @@ public void execute(SlashCommandInteractionEvent event) { UserSettingsManager.save(userSettings); Guild guild = event.getGuild(); - Member removedMember = guild.getMemberById(player.getUserID()); + Member removedMember = JdaService.getMemberById(guild, player.getUserID()); List roles = guild.getRolesByName(game.getName(), true); if (removedMember != null && roles.size() == 1) { guild.removeRoleFromMember(removedMember, roles.getFirst()) diff --git a/src/main/java/ti4/discord/interactions/listeners/AutoCompleteProvider.java b/src/main/java/ti4/discord/interactions/listeners/AutoCompleteProvider.java index cf1c02fdf7..14a5c436b8 100644 --- a/src/main/java/ti4/discord/interactions/listeners/AutoCompleteProvider.java +++ b/src/main/java/ti4/discord/interactions/listeners/AutoCompleteProvider.java @@ -1056,7 +1056,7 @@ private static void handleOptions( DraftManager draftManager = game.getDraftManager(); String enteredValue = event.getFocusedOption().getValue().toLowerCase(); Predicate isUnknownUserId = - id -> game.getPlayer(id) == null || event.getGuild().getMemberById(id) == null; + id -> game.getPlayer(id) == null || JdaService.getMemberById(event.getGuild(), id) == null; List userIDs = draftManager.getPlayerUserIds().stream() .filter(isUnknownUserId) .toList(); diff --git a/src/main/java/ti4/discord/interactions/listeners/UserJoinServerListener.java b/src/main/java/ti4/discord/interactions/listeners/UserJoinServerListener.java index c766fd7994..15f435b396 100644 --- a/src/main/java/ti4/discord/interactions/listeners/UserJoinServerListener.java +++ b/src/main/java/ti4/discord/interactions/listeners/UserJoinServerListener.java @@ -30,6 +30,7 @@ private static boolean validateEvent(GenericGuildEvent event) { private void handleGuildMemberJoin(GuildMemberJoinEvent event) { try { + JdaService.cacheMember(event.getMember()); welcomeNewUserToHUBServer(event); RoleService.checkIfNewUserIsInExistingGamesAndAutoAddRole(event.getGuild(), event.getUser()); RoleService.checkIfNewUserIsInAnyGamesAndAddRole(event.getUser()); diff --git a/src/main/java/ti4/discord/interactions/listeners/UserLeaveServerListener.java b/src/main/java/ti4/discord/interactions/listeners/UserLeaveServerListener.java index e81f0df221..c51bbb411b 100644 --- a/src/main/java/ti4/discord/interactions/listeners/UserLeaveServerListener.java +++ b/src/main/java/ti4/discord/interactions/listeners/UserLeaveServerListener.java @@ -38,6 +38,7 @@ public void onGuildMemberRemove(@Nonnull GuildMemberRemoveEvent event) { private void handleGuildMemberRemove(GuildMemberRemoveEvent event) { try { + JdaService.invalidateMember(event.getGuild(), event.getUser().getId()); event.getGuild() .retrieveAuditLogs() .queueAfter( diff --git a/src/main/java/ti4/game/Game.java b/src/main/java/ti4/game/Game.java index c2a51c1f68..6510160fc2 100644 --- a/src/main/java/ti4/game/Game.java +++ b/src/main/java/ti4/game/Game.java @@ -3650,7 +3650,7 @@ public String getPing() { } StringBuilder sb = new StringBuilder(getName()).append(' '); for (String playerID : getPlayerIDs()) { - User user = JdaService.jda.getUserById(playerID); + User user = JdaService.getUserById(playerID); if (user != null) sb.append(user.getAsMention()).append(' '); } return sb.toString(); @@ -3771,7 +3771,7 @@ public List getPlayersWithGMRole() { Role gmRole = roles.isEmpty() ? null : roles.getFirst(); return players.values().stream() .filter(player -> { - Member user = getGuild().getMemberById(player.getUserID()); + Member user = JdaService.getMemberById(getGuild(), player.getUserID()); return user != null && user.getRoles().contains(gmRole); }) .toList(); diff --git a/src/main/java/ti4/game/Player.java b/src/main/java/ti4/game/Player.java index f8cf631c1d..cb005be975 100644 --- a/src/main/java/ti4/game/Player.java +++ b/src/main/java/ti4/game/Player.java @@ -1512,7 +1512,7 @@ public Member getMember() { if (game == null) return null; Guild guild = game.getGuild(); if (guild == null) return null; - return guild.getMemberById(getUserID()); + return JdaService.getMemberById(guild, getUserID()); } public User getUser() { @@ -1521,7 +1521,7 @@ public User getUser() { private User getUser(String userId) { // TODO: This is to handle JDA being null during tests. We should think of a cleaner solution. - return JdaService.jda == null ? null : JdaService.jda.getUserById(userId); + return JdaService.jda == null ? null : JdaService.getUserById(userId); } @Override @@ -1538,7 +1538,7 @@ public String getUserName() { User userById = getUser(); if (userById == null) return super.getUserName(); - Member member = JdaService.guildPrimary.getMemberById(getUserID()); + Member member = JdaService.getMemberById(JdaService.guildPrimary, getUserID()); if (member == null) { setUserName(userById.getName()); } else { @@ -1605,7 +1605,7 @@ public String getRepresentation(boolean overrideFow, boolean ping, boolean noCol if (roleForCommunity == null && !getTeamMateIDs().isEmpty()) { StringBuilder sb = new StringBuilder((noFactionIcon ? "" : getFactionEmoji())); for (String userID : getTeamMateIDs()) { - User userById = JdaService.jda.getUserById(userID); + User userById = JdaService.getUserById(userID); if (userById == null) { continue; } diff --git a/src/main/java/ti4/helpers/ButtonHelper.java b/src/main/java/ti4/helpers/ButtonHelper.java index d2b8e975f1..1ec3aa006f 100644 --- a/src/main/java/ti4/helpers/ButtonHelper.java +++ b/src/main/java/ti4/helpers/ButtonHelper.java @@ -1,5 +1,6 @@ package ti4.helpers; +import ti4.discord.JdaService; import static org.apache.commons.lang3.StringUtils.countMatches; import static org.apache.commons.lang3.StringUtils.isNotBlank; import static org.apache.commons.lang3.StringUtils.substringAfter; @@ -930,7 +931,7 @@ public static void removeUser( } Guild guild = event.getGuild(); - Member removedMember = guild.getMemberById(player.getUserID()); + Member removedMember = JdaService.getMemberById(guild, player.getUserID()); List roles = guild.getRolesByName(game.getName(), true); if (removedMember != null && roles.size() == 1) { guild.removeRoleFromMember(removedMember, roles.getFirst()) @@ -8391,7 +8392,7 @@ public static void sendMessageToRightStratThread( String threadName = game.getName() + "-round-" + game.getRound() + "-" + stratName.toLowerCase(); for (ThreadChannel threadChannel_ : threadChannels) { if ("pbd1000".equalsIgnoreCase(game.getName()) || "pbd100two".equalsIgnoreCase(game.getName())) { - if (!threadChannel_.getMembers().contains(game.getGuild().getMemberById(player.getUserID()))) { + if (!threadChannel_.getMembers().contains(JdaService.getMemberById(game.getGuild(), player.getUserID()))) { continue; } } diff --git a/src/main/java/ti4/helpers/GameLaunchThreadHelper.java b/src/main/java/ti4/helpers/GameLaunchThreadHelper.java index 98fbc50a55..63f22e5fa8 100644 --- a/src/main/java/ti4/helpers/GameLaunchThreadHelper.java +++ b/src/main/java/ti4/helpers/GameLaunchThreadHelper.java @@ -1,10 +1,8 @@ package ti4.helpers; -import java.util.List; import java.util.concurrent.TimeUnit; import lombok.experimental.UtilityClass; import net.dv8tion.jda.api.entities.Guild; -import net.dv8tion.jda.api.entities.ISnowflake; import net.dv8tion.jda.api.entities.channel.concrete.ThreadChannel; import ti4.discord.JdaService; import ti4.game.Game; @@ -26,10 +24,8 @@ public static void checkIfCanCloseGameLaunchThread(Game game, boolean notify) { if (threadChannel == null) { return; } - List guildMemberIDs = - guild.getMembers().stream().map(ISnowflake::getId).toList(); for (String playerIds : game.getPlayerIDs()) { - if (!guildMemberIDs.contains(playerIds)) { + if (JdaService.getMemberById(guild, playerIds) == null) { return; } } diff --git a/src/main/java/ti4/helpers/Helper.java b/src/main/java/ti4/helpers/Helper.java index b079756e61..0ecf7fc00c 100644 --- a/src/main/java/ti4/helpers/Helper.java +++ b/src/main/java/ti4/helpers/Helper.java @@ -1,5 +1,6 @@ package ti4.helpers; +import ti4.discord.JdaService; import java.awt.Point; import java.awt.image.BufferedImage; import java.text.SimpleDateFormat; @@ -2989,7 +2990,7 @@ private static void addPlayerPermissionsToGameChannel( if (textChannel != null) { TextChannelManager textChannelManager = textChannel.getManager(); for (String playerID : playerIds) { - Member member = guild.getMemberById(playerID); + Member member = JdaService.getMemberById(guild, playerID); if (member == null) continue; long allow = Permission.PIN_MESSAGES.getRawValue() | Permission.VIEW_CHANNEL.getRawValue(); textChannelManager = textChannelManager.putMemberPermissionOverride(member.getIdLong(), allow, 0); @@ -3002,7 +3003,7 @@ public static void removePlayerPermissionsToGameChannel(Guild guild, GuildChanne TextChannel textChannel = guild.getTextChannelById(channel.getId()); if (textChannel != null) { TextChannelManager textChannelManager = textChannel.getManager(); - Member member = guild.getMemberById(playerID); + Member member = JdaService.getMemberById(guild, playerID); long deny = Permission.MESSAGE_MANAGE.getRawValue() | Permission.PIN_MESSAGES.getRawValue() | Permission.VIEW_CHANNEL.getRawValue(); @@ -3026,7 +3027,7 @@ private static void addGameRoleToMapPlayers(Guild guild, Role role, Game game) { if (game.getRound() > 1 && !game.getPlayer(playerId).isRealPlayer()) { continue; } - Member member = guild.getMemberById(playerId); + Member member = JdaService.getMemberById(guild, playerId); if (member != null && !member.getRoles().contains(role)) guild.addRoleToMember(member, role).queue(Consumers.nop(), BotLogger::catchRestError); } diff --git a/src/main/java/ti4/helpers/SearchGameHelper.java b/src/main/java/ti4/helpers/SearchGameHelper.java index 40fdf0df42..516bd46665 100644 --- a/src/main/java/ti4/helpers/SearchGameHelper.java +++ b/src/main/java/ti4/helpers/SearchGameHelper.java @@ -1,5 +1,6 @@ package ti4.helpers; +import ti4.discord.JdaService; import java.time.Duration; import java.util.ArrayList; import java.util.Comparator; @@ -153,8 +154,8 @@ public static void msgGames( for (var managedGame : filteredManagedGames) { if (managedGame.getTableTalkChannel() != null) { String name = user.getName(); - if (managedGame.getTableTalkChannel().getGuild().getMemberById(user.getId()) != null) { - Member mem = managedGame.getTableTalkChannel().getGuild().getMemberById(user.getId()); + if (JdaService.getMemberById(managedGame.getTableTalkChannel().getGuild(), user.getId()) != null) { + Member mem = JdaService.getMemberById(managedGame.getTableTalkChannel().getGuild(), user.getId()); name = mem.getNickname(); if (name == null) { name = mem.getEffectiveName(); diff --git a/src/main/java/ti4/helpers/TIGLHelper.java b/src/main/java/ti4/helpers/TIGLHelper.java index ca39f7ae99..0b766224fa 100644 --- a/src/main/java/ti4/helpers/TIGLHelper.java +++ b/src/main/java/ti4/helpers/TIGLHelper.java @@ -270,7 +270,7 @@ private static TIGLRank getLowestCommonRankBetweenPlayers(List users, bool } private static List getUsersTIGLRanks(User user, boolean isFractured) { - Member hubMember = JdaService.guildPrimary.getMemberById(user.getId()); + Member hubMember = JdaService.getMemberById(JdaService.guildPrimary, user.getId()); if (hubMember == null) { return new ArrayList<>(); } @@ -297,7 +297,7 @@ private static TIGLRank getUsersHighestTIGLRank(User user, boolean isFractured) private static boolean allUsersAreMembersOfHubServer(List users) { for (User user : users) { - Member hubMember = JdaService.guildPrimary.getMemberById(user.getId()); + Member hubMember = JdaService.getMemberById(JdaService.guildPrimary, user.getId()); if (hubMember == null) { return false; } diff --git a/src/main/java/ti4/image/DrawingUtil.java b/src/main/java/ti4/image/DrawingUtil.java index 88842c11a7..693002b131 100644 --- a/src/main/java/ti4/image/DrawingUtil.java +++ b/src/main/java/ti4/image/DrawingUtil.java @@ -343,7 +343,7 @@ public static String getFactionIconPath(String factionID) { @Nullable public static Image getUserDiscordAvatar(String userID) { try { - User user = JdaService.jda.getUserById(userID); + User user = JdaService.getUserById(userID); if (user == null) return null; return ImageHelper.readURLScaled( user.getEffectiveAvatar().getUrl(), DISCORD_AVATAR_SIZE, DISCORD_AVATAR_SIZE); diff --git a/src/main/java/ti4/image/PlayerAreaGenerator.java b/src/main/java/ti4/image/PlayerAreaGenerator.java index 57f06871c8..049357d387 100644 --- a/src/main/java/ti4/image/PlayerAreaGenerator.java +++ b/src/main/java/ti4/image/PlayerAreaGenerator.java @@ -465,7 +465,7 @@ private String factionDisplayName(Player player) { private String getPlayerNameFromID(Player player, String id) { String afk = AFKService.userIsAFK(id) ? " (AFK)" : ""; try { - User user = JdaService.jda.getUserById(id); + User user = JdaService.getUserById(id); Member member = game.getGuild().getMember(user); if (member == null) { member = JdaService.guildPrimary.getMember(user); diff --git a/src/main/java/ti4/message/MessageHelper.java b/src/main/java/ti4/message/MessageHelper.java index e67c199afc..5e5e2f8198 100755 --- a/src/main/java/ti4/message/MessageHelper.java +++ b/src/main/java/ti4/message/MessageHelper.java @@ -779,7 +779,7 @@ public static boolean sendPrivateMessageToPlayer( String failText, String successText) { if (messageText == null || messageText.isEmpty()) return true; // blank message counts as a success - User user = player == null ? null : JdaService.jda.getUserById(player.getUserID()); + User user = player == null ? null : JdaService.getUserById(player.getUserID()); if (user == null) { sendMessageToChannel(feedbackChannel, failText); return false; diff --git a/src/main/java/ti4/service/async/BanCleanupService.java b/src/main/java/ti4/service/async/BanCleanupService.java index 0981515483..bd09b03168 100644 --- a/src/main/java/ti4/service/async/BanCleanupService.java +++ b/src/main/java/ti4/service/async/BanCleanupService.java @@ -34,7 +34,7 @@ public class BanCleanupService { "bot-questions-and-discussions"); private User getUser(UserSnowflake id) { - return JdaService.jda.getUserById(id.getId()); + return JdaService.getUserById(id.getId()); } private String getIdent(UserSnowflake id) { diff --git a/src/main/java/ti4/service/fow/CreateFoWGameService.java b/src/main/java/ti4/service/fow/CreateFoWGameService.java index 09c7d3787e..8abb3e2d37 100644 --- a/src/main/java/ti4/service/fow/CreateFoWGameService.java +++ b/src/main/java/ti4/service/fow/CreateFoWGameService.java @@ -92,7 +92,7 @@ public static synchronized void createFoWGameChannels(ButtonInteractionEvent eve // Get GM String gmLine = StringUtils.substringBetween(buttonMsg, "GM: ", "\n"); String gmId = StringUtils.substringBefore(gmLine, "."); - Member gm = event.getGuild().getMemberById(gmId); + Member gm = JdaService.getMemberById(event.getGuild(), gmId); // Get Members List members = new ArrayList<>(); @@ -114,7 +114,7 @@ public static synchronized void createFoWGameChannels(ButtonInteractionEvent eve // Example line: 1:[userid].([username]]) String userId = StringUtils.substringBetween(line, ":", "."); if (userId != null) { - Member member = event.getGuild().getMemberById(userId); + Member member = JdaService.getMemberById(event.getGuild(), userId); if (member != null) { members.add(member); } diff --git a/src/main/java/ti4/service/game/CloneGameService.java b/src/main/java/ti4/service/game/CloneGameService.java index f9ebc0f5ef..3f88eca255 100644 --- a/src/main/java/ti4/service/game/CloneGameService.java +++ b/src/main/java/ti4/service/game/CloneGameService.java @@ -1,5 +1,6 @@ package ti4.service.game; +import ti4.discord.JdaService; import lombok.experimental.UtilityClass; import net.dv8tion.jda.api.Permission; import net.dv8tion.jda.api.entities.Guild; @@ -30,7 +31,7 @@ public static void cloneGame(Game game) { Role gameRole = guild.createRole().setName(cloneName).setMentionable(true).complete(); for (Player player : game.getRealPlayers()) { - Member member = guild.getMemberById(player.getUserID()); + Member member = JdaService.getMemberById(guild, player.getUserID()); if (member != null) { guild.addRoleToMember(member, gameRole).complete(); } diff --git a/src/main/java/ti4/service/game/CreateGameService.java b/src/main/java/ti4/service/game/CreateGameService.java index 7e1e43acac..2a1f2ebe5d 100644 --- a/src/main/java/ti4/service/game/CreateGameService.java +++ b/src/main/java/ti4/service/game/CreateGameService.java @@ -433,11 +433,9 @@ public static List inviteUsersToServer(Guild guild, List members private static List inviteUsersToServer( Guild guild, List members, MessageChannel channel, String gameName) { - List guildMemberIDs = - guild.getMembers().stream().map(ISnowflake::getId).toList(); List missingMembers = new ArrayList<>(); for (Member member : members) { - if (!guildMemberIDs.contains(member.getId())) { + if (JdaService.getMemberById(guild, member.getId()) == null) { missingMembers.add(member); } } diff --git a/src/main/java/ti4/service/game/EndGameService.java b/src/main/java/ti4/service/game/EndGameService.java index fa2acd2ba1..b74901ebc7 100644 --- a/src/main/java/ti4/service/game/EndGameService.java +++ b/src/main/java/ti4/service/game/EndGameService.java @@ -295,7 +295,7 @@ private static void sendRoundSummariesToThread(ThreadChannel t, Game game) { private static void sendFeedbackMessage(ThreadChannel t, Game game) { StringBuilder message = new StringBuilder(); for (String playerID : game.getRealPlayerIDs()) { // GET ALL PLAYER PINGS - Member member = game.getGuild().getMemberById(playerID); + Member member = JdaService.getMemberById(game.getGuild(), playerID); if (member != null) message.append(member.getAsMention()).append(' '); } message.append( @@ -315,7 +315,7 @@ private static TextChannel getGameSummaryChannel(Game game) { } private static void appendUserName(StringBuilder sb, Player player) { - Optional user = Optional.ofNullable(JdaService.jda.getUserById(player.getUserID())); + Optional user = Optional.ofNullable(JdaService.getUserById(player.getUserID())); if (user.isPresent()) { sb.append(user.get().getAsMention()); } else { diff --git a/src/main/java/ti4/service/game/ManagedGameService.java b/src/main/java/ti4/service/game/ManagedGameService.java index 53dd779b4f..b739367055 100644 --- a/src/main/java/ti4/service/game/ManagedGameService.java +++ b/src/main/java/ti4/service/game/ManagedGameService.java @@ -33,7 +33,7 @@ public String getPingAllPlayers(ManagedGame game) { } StringBuilder sb = new StringBuilder(game.getName()).append(' '); for (var player : game.getPlayers()) { - User user = JdaService.jda.getUserById(player.getId()); + User user = JdaService.getUserById(player.getId()); if (user != null) sb.append(user.getAsMention()).append(' '); } return sb.toString(); diff --git a/src/main/java/ti4/service/game/RematchService.java b/src/main/java/ti4/service/game/RematchService.java index 2542066f9f..c9a7eaeeff 100644 --- a/src/main/java/ti4/service/game/RematchService.java +++ b/src/main/java/ti4/service/game/RematchService.java @@ -1,5 +1,6 @@ package ti4.service.game; +import ti4.discord.JdaService; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -63,7 +64,7 @@ public static void secondHalfOfRematch(GenericInteractionCreateEvent event, Game } else { gameRole = guild.createRole().setName(newName).setMentionable(true).complete(); for (Player player : game.getRealPlayers()) { - Member member = guild.getMemberById(player.getUserID()); + Member member = JdaService.getMemberById(guild, player.getUserID()); if (member != null) { guild.addRoleToMember(member, gameRole).complete(); } @@ -87,10 +88,10 @@ public static void secondHalfOfRematch(GenericInteractionCreateEvent event, Game .getManager() .setName(newName + "-actions") .queue(Consumers.nop(), BotLogger::catchRestError); - Member gameOwner = guild.getMemberById(game.getOwnerID()); + Member gameOwner = JdaService.getMemberById(guild, game.getOwnerID()); if (gameOwner == null) { for (Player player : game.getPlayers().values()) { - gameOwner = guild.getMemberById(player.getUserID()); + gameOwner = JdaService.getMemberById(guild, player.getUserID()); break; } } diff --git a/src/main/java/ti4/service/tigl/TiglReportService.java b/src/main/java/ti4/service/tigl/TiglReportService.java index 5f241fd40e..400fa53dab 100644 --- a/src/main/java/ti4/service/tigl/TiglReportService.java +++ b/src/main/java/ti4/service/tigl/TiglReportService.java @@ -45,7 +45,7 @@ private static String getTIGLFormattedGameEndText(Game game, GenericInteractionC int index = 1; for (Player player : game.getRealAndEliminatedPlayers()) { int playerVP = player.isEliminated() ? 0 : player.getTotalVictoryPoints(); - Optional user = Optional.ofNullable(event.getJDA().getUserById(player.getUserID())); + Optional user = Optional.ofNullable(JdaService.getUserById(player.getUserID())); sb.append(" ").append(index).append(". "); sb.append(player.getFaction()).append(" - "); if (user.isPresent()) { @@ -127,7 +127,7 @@ private static String resolveDiscordTag(Player player) { if (userId == null) { return player.getStatsTrackedUserName(); } - User user = JdaService.jda.getUserById(userId); + User user = JdaService.getUserById(Long.toString(userId)); return user == null ? player.getStatsTrackedUserName() : user.getName(); } diff --git a/src/main/java/ti4/spring/service/tournamentwinner/TourneyWinnerService.java b/src/main/java/ti4/spring/service/tournamentwinner/TourneyWinnerService.java index c7bc7c0f3b..0ae0c3fa82 100644 --- a/src/main/java/ti4/spring/service/tournamentwinner/TourneyWinnerService.java +++ b/src/main/java/ti4/spring/service/tournamentwinner/TourneyWinnerService.java @@ -28,7 +28,7 @@ public String allWinnersToString() { StringBuilder sb = new StringBuilder("__**All Async TI4 Tournament Winners:**__"); List winners = tournamentWinnerRepository.findAll(); for (TournamentWinner winner : winners) { - User user = JdaService.jda.getUserById(winner.getUserId()); + User user = JdaService.getUserById(winner.getUserId()); String name = user != null ? user.getEffectiveName() : winner.getUserName(); sb.append("\n> ").append(name).append(" won ").append(winner.getTourneyName()); } From fcef1e1c0187743d96c1c842b1d1f0516149e5f7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 7 May 2026 12:42:52 +0000 Subject: [PATCH 2/4] Polish Discord cache migration Agent-Logs-Url: https://github.com/AsyncTI4/TI4_map_generator_bot/sessions/65c67648-7b2c-4d4e-abf6-ecebc305d9cd Co-authored-by: wholton <3317595+wholton@users.noreply.github.com> --- src/main/java/ti4/discord/JdaService.java | 3 +-- .../commands/bothelper/CreateFOWGameChannels.java | 1 - src/main/java/ti4/helpers/SearchGameHelper.java | 8 ++++---- src/main/java/ti4/service/game/CreateGameService.java | 1 - src/main/java/ti4/service/tigl/TiglReportService.java | 2 +- 5 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/main/java/ti4/discord/JdaService.java b/src/main/java/ti4/discord/JdaService.java index e994c06c55..f51d33a37f 100644 --- a/src/main/java/ti4/discord/JdaService.java +++ b/src/main/java/ti4/discord/JdaService.java @@ -130,8 +130,7 @@ public static void startJdaAndRegisterListeners(String[] args) { BotLogger.info("STARTING JDA"); jda = JDABuilder.createDefault(args[0]) .setEventPool(EVENT_EXECUTOR) - // Needed to listen for joins/leaves. - // This is required to cache all members of a guild (including chunking) + // Needed to listen for joins/leaves and perform targeted member fetches. .enableIntents(GatewayIntent.GUILD_MEMBERS) // Needed to parse raw user messages. .enableIntents(GatewayIntent.MESSAGE_CONTENT) diff --git a/src/main/java/ti4/discord/interactions/commands/bothelper/CreateFOWGameChannels.java b/src/main/java/ti4/discord/interactions/commands/bothelper/CreateFOWGameChannels.java index ef959acae1..6935cd980a 100644 --- a/src/main/java/ti4/discord/interactions/commands/bothelper/CreateFOWGameChannels.java +++ b/src/main/java/ti4/discord/interactions/commands/bothelper/CreateFOWGameChannels.java @@ -3,7 +3,6 @@ import ti4.discord.JdaService; import java.util.List; import net.dv8tion.jda.api.entities.Guild; -import net.dv8tion.jda.api.entities.ISnowflake; import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; import net.dv8tion.jda.api.interactions.commands.OptionType; diff --git a/src/main/java/ti4/helpers/SearchGameHelper.java b/src/main/java/ti4/helpers/SearchGameHelper.java index 516bd46665..1c1e244a2f 100644 --- a/src/main/java/ti4/helpers/SearchGameHelper.java +++ b/src/main/java/ti4/helpers/SearchGameHelper.java @@ -154,11 +154,11 @@ public static void msgGames( for (var managedGame : filteredManagedGames) { if (managedGame.getTableTalkChannel() != null) { String name = user.getName(); - if (JdaService.getMemberById(managedGame.getTableTalkChannel().getGuild(), user.getId()) != null) { - Member mem = JdaService.getMemberById(managedGame.getTableTalkChannel().getGuild(), user.getId()); - name = mem.getNickname(); + Member member = JdaService.getMemberById(managedGame.getTableTalkChannel().getGuild(), user.getId()); + if (member != null) { + name = member.getNickname(); if (name == null) { - name = mem.getEffectiveName(); + name = member.getEffectiveName(); } } String msg2 = "The player " + name + " sends the following msg:\n" + msg; diff --git a/src/main/java/ti4/service/game/CreateGameService.java b/src/main/java/ti4/service/game/CreateGameService.java index 2a1f2ebe5d..a3055e1c8b 100644 --- a/src/main/java/ti4/service/game/CreateGameService.java +++ b/src/main/java/ti4/service/game/CreateGameService.java @@ -16,7 +16,6 @@ import net.dv8tion.jda.api.Permission; import net.dv8tion.jda.api.components.buttons.Button; import net.dv8tion.jda.api.entities.Guild; -import net.dv8tion.jda.api.entities.ISnowflake; import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.Role; import net.dv8tion.jda.api.entities.channel.Channel; diff --git a/src/main/java/ti4/service/tigl/TiglReportService.java b/src/main/java/ti4/service/tigl/TiglReportService.java index 400fa53dab..f017fa75eb 100644 --- a/src/main/java/ti4/service/tigl/TiglReportService.java +++ b/src/main/java/ti4/service/tigl/TiglReportService.java @@ -127,7 +127,7 @@ private static String resolveDiscordTag(Player player) { if (userId == null) { return player.getStatsTrackedUserName(); } - User user = JdaService.getUserById(Long.toString(userId)); + User user = JdaService.getUserById(String.valueOf(userId)); return user == null ? player.getStatsTrackedUserName() : user.getName(); } From ac0e39d4ff3a8ab6df4c05315915dd05014c25c4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 7 May 2026 13:32:18 +0000 Subject: [PATCH 3/4] Use MemberCachePolicy.ALL instead of parallel Caffeine caches Agent-Logs-Url: https://github.com/AsyncTI4/TI4_map_generator_bot/sessions/cef36229-3f45-40b3-a16d-80fa4766d9a1 Co-authored-by: wholton <3317595+wholton@users.noreply.github.com> --- src/main/java/ti4/discord/JdaService.java | 120 +++++------------- .../listeners/UserJoinServerListener.java | 1 - .../listeners/UserLeaveServerListener.java | 1 - 3 files changed, 31 insertions(+), 91 deletions(-) diff --git a/src/main/java/ti4/discord/JdaService.java b/src/main/java/ti4/discord/JdaService.java index f51d33a37f..f8fd90c579 100644 --- a/src/main/java/ti4/discord/JdaService.java +++ b/src/main/java/ti4/discord/JdaService.java @@ -1,14 +1,10 @@ package ti4.discord; -import com.github.benmanes.caffeine.cache.Cache; -import com.github.benmanes.caffeine.cache.Caffeine; import jakarta.annotation.Nullable; -import java.time.Duration; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Objects; -import java.util.Optional; import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -31,7 +27,6 @@ import net.dv8tion.jda.api.utils.MemberCachePolicy; import org.apache.commons.lang3.function.Consumers; import ti4.AsyncTI4DiscordBot; -import ti4.cache.CacheManager; import ti4.contest.cron.CombatContestJanitorCron; import ti4.contest.cron.CombatReplayCron; import ti4.contest.cron.CombatReplayPromotionCron; @@ -116,12 +111,6 @@ public class JdaService { public static final List serversToCreateNewGamesOn = new ArrayList<>(); public static final List fowServers = new ArrayList<>(); - private static final int USER_CACHE_MAX_SIZE = 20_000; - private static final int MEMBER_CACHE_MAX_SIZE = 20_000; - private static final Duration DISCORD_ENTITY_CACHE_TTL = Duration.ofHours(6); - private static final Cache> USER_CACHE = createUserCache(); - private static final Cache> MEMBER_CACHE = createMemberCache(); - private static final ExecutorService EVENT_EXECUTOR = Executors.newFixedThreadPool( Runtime.getRuntime().availableProcessors(), Thread.ofPlatform().name("ti4-jda-event-", 0).factory()); @@ -136,8 +125,9 @@ public static void startJdaAndRegisterListeners(String[] args) { .enableIntents(GatewayIntent.MESSAGE_CONTENT) // Needed for emoji searches and validation .enableIntents(GatewayIntent.GUILD_EXPRESSIONS) - // Avoid loading every guild member at startup and rely on targeted lookups instead. - .setMemberCachePolicy(MemberCachePolicy.NONE) + // Cache members as they are encountered (events, REST calls) without pre-loading + // all guild members at startup. ChunkingFilter.NONE prevents the startup bulk load. + .setMemberCachePolicy(MemberCachePolicy.ALL) .setChunkingFilter(ChunkingFilter.NONE) // This allows us to use our own ShutdownHook, created below .setEnableShutdownHook(false) @@ -578,38 +568,46 @@ public static String getUsername(String userId) { return null; } + /** + * Resolves a guild member by user ID. Returns from JDA's local cache immediately if present, + * otherwise makes a blocking REST call to Discord. Should only be called from background threads + * (e.g. ExecutorServiceManager tasks), not from JDA event-dispatch threads. + */ @Nullable public static Member getMemberById(@Nullable Guild guild, @Nullable String userId) { if (guild == null || userId == null) return null; Member member = guild.getMemberById(userId); - if (member != null) { - cacheMember(member); - return member; + if (member != null) return member; + try { + return guild.retrieveMemberById(userId).complete(); + } catch (ErrorResponseException e) { + if (e.getErrorResponse() != ErrorResponse.UNKNOWN_MEMBER + && e.getErrorResponse() != ErrorResponse.UNKNOWN_USER) { + BotLogger.warning( + "Unable to retrieve Discord member `" + userId + "` from guild `" + guild.getId() + "`.", e); + } + return null; } - return MEMBER_CACHE.get(new MemberCacheKey(guild.getId(), userId), _ -> retrieveMember(guild, userId)) - .orElse(null); } + /** + * Resolves a Discord user by user ID. Returns from JDA's local cache immediately if present, + * otherwise makes a blocking REST call to Discord. Should only be called from background threads + * (e.g. ExecutorServiceManager tasks), not from JDA event-dispatch threads. + */ @Nullable public static User getUserById(@Nullable String userId) { if (jda == null || userId == null) return null; User user = jda.getUserById(userId); - if (user != null) { - cacheUser(user); - return user; + if (user != null) return user; + try { + return jda.retrieveUserById(userId).complete(); + } catch (ErrorResponseException e) { + if (e.getErrorResponse() != ErrorResponse.UNKNOWN_USER) { + BotLogger.warning("Unable to retrieve Discord user `" + userId + "`.", e); + } + return null; } - return USER_CACHE.get(userId, JdaService::retrieveUser).orElse(null); - } - - public static void cacheMember(@Nullable Member member) { - if (member == null) return; - MEMBER_CACHE.put(new MemberCacheKey(member.getGuild().getId(), member.getId()), Optional.of(member)); - cacheUser(member.getUser()); - } - - public static void invalidateMember(@Nullable Guild guild, @Nullable String userId) { - if (guild == null || userId == null) return; - MEMBER_CACHE.invalidate(new MemberCacheKey(guild.getId(), userId)); } public static void shutdown() { @@ -667,60 +665,4 @@ private static boolean shutdownEventExecutor() { return false; } } - - private static Cache> createUserCache() { - Cache> cache = Caffeine.newBuilder() - .maximumSize(USER_CACHE_MAX_SIZE) - .expireAfterAccess(DISCORD_ENTITY_CACHE_TTL) - .recordStats() - .build(); - CacheManager.registerCache("discordUserCache", cache); - return cache; - } - - private static Cache> createMemberCache() { - Cache> cache = Caffeine.newBuilder() - .maximumSize(MEMBER_CACHE_MAX_SIZE) - .expireAfterAccess(DISCORD_ENTITY_CACHE_TTL) - .recordStats() - .build(); - CacheManager.registerCache("discordMemberCache", cache); - return cache; - } - - private static void cacheUser(@Nullable User user) { - if (user == null) return; - USER_CACHE.put(user.getId(), Optional.of(user)); - } - - private static Optional retrieveUser(String userId) { - try { - return Optional.of(jda.retrieveUserById(userId).complete()); - } catch (ErrorResponseException e) { - if (e.getErrorResponse() == ErrorResponse.UNKNOWN_USER) { - return Optional.empty(); - } - BotLogger.warning("Unable to retrieve Discord user `" + userId + "`.", e); - return Optional.empty(); - } - } - - private static Optional retrieveMember(Guild guild, String userId) { - try { - Member member = guild.retrieveMemberById(userId).complete(); - cacheUser(member.getUser()); - return Optional.of(member); - } catch (ErrorResponseException e) { - if (e.getErrorResponse() == ErrorResponse.UNKNOWN_MEMBER - || e.getErrorResponse() == ErrorResponse.UNKNOWN_USER) { - return Optional.empty(); - } - BotLogger.warning( - "Unable to retrieve Discord member `" + userId + "` from guild `" + guild.getId() + "`.", e); - return Optional.empty(); - } - } - - private record MemberCacheKey(String guildId, String userId) { - } } diff --git a/src/main/java/ti4/discord/interactions/listeners/UserJoinServerListener.java b/src/main/java/ti4/discord/interactions/listeners/UserJoinServerListener.java index 15f435b396..c766fd7994 100644 --- a/src/main/java/ti4/discord/interactions/listeners/UserJoinServerListener.java +++ b/src/main/java/ti4/discord/interactions/listeners/UserJoinServerListener.java @@ -30,7 +30,6 @@ private static boolean validateEvent(GenericGuildEvent event) { private void handleGuildMemberJoin(GuildMemberJoinEvent event) { try { - JdaService.cacheMember(event.getMember()); welcomeNewUserToHUBServer(event); RoleService.checkIfNewUserIsInExistingGamesAndAutoAddRole(event.getGuild(), event.getUser()); RoleService.checkIfNewUserIsInAnyGamesAndAddRole(event.getUser()); diff --git a/src/main/java/ti4/discord/interactions/listeners/UserLeaveServerListener.java b/src/main/java/ti4/discord/interactions/listeners/UserLeaveServerListener.java index c51bbb411b..e81f0df221 100644 --- a/src/main/java/ti4/discord/interactions/listeners/UserLeaveServerListener.java +++ b/src/main/java/ti4/discord/interactions/listeners/UserLeaveServerListener.java @@ -38,7 +38,6 @@ public void onGuildMemberRemove(@Nonnull GuildMemberRemoveEvent event) { private void handleGuildMemberRemove(GuildMemberRemoveEvent event) { try { - JdaService.invalidateMember(event.getGuild(), event.getUser().getId()); event.getGuild() .retrieveAuditLogs() .queueAfter( From c701fdcc5c98dea2c6d4da328a89afea05a56a3b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 7 May 2026 14:01:31 +0000 Subject: [PATCH 4/4] Use MemberCachePolicy.lru and simplify member/user retrieval via CacheRestAction Agent-Logs-Url: https://github.com/AsyncTI4/TI4_map_generator_bot/sessions/69fe1720-7147-43af-a1de-682a046531de Co-authored-by: wholton <3317595+wholton@users.noreply.github.com> --- src/main/java/ti4/discord/JdaService.java | 26 +++++++++++------------ 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/main/java/ti4/discord/JdaService.java b/src/main/java/ti4/discord/JdaService.java index f8fd90c579..a5813035e3 100644 --- a/src/main/java/ti4/discord/JdaService.java +++ b/src/main/java/ti4/discord/JdaService.java @@ -111,6 +111,8 @@ public class JdaService { public static final List serversToCreateNewGamesOn = new ArrayList<>(); public static final List fowServers = new ArrayList<>(); + private static final int MEMBER_CACHE_MAX_SIZE = 20_000; + private static final ExecutorService EVENT_EXECUTOR = Executors.newFixedThreadPool( Runtime.getRuntime().availableProcessors(), Thread.ofPlatform().name("ti4-jda-event-", 0).factory()); @@ -125,9 +127,9 @@ public static void startJdaAndRegisterListeners(String[] args) { .enableIntents(GatewayIntent.MESSAGE_CONTENT) // Needed for emoji searches and validation .enableIntents(GatewayIntent.GUILD_EXPRESSIONS) - // Cache members as they are encountered (events, REST calls) without pre-loading - // all guild members at startup. ChunkingFilter.NONE prevents the startup bulk load. - .setMemberCachePolicy(MemberCachePolicy.ALL) + // Cache up to MEMBER_CACHE_MAX_SIZE most-recently-used members, evicting older + // entries automatically. ChunkingFilter.NONE prevents the startup bulk load. + .setMemberCachePolicy(MemberCachePolicy.lru(MEMBER_CACHE_MAX_SIZE)) .setChunkingFilter(ChunkingFilter.NONE) // This allows us to use our own ShutdownHook, created below .setEnableShutdownHook(false) @@ -569,15 +571,14 @@ public static String getUsername(String userId) { } /** - * Resolves a guild member by user ID. Returns from JDA's local cache immediately if present, - * otherwise makes a blocking REST call to Discord. Should only be called from background threads - * (e.g. ExecutorServiceManager tasks), not from JDA event-dispatch threads. + * Resolves a guild member by user ID. Uses JDA's cache-aware {@code retrieveMemberById}, which + * returns the cached member immediately if available, otherwise makes a blocking REST call. + * Should only be called from background threads (e.g. ExecutorServiceManager tasks), not from + * JDA event-dispatch threads. */ @Nullable public static Member getMemberById(@Nullable Guild guild, @Nullable String userId) { if (guild == null || userId == null) return null; - Member member = guild.getMemberById(userId); - if (member != null) return member; try { return guild.retrieveMemberById(userId).complete(); } catch (ErrorResponseException e) { @@ -591,15 +592,14 @@ public static Member getMemberById(@Nullable Guild guild, @Nullable String userI } /** - * Resolves a Discord user by user ID. Returns from JDA's local cache immediately if present, - * otherwise makes a blocking REST call to Discord. Should only be called from background threads - * (e.g. ExecutorServiceManager tasks), not from JDA event-dispatch threads. + * Resolves a Discord user by user ID. Uses JDA's cache-aware {@code retrieveUserById}, which + * returns the cached user immediately if available, otherwise makes a blocking REST call. + * Should only be called from background threads (e.g. ExecutorServiceManager tasks), not from + * JDA event-dispatch threads. */ @Nullable public static User getUserById(@Nullable String userId) { if (jda == null || userId == null) return null; - User user = jda.getUserById(userId); - if (user != null) return user; try { return jda.retrieveUserById(userId).complete(); } catch (ErrorResponseException e) {