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.
This commit is contained in:
Teriuihi 2025-02-15 22:31:35 +01:00
parent 2c80b2d474
commit c09c55ed7a
11 changed files with 300 additions and 22 deletions

View File

@ -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<Jar> {
manifest {
attributes(

View File

@ -77,8 +77,7 @@ public class CreateTeam extends SubCommand {
int highestId = gameManager.getMaxTeamId();
Team team = new Team(MiniMessage.miniMessage().deserialize(String.format("<color:%s>%s</color>", color, name)),
highestId + 1, player.getLocation(), player.getLocation(), player.getLocation(), teamColor,
Material.RED_BANNER, "§c");
Material.RED_BANNER, "§c", -1L);
return consumer.apply(team);
}

View File

@ -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);

View File

@ -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);
}
}
}

View File

@ -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<SqlSession> 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);
}
}

View File

@ -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);
}

View File

@ -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<GamePhase> 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);
}
}

View File

@ -47,6 +47,7 @@ public class GameManager {
}
public void registerTeam(Team team) {
team.registerGameManger(this);
teams.put(team.getId(), team);
}

View File

@ -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();
}
}
}

View File

@ -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) {

View File

@ -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