diff --git a/LICENSE b/LICENSE index 32fc4d8..1525938 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright 2024 project-connecticut +Copyright 2024 ReconnectedCC Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/build.gradle b/build.gradle index e100180..f241b66 100644 --- a/build.gradle +++ b/build.gradle @@ -43,7 +43,7 @@ dependencies { annotationProcessor modImplementation("io.wispforest:owo-lib:${project.owo_version}") include "io.wispforest:owo-sentinel:${project.owo_version}" - include implementation("org.postgresql:postgresql:${project.postgresql_version}") + compileOnly "net.luckperms:api:${project.luckpermsapi_version}" } diff --git a/gradle.properties b/gradle.properties index a8704be..44a0e03 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,10 +6,10 @@ org.gradle.parallel=true # check these on https://fabricmc.net/develop minecraft_version=1.20.1 yarn_mappings=1.20.1+build.10 -loader_version=0.16.3 +loader_version=0.16.5 # Mod Properties -mod_version=1.7.0 +mod_version=1.8.4 maven_group=cc.reconnected archives_base_name=rcc-server @@ -18,4 +18,4 @@ fabric_version=0.92.2+1.20.1 owo_version=0.11.2+1.20 -postgresql_version=42.7.3 \ No newline at end of file +luckpermsapi_version=5.4 \ No newline at end of file diff --git a/src/main/java/cc/reconnected/server/RccServer.java b/src/main/java/cc/reconnected/server/RccServer.java index dbcc731..a025cd6 100644 --- a/src/main/java/cc/reconnected/server/RccServer.java +++ b/src/main/java/cc/reconnected/server/RccServer.java @@ -1,10 +1,9 @@ package cc.reconnected.server; import cc.reconnected.server.commands.RccCommand; -import cc.reconnected.server.database.DatabaseClient; import cc.reconnected.server.database.PlayerData; -import cc.reconnected.server.database.PlayerTable; import cc.reconnected.server.events.PlayerWelcome; +import cc.reconnected.server.events.Ready; import cc.reconnected.server.http.ServiceServer; import net.fabricmc.api.ModInitializer; import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; @@ -12,15 +11,15 @@ import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; +import net.luckperms.api.LuckPerms; +import net.luckperms.api.LuckPermsProvider; import net.minecraft.server.MinecraftServer; import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.text.Text; -import net.minecraft.util.Formatting; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; -import java.sql.SQLException; import java.util.Date; @@ -44,14 +43,9 @@ public class RccServer implements ModInitializer { return serviceServer; } - private final DatabaseClient database = new DatabaseClient(); - public DatabaseClient database() { - return database; - } - - private final PlayerTable playerTable = new PlayerTable(); - public PlayerTable playerTable() { - return playerTable; + private LuckPerms luckPerms; + public LuckPerms luckPerms() { + return luckPerms; } public static float getTPS() { @@ -77,20 +71,17 @@ public class RccServer implements ModInitializer { CommandRegistrationCallback.EVENT.register(RccCommand::register); - try { - // Jumpstart connection - database.connection(); - playerTable.ensureDatabaseCreated(); - } catch (SQLException e) { - LOGGER.error("Database error", e); - } - try { serviceServer = new ServiceServer(); } catch (IOException e) { LOGGER.error("Unable to start HTTP server", e); } + ServerLifecycleEvents.SERVER_STARTED.register(server -> { + luckPerms = LuckPermsProvider.get(); + Ready.READY.invoker().ready(server, luckPerms); + }); + ServerTickEvents.END_SERVER_TICK.register(server -> { currentMspt = server.getTickTime(); if (currentMspt != 0) { @@ -106,23 +97,17 @@ public class RccServer implements ModInitializer { ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> { currentPlayerCount = server.getCurrentPlayerCount() + 1; var player = handler.getPlayer(); - var playerData = playerTable.getPlayerData(player.getUuid()); - if(playerData == null) { - // new player! - playerData = new PlayerData(handler.getPlayer().getUuid()); - playerData.firstJoinedDate(new Date()); - playerData.name(player.getName().getString()); - playerTable.updatePlayerData(playerData); - + var playerData = PlayerData.getPlayer(player.getUuid()); + playerData.set(PlayerData.KEYS.username, player.getName().getString()); + var firstJoinedDate = playerData.getDate(PlayerData.KEYS.firstJoinedDate); + boolean isNewPlayer = false; + if (firstJoinedDate == null) { + playerData.setDate(PlayerData.KEYS.firstJoinedDate, new Date()); + isNewPlayer = true; + } + if(isNewPlayer) { PlayerWelcome.PLAYER_WELCOME.invoker().playerWelcome(player, playerData, server); - - // TODO: make it customizable via config - broadcastMessage(server, Text.literal("Welcome " + player.getName().getString() + " to the server!").formatted(Formatting.LIGHT_PURPLE)); - } else { - if (!playerData.name().equals(player.getName().getString())) { - playerData.name(player.getName().getString()); - playerTable.updatePlayerData(playerData); - } + LOGGER.info("Player {} joined for the first time!", player.getName().getString()); } }); diff --git a/src/main/java/cc/reconnected/server/RccServerConfigModel.java b/src/main/java/cc/reconnected/server/RccServerConfigModel.java index 91c9229..27e9572 100644 --- a/src/main/java/cc/reconnected/server/RccServerConfigModel.java +++ b/src/main/java/cc/reconnected/server/RccServerConfigModel.java @@ -5,5 +5,4 @@ import io.wispforest.owo.config.annotation.Config; @Config(name = "rcc-server-config", wrapperName = "RccServerConfig") public class RccServerConfigModel { public short httpPort = 25581; - public String databaseUrl = "jdbc:postgresql://127.0.0.1:5432/rcc?user=myuser&password=mypassword"; } diff --git a/src/main/java/cc/reconnected/server/commands/RccCommand.java b/src/main/java/cc/reconnected/server/commands/RccCommand.java index 10ad134..1ad5117 100644 --- a/src/main/java/cc/reconnected/server/commands/RccCommand.java +++ b/src/main/java/cc/reconnected/server/commands/RccCommand.java @@ -7,11 +7,7 @@ import net.minecraft.server.command.CommandManager; import net.minecraft.server.command.ServerCommandSource; import net.minecraft.text.Text; -import static com.mojang.brigadier.arguments.StringArgumentType.getString; -import static com.mojang.brigadier.arguments.StringArgumentType.word; import static net.minecraft.server.command.CommandManager.literal; -import static net.minecraft.server.command.CommandManager.argument; -import static net.minecraft.server.command.CommandManager.*; public class RccCommand { public static void register(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) { @@ -21,13 +17,6 @@ public class RccCommand { .executes(ctx -> { return 1; }) - .then(literal("clearcache") - .executes(context -> { - RccServer.getInstance().playerTable().clearCache(); - context.getSource().sendFeedback(() -> Text.literal("RCC PlayerTable cache cleared!"), false); - return 1; - }) - ) ); } } diff --git a/src/main/java/cc/reconnected/server/database/DatabaseClient.java b/src/main/java/cc/reconnected/server/database/DatabaseClient.java deleted file mode 100644 index 91b76ba..0000000 --- a/src/main/java/cc/reconnected/server/database/DatabaseClient.java +++ /dev/null @@ -1,15 +0,0 @@ -package cc.reconnected.server.database; - -import cc.reconnected.server.RccServer; - -import java.sql.*; - -public class DatabaseClient { - private Connection connection; - public Connection connection() throws SQLException { - if (connection == null || connection.isClosed()) { - connection = DriverManager.getConnection(RccServer.CONFIG.databaseUrl()); - } - return connection; - } -} diff --git a/src/main/java/cc/reconnected/server/database/PlayerData.java b/src/main/java/cc/reconnected/server/database/PlayerData.java index bd54b45..0ab2a78 100644 --- a/src/main/java/cc/reconnected/server/database/PlayerData.java +++ b/src/main/java/cc/reconnected/server/database/PlayerData.java @@ -1,74 +1,165 @@ package cc.reconnected.server.database; +import cc.reconnected.server.RccServer; +import net.luckperms.api.LuckPerms; +import net.luckperms.api.model.user.User; +import net.luckperms.api.node.Node; +import net.luckperms.api.node.NodeBuilder; +import net.luckperms.api.node.NodeType; +import net.luckperms.api.node.types.MetaNode; +import net.minecraft.server.network.ServerPlayerEntity; import org.jetbrains.annotations.Nullable; -import java.util.Date; -import java.util.UUID; +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.stream.Collectors; public class PlayerData { + public static final String nodePrefix = "rcc"; + + private static LuckPerms luckPerms() { + return RccServer.getInstance().luckPerms(); + } + + public static class KEYS { + public static final String username = "username"; + public static final String discordId = "discord_id"; + public static final String isBot = "is_bot"; + public static final String isAlt = "is_alt"; + public static final String pronouns = "pronouns"; + public static final String firstJoinedDate = "first_joined_date"; + public static final String supporterLevel = "supporter_level"; + } + + private final User lpUser; + private final UUID uuid; @Nullable private String name; - private Date firstJoinedDate; - @Nullable - private String discordId; - private boolean isBot = false; - private boolean isAlt = false; + private Set rawNodes; + private Map nodes; - @Nullable - private String pronouns = null; - - public PlayerData(UUID uuid) { + private PlayerData(UUID uuid, User lpUser) { this.uuid = uuid; + this.lpUser = lpUser; + + refreshNodes(); + + RccServer.LOGGER.info("Player {} has the following RCC nodes", this.uuid); + nodes.forEach((key, value) -> { + RccServer.LOGGER.info("{}: {}", key, value); + }); } - public UUID uuid() { + public UUID getUuid() { return uuid; } - public String name() { - if (name == null) { - return uuid.toString(); + public @Nullable String getUsername() { + var username = get(KEYS.username); + if (username == null) { + return name; } - return name; - } - public void name(@Nullable String name) { - this.name = name; + return username; } - public Date firstJoinedDate() { - return firstJoinedDate; - } - public void firstJoinedDate(Date firstJoinedDate) { - this.firstJoinedDate = firstJoinedDate; + public String getEffectiveName() { + var effName = getUsername(); + if (effName == null) + return uuid.toString(); + return effName; } - public @Nullable String discordId() { - return discordId; - } - public void discordId(@Nullable String discordId) { - this.discordId = discordId; + public void refreshNodes() { + rawNodes = lpUser.getNodes(NodeType.META) + .parallelStream() + .filter(node -> node.getMetaKey().startsWith(nodePrefix + ".")) + .collect(Collectors.toSet()); + + nodes = rawNodes.stream().collect(Collectors.toMap(MetaNode::getMetaKey, MetaNode::getMetaValue)); } - public boolean isBot() { - return isBot; - } - public void isBot(boolean isBot) { - this.isBot = isBot; + public void set(String key, @Nullable String value) { + var node = meta(key, value).build(); + luckPerms().getUserManager().modifyUser(uuid, user -> { + user.data().clear(NodeType.META.predicate(mn -> mn.getMetaKey().equals(key))); + user.data().add(node); + refreshNodes(); + }); } - public boolean isAlt() { - return isAlt; - } - public void isAlt(boolean isAlt) { - this.isAlt = isAlt; + public @Nullable String get(String key) { + if (!nodes.containsKey(nodePrefix + "." + key)) + return null; + return nodes.get(nodePrefix + "." + key); } - public String pronouns() { - return pronouns; + public @Nullable MetaNode getNode(String key) { + return rawNodes.stream().filter(rawNode -> rawNode.getMetaKey().equals(key)).findFirst().orElse(null); } - public void pronouns(@Nullable String pronouns) { - this.pronouns = pronouns; + + public void setBoolean(String key, boolean value) { + set(key, Boolean.toString(value)); + } + + public boolean getBoolean(String key) { + if (!nodes.containsKey(nodePrefix + "." + key)) + return false; + return Boolean.parseBoolean(nodes.get(nodePrefix + "." + key)); + } + + public boolean getBoolean(String key, boolean defaultValue) { + if (!nodes.containsKey(nodePrefix + "." + key)) + return defaultValue; + return Boolean.parseBoolean(nodes.get(nodePrefix + "." + key)); + } + + public void setDate(String key, Date date) { + var dateString = DateTimeFormatter.ISO_INSTANT.format(date.toInstant()); + set(key, dateString); + } + + public Date getDate(String key) { + if (!nodes.containsKey(nodePrefix + "." + key)) + return null; + var dateString = nodes.get(nodePrefix + "." + key); + var ta = DateTimeFormatter.ISO_INSTANT.parse(dateString); + return Date.from(Instant.from(ta)); + } + + public void delete(String key) { + luckPerms().getUserManager().modifyUser(uuid, user -> { + user.data().clear(NodeType.META.predicate(mn -> mn.getMetaKey().equals(nodePrefix + "." + key))); + }); + } + + public static PlayerData getPlayer(UUID uuid) { + var lp = luckPerms(); + var userManager = lp.getUserManager(); + + var userFuture = userManager.loadUser(uuid); + // TODO: ouch, not good... + var lpUser = userFuture.join(); + + var playerData = new PlayerData(uuid, lpUser); + playerData.name = lpUser.getUsername(); + return playerData; + } + + public static PlayerData getPlayer(ServerPlayerEntity player) { + var user = luckPerms().getPlayerAdapter(ServerPlayerEntity.class).getUser(player); + var playerData = new PlayerData(player.getUuid(), user); + playerData.name = player.getEntityName(); + return playerData; + } + + public static NodeBuilder node(String key) { + return Node.builder(nodePrefix + "." + key); + } + + public static NodeBuilder meta(String key, String value) { + return MetaNode.builder(nodePrefix + "." + key, value); } } diff --git a/src/main/java/cc/reconnected/server/database/PlayerTable.java b/src/main/java/cc/reconnected/server/database/PlayerTable.java deleted file mode 100644 index d9d09a8..0000000 --- a/src/main/java/cc/reconnected/server/database/PlayerTable.java +++ /dev/null @@ -1,175 +0,0 @@ -package cc.reconnected.server.database; - -import cc.reconnected.server.RccServer; -import org.jetbrains.annotations.Nullable; - -import java.sql.SQLException; -import java.sql.Timestamp; -import java.util.Date; -import java.util.HashMap; -import java.util.UUID; - -public class PlayerTable { - private final HashMap cache = new HashMap<>(); - - private DatabaseClient database() { - return RccServer.getInstance().database(); - } - - public void ensureDatabaseCreated() { - try { - var conn = database().connection(); - - var stmt = conn.prepareStatement( - "CREATE TABLE IF NOT EXISTS players (" + - "uuid UUID NOT NULL PRIMARY KEY," + - "firstJoined TIMESTAMP DEFAULT CURRENT_TIMESTAMP," + - "lastKnownName VARCHAR(16)," + - "discordId VARCHAR," + - "isBot BOOL DEFAULT FALSE," + - "isAlt BOOL DEFAULT FALSE," + - "pronouns VARCHAR DEFAULT NULL" + - ");"); - - stmt.executeUpdate(); - stmt.close(); - - } catch (SQLException e) { - RccServer.LOGGER.error("Could not create players data tables", e); - } - } - - public void refreshPlayerData(UUID uuid) { - cache.remove(uuid); - } - - public void clearCache() { - cache.clear(); - } - - public boolean exists(UUID uuid) { - try { - var conn = database().connection(); - - var stmt = conn.prepareStatement("SELECT uuid FROM players WHERE uuid = ?;"); - stmt.setObject(1, uuid); - var set = stmt.executeQuery(); - var exists = set.next(); - stmt.close(); - - return exists; - } catch (SQLException e) { - RccServer.LOGGER.error("Could not get player data from database", e); - return false; - } - } - - @Nullable - public PlayerData getPlayerData(UUID uuid) { - if (cache.containsKey(uuid)) { - return cache.get(uuid); - } - - try { - var conn = database().connection(); - - var stmt = conn.prepareStatement("SELECT * FROM players WHERE uuid = ?;"); - stmt.setObject(1, uuid); - var set = stmt.executeQuery(); - if (!set.next()) { - return null; - } - - var playerData = new PlayerData(set.getObject("uuid", UUID.class)); - var firstJoinTimestamp = set.getObject("firstJoined", Timestamp.class); - playerData.firstJoinedDate(new Date(firstJoinTimestamp.getTime())); - playerData.name(set.getString("lastKnownName")); - playerData.discordId(set.getString("discordId")); - playerData.isBot(set.getBoolean("isBot")); - playerData.isAlt(set.getBoolean("isAlt")); - playerData.pronouns(set.getString("pronouns")); - - stmt.close(); - - cache.put(uuid, playerData); - return playerData; - } catch (SQLException e) { - RccServer.LOGGER.error("Could not get player data from database", e); - return null; - } - } - - public boolean deletePlayerData(UUID uuid) { - cache.remove(uuid); - try { - var conn = database().connection(); - - var stmt = conn.prepareStatement("DELETE FROM players WHERE uuid = ?;"); - stmt.setObject(1, uuid); - stmt.execute(); - stmt.close(); - - return true; - } catch(SQLException e) { - RccServer.LOGGER.error("Could not delete player data from database", e); - return false; - } - } - - public boolean createPlayerData(PlayerData playerData) { - if(exists(playerData.uuid())) { - return updatePlayerData(playerData); - } - - cache.put(playerData.uuid(), playerData); - - try { - var conn = database().connection(); - - var stmt = conn.prepareStatement("INSERT INTO players(uuid, firstJoined, lastKnownName, discordId, isBot, isAlt, pronouns) VALUES (?,?,?,?,?,?,?);"); - stmt.setObject(1, playerData.uuid()); - var timestamp = new Timestamp(playerData.firstJoinedDate().getTime()); - stmt.setTimestamp(2, timestamp); - stmt.setString(3, playerData.name()); - stmt.setString(4, playerData.discordId()); - stmt.setBoolean(5, playerData.isBot()); - stmt.setBoolean(6, playerData.isAlt()); - stmt.setString(7, playerData.pronouns()); - stmt.execute(); - stmt.close(); - - return true; - } catch(SQLException e) { - RccServer.LOGGER.error("Could not create player data from database", e); - return false; - } - } - - public boolean updatePlayerData(PlayerData playerData) { - if(!exists(playerData.uuid())) { - return createPlayerData(playerData); - } - - cache.put(playerData.uuid(), playerData); - - try { - var conn = database().connection(); - - var stmt = conn.prepareStatement("UPDATE players SET lastknownname = ?, discordid = ?, isBot = ?, isAlt = ?, pronouns = ? WHERE uuid = ?"); - //var stmt = conn.prepareStatement("INSERT INTO players(uuid, firstJoined, lastKnownName, discordId, isBot, isAlt, pronouns) VALUES (?,?,?,?,?,?,?);"); - stmt.setString(1, playerData.name()); - stmt.setString(2, playerData.discordId()); - stmt.setBoolean(3, playerData.isBot()); - stmt.setBoolean(4, playerData.isAlt()); - stmt.setString(5, playerData.pronouns()); - stmt.setObject(6, playerData.uuid()); - stmt.execute(); - stmt.close(); - - return true; - } catch (SQLException e) { - RccServer.LOGGER.error("Could not update player data on database", e); - return false; - } - } -} diff --git a/src/main/java/cc/reconnected/server/events/Ready.java b/src/main/java/cc/reconnected/server/events/Ready.java new file mode 100644 index 0000000..23f63e0 --- /dev/null +++ b/src/main/java/cc/reconnected/server/events/Ready.java @@ -0,0 +1,17 @@ +package cc.reconnected.server.events; + +import net.fabricmc.fabric.api.event.Event; +import net.fabricmc.fabric.api.event.EventFactory; +import net.luckperms.api.LuckPerms; +import net.minecraft.server.MinecraftServer; + +public interface Ready { + Event READY = EventFactory.createArrayBacked(Ready.class, + (listeners) -> (server, luckPerms) -> { + for (Ready listener : listeners) { + listener.ready(server, luckPerms); + } + }); + + void ready(MinecraftServer server, LuckPerms luckPerms); +} diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 001fb7b..ebe0559 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -6,7 +6,8 @@ "description": "Server mod for the ReconnectedCC Minecraft server", "authors": [ "AlexDevs", - "EmmaKnijn" + "EmmaKnijn", + "ReconnectedCC" ], "contact": { "sources": "https://github.com/ReconnectedCC/rcc-server" @@ -25,6 +26,7 @@ "fabricloader": ">=0.16.0", "minecraft": "~1.20.1", "java": ">=17", - "fabric-api": "*" + "fabric-api": "*", + "luckperms": ">=5.4" } } \ No newline at end of file