Begin work on the Plex API

This commit is contained in:
2026-05-19 12:32:56 -04:00
parent 64c691bb58
commit 9fa8d82217
142 changed files with 1960 additions and 566 deletions
+23
View File
@@ -0,0 +1,23 @@
plugins {
java
`maven-publish`
}
dependencies {
compileOnly("io.papermc.paper:paper-api:26.1.2.build.+")
compileOnly("org.apache.logging.log4j:log4j-api:2.26.0")
compileOnly("org.jetbrains:annotations:26.1.0")
}
group = rootProject.group
version = rootProject.version
description = "Plex-API"
publishing {
publications {
create<MavenPublication>("maven") {
from(components["java"])
}
}
}
@@ -0,0 +1,12 @@
package dev.plex.api;
/**
* Describes the module API compatibility level provided by this Plex build.
*/
public interface ApiCompatibility
{
/**
* @return the provided module API compatibility version
*/
int version();
}
@@ -0,0 +1,55 @@
package dev.plex.api;
import dev.plex.api.command.CommandApi;
import dev.plex.api.config.ConfigurationApi;
import dev.plex.api.config.ModuleConfigApi;
import dev.plex.api.listener.ListenerApi;
import dev.plex.api.logging.LoggingApi;
import dev.plex.api.message.MessageApi;
import dev.plex.api.module.ModulesApi;
import dev.plex.api.player.PlayersApi;
import dev.plex.api.punishment.PunishmentsApi;
import dev.plex.api.scheduler.SchedulerApi;
import dev.plex.api.storage.StorageApi;
/**
* Public API facade exposed to Plex modules.
*
* <p>Keep this interface small and deliberate; adding a method here makes it
* part of the supported module API contract.</p>
*/
public interface PlexApi
{
/**
* @return module API compatibility information for this Plex build
*/
ApiCompatibility compatibility();
/**
* @return safe access to shared Plex configuration files
*/
ConfigurationApi configuration();
/**
* @return safe access to module metadata and module-related operations
*/
ModulesApi modules();
CommandApi commands();
ListenerApi listeners();
ModuleConfigApi moduleConfigs();
LoggingApi logging();
MessageApi messages();
PlayersApi players();
PunishmentsApi punishments();
SchedulerApi scheduler();
StorageApi storage();
}
@@ -0,0 +1,10 @@
package dev.plex.api.command;
import org.bukkit.command.Command;
public interface CommandApi
{
void register(Command command);
void unregister(Command command);
}
@@ -0,0 +1,15 @@
package dev.plex.api.config;
/**
* Public configuration access exposed to modules.
*/
public interface ConfigurationApi
{
PlexConfiguration mainConfig();
PlexConfiguration messages();
PlexConfiguration indefiniteBans();
PlexConfiguration toggles();
}
@@ -0,0 +1,8 @@
package dev.plex.api.config;
import dev.plex.module.PlexModule;
public interface ModuleConfigApi
{
ModuleConfiguration create(PlexModule module, String from, String to);
}
@@ -0,0 +1,9 @@
package dev.plex.api.config;
import org.bukkit.configuration.file.YamlConfiguration;
public abstract class ModuleConfiguration extends YamlConfiguration
{
public abstract void load();
public abstract void save();
}
@@ -0,0 +1,23 @@
package dev.plex.api.config;
import java.util.List;
/**
* Stable configuration wrapper exposed through the Plex module API.
*/
public interface PlexConfiguration
{
String getString(String path);
boolean getBoolean(String path);
int getInt(String path);
List<String> getStringList(String path);
void set(String path, Object value);
void setComments(String path, List<String> comments);
void save();
}
@@ -0,0 +1,10 @@
package dev.plex.api.listener;
import org.bukkit.event.Listener;
public interface ListenerApi
{
void register(Listener listener);
void unregister(Listener listener);
}
@@ -0,0 +1,9 @@
package dev.plex.api.logging;
public interface LoggingApi
{
void info(String message, Object... args);
void debug(String message, Object... args);
void warn(String message, Object... args);
void error(String message, Object... args);
}
@@ -0,0 +1,15 @@
package dev.plex.api.message;
import java.util.List;
import net.kyori.adventure.text.Component;
public interface MessageApi
{
Component messageComponent(String entry, Object... objects);
Component messageComponent(String entry, Component... objects);
String messageString(String entry, Object... objects);
Component miniMessage(String input);
void broadcast(String miniMessage);
void broadcast(Component component);
List<String> onlinePlayerNames();
}
@@ -0,0 +1,25 @@
package dev.plex.api.module;
import dev.plex.module.PlexModuleFile;
import java.util.Collection;
import java.util.Optional;
/**
* Public module metadata access exposed to modules.
*/
public interface ModulesApi
{
/**
* @return immutable metadata for all currently discovered modules
*/
Collection<PlexModuleFile> loadedModules();
/**
* Looks up a module by name.
*
* @param name module name from module.yml
* @return module metadata, if a module with this name is loaded
*/
Optional<PlexModuleFile> module(String name);
}
@@ -0,0 +1,12 @@
package dev.plex.api.player;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
public interface PlayersApi
{
Optional<? extends PlexPlayerView> byUuid(UUID uuid);
Optional<? extends PlexPlayerView> byName(String name);
List<String> onlineNames();
}
@@ -0,0 +1,18 @@
package dev.plex.api.player;
import java.util.List;
import java.util.UUID;
import dev.plex.api.punishment.PunishmentView;
import org.bukkit.entity.Player;
public interface PlexPlayerView
{
UUID uuid();
String name();
List<String> ips();
List<? extends PunishmentView> punishments();
boolean frozen();
boolean muted();
boolean lockedUp();
Player bukkitPlayer();
}
@@ -0,0 +1,12 @@
package dev.plex.api.punishment;
import java.util.List;
import java.util.UUID;
public interface IndefiniteBanView
{
List<String> usernames();
List<UUID> uuids();
List<String> ips();
String reason();
}
@@ -0,0 +1,10 @@
package dev.plex.api.punishment;
import java.time.ZonedDateTime;
import java.util.UUID;
public record PunishmentRequest(UUID punished, UUID punisher, String punisherName, String ip,
String punishedUsername, PunishmentType type, String reason,
boolean customTime, boolean active, ZonedDateTime endDate)
{
}
@@ -0,0 +1,11 @@
package dev.plex.api.punishment;
public enum PunishmentType
{
MUTE,
FREEZE,
BAN,
TEMPBAN,
KICK,
SMITE
}
@@ -0,0 +1,19 @@
package dev.plex.api.punishment;
import java.time.ZonedDateTime;
import java.util.UUID;
public interface PunishmentView
{
UUID punished();
UUID punisher();
String punisherName();
String ip();
String punishedUsername();
PunishmentType type();
String reason();
boolean customTime();
boolean active();
ZonedDateTime issueDate();
ZonedDateTime endDate();
}
@@ -0,0 +1,13 @@
package dev.plex.api.punishment;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import dev.plex.api.player.PlexPlayerView;
public interface PunishmentsApi
{
List<? extends IndefiniteBanView> indefiniteBans();
Optional<? extends IndefiniteBanView> indefiniteBanByUuid(UUID uuid);
void punish(PlexPlayerView player, PunishmentRequest punishment);
}
@@ -0,0 +1,8 @@
package dev.plex.api.scheduler;
public interface SchedulerApi
{
Object runSync(Runnable task);
Object runLater(Runnable task, long delayTicks);
Object runTimer(Runnable task, long delayTicks, long periodTicks);
}
@@ -0,0 +1,15 @@
package dev.plex.api.storage;
import java.sql.Connection;
import java.sql.SQLException;
public interface StorageApi
{
<T> T withConnection(SqlFunction<T> function) throws SQLException;
@FunctionalInterface
interface SqlFunction<T>
{
T apply(Connection connection) throws SQLException;
}
}
@@ -0,0 +1,262 @@
package dev.plex.command;
import java.util.ArrayList;
import dev.plex.command.annotation.CommandParameters;
import dev.plex.command.annotation.CommandPermissions;
import dev.plex.command.exception.CommandFailException;
import dev.plex.command.exception.ConsoleMustDefinePlayerException;
import dev.plex.command.exception.ConsoleOnlyException;
import dev.plex.command.exception.PlayerNotBannedException;
import dev.plex.command.exception.PlayerNotFoundException;
import dev.plex.command.source.RequiredCommandSource;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.command.Command;
import org.bukkit.command.CommandMap;
import org.bukkit.command.CommandSender;
import org.bukkit.command.ConsoleCommandSender;
import org.bukkit.entity.Player;
import org.bukkit.util.StringUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/** Public base class for module commands. */
public abstract class PlexCommand extends Command
{
private static Runtime runtime;
private final CommandParameters params;
private final CommandPermissions perms;
private final RequiredCommandSource commandSource;
public static void setRuntime(Runtime runtime)
{
PlexCommand.runtime = runtime;
}
public PlexCommand(boolean register)
{
super("");
this.params = getClass().getAnnotation(CommandParameters.class);
this.perms = getClass().getAnnotation(CommandPermissions.class);
if (params == null || perms == null)
{
throw new IllegalStateException("PlexCommand requires CommandParameters and CommandPermissions annotations");
}
setName(params.name());
setLabel(params.name());
setDescription(params.description());
setPermission(perms.permission());
setUsage(params.usage().replace("<command>", params.name()));
if (!params.aliases().isEmpty())
{
setAliases(Arrays.asList(params.aliases().split(",")));
}
this.commandSource = perms.source();
if (register)
{
requireRuntime().register(this);
}
}
public PlexCommand()
{
this(true);
}
protected abstract Component execute(@NotNull CommandSender sender, @Nullable Player playerSender, @NotNull String[] args);
@Override
public boolean execute(@NotNull CommandSender sender, @NotNull String label, String[] args)
{
if (!matches(label))
{
return false;
}
if (commandSource == RequiredCommandSource.CONSOLE && sender instanceof Player)
{
send(sender, messageComponent("noPermissionInGame"));
return true;
}
if (commandSource == RequiredCommandSource.IN_GAME && sender instanceof ConsoleCommandSender)
{
send(sender, messageComponent("noPermissionConsole"));
return true;
}
if (!perms.permission().isEmpty() && sender instanceof Player player && !player.hasPermission(perms.permission()))
{
send(sender, messageComponent("noPermissionNode", perms.permission()));
return true;
}
try
{
Component component = execute(sender, isConsole(sender) ? null : (Player)sender, args);
if (component != null)
{
send(sender, component);
}
}
catch (PlayerNotFoundException | CommandFailException | ConsoleOnlyException |
ConsoleMustDefinePlayerException | PlayerNotBannedException | NumberFormatException ex)
{
send(sender, mmString(ex.getMessage()));
}
return true;
}
@NotNull
public abstract List<String> smartTabComplete(@NotNull CommandSender sender, @NotNull String alias, @NotNull String[] args) throws IllegalArgumentException;
@NotNull
@Override
public List<String> tabComplete(@NotNull CommandSender sender, @NotNull String alias, @NotNull String[] args) throws IllegalArgumentException
{
return StringUtil.copyPartialMatches(args[args.length - 1], smartTabComplete(sender, alias, args), new ArrayList<>());
}
private boolean matches(String label)
{
return getName().equalsIgnoreCase(label) || getAliases().stream().anyMatch(alias -> alias.equalsIgnoreCase(label));
}
protected void send(Audience audience, String s)
{
audience.sendMessage(componentFromString(s));
}
protected void send(Audience audience, Component component)
{
audience.sendMessage(component);
}
protected boolean checkPermission(CommandSender sender, String permission)
{
return isConsole(sender) || checkPermission((Player)sender, permission);
}
protected boolean silentCheckPermission(CommandSender sender, String permission)
{
return isConsole(sender) || silentCheckPermission((Player)sender, permission);
}
protected boolean checkPermission(Player player, String permission)
{
if (!permission.isEmpty() && !player.hasPermission(permission))
{
throw new CommandFailException(messageString("noPermissionNode", permission));
}
return true;
}
protected boolean silentCheckPermission(Player player, String permission)
{
return permission.isEmpty() || player.hasPermission(permission);
}
protected UUID getUUID(CommandSender sender)
{
return sender instanceof Player player ? player.getUniqueId() : null;
}
protected boolean isConsole(CommandSender sender)
{
return !(sender instanceof Player);
}
protected Component messageComponent(String s, Object... objects)
{
return requireRuntime().messageComponent(s, objects);
}
protected Component messageComponent(String s, Component... objects)
{
return requireRuntime().messageComponent(s, objects);
}
protected String messageString(String s, Object... objects)
{
return requireRuntime().messageString(s, objects);
}
protected Component usage()
{
return Component.text("Correct Usage: ").color(NamedTextColor.YELLOW).append(componentFromString(getUsage()).color(NamedTextColor.GRAY));
}
protected Component usage(String s)
{
return Component.text("Correct Usage: ").color(NamedTextColor.YELLOW).append(componentFromString(s).color(NamedTextColor.GRAY));
}
protected Player getNonNullPlayer(String name)
{
Player player;
try
{
player = Bukkit.getPlayer(UUID.fromString(name));
}
catch (IllegalArgumentException ignored)
{
player = Bukkit.getPlayer(name);
}
if (player == null)
{
throw new PlayerNotFoundException();
}
return player;
}
protected World getNonNullWorld(String name)
{
World world = Bukkit.getWorld(name);
if (world == null)
{
throw new CommandFailException(messageString("worldNotFound"));
}
return world;
}
protected Component componentFromString(String s)
{
return LegacyComponentSerializer.legacyAmpersand().deserialize(s).colorIfAbsent(NamedTextColor.GRAY);
}
protected Component noColorComponentFromString(String s)
{
return LegacyComponentSerializer.legacyAmpersand().deserialize(s);
}
protected Component mmString(String s)
{
return requireRuntime().miniMessage(s);
}
public CommandMap getMap()
{
return Bukkit.getCommandMap();
}
private static Runtime requireRuntime()
{
if (runtime == null)
{
throw new IllegalStateException("PlexCommand runtime has not been installed by Plex");
}
return runtime;
}
public interface Runtime
{
void register(Command command);
Component messageComponent(String entry, Object... objects);
Component messageComponent(String entry, Component... objects);
String messageString(String entry, Object... objects);
Component miniMessage(String input);
}
}
@@ -0,0 +1,13 @@
package dev.plex.command.annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
public @interface CommandParameters
{
String name();
String description() default "";
String usage() default "/<command>";
String aliases() default "";
}
@@ -0,0 +1,12 @@
package dev.plex.command.annotation;
import dev.plex.command.source.RequiredCommandSource;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
public @interface CommandPermissions
{
String permission() default "";
RequiredCommandSource source() default RequiredCommandSource.ANY;
}
@@ -0,0 +1,14 @@
package dev.plex.command.exception;
public class CommandFailException extends RuntimeException
{
public CommandFailException()
{
super("CommandFailException");
}
public CommandFailException(String message)
{
super(message);
}
}
@@ -0,0 +1,14 @@
package dev.plex.command.exception;
public class ConsoleMustDefinePlayerException extends RuntimeException
{
public ConsoleMustDefinePlayerException()
{
super("ConsoleMustDefinePlayerException");
}
public ConsoleMustDefinePlayerException(String message)
{
super(message);
}
}
@@ -0,0 +1,14 @@
package dev.plex.command.exception;
public class ConsoleOnlyException extends RuntimeException
{
public ConsoleOnlyException()
{
super("ConsoleOnlyException");
}
public ConsoleOnlyException(String message)
{
super(message);
}
}
@@ -0,0 +1,14 @@
package dev.plex.command.exception;
public class PlayerNotBannedException extends RuntimeException
{
public PlayerNotBannedException()
{
super("PlayerNotBannedException");
}
public PlayerNotBannedException(String message)
{
super(message);
}
}
@@ -0,0 +1,14 @@
package dev.plex.command.exception;
public class PlayerNotFoundException extends RuntimeException
{
public PlayerNotFoundException()
{
super("PlayerNotFoundException");
}
public PlayerNotFoundException(String message)
{
super(message);
}
}
@@ -0,0 +1,8 @@
package dev.plex.command.source;
public enum RequiredCommandSource
{
ANY,
IN_GAME,
CONSOLE
}
@@ -0,0 +1,87 @@
package dev.plex.config;
import dev.plex.api.config.ModuleConfiguration;
import dev.plex.module.PlexModule;
/**
* Public module config entry point. The platform installs a factory at runtime.
*/
public class ModuleConfig extends ModuleConfiguration
{
private static Factory factory;
private final ModuleConfiguration delegate;
public static void setFactory(Factory factory)
{
ModuleConfig.factory = factory;
}
public ModuleConfig(PlexModule module, String from, String to)
{
if (factory == null)
{
throw new IllegalStateException("ModuleConfig factory has not been installed by Plex");
}
this.delegate = factory.create(module, from, to);
}
@Override
public void load()
{
delegate.load();
}
@Override
public void save()
{
delegate.save();
}
@Override
public Object get(String path)
{
return delegate.get(path);
}
@Override
public String getString(String path)
{
return delegate.getString(path);
}
@Override
public String getString(String path, String def)
{
return delegate.getString(path, def);
}
@Override
public int getInt(String path)
{
return delegate.getInt(path);
}
@Override
public int getInt(String path, int def)
{
return delegate.getInt(path, def);
}
@Override
public boolean getBoolean(String path)
{
return delegate.getBoolean(path);
}
@Override
public void set(String path, Object value)
{
delegate.set(path, value);
}
@FunctionalInterface
public interface Factory
{
ModuleConfiguration create(PlexModule module, String from, String to);
}
}
@@ -0,0 +1,10 @@
package dev.plex.listener;
import org.bukkit.event.Listener;
public abstract class PlexListener implements Listener
{
protected PlexListener()
{
}
}
@@ -0,0 +1,172 @@
package dev.plex.module;
import dev.plex.api.PlexApi;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import org.apache.logging.log4j.Logger;
import org.bukkit.command.Command;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Base class for Plex modules.
*
* <p>This class is part of the public module API. Modules use {@link #api()} for
* supported integration points.</p>
*/
public abstract class PlexModule
{
private final List<Command> commands = new ArrayList<>();
private final List<Listener> listeners = new ArrayList<>();
private PlexApi api;
private PlexModuleFile plexModuleFile;
private File dataFolder;
private Logger logger;
public PlexApi api()
{
return api;
}
public void load()
{
}
public void enable()
{
}
public void disable()
{
}
public void registerListener(Listener listener)
{
listeners.add(listener);
}
public void unregisterListener(Listener listener)
{
listeners.remove(listener);
HandlerList.unregisterAll(listener);
}
public void registerCommand(Command command)
{
commands.add(command);
}
public void unregisterCommand(Command command)
{
commands.remove(command);
}
@Nullable
public Command getCommand(String name)
{
return commands.stream()
.filter(command -> command.getName().equalsIgnoreCase(name) || command.getAliases().stream().map(String::toLowerCase).toList().contains(name.toLowerCase(Locale.ROOT)))
.findFirst()
.orElse(null);
}
public void addDefaultMessage(String message, Object initValue)
{
if (api.configuration().messages().getString(message) == null)
{
api.configuration().messages().set(message, initValue);
api.configuration().messages().save();
logger.debug("'{}' message added from {}", message, plexModuleFile.getName());
}
}
public void addDefaultMessage(String message, Object initValue, String... comments)
{
if (api.configuration().messages().getString(message) == null)
{
api.configuration().messages().set(message, initValue);
api.configuration().messages().save();
api.configuration().messages().setComments(message, Arrays.asList(comments));
api.configuration().messages().save();
logger.debug("'{}' message added from {}", message, plexModuleFile.getName());
}
}
@Nullable
public InputStream getResource(@NotNull String filename)
{
try
{
URL url = this.getClass().getClassLoader().getResource(filename);
if (url == null)
{
return null;
}
URLConnection connection = url.openConnection();
connection.setUseCaches(false);
return connection.getInputStream();
}
catch (IOException ex)
{
return null;
}
}
public List<Command> getCommands()
{
return commands;
}
public List<Listener> getListeners()
{
return listeners;
}
public PlexModuleFile getPlexModuleFile()
{
return plexModuleFile;
}
public File getDataFolder()
{
return dataFolder;
}
public Logger getLogger()
{
return logger;
}
public void setApi(PlexApi api)
{
this.api = api;
}
public void setPlexModuleFile(PlexModuleFile plexModuleFile)
{
this.plexModuleFile = plexModuleFile;
}
public void setDataFolder(File dataFolder)
{
this.dataFolder = dataFolder;
}
public void setLogger(Logger logger)
{
this.logger = logger;
}
}
@@ -0,0 +1,60 @@
package dev.plex.module;
import java.util.List;
/**
* Metadata read from a module's module.yml file.
*/
public final class PlexModuleFile
{
private final String name;
private final String main;
private final String description;
private final String version;
private final int apiCompatibility;
private List<String> libraries = List.of();
public PlexModuleFile(String name, String main, String description, String version, int apiCompatibility)
{
this.name = name;
this.main = main;
this.description = description;
this.version = version;
this.apiCompatibility = apiCompatibility;
}
public String getName()
{
return name;
}
public String getMain()
{
return main;
}
public String getDescription()
{
return description;
}
public String getVersion()
{
return version;
}
public int getApiCompatibility()
{
return apiCompatibility;
}
public List<String> getLibraries()
{
return libraries;
}
public void setLibraries(List<String> libraries)
{
this.libraries = List.copyOf(libraries);
}
}