From c09c55ed7a344ef13932ffb8b903c01f88111f20 Mon Sep 17 00:00:00 2001 From: Teriuihi Date: Sat, 15 Feb 2025 22:31:35 +0100 Subject: [PATCH] Add Discord integration for teams and user linking Introduced database connectivity and Discord role management via MyBatis and JDA. Enhanced `Team` functionality to support Discord roles and integrated player-Discord linking during server join. Updated Gradle build to include required dependencies. --- build.gradle.kts | 24 ++++ .../ctf/commands/subcommands/CreateTeam.java | 3 +- .../com/alttd/ctf/config/AbstractConfig.java | 6 + .../java/com/alttd/ctf/config/Config.java | 32 ++++++ .../com/alttd/ctf/database/Connections.java | 49 ++++++++ .../alttd/ctf/database/DiscordUserMapper.java | 9 ++ .../ctf/events/OnPlayerOnlineStatus.java | 54 +++++++-- .../java/com/alttd/ctf/game/GameManager.java | 1 + .../java/com/alttd/ctf/team/DiscordTeam.java | 106 ++++++++++++++++++ src/main/java/com/alttd/ctf/team/Team.java | 34 +++++- version.properties | 4 +- 11 files changed, 300 insertions(+), 22 deletions(-) create mode 100644 src/main/java/com/alttd/ctf/database/Connections.java create mode 100644 src/main/java/com/alttd/ctf/database/DiscordUserMapper.java create mode 100644 src/main/java/com/alttd/ctf/team/DiscordTeam.java diff --git a/build.gradle.kts b/build.gradle.kts index d8b8103..ec94c85 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,6 +2,7 @@ import java.util.Properties plugins { id("java") + id("com.gradleup.shadow") version "9.0.0-beta4" } group = "com.alttd.ctf" @@ -33,6 +34,10 @@ dependencies { // End JSON config dependencies // WorldBorderAPI compileOnly("com.github.yannicklamprecht:worldborderapi:1.210.0:dev") + + //Database + implementation("org.mybatis:mybatis:3.5.13") + implementation("mysql:mysql-connector-java:8.0.33") } tasks.test { @@ -43,6 +48,21 @@ tasks.jar { archiveFileName.set("CaptureTheFlag.jar") } +tasks { + shadowJar { + archiveFileName.set("CaptureTheFlag.jar") + listOf( + "org.apache.ibatis" + ).forEach { relocate(it, "${rootProject.group}.lib.$it") } + } + + build { + dependsOn(shadowJar) + } +} + + + val versionPropsFile = file("version.properties") val versionProps = Properties().apply { if (versionPropsFile.exists()) { @@ -70,6 +90,10 @@ tasks.named("build") { dependsOn(incrementBuildNumber) } +tasks.named("shadowJar") { + dependsOn(incrementBuildNumber) +} + tasks.withType { manifest { attributes( diff --git a/src/main/java/com/alttd/ctf/commands/subcommands/CreateTeam.java b/src/main/java/com/alttd/ctf/commands/subcommands/CreateTeam.java index 2a54947..0f55f63 100644 --- a/src/main/java/com/alttd/ctf/commands/subcommands/CreateTeam.java +++ b/src/main/java/com/alttd/ctf/commands/subcommands/CreateTeam.java @@ -77,8 +77,7 @@ public class CreateTeam extends SubCommand { int highestId = gameManager.getMaxTeamId(); Team team = new Team(MiniMessage.miniMessage().deserialize(String.format("%s", color, name)), highestId + 1, player.getLocation(), player.getLocation(), player.getLocation(), teamColor, - Material.RED_BANNER, "§c"); - + Material.RED_BANNER, "§c", -1L); return consumer.apply(team); } diff --git a/src/main/java/com/alttd/ctf/config/AbstractConfig.java b/src/main/java/com/alttd/ctf/config/AbstractConfig.java index 7cb3a8e..3f08e3b 100644 --- a/src/main/java/com/alttd/ctf/config/AbstractConfig.java +++ b/src/main/java/com/alttd/ctf/config/AbstractConfig.java @@ -102,6 +102,12 @@ abstract class AbstractConfig { return yaml.getInt(path, yaml.getInt(path)); } + public long getLong(String prefix, String path, long def) { + path = prefix + path; + yaml.addDefault(path, def); + return yaml.getLong(path, yaml.getLong(path)); + } + double getDouble(String prefix, String path, double def) { path = prefix + path; yaml.addDefault(path, def); diff --git a/src/main/java/com/alttd/ctf/config/Config.java b/src/main/java/com/alttd/ctf/config/Config.java index 7958991..cceabdc 100644 --- a/src/main/java/com/alttd/ctf/config/Config.java +++ b/src/main/java/com/alttd/ctf/config/Config.java @@ -41,4 +41,36 @@ public class Config extends AbstractConfig{ } } + @SuppressWarnings("unused") + public static class DISCORD { + private static final String prefix = "discord."; + + public static long PLAYER_SERVER_ID = 0L; + + @SuppressWarnings("unused") + private static void load() { + PLAYER_SERVER_ID = config.getLong(prefix, "player-server-id", PLAYER_SERVER_ID); + } + } + + + @SuppressWarnings("unused") + public static class DISCORD_DATABASE { + private static final String prefix = "discord-database."; + + public static String HOST = "localhost"; + public static int PORT = 3306; + public static String DATABASE = "discordLink"; + public static String USERNAME = "username"; + public static String PASSWORD = "password"; + + @SuppressWarnings("unused") + private static void load() { + HOST = config.getString(prefix, "host", HOST); + DATABASE = config.getString(prefix, "database", DATABASE); + PORT = config.getInt(prefix, "port", PORT); + USERNAME = config.getString(prefix, "username", USERNAME); + PASSWORD = config.getString(prefix, "password", PASSWORD); + } + } } diff --git a/src/main/java/com/alttd/ctf/database/Connections.java b/src/main/java/com/alttd/ctf/database/Connections.java new file mode 100644 index 0000000..58f4153 --- /dev/null +++ b/src/main/java/com/alttd/ctf/database/Connections.java @@ -0,0 +1,49 @@ +package com.alttd.ctf.database; + +import com.alttd.ctf.config.Config; +import lombok.extern.slf4j.Slf4j; +import org.apache.ibatis.datasource.pooled.PooledDataSource; +import org.apache.ibatis.mapping.Environment; +import org.apache.ibatis.session.Configuration; +import org.apache.ibatis.session.SqlSession; +import org.apache.ibatis.session.SqlSessionFactory; +import org.apache.ibatis.session.SqlSessionFactoryBuilder; +import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory; + +import java.util.function.Consumer; + +@Slf4j +public class Connections { + + private static SqlSessionFactory discordSqlSessionFactory = null; + + public static void runDiscordQuery(Consumer consumer) { + new Thread(() -> { + if (discordSqlSessionFactory == null) { + discordSqlSessionFactory = createDiscordSqlSessionFactory(); + } + + try (SqlSession session = discordSqlSessionFactory.openSession()) { + consumer.accept(session); + } catch (Exception e) { + log.error("Failed to run discord query", e); + } + }).start(); + } + + private static SqlSessionFactory createDiscordSqlSessionFactory() { + PooledDataSource dataSource = new PooledDataSource(); + dataSource.setDriver("com.mysql.cj.jdbc.Driver"); + dataSource.setUrl(String.format("jdbc:mysql://%s:%d/%s", Config.DISCORD_DATABASE.HOST, + Config.DISCORD_DATABASE.PORT, Config.DISCORD_DATABASE.DATABASE)); + dataSource.setUsername(Config.DISCORD_DATABASE.USERNAME); + dataSource.setPassword(Config.DISCORD_DATABASE.PASSWORD); + + Environment environment = new Environment("production", new JdbcTransactionFactory(), dataSource); + Configuration configuration = new Configuration(environment); + configuration.addMapper(DiscordUserMapper.class); + + return new SqlSessionFactoryBuilder().build(configuration); + } + +} diff --git a/src/main/java/com/alttd/ctf/database/DiscordUserMapper.java b/src/main/java/com/alttd/ctf/database/DiscordUserMapper.java new file mode 100644 index 0000000..7669f81 --- /dev/null +++ b/src/main/java/com/alttd/ctf/database/DiscordUserMapper.java @@ -0,0 +1,9 @@ +package com.alttd.ctf.database; + +import org.apache.ibatis.annotations.Select; + +public interface DiscordUserMapper { + @Select("SELECT discord_id FROM linked_accounts WHERE player_uuid = #{uuid}") + Long getDiscordId(String uuid); + +} diff --git a/src/main/java/com/alttd/ctf/events/OnPlayerOnlineStatus.java b/src/main/java/com/alttd/ctf/events/OnPlayerOnlineStatus.java index ae827cf..ae7bccf 100644 --- a/src/main/java/com/alttd/ctf/events/OnPlayerOnlineStatus.java +++ b/src/main/java/com/alttd/ctf/events/OnPlayerOnlineStatus.java @@ -1,11 +1,14 @@ package com.alttd.ctf.events; +import com.alttd.ctf.database.Connections; +import com.alttd.ctf.database.DiscordUserMapper; import com.alttd.ctf.flag.Flag; import com.alttd.ctf.game.GameManager; import com.alttd.ctf.game.GamePhase; import com.alttd.ctf.team.Team; import com.alttd.ctf.team.TeamPlayer; import lombok.extern.slf4j.Slf4j; +import org.apache.ibatis.exceptions.PersistenceException; import org.bukkit.attribute.Attribute; import org.bukkit.attribute.AttributeInstance; import org.bukkit.entity.Player; @@ -32,17 +35,12 @@ public class OnPlayerOnlineStatus implements Listener { @EventHandler public void onPlayerJoin(PlayerJoinEvent event) { Player player = event.getPlayer(); - AttributeInstance maxHealthAttribute = player.getAttribute(Attribute.GENERIC_MAX_HEALTH); - if (maxHealthAttribute == null) { - log.error("Player does not have max health attribute"); - return; - } - maxHealthAttribute.setBaseValue(20); - player.setHealth(20); + resetPlayer(player); + handleRunningGame(player); + handleDiscordLink(player); + } - flag.addPlayer(player); - player.getInventory().clear(); - player.updateInventory(); + private void handleRunningGame(Player player) { Optional optionalGamePhase = gameManager.getGamePhase(); if (optionalGamePhase.isEmpty()) { return; @@ -66,9 +64,41 @@ public class OnPlayerOnlineStatus implements Listener { player.teleportAsync(teamPlayer.getTeam().getSpawnLocation()); } + private void resetPlayer(Player player) { + AttributeInstance maxHealthAttribute = player.getAttribute(Attribute.GENERIC_MAX_HEALTH); + if (maxHealthAttribute == null) { + log.error("Player does not have max health attribute"); + return; + } + maxHealthAttribute.setBaseValue(20); + player.setHealth(20); + + flag.addPlayer(player); + player.getInventory().clear(); + player.updateInventory(); + } + + private void handleDiscordLink(Player player) { + Connections.runDiscordQuery(sqlSession -> { + try { + DiscordUserMapper mapper = sqlSession.getMapper(DiscordUserMapper.class); + Long discordId = mapper.getDiscordId(player.getUniqueId().toString()); + if (discordId == null) { + log.info("Player {} is not linked", player.getName()); + return; + } + log.info("Set discord id to {} for {}", discordId, player.getName()); + player.setDiscordId(discordId); + } catch (PersistenceException e) { + log.error("Failed to set discord id for {}", player.getName(), e); + } + }); + } + @EventHandler - public void onPlayerJoin(@NotNull PlayerQuitEvent event) { - flag.handleCarrierDeathOrDisconnect(event.getPlayer()); + public void onPlayerQuit(@NotNull PlayerQuitEvent event) { + Player player = event.getPlayer(); + flag.handleCarrierDeathOrDisconnect(player); } } diff --git a/src/main/java/com/alttd/ctf/game/GameManager.java b/src/main/java/com/alttd/ctf/game/GameManager.java index d889bab..f004a43 100644 --- a/src/main/java/com/alttd/ctf/game/GameManager.java +++ b/src/main/java/com/alttd/ctf/game/GameManager.java @@ -47,6 +47,7 @@ public class GameManager { } public void registerTeam(Team team) { + team.registerGameManger(this); teams.put(team.getId(), team); } diff --git a/src/main/java/com/alttd/ctf/team/DiscordTeam.java b/src/main/java/com/alttd/ctf/team/DiscordTeam.java new file mode 100644 index 0000000..f2aa7bb --- /dev/null +++ b/src/main/java/com/alttd/ctf/team/DiscordTeam.java @@ -0,0 +1,106 @@ +package com.alttd.ctf.team; + +import com.alttd.ctf.config.Config; +import com.alttd.ctf.game.GameManager; +import com.alttd.galaxy.discord.Bot; +import lombok.extern.slf4j.Slf4j; +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.entities.*; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +@Slf4j +public class DiscordTeam { + + private final GameManager gameManager; + + public DiscordTeam(GameManager gameManager) { + this.gameManager = gameManager; + } + + @FunctionalInterface + private interface Validate { + void apply(Role role, Member member); + } + + private void validateModifyRole(Team team, Player player, Validate consumer) { + if (!player.isDiscordLinked()) { + log.info("{} is not discord linked", player.getName()); + return; + } + if (team.getDiscordRole() == -1) { + log.warn("No valid discord role set for team {}", team.getId()); + return; + } + Bot bot = Bukkit.getBot(); + if (bot == null) { + log.error("Unable to get bot"); + return; + } + JDA jda = bot.getJDA(); + if (jda == null) { + log.error("Unable to get JDA"); + return; + } + Guild guild = jda.getGuildById(Config.DISCORD.PLAYER_SERVER_ID); + if (guild == null) { + log.warn("Unable to get guild {}", Config.DISCORD.PLAYER_SERVER_ID); + return; + } + Role role = guild.getRoleById(team.getDiscordRole()); + if (role == null) { + log.warn("Unable to get role {} for team {}", team.getDiscordRole(), team.getId()); + return; + } + Member nullableMember = guild.getMemberById(player.getDiscordID()); + if (nullableMember == null) { + guild.retrieveMemberById(player.getDiscordID()).queue(member -> { + consumer.apply(role, member); + }); + } else { + consumer.apply(role, nullableMember); + } + } + + public void removeRole(Team team, @NotNull Player player) { + validateModifyRole(team, player, (role, member) -> { + if (member.isOwner()) { + log.info("Unable to remove team role from server owner"); + return; + } + member.getGuild().removeRoleFromMember(member, role).queue(ignored -> kickFromVoiceIfNeeded(member)); + }); + } + + public void addRole(Team team, Player player) { + validateModifyRole(team, player, (role, member) -> { + if (member.isOwner()) { + log.info("Unable to add team role to server owner"); + return; + } + gameManager.getTeams().forEach(otherTeam -> + member.getRoles().stream() + .filter(otherRole -> otherRole.getIdLong() == otherTeam.getDiscordRole()) + .forEach(otherRole -> { + member.getGuild().removeRoleFromMember(member, otherRole).queue(); + })); + member.getGuild().addRoleToMember(member, role).queue(ignored -> kickFromVoiceIfNeeded(member)); + }); + } + + private void kickFromVoiceIfNeeded(@NotNull Member member) { + GuildVoiceState voiceState = member.getVoiceState(); + if (voiceState == null || !voiceState.inAudioChannel()) { + return; + } + AudioChannel channel = voiceState.getChannel(); + if (channel == null) { + return; + } + if (!member.hasPermission(channel, Permission.VOICE_CONNECT)) { + member.getGuild().kickVoiceMember(member).queue(); + } + } +} diff --git a/src/main/java/com/alttd/ctf/team/Team.java b/src/main/java/com/alttd/ctf/team/Team.java index 3f8f5df..a3e8c9b 100644 --- a/src/main/java/com/alttd/ctf/team/Team.java +++ b/src/main/java/com/alttd/ctf/team/Team.java @@ -1,16 +1,16 @@ package com.alttd.ctf.team; +import com.alttd.ctf.game.GameManager; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.TextColor; import net.kyori.adventure.text.minimessage.MiniMessage; -import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; import org.bukkit.Bukkit; import org.bukkit.Location; @@ -23,7 +23,7 @@ import java.util.*; @Slf4j @NoArgsConstructor -@AllArgsConstructor +@RequiredArgsConstructor public class Team { @JsonIgnore @@ -36,7 +36,8 @@ public class Team { private Component name; @JsonProperty("id") @Getter - private int id; + @NotNull + private Integer id; @JsonProperty("spawnLocation") @NotNull @Getter @@ -61,6 +62,16 @@ public class Team { @NotNull @Getter private String legacyTeamColor; + @JsonProperty("discordRole") + @Getter + @NotNull + private Long discordRole; + @JsonIgnore + private DiscordTeam discordTeam; + + public void registerGameManger(GameManager gameManager) { + discordTeam = new DiscordTeam(gameManager); + } public TeamPlayer addPlayer(Player player) { removeFromScoreBoard(player); @@ -68,6 +79,11 @@ public class Team { TeamPlayer teamPlayer = new TeamPlayer(uuid, this); players.put(uuid, teamPlayer); addToScoreboard(player); + if (discordTeam != null) { + discordTeam.addRole(this, player); + } else { + log.warn("No discord team set for {} to add role", id); + } log.debug("Added player {} to team with id {}", player.getName(), id); return teamPlayer; } @@ -86,9 +102,15 @@ public class Team { public void removePlayer(@NotNull Player player) { removeFromScoreBoard(player); TeamPlayer remove = players.remove(player.getUniqueId()); - if (remove != null) { - log.debug("Removed player {} from team with id {}", player.getName(), id); + if (remove == null) { + return; } + if (discordTeam != null) { + discordTeam.removeRole(this, player); + } else { + log.warn("No discord team set for {} to remove role", id); + } + log.debug("Removed player {} from team with id {}", player.getName(), id); } private void addToScoreboard(Player player) { diff --git a/version.properties b/version.properties index c5b313e..256d771 100644 --- a/version.properties +++ b/version.properties @@ -1,3 +1,3 @@ -#Sat Feb 15 04:08:38 CET 2025 -buildNumber=55 +#Sat Feb 15 22:27:11 CET 2025 +buildNumber=66 version=0.1