From 3c72688e0ed4fe718a293bc790c16c26d83589a7 Mon Sep 17 00:00:00 2001 From: Telesphoreo Date: Sun, 24 May 2026 22:37:32 -0400 Subject: [PATCH] New database API --- api/build.gradle.kts | 8 +- .../dev/plex/api/player/PlayerModuleData.java | 78 +++++++ .../java/dev/plex/api/player/PlayersApi.java | 12 +- .../api/punishment/PunishmentRequest.java | 11 +- .../plex/api/punishment/PunishmentSource.java | 20 ++ .../plex/api/punishment/PunishmentView.java | 27 ++- .../plex/api/storage/ModuleMigrations.java | 27 +++ .../java/dev/plex/api/storage/ModuleOrm.java | 22 ++ .../dev/plex/api/storage/ModuleStorage.java | 48 ++++ .../dev/plex/api/storage/SqlCallable.java | 20 ++ .../java/dev/plex/api/storage/SqlDialect.java | 37 ++++ .../java/dev/plex/api/storage/StorageApi.java | 16 ++ .../dev/plex/command/SimplePlexCommand.java | 164 ++++++++++++++ server/src/main/java/dev/plex/Plex.java | 7 + .../api/impl/DefaultPlayerModuleData.java | 100 +++++++++ .../dev/plex/api/impl/DefaultPlayersApi.java | 8 +- .../plex/api/impl/DefaultPlexPlayerView.java | 5 +- .../plex/api/impl/DefaultPunishmentView.java | 9 +- .../plex/api/impl/DefaultPunishmentsApi.java | 4 +- .../dev/plex/api/impl/DefaultStorageApi.java | 16 ++ .../java/dev/plex/command/impl/BanCMD.java | 3 +- .../dev/plex/command/impl/BanListCommand.java | 5 +- .../java/dev/plex/command/impl/FreezeCMD.java | 1 - .../java/dev/plex/command/impl/KickCMD.java | 3 +- .../java/dev/plex/command/impl/MuteCMD.java | 1 - .../java/dev/plex/command/impl/NotesCMD.java | 3 +- .../java/dev/plex/command/impl/SmiteCMD.java | 1 - .../dev/plex/command/impl/TempbanCMD.java | 3 +- .../dev/plex/command/impl/TempmuteCMD.java | 1 - .../dev/plex/listener/impl/BanListener.java | 6 +- .../plex/listener/impl/PlayerListener.java | 2 +- .../dev/plex/player/PlayerNameResolver.java | 96 ++++++++ .../main/java/dev/plex/player/PlexPlayer.java | 6 - .../java/dev/plex/punishment/Punishment.java | 43 ++-- .../dev/plex/services/impl/TimingService.java | 1 - .../java/dev/plex/storage/StorageType.java | 54 ++++- .../dev/plex/storage/database/Database.java | 113 +--------- .../storage/database/MigrationRunner.java | 206 ++++++++++++++++++ .../storage/database/entity/NoteEntity.java | 4 +- .../storage/database/entity/PlayerEntity.java | 10 +- .../database/entity/PunishmentEntity.java | 16 +- .../dev/plex/storage/module/ModuleNames.java | 47 ++++ .../module/ServerModuleMigrations.java | 41 ++++ .../plex/storage/module/ServerModuleOrm.java | 42 ++++ .../storage/module/ServerModuleStorage.java | 69 ++++++ .../player/PlayerModuleDataRepository.java | 15 ++ .../plex/storage/player/SQLPlayerData.java | 14 +- .../storage/player/SQLPlayerModuleData.java | 86 ++++++++ .../dev/plex/storage/punishment/SQLNotes.java | 29 ++- .../storage/punishment/SQLPunishment.java | 22 +- .../migration/mariadb/001_initial_schema.sql | 27 ++- .../migration/postgres/001_initial_schema.sql | 27 ++- .../migration/sqlite/001_initial_schema.sql | 27 ++- 53 files changed, 1398 insertions(+), 265 deletions(-) create mode 100644 api/src/main/java/dev/plex/api/player/PlayerModuleData.java create mode 100644 api/src/main/java/dev/plex/api/punishment/PunishmentSource.java create mode 100644 api/src/main/java/dev/plex/api/storage/ModuleMigrations.java create mode 100644 api/src/main/java/dev/plex/api/storage/ModuleOrm.java create mode 100644 api/src/main/java/dev/plex/api/storage/ModuleStorage.java create mode 100644 api/src/main/java/dev/plex/api/storage/SqlCallable.java create mode 100644 api/src/main/java/dev/plex/api/storage/SqlDialect.java create mode 100644 server/src/main/java/dev/plex/api/impl/DefaultPlayerModuleData.java create mode 100644 server/src/main/java/dev/plex/player/PlayerNameResolver.java create mode 100644 server/src/main/java/dev/plex/storage/database/MigrationRunner.java create mode 100644 server/src/main/java/dev/plex/storage/module/ModuleNames.java create mode 100644 server/src/main/java/dev/plex/storage/module/ServerModuleMigrations.java create mode 100644 server/src/main/java/dev/plex/storage/module/ServerModuleOrm.java create mode 100644 server/src/main/java/dev/plex/storage/module/ServerModuleStorage.java create mode 100644 server/src/main/java/dev/plex/storage/player/PlayerModuleDataRepository.java create mode 100644 server/src/main/java/dev/plex/storage/player/SQLPlayerModuleData.java diff --git a/api/build.gradle.kts b/api/build.gradle.kts index 4b7dded..384716e 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -1,12 +1,14 @@ plugins { - java + `java-library` `maven-publish` } dependencies { + api("com.j256.ormlite:ormlite-core:6.1") + api("com.google.code.gson:gson:2.13.2") + compileOnly("io.papermc.paper:paper-api:26.1.2.build.+") compileOnly("org.apache.logging.log4j:log4j-api:2.26.0") - compileOnly("com.google.code.gson:gson:2.13.2") compileOnly("org.jetbrains:annotations:26.1.0") } @@ -24,4 +26,4 @@ publishing { from(components["java"]) } } -} \ No newline at end of file +} diff --git a/api/src/main/java/dev/plex/api/player/PlayerModuleData.java b/api/src/main/java/dev/plex/api/player/PlayerModuleData.java new file mode 100644 index 0000000..77296a8 --- /dev/null +++ b/api/src/main/java/dev/plex/api/player/PlayerModuleData.java @@ -0,0 +1,78 @@ +package dev.plex.api.player; + +import com.google.gson.JsonElement; +import java.util.Optional; + +/** + * Typed key-value JSON storage for a player's module data. + */ +public interface PlayerModuleData +{ + /** + * Gets a raw JSON value. + * + * @param key data key + * @return stored JSON value, if present + */ + Optional get(String key); + + /** + * Gets and maps a JSON value to a Java type. + * + * @param key data key + * @param type target type + * @param target type + * @return mapped value, if present + */ + Optional get(String key, Class type); + + /** + * Gets a string value. + * + * @param key data key + * @param fallback value returned when the key is absent or incompatible + * @return stored string or fallback + */ + String getString(String key, String fallback); + + /** + * Gets a long value. + * + * @param key data key + * @param fallback value returned when the key is absent or incompatible + * @return stored long or fallback + */ + long getLong(String key, long fallback); + + /** + * Gets a boolean value. + * + * @param key data key + * @param fallback value returned when the key is absent or incompatible + * @return stored boolean or fallback + */ + boolean getBoolean(String key, boolean fallback); + + /** + * Stores a raw JSON value. + * + * @param key data key + * @param value JSON value to store + */ + void set(String key, JsonElement value); + + /** + * Stores a Java value as JSON. + * + * @param key data key + * @param value value to serialize and store + */ + void set(String key, Object value); + + /** + * Removes a stored value. + * + * @param key data key + */ + void remove(String key); +} diff --git a/api/src/main/java/dev/plex/api/player/PlayersApi.java b/api/src/main/java/dev/plex/api/player/PlayersApi.java index b145e3e..7879338 100644 --- a/api/src/main/java/dev/plex/api/player/PlayersApi.java +++ b/api/src/main/java/dev/plex/api/player/PlayersApi.java @@ -3,6 +3,7 @@ package dev.plex.api.player; import java.util.List; import java.util.Optional; import java.util.UUID; +import dev.plex.module.PlexModule; /** * Looks up Plex players through read-only API views. @@ -15,7 +16,7 @@ public interface PlayersApi * @param uuid player UUID * @return player view, if known */ - Optional byUuid(UUID uuid); + Optional player(UUID uuid); /** * Looks up a player by name. @@ -31,4 +32,13 @@ public interface PlayersApi * @return names of currently online players */ List onlineNames(); + + /** + * Returns module-scoped data storage for a player. + * + * @param module module requesting player data + * @param playerUuid player UUID + * @return module-scoped player data storage + */ + PlayerModuleData moduleData(PlexModule module, UUID playerUuid); } diff --git a/api/src/main/java/dev/plex/api/punishment/PunishmentRequest.java b/api/src/main/java/dev/plex/api/punishment/PunishmentRequest.java index f8b4076..ab1d5af 100644 --- a/api/src/main/java/dev/plex/api/punishment/PunishmentRequest.java +++ b/api/src/main/java/dev/plex/api/punishment/PunishmentRequest.java @@ -8,17 +8,18 @@ import java.util.UUID; * * @param punished UUID of the player being punished * @param punisher UUID of the actor issuing the punishment - * @param punisherName display name of the actor issuing the punishment + * @param source source that issued the punishment + * @param punisherReference source-specific actor reference * @param ip IP address associated with the punished player - * @param punishedUsername username of the punished player * @param type punishment type to apply * @param reason punishment reason * @param customTime whether the punishment uses a custom duration * @param active whether the punishment should start active * @param endDate punishment end date, or {@code null} for punishments without an end date */ -public record PunishmentRequest(UUID punished, UUID punisher, String punisherName, String ip, - String punishedUsername, PunishmentType type, String reason, - boolean customTime, boolean active, ZonedDateTime endDate) +public record PunishmentRequest(UUID punished, UUID punisher, PunishmentSource source, + String punisherReference, String ip, PunishmentType type, + String reason, boolean customTime, boolean active, + ZonedDateTime endDate) { } diff --git a/api/src/main/java/dev/plex/api/punishment/PunishmentSource.java b/api/src/main/java/dev/plex/api/punishment/PunishmentSource.java new file mode 100644 index 0000000..d76ffb0 --- /dev/null +++ b/api/src/main/java/dev/plex/api/punishment/PunishmentSource.java @@ -0,0 +1,20 @@ +package dev.plex.api.punishment; + +/** + * Source that issued a punishment. + */ +public enum PunishmentSource +{ + /** + * Punishment issued by an in-game player. + */ + PLAYER, + /** + * Punishment issued by the server console. + */ + CONSOLE, + /** + * Punishment issued by a web or external integration. + */ + WEB +} diff --git a/api/src/main/java/dev/plex/api/punishment/PunishmentView.java b/api/src/main/java/dev/plex/api/punishment/PunishmentView.java index 13f408b..6b8523e 100644 --- a/api/src/main/java/dev/plex/api/punishment/PunishmentView.java +++ b/api/src/main/java/dev/plex/api/punishment/PunishmentView.java @@ -23,11 +23,25 @@ public interface PunishmentView UUID punisher(); /** - * Returns the display name of the actor who issued the punishment. + * Returns the source that issued the punishment. * - * @return display name of the actor who issued the punishment + * @return punishment source */ - String punisherName(); + PunishmentSource source(); + + /** + * Returns the source-specific punisher reference. + * + * @return punisher reference, or {@code null} when not applicable + */ + String punisherReference(); + + /** + * Returns the resolved display name for the punishment actor. + * + * @return display name for the punishment actor + */ + String punisherDisplayName(); /** * Returns the IP address associated with the punished player. @@ -36,13 +50,6 @@ public interface PunishmentView */ String ip(); - /** - * Returns the username of the punished player. - * - * @return username of the punished player - */ - String punishedUsername(); - /** * Returns the punishment type. * diff --git a/api/src/main/java/dev/plex/api/storage/ModuleMigrations.java b/api/src/main/java/dev/plex/api/storage/ModuleMigrations.java new file mode 100644 index 0000000..0cb541e --- /dev/null +++ b/api/src/main/java/dev/plex/api/storage/ModuleMigrations.java @@ -0,0 +1,27 @@ +package dev.plex.api.storage; + +import java.sql.SQLException; +import java.util.List; + +/** + * Runs database migrations for a module storage namespace. + */ +public interface ModuleMigrations +{ + /** + * Runs migrations from the default module migration resource root. + * + * @param versions migration versions to apply + * @throws SQLException if a migration cannot be applied + */ + void run(List versions) throws SQLException; + + /** + * Runs migrations from a custom module migration resource root. + * + * @param resourceRoot resource root containing dialect migration folders + * @param versions migration versions to apply + * @throws SQLException if a migration cannot be applied + */ + void run(String resourceRoot, List versions) throws SQLException; +} diff --git a/api/src/main/java/dev/plex/api/storage/ModuleOrm.java b/api/src/main/java/dev/plex/api/storage/ModuleOrm.java new file mode 100644 index 0000000..bf13013 --- /dev/null +++ b/api/src/main/java/dev/plex/api/storage/ModuleOrm.java @@ -0,0 +1,22 @@ +package dev.plex.api.storage; + +import com.j256.ormlite.dao.Dao; +import java.sql.SQLException; + +/** + * Creates ORMLite DAOs for module-scoped tables. + */ +public interface ModuleOrm +{ + /** + * Creates or returns a cached DAO for a module-local table. + * + * @param entityClass ORMLite entity class + * @param localTableName module-local table name + * @param entity type + * @param entity ID type + * @return ORMLite DAO using the module-prefixed physical table + * @throws SQLException if the DAO cannot be created + */ + Dao dao(Class entityClass, String localTableName) throws SQLException; +} diff --git a/api/src/main/java/dev/plex/api/storage/ModuleStorage.java b/api/src/main/java/dev/plex/api/storage/ModuleStorage.java new file mode 100644 index 0000000..4887735 --- /dev/null +++ b/api/src/main/java/dev/plex/api/storage/ModuleStorage.java @@ -0,0 +1,48 @@ +package dev.plex.api.storage; + +import java.sql.SQLException; + +/** + * Module-scoped storage namespace. + */ +public interface ModuleStorage +{ + /** + * Returns the validated module table prefix. + * + * @return module table prefix + */ + String prefix(); + + /** + * Resolves a local table name to the module's physical table name. + * + * @param localName module-local table name + * @return physical table name + */ + String table(String localName); + + /** + * Returns module migration operations. + * + * @return module migration operations + */ + ModuleMigrations migrations(); + + /** + * Returns module ORMLite DAO operations. + * + * @return module ORMLite DAO operations + */ + ModuleOrm orm(); + + /** + * Runs work inside a storage transaction. + * + * @param callable work to run + * @param callback result type + * @return callback result + * @throws SQLException if the transaction cannot complete + */ + T transaction(SqlCallable callable) throws SQLException; +} diff --git a/api/src/main/java/dev/plex/api/storage/SqlCallable.java b/api/src/main/java/dev/plex/api/storage/SqlCallable.java new file mode 100644 index 0000000..89a7ae3 --- /dev/null +++ b/api/src/main/java/dev/plex/api/storage/SqlCallable.java @@ -0,0 +1,20 @@ +package dev.plex.api.storage; + +import java.sql.SQLException; + +/** + * Callback used for SQL work that does not receive a connection directly. + * + * @param callback result type + */ +@FunctionalInterface +public interface SqlCallable +{ + /** + * Runs SQL work. + * + * @return callback result + * @throws SQLException if the SQL work cannot complete + */ + T call() throws SQLException; +} diff --git a/api/src/main/java/dev/plex/api/storage/SqlDialect.java b/api/src/main/java/dev/plex/api/storage/SqlDialect.java new file mode 100644 index 0000000..a97071e --- /dev/null +++ b/api/src/main/java/dev/plex/api/storage/SqlDialect.java @@ -0,0 +1,37 @@ +package dev.plex.api.storage; + +/** + * SQL dialects supported by Plex storage. + */ +public enum SqlDialect +{ + /** + * SQLite storage. + */ + SQLITE("sqlite"), + /** + * MariaDB or MySQL-compatible storage. + */ + MARIADB("mariadb"), + /** + * PostgreSQL storage. + */ + POSTGRES("postgres"); + + private final String migrationDirectory; + + SqlDialect(String migrationDirectory) + { + this.migrationDirectory = migrationDirectory; + } + + /** + * Returns the resource directory name used for dialect-specific migrations. + * + * @return migration resource directory name + */ + public String migrationDirectory() + { + return migrationDirectory; + } +} diff --git a/api/src/main/java/dev/plex/api/storage/StorageApi.java b/api/src/main/java/dev/plex/api/storage/StorageApi.java index 27695a8..767bde8 100644 --- a/api/src/main/java/dev/plex/api/storage/StorageApi.java +++ b/api/src/main/java/dev/plex/api/storage/StorageApi.java @@ -2,6 +2,7 @@ package dev.plex.api.storage; import java.sql.Connection; import java.sql.SQLException; +import dev.plex.module.PlexModule; /** * Provides controlled access to Plex SQL storage. @@ -18,6 +19,21 @@ public interface StorageApi */ T withConnection(SqlFunction function) throws SQLException; + /** + * Returns storage operations scoped to a module namespace. + * + * @param module module requesting storage + * @return module-scoped storage operations + */ + ModuleStorage forModule(PlexModule module); + + /** + * Returns the configured SQL dialect. + * + * @return configured SQL dialect + */ + SqlDialect dialect(); + /** * SQL callback used by {@link #withConnection(SqlFunction)}. * diff --git a/api/src/main/java/dev/plex/command/SimplePlexCommand.java b/api/src/main/java/dev/plex/command/SimplePlexCommand.java index 92f8f87..ee5e313 100644 --- a/api/src/main/java/dev/plex/command/SimplePlexCommand.java +++ b/api/src/main/java/dev/plex/command/SimplePlexCommand.java @@ -45,11 +45,22 @@ public abstract class SimplePlexCommand implements PlexCommand private PlexApi api; private PlexModule module; + /** + * Creates a command using explicit command metadata. + * + * @param commandSpec command metadata + */ protected SimplePlexCommand(CommandSpec commandSpec) { this.commandSpec = commandSpec; } + /** + * Starts a command spec builder for the given command name. + * + * @param name primary command name + * @return command spec builder + */ protected static CommandSpec.Builder command(String name) { return CommandSpec.builder(name); @@ -82,6 +93,14 @@ public abstract class SimplePlexCommand implements PlexCommand return command.build(); } + /** + * Configures the Brigadier command tree for this command. + * + *

The default tree accepts optional greedy string arguments and dispatches + * them to {@link #execute(CommandSender, Player, String[])}.

+ * + * @param command root command literal builder + */ protected void configureCommand(LiteralArgumentBuilder command) { command.executes(context -> dispatchCommand(context, new String[0])); @@ -90,13 +109,36 @@ public abstract class SimplePlexCommand implements PlexCommand .executes(context -> dispatchCommand(context, splitExecutionArgs(StringArgumentType.getString(context, "args"))))); } + /** + * Executes this command. + * + * @param sender command sender + * @param player player sender, or {@code null} when the sender is not a player + * @param args command arguments + * @return component to send to the sender, or {@code null} to send no response + */ protected abstract Component execute(@NotNull CommandSender sender, @Nullable Player player, @NotNull String[] args); + /** + * Returns tab-completion suggestions for this command. + * + * @param sender command sender + * @param alias command alias used by the sender + * @param args current command arguments + * @return suggested completions + * @throws IllegalArgumentException when suggestions cannot be produced for the supplied arguments + */ protected @NotNull List suggestions(@NotNull CommandSender sender, @NotNull String alias, @NotNull String[] args) throws IllegalArgumentException { return List.of(); } + /** + * Returns the bound Plex API. + * + * @return bound Plex API + * @throws IllegalStateException when the command has not been bound to the API + */ protected PlexApi api() { if (api == null) @@ -106,26 +148,56 @@ public abstract class SimplePlexCommand implements PlexCommand return api; } + /** + * Sends an ampersand-colorized legacy message to an audience. + * + * @param audience message recipient + * @param message legacy message text + */ protected void send(Audience audience, String message) { audience.sendMessage(componentFromString(message)); } + /** + * Sends a component to an audience. + * + * @param audience message recipient + * @param component component to send + */ protected void send(Audience audience, Component component) { audience.sendMessage(component); } + /** + * Broadcasts a MiniMessage-formatted message. + * + * @param miniMessage MiniMessage-formatted message + */ protected void broadcast(String miniMessage) { api().messages().broadcast(miniMessage); } + /** + * Broadcasts a component. + * + * @param component component to broadcast + */ protected void broadcast(Component component) { api().messages().broadcast(component); } + /** + * Checks whether a sender has a permission node. + * + * @param sender command sender + * @param permission permission node to check + * @return {@code true} when the sender can use the permission + * @throws CommandFailException when the sender lacks the permission + */ protected boolean checkPermission(CommandSender sender, String permission) { if (permission.isEmpty() || isConsole(sender) || sender.hasPermission(permission)) @@ -135,31 +207,68 @@ public abstract class SimplePlexCommand implements PlexCommand throw new CommandFailException(messageString("noPermissionNode", permission)); } + /** + * Checks whether a sender has a permission node without throwing an exception. + * + * @param sender command sender + * @param permission permission node to check + * @return {@code true} when the sender can use the permission + */ protected boolean silentCheckPermission(CommandSender sender, String permission) { return permission.isEmpty() || isConsole(sender) || sender.hasPermission(permission); } + /** + * Returns the standard no-permission message for this command's permission node. + * + * @return no-permission component + */ protected Component permissionMessage() { return permissionMessage(getPermission()); } + /** + * Returns the standard no-permission message for a permission node. + * + * @param permission permission node + * @return no-permission component + */ protected Component permissionMessage(String permission) { return messageComponent("noPermissionNode", permission); } + /** + * Returns a sender's UUID when the sender is a player. + * + * @param sender command sender + * @return player UUID, or {@code null} for non-player senders + */ protected @Nullable UUID getUUID(CommandSender sender) { return sender instanceof Player player ? player.getUniqueId() : null; } + /** + * Returns whether a sender is not a player. + * + * @param sender command sender + * @return {@code true} when the sender is not a player + */ protected boolean isConsole(CommandSender sender) { return !(sender instanceof Player); } + /** + * Resolves a configured message as a component. + * + * @param key message key + * @param objects replacement values + * @return resolved message component + */ protected Component messageComponent(String key, Object... objects) { if (module != null) @@ -169,6 +278,13 @@ public abstract class SimplePlexCommand implements PlexCommand return api().messages().messageComponent(key, objects); } + /** + * Resolves a configured message as a component using component replacements. + * + * @param key message key + * @param objects replacement components + * @return resolved message component + */ protected Component messageComponent(String key, Component... objects) { if (module != null) @@ -178,6 +294,13 @@ public abstract class SimplePlexCommand implements PlexCommand return api().messages().messageComponent(key, objects); } + /** + * Resolves a configured message as plain text. + * + * @param key message key + * @param objects replacement values + * @return resolved message text + */ protected String messageString(String key, Object... objects) { if (module != null) @@ -187,16 +310,34 @@ public abstract class SimplePlexCommand implements PlexCommand return api().messages().messageString(key, objects); } + /** + * Returns this command's formatted usage component. + * + * @return formatted usage component + */ protected Component usage() { return usage(getUsage()); } + /** + * Formats command usage text with the standard usage prefix. + * + * @param usage usage text + * @return formatted usage component + */ protected Component usage(String usage) { return messageComponent("correctUsagePrefix").append(componentFromString(usage).color(NamedTextColor.GRAY)); } + /** + * Returns an online player by UUID string or name. + * + * @param name UUID string or player name + * @return matching online player + * @throws PlayerNotFoundException when no matching online player exists + */ protected Player getNonNullPlayer(String name) { try @@ -220,21 +361,44 @@ public abstract class SimplePlexCommand implements PlexCommand return player; } + /** + * Returns the names of currently online players. + * + * @return online player names + */ protected List onlinePlayerNames() { return api().players().onlineNames(); } + /** + * Converts ampersand-colorized legacy text to a gray-default component. + * + * @param value legacy text + * @return deserialized component + */ protected Component componentFromString(String value) { return LegacyComponentSerializer.legacyAmpersand().deserialize(value).colorIfAbsent(NamedTextColor.GRAY); } + /** + * Converts ampersand-colorized legacy text to a component without adding a default color. + * + * @param value legacy text + * @return deserialized component + */ protected Component noColorComponentFromString(String value) { return LegacyComponentSerializer.legacyAmpersand().deserialize(value); } + /** + * Converts MiniMessage-formatted text to a component. + * + * @param value MiniMessage-formatted text + * @return deserialized component + */ protected Component mmString(String value) { return api().messages().miniMessage(value); diff --git a/server/src/main/java/dev/plex/Plex.java b/server/src/main/java/dev/plex/Plex.java index c19910b..c4593e3 100644 --- a/server/src/main/java/dev/plex/Plex.java +++ b/server/src/main/java/dev/plex/Plex.java @@ -13,6 +13,7 @@ import dev.plex.hook.CoreProtectHook; import dev.plex.hook.PrismHook; import dev.plex.hook.RollbackManager; import dev.plex.module.ModuleManager; +import dev.plex.player.PlayerNameResolver; import dev.plex.player.PlayerService; import dev.plex.player.PlexPlayer; import dev.plex.punishment.PunishmentManager; @@ -21,6 +22,8 @@ import dev.plex.storage.RedisConnection; import dev.plex.storage.SQLConnection; import dev.plex.storage.StorageType; import dev.plex.storage.player.SQLPlayerData; +import dev.plex.storage.player.PlayerModuleDataRepository; +import dev.plex.storage.player.SQLPlayerModuleData; import dev.plex.storage.punishment.SQLNotes; import dev.plex.storage.punishment.SQLPunishment; import dev.plex.storage.repository.NoteRepository; @@ -65,7 +68,9 @@ public class Plex extends JavaPlugin private PlayerCache playerCache; private PlayerRepository playerRepository; + private PlayerModuleDataRepository playerModuleDataRepository; private PlayerService playerService; + private PlayerNameResolver playerNameResolver; private PunishmentRepository punishmentRepository; private NoteRepository noteRepository; @@ -217,8 +222,10 @@ public class Plex extends JavaPlugin punishmentRepository = new SQLPunishment(sqlConnection.getConnectionSource(), api.scheduler().asyncExecutor()); playerRepository = new SQLPlayerData(sqlConnection.getConnectionSource(), punishmentRepository); + playerModuleDataRepository = new SQLPlayerModuleData(sqlConnection, storageType); noteRepository = new SQLNotes(sqlConnection.getConnectionSource(), api.scheduler().asyncExecutor()); playerService = new PlayerService(playerCache, playerRepository); + playerNameResolver = new PlayerNameResolver(playerService); new ListenerHandler(this); commandHandler = new CommandHandler(this); diff --git a/server/src/main/java/dev/plex/api/impl/DefaultPlayerModuleData.java b/server/src/main/java/dev/plex/api/impl/DefaultPlayerModuleData.java new file mode 100644 index 0000000..1820dd2 --- /dev/null +++ b/server/src/main/java/dev/plex/api/impl/DefaultPlayerModuleData.java @@ -0,0 +1,100 @@ +package dev.plex.api.impl; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import dev.plex.api.player.PlayerModuleData; +import dev.plex.storage.player.PlayerModuleDataRepository; + +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; +import java.util.regex.Pattern; + +public class DefaultPlayerModuleData implements PlayerModuleData +{ + private static final Gson GSON = new Gson(); + private static final Pattern KEY_PATTERN = Pattern.compile("^[a-z][a-z0-9_]{0,63}$"); + + private final PlayerModuleDataRepository repository; + private final String modulePrefix; + private final UUID playerUuid; + + public DefaultPlayerModuleData(PlayerModuleDataRepository repository, String modulePrefix, UUID playerUuid) + { + this.repository = repository; + this.modulePrefix = modulePrefix; + this.playerUuid = playerUuid; + } + + @Override + public Optional get(String key) + { + return repository.get(playerUuid, modulePrefix, validateKey(key)); + } + + @Override + public Optional get(String key, Class type) + { + return get(key).map(element -> GSON.fromJson(element, type)); + } + + @Override + public String getString(String key, String fallback) + { + return get(key) + .filter(JsonElement::isJsonPrimitive) + .map(JsonElement::getAsJsonPrimitive) + .filter(primitive -> primitive.isString()) + .map(primitive -> primitive.getAsString()) + .orElse(fallback); + } + + @Override + public long getLong(String key, long fallback) + { + return get(key) + .filter(JsonElement::isJsonPrimitive) + .map(JsonElement::getAsJsonPrimitive) + .filter(primitive -> primitive.isNumber()) + .map(primitive -> primitive.getAsLong()) + .orElse(fallback); + } + + @Override + public boolean getBoolean(String key, boolean fallback) + { + return get(key) + .filter(JsonElement::isJsonPrimitive) + .map(JsonElement::getAsJsonPrimitive) + .filter(primitive -> primitive.isBoolean()) + .map(primitive -> primitive.getAsBoolean()) + .orElse(fallback); + } + + @Override + public void set(String key, JsonElement value) + { + repository.set(playerUuid, modulePrefix, validateKey(key), Objects.requireNonNull(value, "value")); + } + + @Override + public void set(String key, Object value) + { + set(key, GSON.toJsonTree(value)); + } + + @Override + public void remove(String key) + { + repository.remove(playerUuid, modulePrefix, validateKey(key)); + } + + private String validateKey(String key) + { + if (key == null || !KEY_PATTERN.matcher(key).matches()) + { + throw new IllegalArgumentException("Invalid player module data key: " + key); + } + return key; + } +} diff --git a/server/src/main/java/dev/plex/api/impl/DefaultPlayersApi.java b/server/src/main/java/dev/plex/api/impl/DefaultPlayersApi.java index 49c3ab3..ede5d50 100644 --- a/server/src/main/java/dev/plex/api/impl/DefaultPlayersApi.java +++ b/server/src/main/java/dev/plex/api/impl/DefaultPlayersApi.java @@ -1,9 +1,12 @@ package dev.plex.api.impl; import dev.plex.Plex; +import dev.plex.api.player.PlayerModuleData; import dev.plex.api.player.PlayersApi; import dev.plex.api.player.PlexPlayerView; +import dev.plex.module.PlexModule; import dev.plex.player.PlexPlayer; +import dev.plex.storage.module.ModuleNames; import dev.plex.util.PlexUtils; import java.util.List; import java.util.Optional; @@ -15,9 +18,10 @@ final class DefaultPlayersApi implements PlayersApi DefaultPlayersApi(Plex plugin) { this.plugin = plugin; } - @Override public Optional byUuid(UUID uuid) { return Optional.ofNullable(plugin.getPlayerService().getPlayer(uuid)).map(DefaultPlexPlayerView::new); } - @Override public Optional byName(String name) { return Optional.ofNullable(plugin.getPlayerService().getPlayer(name)).map(DefaultPlexPlayerView::new); } + @Override public Optional player(UUID uuid) { return Optional.ofNullable(plugin.getPlayerService().getPlayer(uuid)).map(player -> new DefaultPlexPlayerView(player, plugin.getPlayerNameResolver())); } + @Override public Optional byName(String name) { return Optional.ofNullable(plugin.getPlayerService().getPlayer(name)).map(player -> new DefaultPlexPlayerView(player, plugin.getPlayerNameResolver())); } @Override public List onlineNames() { return PlexUtils.getPlayerNameList(); } + @Override public PlayerModuleData moduleData(PlexModule module, UUID playerUuid) { return new DefaultPlayerModuleData(plugin.getPlayerModuleDataRepository(), ModuleNames.prefix(module), playerUuid); } static PlexPlayer unwrap(PlexPlayerView view) { diff --git a/server/src/main/java/dev/plex/api/impl/DefaultPlexPlayerView.java b/server/src/main/java/dev/plex/api/impl/DefaultPlexPlayerView.java index 3760d03..2afeb8f 100644 --- a/server/src/main/java/dev/plex/api/impl/DefaultPlexPlayerView.java +++ b/server/src/main/java/dev/plex/api/impl/DefaultPlexPlayerView.java @@ -2,17 +2,18 @@ package dev.plex.api.impl; import dev.plex.api.player.PlexPlayerView; import dev.plex.api.punishment.PunishmentView; +import dev.plex.player.PlayerNameResolver; import dev.plex.player.PlexPlayer; import java.util.List; import java.util.UUID; import org.bukkit.entity.Player; -record DefaultPlexPlayerView(PlexPlayer player) implements PlexPlayerView +record DefaultPlexPlayerView(PlexPlayer player, PlayerNameResolver playerNameResolver) implements PlexPlayerView { @Override public UUID uuid() { return player.getUuid(); } @Override public String name() { return player.getName(); } @Override public List ips() { return List.copyOf(player.getIps()); } - @Override public List punishments() { return player.getPunishments().stream().map(DefaultPunishmentView::new).toList(); } + @Override public List punishments() { return player.getPunishments().stream().map(punishment -> new DefaultPunishmentView(punishment, playerNameResolver)).toList(); } @Override public boolean frozen() { return player.isFrozen(); } @Override public boolean muted() { return player.isMuted(); } @Override public boolean lockedUp() { return player.isLockedUp(); } diff --git a/server/src/main/java/dev/plex/api/impl/DefaultPunishmentView.java b/server/src/main/java/dev/plex/api/impl/DefaultPunishmentView.java index d94fddd..9ccccfe 100644 --- a/server/src/main/java/dev/plex/api/impl/DefaultPunishmentView.java +++ b/server/src/main/java/dev/plex/api/impl/DefaultPunishmentView.java @@ -1,18 +1,21 @@ package dev.plex.api.impl; +import dev.plex.api.punishment.PunishmentSource; import dev.plex.api.punishment.PunishmentType; import dev.plex.api.punishment.PunishmentView; +import dev.plex.player.PlayerNameResolver; import dev.plex.punishment.Punishment; import java.time.ZonedDateTime; import java.util.UUID; -record DefaultPunishmentView(Punishment punishment) implements PunishmentView +record DefaultPunishmentView(Punishment punishment, PlayerNameResolver playerNameResolver) implements PunishmentView { @Override public UUID punished() { return punishment.getPunished(); } @Override public UUID punisher() { return punishment.getPunisher(); } - @Override public String punisherName() { return punishment.getPunisherName(); } + @Override public PunishmentSource source() { return punishment.getSource() == null ? (punishment.getPunisher() == null ? PunishmentSource.CONSOLE : PunishmentSource.PLAYER) : punishment.getSource(); } + @Override public String punisherReference() { return punishment.getPunisherReference(); } + @Override public String punisherDisplayName() { return Punishment.punisherDisplayName(punishment, playerNameResolver); } @Override public String ip() { return punishment.getIp(); } - @Override public String punishedUsername() { return punishment.getPunishedUsername(); } @Override public PunishmentType type() { return PunishmentType.valueOf(punishment.getType().name()); } @Override public String reason() { return punishment.getReason(); } @Override public boolean customTime() { return punishment.isCustomTime(); } diff --git a/server/src/main/java/dev/plex/api/impl/DefaultPunishmentsApi.java b/server/src/main/java/dev/plex/api/impl/DefaultPunishmentsApi.java index fd2f84f..dd323e7 100644 --- a/server/src/main/java/dev/plex/api/impl/DefaultPunishmentsApi.java +++ b/server/src/main/java/dev/plex/api/impl/DefaultPunishmentsApi.java @@ -27,9 +27,9 @@ final class DefaultPunishmentsApi implements PunishmentsApi PlexPlayer player = DefaultPlayersApi.unwrap(playerView); if (player == null) player = plugin.getPlayerService().getPlayer(playerView.uuid()); Punishment punishment = new Punishment(request.punished(), request.punisher()); - punishment.setPunisherName(request.punisherName()); + punishment.setSource(request.source()); + punishment.setPunisherReference(request.punisherReference()); punishment.setIp(request.ip()); - punishment.setPunishedUsername(request.punishedUsername()); punishment.setType(PunishmentType.valueOf(request.type().name())); punishment.setReason(request.reason()); punishment.setCustomTime(request.customTime()); diff --git a/server/src/main/java/dev/plex/api/impl/DefaultStorageApi.java b/server/src/main/java/dev/plex/api/impl/DefaultStorageApi.java index 1de916a..6dd8554 100644 --- a/server/src/main/java/dev/plex/api/impl/DefaultStorageApi.java +++ b/server/src/main/java/dev/plex/api/impl/DefaultStorageApi.java @@ -1,7 +1,11 @@ package dev.plex.api.impl; import dev.plex.Plex; +import dev.plex.api.storage.ModuleStorage; +import dev.plex.api.storage.SqlDialect; import dev.plex.api.storage.StorageApi; +import dev.plex.module.PlexModule; +import dev.plex.storage.module.ServerModuleStorage; import java.sql.Connection; import java.sql.SQLException; @@ -19,4 +23,16 @@ final class DefaultStorageApi implements StorageApi return function.apply(connection); } } + + @Override + public ModuleStorage forModule(PlexModule module) + { + return new ServerModuleStorage(plugin, module); + } + + @Override + public SqlDialect dialect() + { + return plugin.getStorageType().dialect(); + } } diff --git a/server/src/main/java/dev/plex/command/impl/BanCMD.java b/server/src/main/java/dev/plex/command/impl/BanCMD.java index e2792af..843c295 100644 --- a/server/src/main/java/dev/plex/command/impl/BanCMD.java +++ b/server/src/main/java/dev/plex/command/impl/BanCMD.java @@ -97,7 +97,6 @@ public class BanCMD extends ServerCommand { punishment.setReason(context.messageString("noReasonProvided")); } - punishment.setPunishedUsername(plexPlayer.getName()); ZonedDateTime date = ZonedDateTime.now(ZoneId.of(TimeUtils.TIMEZONE)); punishment.setEndDate(date.plusDays(1)); punishment.setCustomTime(false); @@ -107,7 +106,7 @@ public class BanCMD extends ServerCommand PlexUtils.broadcast(context.messageComponent("banningPlayer", sender.getName(), plexPlayer.getName())); if (player != null) { - plugin.getApi().scheduler().runEntity(player, () -> BungeeUtil.kickPlayer(plugin, player, Punishment.generateBanMessage(punishment, plugin.config.getString("banning.ban_url"), plugin.getPlayerService()))); + plugin.getApi().scheduler().runEntity(player, () -> BungeeUtil.kickPlayer(plugin, player, Punishment.generateBanMessage(punishment, plugin.config.getString("banning.ban_url"), plugin.getPlayerNameResolver()))); } PlexLog.debug("(From /ban command) PunishedPlayer UUID: " + plexPlayer.getUuid()); diff --git a/server/src/main/java/dev/plex/command/impl/BanListCommand.java b/server/src/main/java/dev/plex/command/impl/BanListCommand.java index a39ef30..429af52 100755 --- a/server/src/main/java/dev/plex/command/impl/BanListCommand.java +++ b/server/src/main/java/dev/plex/command/impl/BanListCommand.java @@ -3,9 +3,6 @@ package dev.plex.command.impl; import com.mojang.brigadier.builder.LiteralArgumentBuilder; import dev.plex.command.ServerCommand; import dev.plex.command.ServerCommandContext; -import dev.plex.punishment.Punishment; - -import java.util.stream.Collectors; import io.papermc.paper.command.brigadier.CommandSourceStack; import net.kyori.adventure.text.Component; @@ -44,7 +41,7 @@ public class BanListCommand extends ServerCommand { plugin.getPunishmentManager().getActiveBans().whenComplete((punishments, throwable) -> { - context.send(sender, context.messageComponent("activeBansList", punishments.size(), StringUtils.join(punishments.stream().map(Punishment::getPunishedUsername).collect(Collectors.toList()), ", "))); + context.send(sender, context.messageComponent("activeBansList", punishments.size(), StringUtils.join(punishments.stream().map(punishment -> plugin.getPlayerNameResolver().resolve(punishment.getPunished())).toList(), ", "))); }); return null; } diff --git a/server/src/main/java/dev/plex/command/impl/FreezeCMD.java b/server/src/main/java/dev/plex/command/impl/FreezeCMD.java index 10ba9c1..620fe3d 100644 --- a/server/src/main/java/dev/plex/command/impl/FreezeCMD.java +++ b/server/src/main/java/dev/plex/command/impl/FreezeCMD.java @@ -60,7 +60,6 @@ public class FreezeCMD extends ServerCommand ZonedDateTime date = ZonedDateTime.now(ZoneId.of(TimeUtils.TIMEZONE)); punishment.setEndDate(date.plusSeconds(plugin.config.getInt("punishments.freeze-timer", 300))); punishment.setType(PunishmentType.FREEZE); - punishment.setPunishedUsername(player.getName()); punishment.setIp(player.getAddress().getAddress().getHostAddress().trim()); punishment.setReason(""); punishment.setActive(true); diff --git a/server/src/main/java/dev/plex/command/impl/KickCMD.java b/server/src/main/java/dev/plex/command/impl/KickCMD.java index 36c3063..098db83 100644 --- a/server/src/main/java/dev/plex/command/impl/KickCMD.java +++ b/server/src/main/java/dev/plex/command/impl/KickCMD.java @@ -76,14 +76,13 @@ public class KickCMD extends ServerCommand } punishment.setReason(reason); - punishment.setPunishedUsername(plexPlayer.getName()); punishment.setEndDate(ZonedDateTime.now(ZoneId.of(TimeUtils.TIMEZONE))); punishment.setCustomTime(false); punishment.setActive(false); punishment.setIp(player.getAddress().getAddress().getHostAddress().trim()); plugin.getPunishmentManager().punish(plexPlayer, punishment); PlexUtils.broadcast(context.messageComponent("kickedPlayer", sender.getName(), plexPlayer.getName())); - BungeeUtil.kickPlayer(plugin, player, Punishment.generateKickMessage(punishment, plugin.getPlayerService())); + BungeeUtil.kickPlayer(plugin, player, Punishment.generateKickMessage(punishment, plugin.getPlayerNameResolver())); return null; } diff --git a/server/src/main/java/dev/plex/command/impl/MuteCMD.java b/server/src/main/java/dev/plex/command/impl/MuteCMD.java index b55cadb..22df2bb 100644 --- a/server/src/main/java/dev/plex/command/impl/MuteCMD.java +++ b/server/src/main/java/dev/plex/command/impl/MuteCMD.java @@ -66,7 +66,6 @@ public class MuteCMD extends ServerCommand ZonedDateTime date = ZonedDateTime.now(ZoneId.of(TimeUtils.TIMEZONE)); punishment.setEndDate(date.plusSeconds(plugin.config.getInt("punishments.mute-timer", 300))); punishment.setType(PunishmentType.MUTE); - punishment.setPunishedUsername(player.getName()); punishment.setIp(player.getAddress().getAddress().getHostAddress().trim()); punishment.setReason(""); punishment.setActive(true); diff --git a/server/src/main/java/dev/plex/command/impl/NotesCMD.java b/server/src/main/java/dev/plex/command/impl/NotesCMD.java index 612cc3f..d5fde0e 100644 --- a/server/src/main/java/dev/plex/command/impl/NotesCMD.java +++ b/server/src/main/java/dev/plex/command/impl/NotesCMD.java @@ -150,7 +150,8 @@ public class NotesCMD extends ServerCommand AtomicReference noteList = new AtomicReference<>(context.messageComponent("notesHeader", plexPlayer.getName())); for (Note note : notes) { - Component noteLine = context.messageComponent("notePrefix", note.getId(), plugin.getPlayerService().getPlayer(note.getWrittenBy()).getName(), TimeUtils.useTimezone(note.getTimestamp())); + String author = plugin.getPlayerNameResolver().resolve(note.getWrittenBy()); + Component noteLine = context.messageComponent("notePrefix", note.getId(), author, TimeUtils.useTimezone(note.getTimestamp())); noteLine = noteLine.append(context.messageComponent("noteLine", note.getNote())); noteList.set(noteList.get().append(Component.newline())); noteList.set(noteList.get().append(noteLine)); diff --git a/server/src/main/java/dev/plex/command/impl/SmiteCMD.java b/server/src/main/java/dev/plex/command/impl/SmiteCMD.java index fbdc326..cfeafe2 100644 --- a/server/src/main/java/dev/plex/command/impl/SmiteCMD.java +++ b/server/src/main/java/dev/plex/command/impl/SmiteCMD.java @@ -128,7 +128,6 @@ public class SmiteCMD extends ServerCommand punishment.setCustomTime(false); punishment.setEndDate(ZonedDateTime.now(ZoneId.of(TimeUtils.TIMEZONE))); punishment.setType(PunishmentType.SMITE); - punishment.setPunishedUsername(player.getName()); punishment.setIp(player.getAddress().getAddress().getHostAddress().trim()); if (reason != null) diff --git a/server/src/main/java/dev/plex/command/impl/TempbanCMD.java b/server/src/main/java/dev/plex/command/impl/TempbanCMD.java index 27819fc..704e3dc 100644 --- a/server/src/main/java/dev/plex/command/impl/TempbanCMD.java +++ b/server/src/main/java/dev/plex/command/impl/TempbanCMD.java @@ -83,7 +83,6 @@ public class TempbanCMD extends ServerCommand { punishment.setReason(context.messageString("noReasonProvided")); } - punishment.setPunishedUsername(target.getName()); punishment.setEndDate(TimeUtils.createDate(args[1])); punishment.setCustomTime(false); punishment.setActive(true); @@ -92,7 +91,7 @@ public class TempbanCMD extends ServerCommand PlexUtils.broadcast(context.messageComponent("banningPlayer", sender.getName(), target.getName())); if (player != null) { - plugin.getApi().scheduler().runEntity(player, () -> BungeeUtil.kickPlayer(plugin, player, Punishment.generateBanMessage(punishment, plugin.config.getString("banning.ban_url"), plugin.getPlayerService()))); + plugin.getApi().scheduler().runEntity(player, () -> BungeeUtil.kickPlayer(plugin, player, Punishment.generateBanMessage(punishment, plugin.config.getString("banning.ban_url"), plugin.getPlayerNameResolver()))); } if (rollBack) { diff --git a/server/src/main/java/dev/plex/command/impl/TempmuteCMD.java b/server/src/main/java/dev/plex/command/impl/TempmuteCMD.java index 8ce0775..5b296cb 100644 --- a/server/src/main/java/dev/plex/command/impl/TempmuteCMD.java +++ b/server/src/main/java/dev/plex/command/impl/TempmuteCMD.java @@ -93,7 +93,6 @@ public class TempmuteCMD extends ServerCommand punishment.setCustomTime(true); punishment.setEndDate(endDate); punishment.setType(PunishmentType.MUTE); - punishment.setPunishedUsername(player.getName()); punishment.setIp(player.getAddress().getAddress().getHostAddress().trim()); punishment.setReason(reason); punishment.setActive(true); diff --git a/server/src/main/java/dev/plex/listener/impl/BanListener.java b/server/src/main/java/dev/plex/listener/impl/BanListener.java index 2d8ba6a..d0ddbc4 100644 --- a/server/src/main/java/dev/plex/listener/impl/BanListener.java +++ b/server/src/main/java/dev/plex/listener/impl/BanListener.java @@ -54,7 +54,7 @@ public class BanListener extends ServerListenerBase PlexPlayer player = plugin.getPlayerService().getPlayer(event.getUniqueId()); player.getPunishments().stream().filter(punishment -> (punishment.getType() == PunishmentType.BAN || punishment.getType() == PunishmentType.TEMPBAN) && punishment.isActive()).findFirst().ifPresent(punishment -> event.disallow(AsyncPlayerPreLoginEvent.Result.KICK_BANNED, - Punishment.generateBanMessage(punishment, plugin.config.getString("banning.ban_url"), plugin.getPlayerService()))); + Punishment.generateBanMessage(punishment, plugin.config.getString("banning.ban_url"), plugin.getPlayerNameResolver()))); return; } Punishment ipBannedPunishment = plugin.getPunishmentManager().getBanByIP(event.getAddress().getHostAddress()); @@ -66,7 +66,7 @@ public class BanListener extends ServerListenerBase return; } event.disallow(AsyncPlayerPreLoginEvent.Result.KICK_BANNED, - Punishment.generateBanMessage(ipBannedPunishment, plugin.config.getString("banning.ban_url"), plugin.getPlayerService())); + Punishment.generateBanMessage(ipBannedPunishment, plugin.config.getString("banning.ban_url"), plugin.getPlayerNameResolver())); } } -} \ No newline at end of file +} diff --git a/server/src/main/java/dev/plex/listener/impl/PlayerListener.java b/server/src/main/java/dev/plex/listener/impl/PlayerListener.java index 0af5c4f..dace4d9 100644 --- a/server/src/main/java/dev/plex/listener/impl/PlayerListener.java +++ b/server/src/main/java/dev/plex/listener/impl/PlayerListener.java @@ -54,7 +54,7 @@ public class PlayerListener extends ServerListenerBase } if (!plexPlayer.getName().equals(player.getName())) { - PlexLog.log(plexPlayer.getName() + " has a new name. Changing it to " + player.getName()); + PlexLog.log(plexPlayer.getName() + " has a new last known name. Changing it to " + player.getName()); plexPlayer.setName(player.getName()); plugin.getPlayerService().update(plexPlayer); } diff --git a/server/src/main/java/dev/plex/player/PlayerNameResolver.java b/server/src/main/java/dev/plex/player/PlayerNameResolver.java new file mode 100644 index 0000000..b9e647d --- /dev/null +++ b/server/src/main/java/dev/plex/player/PlayerNameResolver.java @@ -0,0 +1,96 @@ +package dev.plex.player; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +public class PlayerNameResolver +{ + private final PlayerService playerService; + private final HttpClient httpClient = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(3)) + .build(); + private final Map profileCache = new ConcurrentHashMap<>(); + + public PlayerNameResolver(PlayerService playerService) + { + this.playerService = playerService; + } + + public String resolve(UUID uuid) + { + if (uuid == null) + { + return "CONSOLE"; + } + + Player online = Bukkit.getPlayer(uuid); + if (online != null) + { + return online.getName(); + } + + String local = playerService.getNameByUUID(uuid); + if (local != null && !local.isBlank()) + { + return local; + } + + String cached = profileCache.get(uuid); + if (cached != null && !cached.isBlank()) + { + return cached; + } + + return lookupMojangName(uuid) + .map(name -> + { + profileCache.put(uuid, name); + return name; + }) + .orElse(uuid.toString()); + } + + private Optional lookupMojangName(UUID uuid) + { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create("https://sessionserver.mojang.com/session/minecraft/profile/" + uuid.toString().replace("-", ""))) + .timeout(Duration.ofSeconds(5)) + .GET() + .build(); + try + { + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() != 200) + { + return Optional.empty(); + } + JsonObject object = JsonParser.parseString(response.body()).getAsJsonObject(); + if (!object.has("name") || !object.get("name").isJsonPrimitive()) + { + return Optional.empty(); + } + return Optional.ofNullable(object.get("name").getAsString()).filter(name -> !name.isBlank()); + } + catch (IOException | InterruptedException | RuntimeException e) + { + if (e instanceof InterruptedException) + { + Thread.currentThread().interrupt(); + } + return Optional.empty(); + } + } +} diff --git a/server/src/main/java/dev/plex/player/PlexPlayer.java b/server/src/main/java/dev/plex/player/PlexPlayer.java index 8aa9d95..b761da8 100644 --- a/server/src/main/java/dev/plex/player/PlexPlayer.java +++ b/server/src/main/java/dev/plex/player/PlexPlayer.java @@ -34,7 +34,6 @@ public class PlexPlayer private String prefix; private boolean staffChat; - private boolean vanished; private boolean commandSpy; // These fields are transient so MongoDB doesn't automatically drop them in. @@ -42,8 +41,6 @@ public class PlexPlayer private transient boolean muted; private transient boolean lockedUp; - private long coins; - private List ips = Lists.newArrayList(); private List punishments = Lists.newArrayList(); @@ -62,11 +59,8 @@ public class PlexPlayer this.loginMessage = ""; this.prefix = ""; - this.vanished = false; this.commandSpy = false; - this.coins = 0; - if (loadPunishments) { this.checkMutesAndFreeze(); diff --git a/server/src/main/java/dev/plex/punishment/Punishment.java b/server/src/main/java/dev/plex/punishment/Punishment.java index 008b882..66e0b4c 100644 --- a/server/src/main/java/dev/plex/punishment/Punishment.java +++ b/server/src/main/java/dev/plex/punishment/Punishment.java @@ -2,7 +2,8 @@ package dev.plex.punishment; import com.google.gson.Gson; import com.google.gson.GsonBuilder; -import dev.plex.player.PlayerService; +import dev.plex.api.punishment.PunishmentSource; +import dev.plex.player.PlayerNameResolver; import dev.plex.util.PlexUtils; import dev.plex.util.TimeUtils; import dev.plex.util.adapter.ZonedDateTimeAdapter; @@ -24,12 +25,9 @@ public class Punishment @NotNull private final UUID punished; private final UUID punisher; - // Optional display attribution for punishers without a Minecraft UUID - // (e.g. web staff signed in via XenForo). When non-null, render this in - // place of the UUID-based name lookup. - private String punisherName; + private PunishmentSource source; + private String punisherReference; private String ip; - private String punishedUsername; private PunishmentType type; private String reason; private boolean customTime; @@ -41,32 +39,33 @@ public class Punishment { this.punished = punished; this.punisher = punisher; + this.source = punisher == null ? PunishmentSource.CONSOLE : PunishmentSource.PLAYER; this.issueDate = ZonedDateTime.now(ZoneId.of(TimeUtils.TIMEZONE)); } - public static Component generateBanMessage(Punishment punishment, String banUrl, PlayerService playerService) + public static Component generateBanMessage(Punishment punishment, String banUrl, PlayerNameResolver playerNameResolver) { - return PlexUtils.messageComponent("banMessage", banUrl, punishment.getReason(), TimeUtils.useTimezone(punishment.getEndDate()), punisherDisplayName(punishment, playerService)); + return PlexUtils.messageComponent("banMessage", banUrl, punishment.getReason(), TimeUtils.useTimezone(punishment.getEndDate()), punisherDisplayName(punishment, playerNameResolver)); } - public static Component generateKickMessage(Punishment punishment, PlayerService playerService) + public static Component generateKickMessage(Punishment punishment, PlayerNameResolver playerNameResolver) { - return PlexUtils.messageComponent("kickMessage", punishment.getReason(), punisherDisplayName(punishment, playerService)); + return PlexUtils.messageComponent("kickMessage", punishment.getReason(), punisherDisplayName(punishment, playerNameResolver)); } - /** - * Resolves the human-readable punisher attribution for display. - * Prefers the explicit {@link #punisherName} (used for off-server - * sources such as XenForo staff acting via the web HTTPD), falling - * back to a UUID lookup, and finally "CONSOLE" when the punisher is - * truly unknown. - */ - public static String punisherDisplayName(Punishment punishment, PlayerService playerService) + public static String punisherDisplayName(Punishment punishment, PlayerNameResolver playerNameResolver) { - String explicit = punishment.getPunisherName(); - if (explicit != null && !explicit.isEmpty()) return explicit; - if (punishment.getPunisher() == null) return "CONSOLE"; - return playerService.getNameByUUID(punishment.getPunisher()); + PunishmentSource source = punishment.getSource(); + if (source == null) + { + source = punishment.getPunisher() == null ? PunishmentSource.CONSOLE : PunishmentSource.PLAYER; + } + return switch (source) + { + case PLAYER -> punishment.getPunisher() == null ? "CONSOLE" : playerNameResolver.resolve(punishment.getPunisher()); + case CONSOLE -> "CONSOLE"; + case WEB -> punishment.getPunisherReference() == null || punishment.getPunisherReference().isBlank() ? "WEB" : punishment.getPunisherReference(); + }; } public static Component generateIndefBanMessageWithReason(String type, String banUrl, String reason) diff --git a/server/src/main/java/dev/plex/services/impl/TimingService.java b/server/src/main/java/dev/plex/services/impl/TimingService.java index c791e56..68c334a 100644 --- a/server/src/main/java/dev/plex/services/impl/TimingService.java +++ b/server/src/main/java/dev/plex/services/impl/TimingService.java @@ -55,7 +55,6 @@ public class TimingService extends AbstractService punishment.setReason(PlexUtils.messageString("nukerTempbanReason")); if (player != null) { - punishment.setPunishedUsername(player.getName()); punishment.setIp(player.getAddress().getAddress().getHostAddress()); } punishment.setEndDate(TimeUtils.createDate("5m")); diff --git a/server/src/main/java/dev/plex/storage/StorageType.java b/server/src/main/java/dev/plex/storage/StorageType.java index a5e16cb..a011434 100644 --- a/server/src/main/java/dev/plex/storage/StorageType.java +++ b/server/src/main/java/dev/plex/storage/StorageType.java @@ -2,6 +2,7 @@ package dev.plex.storage; import com.zaxxer.hikari.HikariConfig; import dev.plex.Plex; +import dev.plex.api.storage.SqlDialect; import java.io.File; import java.util.Arrays; @@ -24,7 +25,7 @@ public enum StorageType @Override public String migrationHistoryTableSql(String tableName) { - return "CREATE TABLE IF NOT EXISTS " + tableName + " (version VARCHAR(100) NOT NULL PRIMARY KEY, installed_at INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000))"; + return "CREATE TABLE IF NOT EXISTS " + quoteIdentifier(tableName) + " (scope VARCHAR(100) NOT NULL, version VARCHAR(100) NOT NULL, installed_at INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000), PRIMARY KEY (scope, version))"; } }, @@ -42,7 +43,7 @@ public enum StorageType @Override public String migrationHistoryTableSql(String tableName) { - return "CREATE TABLE IF NOT EXISTS `" + tableName + "` (`version` VARCHAR(100) NOT NULL PRIMARY KEY, `installed_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP)"; + return "CREATE TABLE IF NOT EXISTS " + quoteIdentifier(tableName) + " (`scope` VARCHAR(100) NOT NULL, `version` VARCHAR(100) NOT NULL, `installed_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`scope`, `version`))"; } }, @@ -81,9 +82,56 @@ public enum StorageType public abstract void configure(HikariConfig config, Plex plugin); + public SqlDialect dialect() + { + return switch (this) + { + case SQLITE -> SqlDialect.SQLITE; + case MARIADB -> SqlDialect.MARIADB; + case POSTGRES -> SqlDialect.POSTGRES; + }; + } + + public String quoteIdentifier(String identifier) + { + return switch (this) + { + case MARIADB -> "`" + identifier + "`"; + case SQLITE, POSTGRES -> "\"" + identifier + "\""; + }; + } + public String migrationHistoryTableSql(String tableName) { - return "CREATE TABLE IF NOT EXISTS " + tableName + " (version VARCHAR(100) NOT NULL PRIMARY KEY, installed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP)"; + return "CREATE TABLE IF NOT EXISTS " + quoteIdentifier(tableName) + " (scope VARCHAR(100) NOT NULL, version VARCHAR(100) NOT NULL, installed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (scope, version))"; + } + + public String playerModuleDataUpsertSql() + { + return switch (this) + { + case SQLITE -> """ + INSERT INTO player_module_data (player_uuid, module, data_key, value_json, updated_at) + VALUES (?, ?, ?, ?, ?) + ON CONFLICT(player_uuid, module, data_key) DO UPDATE SET + value_json = excluded.value_json, + updated_at = excluded.updated_at + """; + case MARIADB -> """ + INSERT INTO `player_module_data` (`player_uuid`, `module`, `data_key`, `value_json`, `updated_at`) + VALUES (?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + `value_json` = VALUES(`value_json`), + `updated_at` = VALUES(`updated_at`) + """; + case POSTGRES -> """ + INSERT INTO player_module_data (player_uuid, module, data_key, value_json, updated_at) + VALUES (?, ?, ?, ?, ?) + ON CONFLICT(player_uuid, module, data_key) DO UPDATE SET + value_json = excluded.value_json, + updated_at = excluded.updated_at + """; + }; } public String getDisplayName() diff --git a/server/src/main/java/dev/plex/storage/database/Database.java b/server/src/main/java/dev/plex/storage/database/Database.java index 135b520..cbcb006 100644 --- a/server/src/main/java/dev/plex/storage/database/Database.java +++ b/server/src/main/java/dev/plex/storage/database/Database.java @@ -9,24 +9,16 @@ import dev.plex.storage.StorageType; import dev.plex.util.PlexLog; import lombok.Getter; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.sql.Connection; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Statement; -import java.util.ArrayList; import java.util.List; @Getter public class Database { - private static final String MIGRATION_TABLE = "plex_schema_history"; - protected final Plex plugin; private final HikariDataSource dataSource; private final ConnectionSource connectionSource; private final StorageType storageType; + private final MigrationRunner migrationRunner; public Database(Plex plugin) { @@ -48,7 +40,8 @@ public class Database try { this.connectionSource = new DataSourceConnectionSource(dataSource, config.getJdbcUrl()); - runMigrations(); + this.migrationRunner = new MigrationRunner(storageType); + this.migrationRunner.runCore(dataSource, getClass().getClassLoader(), List.of("001_initial_schema")); } catch (Exception e) { @@ -57,105 +50,7 @@ public class Database } } - private void runMigrations() throws Exception - { - try (Connection connection = dataSource.getConnection()) - { - ensureMigrationTable(connection); - for (String migration : List.of("001_initial_schema")) - { - if (hasMigration(connection, migration)) - { - continue; - } - - executeMigration(connection, migration); - try (Statement statement = connection.createStatement()) - { - statement.executeUpdate("INSERT INTO " + MIGRATION_TABLE + " (version) VALUES ('" + migration + "')"); - } - PlexLog.log("Applied database migration " + migration); - } - } - } - - private void ensureMigrationTable(Connection connection) throws SQLException - { - try (Statement statement = connection.createStatement()) - { - statement.execute(storageType.migrationHistoryTableSql(MIGRATION_TABLE)); - } - } - - private boolean hasMigration(Connection connection, String migration) throws SQLException - { - try (Statement statement = connection.createStatement(); ResultSet resultSet = statement.executeQuery("SELECT version FROM " + MIGRATION_TABLE + " WHERE version = '" + migration + "'")) - { - return resultSet.next(); - } - } - - private void executeMigration(Connection connection, String migration) throws Exception - { - String resource = "db/migration/" + storageType.getMigrationDirectory() + "/" + migration + ".sql"; - try (InputStream stream = getClass().getClassLoader().getResourceAsStream(resource)) - { - if (stream == null) - { - throw new IllegalStateException("Missing database migration resource: " + resource); - } - - for (String sql : splitStatements(new String(stream.readAllBytes(), StandardCharsets.UTF_8))) - { - try (Statement statement = connection.createStatement()) - { - statement.execute(sql); - } - } - } - } - - private List splitStatements(String script) - { - List statements = new ArrayList<>(); - StringBuilder current = new StringBuilder(); - boolean inSingleQuote = false; - boolean inDoubleQuote = false; - - for (int i = 0; i < script.length(); i++) - { - char c = script.charAt(i); - if (c == '\'' && !inDoubleQuote) - { - inSingleQuote = !inSingleQuote; - } - else if (c == '"' && !inSingleQuote) - { - inDoubleQuote = !inDoubleQuote; - } - - if (c == ';' && !inSingleQuote && !inDoubleQuote) - { - addStatement(statements, current); - current.setLength(0); - continue; - } - current.append(c); - } - addStatement(statements, current); - return statements; - } - - private void addStatement(List statements, StringBuilder statement) - { - String sql = statement.toString().replaceAll("(?m)^\\s*--.*$", "").trim(); - if (!sql.isEmpty()) - { - statements.add(sql); - } - } - - public Connection getConnection() throws SQLException + public java.sql.Connection getConnection() throws java.sql.SQLException { return dataSource.getConnection(); } diff --git a/server/src/main/java/dev/plex/storage/database/MigrationRunner.java b/server/src/main/java/dev/plex/storage/database/MigrationRunner.java new file mode 100644 index 0000000..3916c57 --- /dev/null +++ b/server/src/main/java/dev/plex/storage/database/MigrationRunner.java @@ -0,0 +1,206 @@ +package dev.plex.storage.database; + +import dev.plex.module.PlexModule; +import dev.plex.storage.StorageType; +import dev.plex.util.PlexLog; + +import javax.sql.DataSource; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class MigrationRunner +{ + private static final String MIGRATION_TABLE = "plex_schema_history"; + private static final Pattern VERSION_PATTERN = Pattern.compile("^[0-9]{3}_[a-z0-9_]+$"); + private static final Pattern TABLE_TOKEN_PATTERN = Pattern.compile("\\{\\{table:([a-z0-9_]+)}}"); + + private final StorageType storageType; + + public MigrationRunner(StorageType storageType) + { + this.storageType = storageType; + } + + public void runCore(DataSource dataSource, ClassLoader classLoader, List versions) throws SQLException + { + run(dataSource, "core", versions, version -> readCore(classLoader, version), Function.identity()); + } + + public void runModule(DataSource dataSource, PlexModule module, String scope, String resourceRoot, List versions, Function tableResolver) throws SQLException + { + run(dataSource, scope, versions, version -> readModule(module, resourceRoot, version), tableResolver); + } + + private void run(DataSource dataSource, String scope, List versions, ResourceReader reader, Function tableResolver) throws SQLException + { + try (Connection connection = dataSource.getConnection()) + { + ensureMigrationTable(connection); + for (String version : versions) + { + validateVersion(version); + if (hasMigration(connection, scope, version)) + { + continue; + } + + String script = replaceTableTokens(reader.read(version), tableResolver); + for (String sql : splitStatements(script)) + { + try (Statement statement = connection.createStatement()) + { + statement.execute(sql); + } + } + insertMigration(connection, scope, version); + PlexLog.log("Applied database migration " + scope + ":" + version); + } + } + } + + private void ensureMigrationTable(Connection connection) throws SQLException + { + try (Statement statement = connection.createStatement()) + { + statement.execute(storageType.migrationHistoryTableSql(MIGRATION_TABLE)); + } + } + + private boolean hasMigration(Connection connection, String scope, String version) throws SQLException + { + try (PreparedStatement statement = connection.prepareStatement("SELECT version FROM " + MIGRATION_TABLE + " WHERE scope = ? AND version = ?")) + { + statement.setString(1, scope); + statement.setString(2, version); + try (ResultSet resultSet = statement.executeQuery()) + { + return resultSet.next(); + } + } + } + + private void insertMigration(Connection connection, String scope, String version) throws SQLException + { + try (PreparedStatement statement = connection.prepareStatement("INSERT INTO " + MIGRATION_TABLE + " (scope, version) VALUES (?, ?)")) + { + statement.setString(1, scope); + statement.setString(2, version); + statement.executeUpdate(); + } + } + + private void validateVersion(String version) throws SQLException + { + if (!VERSION_PATTERN.matcher(version).matches()) + { + throw new SQLException("Invalid migration version: " + version); + } + } + + private String readCore(ClassLoader classLoader, String version) throws SQLException + { + String resource = "db/migration/" + storageType.dialect().migrationDirectory() + "/" + version + ".sql"; + try (InputStream stream = classLoader.getResourceAsStream(resource)) + { + if (stream == null) + { + throw new SQLException("Missing database migration resource: " + resource); + } + return new String(stream.readAllBytes(), StandardCharsets.UTF_8); + } + catch (IOException e) + { + throw new SQLException("Failed to read database migration resource: " + resource, e); + } + } + + private String readModule(PlexModule module, String resourceRoot, String version) throws SQLException + { + String resource = resourceRoot + "/" + storageType.dialect().migrationDirectory() + "/" + version + ".sql"; + try (InputStream stream = module.getResource(resource)) + { + if (stream == null) + { + throw new SQLException("Missing module migration resource: " + resource); + } + return new String(stream.readAllBytes(), StandardCharsets.UTF_8); + } + catch (IOException e) + { + throw new SQLException("Failed to read module migration resource: " + resource, e); + } + } + + private String replaceTableTokens(String script, Function tableResolver) throws SQLException + { + Matcher matcher = TABLE_TOKEN_PATTERN.matcher(script); + StringBuilder replaced = new StringBuilder(); + while (matcher.find()) + { + matcher.appendReplacement(replaced, Matcher.quoteReplacement(tableResolver.apply(matcher.group(1)))); + } + matcher.appendTail(replaced); + if (replaced.toString().contains("{{table:")) + { + throw new SQLException("Unsupported table token in migration"); + } + return replaced.toString(); + } + + private List splitStatements(String script) + { + List statements = new ArrayList<>(); + StringBuilder current = new StringBuilder(); + boolean inSingleQuote = false; + boolean inDoubleQuote = false; + + for (int i = 0; i < script.length(); i++) + { + char c = script.charAt(i); + if (c == '\'' && !inDoubleQuote) + { + inSingleQuote = !inSingleQuote; + } + else if (c == '"' && !inSingleQuote) + { + inDoubleQuote = !inDoubleQuote; + } + + if (c == ';' && !inSingleQuote && !inDoubleQuote) + { + addStatement(statements, current); + current.setLength(0); + continue; + } + current.append(c); + } + addStatement(statements, current); + return statements; + } + + private void addStatement(List statements, StringBuilder statement) + { + String sql = statement.toString().replaceAll("(?m)^\\s*--.*$", "").trim(); + if (!sql.isEmpty()) + { + statements.add(sql); + } + } + + @FunctionalInterface + private interface ResourceReader + { + String read(String version) throws SQLException; + } +} diff --git a/server/src/main/java/dev/plex/storage/database/entity/NoteEntity.java b/server/src/main/java/dev/plex/storage/database/entity/NoteEntity.java index 4d08d41..21a7444 100644 --- a/server/src/main/java/dev/plex/storage/database/entity/NoteEntity.java +++ b/server/src/main/java/dev/plex/storage/database/entity/NoteEntity.java @@ -19,8 +19,8 @@ public class NoteEntity @DatabaseField(columnName = "uuid", canBeNull = false, index = true, width = 46) private String uuid; - @DatabaseField(columnName = "written_by", width = 46) - private String writtenBy; + @DatabaseField(columnName = "written_by_uuid", width = 46) + private String writtenByUuid; @DatabaseField(columnName = "note", width = 2000) private String note; diff --git a/server/src/main/java/dev/plex/storage/database/entity/PlayerEntity.java b/server/src/main/java/dev/plex/storage/database/entity/PlayerEntity.java index 5a1ccd2..d9c672f 100644 --- a/server/src/main/java/dev/plex/storage/database/entity/PlayerEntity.java +++ b/server/src/main/java/dev/plex/storage/database/entity/PlayerEntity.java @@ -13,8 +13,8 @@ public class PlayerEntity @DatabaseField(id = true, columnName = "uuid", width = 46) private String uuid; - @DatabaseField(columnName = "name", width = 18) - private String name; + @DatabaseField(columnName = "last_known_name", width = 18, index = true) + private String lastKnownName; @DatabaseField(columnName = "login_msg", width = 2000) private String loginMessage; @@ -28,12 +28,6 @@ public class PlayerEntity @DatabaseField(columnName = "ips", width = 2000) private String ips; - @DatabaseField(columnName = "coins") - private long coins; - - @DatabaseField(columnName = "vanished") - private boolean vanished; - @DatabaseField(columnName = "commandspy") private boolean commandSpy; diff --git a/server/src/main/java/dev/plex/storage/database/entity/PunishmentEntity.java b/server/src/main/java/dev/plex/storage/database/entity/PunishmentEntity.java index 58228c9..335b8dd 100644 --- a/server/src/main/java/dev/plex/storage/database/entity/PunishmentEntity.java +++ b/server/src/main/java/dev/plex/storage/database/entity/PunishmentEntity.java @@ -13,17 +13,17 @@ public class PunishmentEntity @DatabaseField(generatedId = true, columnName = "id") private long id; - @DatabaseField(columnName = "punished", canBeNull = false, index = true, width = 46) - private String punished; + @DatabaseField(columnName = "punished_uuid", canBeNull = false, index = true, width = 46) + private String punishedUuid; - @DatabaseField(columnName = "punisher", width = 46) - private String punisher; + @DatabaseField(columnName = "punisher_uuid", width = 46) + private String punisherUuid; - @DatabaseField(columnName = "punisherName", width = 64) - private String punisherName; + @DatabaseField(columnName = "source", width = 20) + private String source; - @DatabaseField(columnName = "punishedUsername", width = 16) - private String punishedUsername; + @DatabaseField(columnName = "punisher_reference", width = 200) + private String punisherReference; @DatabaseField(columnName = "ip", width = 2000, index = true) private String ip; diff --git a/server/src/main/java/dev/plex/storage/module/ModuleNames.java b/server/src/main/java/dev/plex/storage/module/ModuleNames.java new file mode 100644 index 0000000..b1fa8d5 --- /dev/null +++ b/server/src/main/java/dev/plex/storage/module/ModuleNames.java @@ -0,0 +1,47 @@ +package dev.plex.storage.module; + +import dev.plex.module.PlexModule; + +import java.util.Locale; + +public final class ModuleNames +{ + private static final int MAX_PREFIX_LENGTH = 40; + + private ModuleNames() + { + } + + public static String prefix(PlexModule module) + { + String name = module.getPlexModuleFile().getName().toLowerCase(Locale.ROOT); + if (name.startsWith("module-")) + { + name = name.substring("module-".length()); + } + name = name.replaceAll("[^a-z0-9]+", "_").replaceAll("^_+|_+$", ""); + if (name.isBlank()) + { + throw new IllegalArgumentException("Module name does not produce a valid storage prefix"); + } + if (name.length() > MAX_PREFIX_LENGTH) + { + name = name.substring(0, MAX_PREFIX_LENGTH).replaceAll("_+$", ""); + } + return name; + } + + public static String table(String prefix, String localName) + { + return prefix + "_" + validateLocalName(localName); + } + + public static String validateLocalName(String localName) + { + if (localName == null || !localName.matches("[a-z][a-z0-9_]{0,47}")) + { + throw new IllegalArgumentException("Invalid module table name: " + localName); + } + return localName; + } +} diff --git a/server/src/main/java/dev/plex/storage/module/ServerModuleMigrations.java b/server/src/main/java/dev/plex/storage/module/ServerModuleMigrations.java new file mode 100644 index 0000000..59654f3 --- /dev/null +++ b/server/src/main/java/dev/plex/storage/module/ServerModuleMigrations.java @@ -0,0 +1,41 @@ +package dev.plex.storage.module; + +import dev.plex.Plex; +import dev.plex.api.storage.ModuleMigrations; +import dev.plex.module.PlexModule; + +import java.sql.SQLException; +import java.util.List; + +public class ServerModuleMigrations implements ModuleMigrations +{ + private final Plex plugin; + private final PlexModule module; + private final ServerModuleStorage storage; + + public ServerModuleMigrations(Plex plugin, PlexModule module, ServerModuleStorage storage) + { + this.plugin = plugin; + this.module = module; + this.storage = storage; + } + + @Override + public void run(List versions) throws SQLException + { + run("db/migration", versions); + } + + @Override + public void run(String resourceRoot, List versions) throws SQLException + { + plugin.getSqlConnection().getMigrationRunner().runModule( + plugin.getSqlConnection().getDataSource(), + module, + storage.scope(), + resourceRoot, + versions, + storage::quotedTable + ); + } +} diff --git a/server/src/main/java/dev/plex/storage/module/ServerModuleOrm.java b/server/src/main/java/dev/plex/storage/module/ServerModuleOrm.java new file mode 100644 index 0000000..83041b9 --- /dev/null +++ b/server/src/main/java/dev/plex/storage/module/ServerModuleOrm.java @@ -0,0 +1,42 @@ +package dev.plex.storage.module; + +import com.j256.ormlite.dao.Dao; +import com.j256.ormlite.dao.DaoManager; +import com.j256.ormlite.support.ConnectionSource; +import com.j256.ormlite.table.DatabaseTableConfig; +import dev.plex.api.storage.ModuleOrm; + +import java.sql.SQLException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class ServerModuleOrm implements ModuleOrm +{ + private final ConnectionSource connectionSource; + private final ServerModuleStorage storage; + private final Map> daos = new ConcurrentHashMap<>(); + + public ServerModuleOrm(ConnectionSource connectionSource, ServerModuleStorage storage) + { + this.connectionSource = connectionSource; + this.storage = storage; + } + + @Override + @SuppressWarnings("unchecked") + public Dao dao(Class entityClass, String localTableName) throws SQLException + { + String key = entityClass.getName() + ":" + localTableName; + Dao existing = daos.get(key); + if (existing != null) + { + return (Dao) existing; + } + + DatabaseTableConfig tableConfig = DatabaseTableConfig.fromClass(connectionSource.getDatabaseType(), entityClass); + tableConfig.setTableName(storage.table(localTableName)); + Dao dao = DaoManager.createDao(connectionSource, tableConfig); + daos.put(key, dao); + return dao; + } +} diff --git a/server/src/main/java/dev/plex/storage/module/ServerModuleStorage.java b/server/src/main/java/dev/plex/storage/module/ServerModuleStorage.java new file mode 100644 index 0000000..1cc1469 --- /dev/null +++ b/server/src/main/java/dev/plex/storage/module/ServerModuleStorage.java @@ -0,0 +1,69 @@ +package dev.plex.storage.module; + +import com.j256.ormlite.misc.TransactionManager; +import dev.plex.Plex; +import dev.plex.api.storage.ModuleMigrations; +import dev.plex.api.storage.ModuleOrm; +import dev.plex.api.storage.ModuleStorage; +import dev.plex.api.storage.SqlCallable; +import dev.plex.module.PlexModule; + +import java.sql.SQLException; + +public class ServerModuleStorage implements ModuleStorage +{ + private final Plex plugin; + private final PlexModule module; + private final String prefix; + private final ModuleMigrations migrations; + private final ModuleOrm orm; + + public ServerModuleStorage(Plex plugin, PlexModule module) + { + this.plugin = plugin; + this.module = module; + this.prefix = ModuleNames.prefix(module); + this.migrations = new ServerModuleMigrations(plugin, module, this); + this.orm = new ServerModuleOrm(plugin.getSqlConnection().getConnectionSource(), this); + } + + @Override + public String prefix() + { + return prefix; + } + + @Override + public String table(String localName) + { + return ModuleNames.table(prefix, localName); + } + + String quotedTable(String localName) + { + return plugin.getStorageType().quoteIdentifier(table(localName)); + } + + String scope() + { + return "module:" + prefix; + } + + @Override + public ModuleMigrations migrations() + { + return migrations; + } + + @Override + public ModuleOrm orm() + { + return orm; + } + + @Override + public T transaction(SqlCallable callable) throws SQLException + { + return TransactionManager.callInTransaction(plugin.getSqlConnection().getConnectionSource(), callable::call); + } +} diff --git a/server/src/main/java/dev/plex/storage/player/PlayerModuleDataRepository.java b/server/src/main/java/dev/plex/storage/player/PlayerModuleDataRepository.java new file mode 100644 index 0000000..f3d9b1b --- /dev/null +++ b/server/src/main/java/dev/plex/storage/player/PlayerModuleDataRepository.java @@ -0,0 +1,15 @@ +package dev.plex.storage.player; + +import com.google.gson.JsonElement; + +import java.util.Optional; +import java.util.UUID; + +public interface PlayerModuleDataRepository +{ + Optional get(UUID playerUuid, String module, String key); + + void set(UUID playerUuid, String module, String key, JsonElement value); + + void remove(UUID playerUuid, String module, String key); +} diff --git a/server/src/main/java/dev/plex/storage/player/SQLPlayerData.java b/server/src/main/java/dev/plex/storage/player/SQLPlayerData.java index 2b1556d..d2361d7 100644 --- a/server/src/main/java/dev/plex/storage/player/SQLPlayerData.java +++ b/server/src/main/java/dev/plex/storage/player/SQLPlayerData.java @@ -57,7 +57,7 @@ public class SQLPlayerData implements PlayerRepository { try { - return players.queryBuilder().where().eq("name", username).queryForFirst() != null; + return players.queryBuilder().where().eq("last_known_name", username).queryForFirst() != null; } catch (SQLException e) { @@ -84,7 +84,7 @@ public class SQLPlayerData implements PlayerRepository try { PlayerEntity entity = players.queryForId(uuid.toString()); - return entity == null ? null : entity.getName(); + return entity == null ? null : entity.getLastKnownName(); } catch (SQLException e) { @@ -102,7 +102,7 @@ public class SQLPlayerData implements PlayerRepository { try { - return toPlayer(players.queryBuilder().limit(1L).where().eq("name", username).queryForFirst(), loadExtraData); + return toPlayer(players.queryBuilder().limit(1L).where().eq("last_known_name", username).queryForFirst(), loadExtraData); } catch (SQLException e) { @@ -169,13 +169,11 @@ public class SQLPlayerData implements PlayerRepository } PlexPlayer plexPlayer = new PlexPlayer(UUID.fromString(entity.getUuid()), false); - plexPlayer.setName(entity.getName()); + plexPlayer.setName(entity.getLastKnownName()); plexPlayer.setLoginMessage(entity.getLoginMessage()); plexPlayer.setPrefix(entity.getPrefix()); plexPlayer.setStaffChat(entity.isStaffChat()); plexPlayer.setIps(parseIps(entity.getIps())); - plexPlayer.setCoins(entity.getCoins()); - plexPlayer.setVanished(entity.isVanished()); plexPlayer.setCommandSpy(entity.isCommandSpy()); if (loadExtraData) { @@ -189,13 +187,11 @@ public class SQLPlayerData implements PlayerRepository { PlayerEntity entity = new PlayerEntity(); entity.setUuid(player.getUuid().toString()); - entity.setName(player.getName()); + entity.setLastKnownName(player.getName()); entity.setLoginMessage(player.getLoginMessage()); entity.setPrefix(player.getPrefix()); entity.setStaffChat(player.isStaffChat()); entity.setIps(GSON.toJson(player.getIps())); - entity.setCoins(player.getCoins()); - entity.setVanished(player.isVanished()); entity.setCommandSpy(player.isCommandSpy()); return entity; } diff --git a/server/src/main/java/dev/plex/storage/player/SQLPlayerModuleData.java b/server/src/main/java/dev/plex/storage/player/SQLPlayerModuleData.java new file mode 100644 index 0000000..b7aab47 --- /dev/null +++ b/server/src/main/java/dev/plex/storage/player/SQLPlayerModuleData.java @@ -0,0 +1,86 @@ +package dev.plex.storage.player; + +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; +import com.google.gson.JsonSyntaxException; +import dev.plex.storage.SQLConnection; +import dev.plex.storage.StorageType; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Optional; +import java.util.UUID; + +public class SQLPlayerModuleData implements PlayerModuleDataRepository +{ + private final SQLConnection sqlConnection; + private final StorageType storageType; + + public SQLPlayerModuleData(SQLConnection sqlConnection, StorageType storageType) + { + this.sqlConnection = sqlConnection; + this.storageType = storageType; + } + + @Override + public Optional get(UUID playerUuid, String module, String key) + { + String sql = "SELECT value_json FROM player_module_data WHERE player_uuid = ? AND module = ? AND data_key = ?"; + try (Connection connection = sqlConnection.getConnection(); PreparedStatement statement = connection.prepareStatement(sql)) + { + statement.setString(1, playerUuid.toString()); + statement.setString(2, module); + statement.setString(3, key); + try (ResultSet resultSet = statement.executeQuery()) + { + if (!resultSet.next()) + { + return Optional.empty(); + } + return Optional.of(JsonParser.parseString(resultSet.getString("value_json"))); + } + } + catch (SQLException | JsonSyntaxException e) + { + e.printStackTrace(); + return Optional.empty(); + } + } + + @Override + public void set(UUID playerUuid, String module, String key, JsonElement value) + { + try (Connection connection = sqlConnection.getConnection(); PreparedStatement statement = connection.prepareStatement(storageType.playerModuleDataUpsertSql())) + { + statement.setString(1, playerUuid.toString()); + statement.setString(2, module); + statement.setString(3, key); + statement.setString(4, value.toString()); + statement.setLong(5, System.currentTimeMillis()); + statement.executeUpdate(); + } + catch (SQLException e) + { + e.printStackTrace(); + } + } + + @Override + public void remove(UUID playerUuid, String module, String key) + { + String sql = "DELETE FROM player_module_data WHERE player_uuid = ? AND module = ? AND data_key = ?"; + try (Connection connection = sqlConnection.getConnection(); PreparedStatement statement = connection.prepareStatement(sql)) + { + statement.setString(1, playerUuid.toString()); + statement.setString(2, module); + statement.setString(3, key); + statement.executeUpdate(); + } + catch (SQLException e) + { + e.printStackTrace(); + } + } +} diff --git a/server/src/main/java/dev/plex/storage/punishment/SQLNotes.java b/server/src/main/java/dev/plex/storage/punishment/SQLNotes.java index 331bff5..d9412a6 100644 --- a/server/src/main/java/dev/plex/storage/punishment/SQLNotes.java +++ b/server/src/main/java/dev/plex/storage/punishment/SQLNotes.java @@ -16,6 +16,7 @@ import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.Comparator; import java.util.List; +import java.util.Optional; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; @@ -47,6 +48,7 @@ public class SQLNotes implements NoteRepository return notes.queryForEq("uuid", uuid.toString()).stream() .sorted(Comparator.comparingInt(NoteEntity::getId)) .map(this::toNote) + .flatMap(Optional::stream) .toList(); } catch (SQLException e) @@ -96,16 +98,23 @@ public class SQLNotes implements NoteRepository }, executor); } - private Note toNote(NoteEntity entity) + private Optional toNote(NoteEntity entity) { - Note note = new Note( - UUID.fromString(entity.getUuid()), - entity.getNote(), - UUID.fromString(entity.getWrittenBy()), - ZonedDateTime.ofInstant(Instant.ofEpochMilli(entity.getTimestamp()), ZoneId.of(TimeUtils.TIMEZONE)) - ); - note.setId(entity.getId()); - return note; + try + { + Note note = new Note( + UUID.fromString(entity.getUuid()), + entity.getNote(), + UUID.fromString(entity.getWrittenByUuid()), + ZonedDateTime.ofInstant(Instant.ofEpochMilli(entity.getTimestamp()), ZoneId.of(TimeUtils.TIMEZONE)) + ); + note.setId(entity.getId()); + return Optional.of(note); + } + catch (IllegalArgumentException | NullPointerException e) + { + return Optional.empty(); + } } private NoteEntity toEntity(Note note) @@ -113,7 +122,7 @@ public class SQLNotes implements NoteRepository NoteEntity entity = new NoteEntity(); entity.setId(note.getId()); entity.setUuid(note.getUuid().toString()); - entity.setWrittenBy(note.getWrittenBy().toString()); + entity.setWrittenByUuid(note.getWrittenBy().toString()); entity.setNote(note.getNote()); entity.setTimestamp(note.getTimestamp().toInstant().toEpochMilli()); return entity; diff --git a/server/src/main/java/dev/plex/storage/punishment/SQLPunishment.java b/server/src/main/java/dev/plex/storage/punishment/SQLPunishment.java index aa2e01c..3a8a496 100644 --- a/server/src/main/java/dev/plex/storage/punishment/SQLPunishment.java +++ b/server/src/main/java/dev/plex/storage/punishment/SQLPunishment.java @@ -5,6 +5,7 @@ import com.j256.ormlite.dao.Dao; import com.j256.ormlite.dao.DaoManager; import com.j256.ormlite.support.ConnectionSource; import com.j256.ormlite.stmt.UpdateBuilder; +import dev.plex.api.punishment.PunishmentSource; import dev.plex.punishment.Punishment; import dev.plex.punishment.PunishmentType; import dev.plex.storage.database.entity.PunishmentEntity; @@ -59,7 +60,7 @@ public class SQLPunishment implements PunishmentRepository { try { - return punishments.queryForEq("punished", uuid.toString()).stream().map(this::toPunishment).toList(); + return punishments.queryForEq("punished_uuid", uuid.toString()).stream().map(this::toPunishment).toList(); } catch (SQLException e) { @@ -119,7 +120,7 @@ public class SQLPunishment implements PunishmentRepository { UpdateBuilder update = punishments.updateBuilder(); update.updateColumnValue("active", active); - update.where().eq("punished", punished.toString()).and().eq("type", type.name()); + update.where().eq("punished_uuid", punished.toString()).and().eq("type", type.name()); update.update(); } catch (SQLException e) @@ -130,13 +131,13 @@ public class SQLPunishment implements PunishmentRepository private Punishment toPunishment(PunishmentEntity entity) { - UUID punisher = entity.getPunisher() == null || entity.getPunisher().isBlank() ? null : UUID.fromString(entity.getPunisher()); - Punishment punishment = new Punishment(UUID.fromString(entity.getPunished()), punisher); + UUID punisher = entity.getPunisherUuid() == null || entity.getPunisherUuid().isBlank() ? null : UUID.fromString(entity.getPunisherUuid()); + Punishment punishment = new Punishment(UUID.fromString(entity.getPunishedUuid()), punisher); punishment.setActive(entity.isActive()); punishment.setType(PunishmentType.valueOf(entity.getType())); punishment.setCustomTime(entity.isCustomTime()); - punishment.setPunishedUsername(entity.getPunishedUsername()); - punishment.setPunisherName(entity.getPunisherName()); + punishment.setSource(entity.getSource() == null ? punishment.getSource() : PunishmentSource.valueOf(entity.getSource())); + punishment.setPunisherReference(entity.getPunisherReference()); punishment.setIssueDate(ZonedDateTime.ofInstant(Instant.ofEpochMilli(entity.getIssueDate()), ZoneId.of(TimeUtils.TIMEZONE))); punishment.setEndDate(ZonedDateTime.ofInstant(Instant.ofEpochMilli(entity.getEndDate()), ZoneId.of(TimeUtils.TIMEZONE))); punishment.setReason(entity.getReason()); @@ -147,10 +148,11 @@ public class SQLPunishment implements PunishmentRepository private PunishmentEntity toEntity(Punishment punishment) { PunishmentEntity entity = new PunishmentEntity(); - entity.setPunished(punishment.getPunished().toString()); - entity.setPunisher(punishment.getPunisher() == null ? null : punishment.getPunisher().toString()); - entity.setPunisherName(punishment.getPunisherName()); - entity.setPunishedUsername(punishment.getPunishedUsername()); + entity.setPunishedUuid(punishment.getPunished().toString()); + entity.setPunisherUuid(punishment.getPunisher() == null ? null : punishment.getPunisher().toString()); + PunishmentSource source = punishment.getSource() == null ? (punishment.getPunisher() == null ? PunishmentSource.CONSOLE : PunishmentSource.PLAYER) : punishment.getSource(); + entity.setSource(source.name()); + entity.setPunisherReference(punishment.getPunisherReference()); entity.setIp(punishment.getIp()); entity.setType(punishment.getType().name()); entity.setReason(punishment.getReason()); diff --git a/server/src/main/resources/db/migration/mariadb/001_initial_schema.sql b/server/src/main/resources/db/migration/mariadb/001_initial_schema.sql index a945040..79c2eb4 100644 --- a/server/src/main/resources/db/migration/mariadb/001_initial_schema.sql +++ b/server/src/main/resources/db/migration/mariadb/001_initial_schema.sql @@ -1,23 +1,21 @@ CREATE TABLE IF NOT EXISTS `players` ( `uuid` VARCHAR(46) NOT NULL, - `name` VARCHAR(18), + `last_known_name` VARCHAR(18), `login_msg` VARCHAR(2000), `prefix` VARCHAR(2000), `staffChat` BOOLEAN, `ips` VARCHAR(2000), - `coins` BIGINT, - `vanished` BOOLEAN, `commandspy` BOOLEAN, PRIMARY KEY (`uuid`), - INDEX `idx_players_name` (`name`) + INDEX `idx_players_last_known_name` (`last_known_name`) ); CREATE TABLE IF NOT EXISTS `punishments` ( `id` BIGINT NOT NULL AUTO_INCREMENT, - `punished` VARCHAR(46) NOT NULL, - `punisher` VARCHAR(46), - `punisherName` VARCHAR(64), - `punishedUsername` VARCHAR(16), + `punished_uuid` VARCHAR(46) NOT NULL, + `punisher_uuid` VARCHAR(46), + `source` VARCHAR(30), + `punisher_reference` VARCHAR(200), `ip` VARCHAR(2000), `type` VARCHAR(30), `reason` VARCHAR(2000), @@ -26,7 +24,7 @@ CREATE TABLE IF NOT EXISTS `punishments` ( `issueDate` BIGINT NOT NULL, `endDate` BIGINT, PRIMARY KEY (`id`), - INDEX `idx_punishments_punished` (`punished`), + INDEX `idx_punishments_punished` (`punished_uuid`), INDEX `idx_punishments_ip` (`ip`(64)) ); @@ -34,7 +32,7 @@ CREATE TABLE IF NOT EXISTS `notes` ( `row_id` BIGINT NOT NULL AUTO_INCREMENT, `id` INT NOT NULL, `uuid` VARCHAR(46) NOT NULL, - `written_by` VARCHAR(46), + `written_by_uuid` VARCHAR(46), `note` VARCHAR(2000), `timestamp` BIGINT, PRIMARY KEY (`row_id`), @@ -49,3 +47,12 @@ CREATE TABLE IF NOT EXISTS `player_ips` ( UNIQUE KEY `uq_player_ips_player_ip` (`player_uuid`, `ip`), INDEX `idx_player_ips_ip` (`ip`) ); + +CREATE TABLE IF NOT EXISTS `player_module_data` ( + `player_uuid` VARCHAR(46) NOT NULL, + `module` VARCHAR(100) NOT NULL, + `data_key` VARCHAR(64) NOT NULL, + `value_json` LONGTEXT NOT NULL, + `updated_at` BIGINT NOT NULL, + PRIMARY KEY (`player_uuid`, `module`, `data_key`) +); diff --git a/server/src/main/resources/db/migration/postgres/001_initial_schema.sql b/server/src/main/resources/db/migration/postgres/001_initial_schema.sql index e6dbeb7..7daf92a 100644 --- a/server/src/main/resources/db/migration/postgres/001_initial_schema.sql +++ b/server/src/main/resources/db/migration/postgres/001_initial_schema.sql @@ -1,23 +1,21 @@ CREATE TABLE IF NOT EXISTS players ( uuid VARCHAR(46) NOT NULL PRIMARY KEY, - name VARCHAR(18), + last_known_name VARCHAR(18), login_msg VARCHAR(2000), prefix VARCHAR(2000), staffChat BOOLEAN, ips VARCHAR(2000), - coins BIGINT, - vanished BOOLEAN, commandspy BOOLEAN ); -CREATE INDEX IF NOT EXISTS idx_players_name ON players(name); +CREATE INDEX IF NOT EXISTS idx_players_last_known_name ON players(last_known_name); CREATE TABLE IF NOT EXISTS punishments ( id BIGSERIAL PRIMARY KEY, - punished VARCHAR(46) NOT NULL, - punisher VARCHAR(46), - punisherName VARCHAR(64), - punishedUsername VARCHAR(16), + punished_uuid VARCHAR(46) NOT NULL, + punisher_uuid VARCHAR(46), + source VARCHAR(30), + punisher_reference VARCHAR(200), ip VARCHAR(2000), type VARCHAR(30), reason VARCHAR(2000), @@ -27,14 +25,14 @@ CREATE TABLE IF NOT EXISTS punishments ( endDate BIGINT ); -CREATE INDEX IF NOT EXISTS idx_punishments_punished ON punishments(punished); +CREATE INDEX IF NOT EXISTS idx_punishments_punished ON punishments(punished_uuid); CREATE INDEX IF NOT EXISTS idx_punishments_ip ON punishments(ip); CREATE TABLE IF NOT EXISTS notes ( row_id BIGSERIAL PRIMARY KEY, id INT NOT NULL, uuid VARCHAR(46) NOT NULL, - written_by VARCHAR(46), + written_by_uuid VARCHAR(46), note VARCHAR(2000), timestamp BIGINT ); @@ -49,3 +47,12 @@ CREATE TABLE IF NOT EXISTS player_ips ( ); CREATE INDEX IF NOT EXISTS idx_player_ips_ip ON player_ips(ip); + +CREATE TABLE IF NOT EXISTS player_module_data ( + player_uuid VARCHAR(46) NOT NULL, + module VARCHAR(100) NOT NULL, + data_key VARCHAR(64) NOT NULL, + value_json TEXT NOT NULL, + updated_at BIGINT NOT NULL, + PRIMARY KEY (player_uuid, module, data_key) +); diff --git a/server/src/main/resources/db/migration/sqlite/001_initial_schema.sql b/server/src/main/resources/db/migration/sqlite/001_initial_schema.sql index d4d23f1..5a17141 100644 --- a/server/src/main/resources/db/migration/sqlite/001_initial_schema.sql +++ b/server/src/main/resources/db/migration/sqlite/001_initial_schema.sql @@ -1,21 +1,19 @@ CREATE TABLE IF NOT EXISTS players ( uuid VARCHAR(46) NOT NULL PRIMARY KEY, - name VARCHAR(18), + last_known_name VARCHAR(18), login_msg VARCHAR(2000), prefix VARCHAR(2000), staffChat BOOLEAN, ips VARCHAR(2000), - coins BIGINT, - vanished BOOLEAN, commandspy BOOLEAN ); CREATE TABLE IF NOT EXISTS punishments ( id INTEGER PRIMARY KEY AUTOINCREMENT, - punished VARCHAR(46) NOT NULL, - punisher VARCHAR(46), - punisherName VARCHAR(64), - punishedUsername VARCHAR(16), + punished_uuid VARCHAR(46) NOT NULL, + punisher_uuid VARCHAR(46), + source VARCHAR(30), + punisher_reference VARCHAR(200), ip VARCHAR(2000), type VARCHAR(30), reason VARCHAR(2000), @@ -29,7 +27,7 @@ CREATE TABLE IF NOT EXISTS notes ( row_id INTEGER PRIMARY KEY AUTOINCREMENT, id INT NOT NULL, uuid VARCHAR(46) NOT NULL, - written_by VARCHAR(46), + written_by_uuid VARCHAR(46), note VARCHAR(2000), timestamp BIGINT ); @@ -40,8 +38,17 @@ CREATE TABLE IF NOT EXISTS player_ips ( ip VARCHAR(64) NOT NULL ); -CREATE INDEX IF NOT EXISTS idx_players_name ON players(name); -CREATE INDEX IF NOT EXISTS idx_punishments_punished ON punishments(punished); +CREATE TABLE IF NOT EXISTS player_module_data ( + player_uuid VARCHAR(46) NOT NULL, + module VARCHAR(100) NOT NULL, + data_key VARCHAR(64) NOT NULL, + value_json TEXT NOT NULL, + updated_at INTEGER NOT NULL, + PRIMARY KEY (player_uuid, module, data_key) +); + +CREATE INDEX IF NOT EXISTS idx_players_last_known_name ON players(last_known_name); +CREATE INDEX IF NOT EXISTS idx_punishments_punished ON punishments(punished_uuid); CREATE INDEX IF NOT EXISTS idx_punishments_ip ON punishments(ip); CREATE INDEX IF NOT EXISTS idx_notes_uuid ON notes(uuid); CREATE UNIQUE INDEX IF NOT EXISTS uq_player_ips_player_ip ON player_ips(player_uuid, ip);