New database API

This commit is contained in:
2026-05-24 22:37:32 -04:00
parent 2b9f7c7de7
commit 3c72688e0e
53 changed files with 1398 additions and 265 deletions
+5 -3
View File
@@ -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"])
}
}
}
}
@@ -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<JsonElement> get(String key);
/**
* Gets and maps a JSON value to a Java type.
*
* @param key data key
* @param type target type
* @param <T> target type
* @return mapped value, if present
*/
<T> Optional<T> get(String key, Class<T> 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);
}
@@ -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<? extends PlexPlayerView> byUuid(UUID uuid);
Optional<? extends PlexPlayerView> player(UUID uuid);
/**
* Looks up a player by name.
@@ -31,4 +32,13 @@ public interface PlayersApi
* @return names of currently online players
*/
List<String> 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);
}
@@ -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)
{
}
@@ -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
}
@@ -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.
*
@@ -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<String> 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<String> versions) throws SQLException;
}
@@ -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 <T> entity type
* @param <ID> entity ID type
* @return ORMLite DAO using the module-prefixed physical table
* @throws SQLException if the DAO cannot be created
*/
<T, ID> Dao<T, ID> dao(Class<T> entityClass, String localTableName) throws SQLException;
}
@@ -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 <T> callback result type
* @return callback result
* @throws SQLException if the transaction cannot complete
*/
<T> T transaction(SqlCallable<T> callable) throws SQLException;
}
@@ -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 <T> callback result type
*/
@FunctionalInterface
public interface SqlCallable<T>
{
/**
* Runs SQL work.
*
* @return callback result
* @throws SQLException if the SQL work cannot complete
*/
T call() throws SQLException;
}
@@ -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;
}
}
@@ -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> T withConnection(SqlFunction<T> 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)}.
*
@@ -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.
*
* <p>The default tree accepts optional greedy string arguments and dispatches
* them to {@link #execute(CommandSender, Player, String[])}.</p>
*
* @param command root command literal builder
*/
protected void configureCommand(LiteralArgumentBuilder<CommandSourceStack> 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<String> 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<String> 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);