Allow installing/uninstalling modules in-game

This commit is contained in:
2026-05-28 19:14:34 -04:00
parent 1a83e8355c
commit daac12792a
6 changed files with 173 additions and 54 deletions
@@ -36,6 +36,7 @@ public abstract class PlexModule
private ModuleConfiguration messages; private ModuleConfiguration messages;
private PlexModuleFile plexModuleFile; private PlexModuleFile plexModuleFile;
private File dataFolder; private File dataFolder;
private File moduleJar;
private Logger logger; private Logger logger;
/** /**
@@ -228,6 +229,16 @@ public abstract class PlexModule
return dataFolder; return dataFolder;
} }
/**
* Returns the JAR file this module was loaded from.
*
* @return the JAR file this module was loaded from
*/
public File getModuleJar()
{
return moduleJar;
}
/** /**
* Returns the module logger. * Returns the module logger.
* *
@@ -362,6 +373,16 @@ public abstract class PlexModule
this.dataFolder = dataFolder; this.dataFolder = dataFolder;
} }
/**
* Sets the JAR file this module was loaded from.
*
* @param moduleJar JAR file this module was loaded from
*/
public void setModuleJar(File moduleJar)
{
this.moduleJar = moduleJar;
}
/** /**
* Sets the module logger. * Sets the module logger.
* *
@@ -4,6 +4,7 @@ import dev.plex.command.PlexCommand;
import com.mojang.brigadier.builder.LiteralArgumentBuilder; import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import dev.plex.command.ServerCommand; import dev.plex.command.ServerCommand;
import dev.plex.command.ServerCommandContext; import dev.plex.command.ServerCommandContext;
import dev.plex.command.exception.CommandFailException;
import dev.plex.menu.impl.MaterialMenu; import dev.plex.menu.impl.MaterialMenu;
import dev.plex.util.GameRuleUtil; import dev.plex.util.GameRuleUtil;
import dev.plex.util.PlexLog; import dev.plex.util.PlexLog;
@@ -26,7 +27,7 @@ public class DebugCMD extends ServerCommand
{ {
super(command("pdebug") super(command("pdebug")
.description("Plex's debug command") .description("Plex's debug command")
.usage("/<command> <aliases <command> | redis-reset <player> | gamerules>") .usage("/<command> <aliases <command> | redis | redis-reset <player> | gamerules>")
.permission("plex.debug") .permission("plex.debug")
.build()); .build());
} }
@@ -34,6 +35,8 @@ public class DebugCMD extends ServerCommand
protected void buildCommand(LiteralArgumentBuilder<CommandSourceStack> command) protected void buildCommand(LiteralArgumentBuilder<CommandSourceStack> command)
{ {
command.executes(context -> executeCommand(context)); command.executes(context -> executeCommand(context));
command.then(literal("redis")
.executes(context -> executeCommand(context, "redis")));
command.then(literal("redis-reset") command.then(literal("redis-reset")
.then(playerArgument("player") .then(playerArgument("player")
.executes(context -> executeCommand(context, "redis-reset", string(context, "player"))))); .executes(context -> executeCommand(context, "redis-reset", string(context, "player")))));
@@ -56,6 +59,18 @@ public class DebugCMD extends ServerCommand
{ {
return context.usage(); return context.usage();
} }
if (args[0].equalsIgnoreCase("redis"))
{
if (!plugin.getRedisConnection().isEnabled())
{
throw new CommandFailException("&cRedis is not enabled.");
}
plugin.getRedisConnection().execute(jedis -> jedis.set("test", "123"));
context.send(sender, "Set test to 123. Now outputting key test...");
String test = plugin.getRedisConnection().query(jedis -> jedis.get("test"));
context.send(sender, test);
return null;
}
if (args[0].equalsIgnoreCase("redis-reset")) if (args[0].equalsIgnoreCase("redis-reset"))
{ {
if (args.length == 2) if (args.length == 2)
@@ -3,7 +3,7 @@ package dev.plex.command.impl;
import com.mojang.brigadier.builder.LiteralArgumentBuilder; import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import dev.plex.command.ServerCommand; import dev.plex.command.ServerCommand;
import dev.plex.command.ServerCommandContext; import dev.plex.command.ServerCommandContext;
import dev.plex.command.exception.CommandFailException; import dev.plex.module.ModuleManager;
import dev.plex.module.PlexModule; import dev.plex.module.PlexModule;
import dev.plex.module.PlexModuleFile; import dev.plex.module.PlexModuleFile;
import dev.plex.util.BuildInfo; import dev.plex.util.BuildInfo;
@@ -17,10 +17,7 @@ import java.util.stream.Collectors;
import io.papermc.paper.command.brigadier.CommandSourceStack; import io.papermc.paper.command.brigadier.CommandSourceStack;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.CommandSender; import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
public class PlexCMD extends ServerCommand public class PlexCMD extends ServerCommand
@@ -29,7 +26,7 @@ public class PlexCMD extends ServerCommand
{ {
super(command("plex") super(command("plex")
.description("Show information about Plex or reload it") .description("Show information about Plex or reload it")
.usage("/<command> [reload | redis | update | modules [reload | update]]") .usage("/<command> [reload | update | modules [reload | update | install <name> | uninstall <name> [-rmdir]]]")
.build()); .build());
} }
// Don't modify this command // Don't modify this command
@@ -39,8 +36,6 @@ public class PlexCMD extends ServerCommand
command.executes(context -> executeCommand(context)); command.executes(context -> executeCommand(context));
command.then(literal("reload") command.then(literal("reload")
.executes(context -> executeCommand(context, "reload"))); .executes(context -> executeCommand(context, "reload")));
command.then(literal("redis")
.executes(context -> executeCommand(context, "redis")));
command.then(literal("update") command.then(literal("update")
.executes(context -> executeCommand(context, "update"))); .executes(context -> executeCommand(context, "update")));
command.then(literal("modules") command.then(literal("modules")
@@ -48,14 +43,24 @@ public class PlexCMD extends ServerCommand
.then(literal("reload") .then(literal("reload")
.executes(context -> executeCommand(context, "modules", "reload"))) .executes(context -> executeCommand(context, "modules", "reload")))
.then(literal("update") .then(literal("update")
.executes(context -> executeCommand(context, "modules", "update")))); .executes(context -> executeCommand(context, "modules", "update")))
.then(literal("install")
.then(word("name")
.executes(context -> executeCommand(context, "modules", "install", string(context, "name")))))
.then(literal("uninstall")
.then(word("name")
.suggests(suggest(() -> plugin.getModuleManager().getModules().stream()
.map(module -> module.getPlexModuleFile().getName())
.collect(Collectors.toList())))
.executes(context -> executeCommand(context, "modules", "uninstall", string(context, "name")))
.then(literal("-rmdir")
.executes(context -> executeCommand(context, "modules", "uninstall", string(context, "name"), "-rmdir"))))));
} }
@Override @Override
protected Component execute(@NotNull ServerCommandContext context) protected Component execute(@NotNull ServerCommandContext context)
{ {
CommandSender sender = context.sender(); CommandSender sender = context.sender();
Player playerSender = context.player();
String[] args = context.args(); String[] args = context.args();
if (args.length == 0) if (args.length == 0)
{ {
@@ -91,19 +96,6 @@ public class PlexCMD extends ServerCommand
context.send(sender, "Plex successfully reloaded."); context.send(sender, "Plex successfully reloaded.");
return null; return null;
} }
else if (args[0].equalsIgnoreCase("redis"))
{
context.checkPermission(sender, "plex.redis");
if (!plugin.getRedisConnection().isEnabled())
{
throw new CommandFailException("&cRedis is not enabled.");
}
plugin.getRedisConnection().execute(jedis -> jedis.set("test", "123"));
context.send(sender, "Set test to 123. Now outputting key test...");
String test = plugin.getRedisConnection().query(jedis -> jedis.get("test"));
context.send(sender, test);
return null;
}
else if (args[0].equalsIgnoreCase("modules")) else if (args[0].equalsIgnoreCase("modules"))
{ {
if (args.length == 1) if (args.length == 1)
@@ -118,10 +110,7 @@ public class PlexCMD extends ServerCommand
} }
else if (args[1].equalsIgnoreCase("update")) else if (args[1].equalsIgnoreCase("update"))
{ {
if (!hasUpdateAccess(context, playerSender, sender)) context.checkPermission(sender, "plex.modules.update");
{
return context.mmString("<red>You must be a Developer to use this command.");
}
for (PlexModule module : plugin.getModuleManager().getModules()) for (PlexModule module : plugin.getModuleManager().getModules())
{ {
plugin.getUpdateChecker().updateModuleJar(sender, module); plugin.getUpdateChecker().updateModuleJar(sender, module);
@@ -129,13 +118,42 @@ public class PlexCMD extends ServerCommand
plugin.getModuleManager().reloadModules(); plugin.getModuleManager().reloadModules();
return context.mmString("<green>All modules updated and reloaded!"); return context.mmString("<green>All modules updated and reloaded!");
} }
else if (args[1].equalsIgnoreCase("install"))
{
context.checkPermission(sender, "plex.modules.install");
if (args.length < 3)
{
return context.usage();
}
String moduleName = args[2];
plugin.getUpdateChecker().installModuleJar(sender, moduleName);
return context.mmString("<green>Installing module <yellow>" + moduleName + "<green>...");
}
else if (args[1].equalsIgnoreCase("uninstall"))
{
context.checkPermission(sender, "plex.modules.uninstall");
if (args.length < 3)
{
return context.usage();
}
String moduleName = args[2];
boolean removeData = args.length >= 4 && args[3].equalsIgnoreCase("-rmdir");
ModuleManager.UninstallResult result = plugin.getModuleManager().uninstallModule(moduleName, removeData);
switch (result)
{
case NOT_FOUND:
return context.mmString("<red>No installed module named <yellow>" + moduleName + "<red> was found.");
case FAILED:
return context.mmString("<red>Failed to delete the JAR for <yellow>" + moduleName + "<red>. Check the server log.");
case REMOVED:
context.send(sender, context.mmString("<green>Uninstalled module <yellow>" + moduleName + "<green>" + (removeData ? " and its data folder" : "") + "."));
return context.messageComponent("moduleRestartRequired");
}
}
} }
else if (args[0].equalsIgnoreCase("update")) else if (args[0].equalsIgnoreCase("update"))
{ {
if (!hasUpdateAccess(context, playerSender, sender)) context.checkPermission(sender, "plex.update");
{
return context.mmString("<red>You must be a Developer to use this command.");
}
if (!plugin.getUpdateChecker().getUpdateStatusMessage(sender, false, 0)) if (!plugin.getUpdateChecker().getUpdateStatusMessage(sender, false, 0))
{ {
return context.mmString("<red>Plex is already up to date!"); return context.mmString("<red>Plex is already up to date!");
@@ -149,25 +167,4 @@ public class PlexCMD extends ServerCommand
} }
return null; return null;
} }
// Owners and developers only have access
private boolean hasUpdateAccess(ServerCommandContext context, Player player, CommandSender sender)
{
// Allow CONSOLE, get OfflinePlayer for Telnet
if (context.isConsole(sender))
{
if (sender.getName().equalsIgnoreCase("CONSOLE"))
{
return true;
}
OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(sender.getName());
if (offlinePlayer.hasPlayedBefore())
{
return PlexUtils.DEVELOPERS.contains(offlinePlayer.getUniqueId().toString());
}
return false;
}
assert player != null;
return PlexUtils.DEVELOPERS.contains(player.getUniqueId().toString());
}
} }
@@ -118,6 +118,7 @@ public class ModuleManager
PlexModule plexModule = module.getConstructor().newInstance(); PlexModule plexModule = module.getConstructor().newInstance();
plexModule.setApi(plugin.getApi()); plexModule.setApi(plugin.getApi());
plexModule.setPlexModuleFile(plexModuleFile); plexModule.setPlexModuleFile(plexModuleFile);
plexModule.setModuleJar(file);
plexModule.setDataFolder(new File(plugin.getModulesFolder() + File.separator + plexModuleFile.getName())); plexModule.setDataFolder(new File(plugin.getModulesFolder() + File.separator + plexModuleFile.getName()));
if (!plexModule.getDataFolder().exists()) if (!plexModule.getDataFolder().exists())
@@ -217,6 +218,11 @@ public class ModuleManager
public void reloadModules() public void reloadModules()
{ {
unloadModules(); unloadModules();
reloadFromDisk();
}
private void reloadFromDisk()
{
loadAllModules(); loadAllModules();
loadModules(); loadModules();
enableModules(); enableModules();
@@ -225,4 +231,65 @@ public class ModuleManager
PlexLog.warn("Module command changes were staged after Paper's Brigadier command lifecycle. Restart the server for the live command dispatcher to match the loaded modules."); PlexLog.warn("Module command changes were staged after Paper's Brigadier command lifecycle. Restart the server for the live command dispatcher to match the loaded modules.");
} }
} }
/**
* Outcome of an uninstall request.
*/
public enum UninstallResult
{
NOT_FOUND,
REMOVED,
FAILED
}
/**
* Uninstalls a loaded module by name: deletes its JAR and, optionally, its data
* folder, then reloads the remaining modules.
*
* @param name module name as declared in the module's module.yml
* @param removeData whether to also delete the module's data folder
* @return the outcome of the uninstall request
*/
public UninstallResult uninstallModule(String name, boolean removeData)
{
PlexModule target = modules.stream()
.filter(module -> module.getPlexModuleFile().getName().equalsIgnoreCase(name))
.findFirst()
.orElse(null);
if (target == null)
{
return UninstallResult.NOT_FOUND;
}
File moduleJar = target.getModuleJar();
File dataFolder = target.getDataFolder();
unloadModules();
boolean deleted = moduleJar.delete();
if (deleted && removeData && dataFolder.isDirectory())
{
deleteRecursively(dataFolder);
}
reloadFromDisk();
return deleted ? UninstallResult.REMOVED : UninstallResult.FAILED;
}
private void deleteRecursively(File file)
{
File[] children = file.listFiles();
if (children != null)
{
for (File child : children)
{
deleteRecursively(child);
}
}
if (!file.delete())
{
PlexLog.warn("Unable to delete " + file.getAbsolutePath());
}
}
} }
@@ -91,6 +91,15 @@ public class UpdateChecker
updateJar(sender, name, module, List.of()); updateJar(sender, name, module, List.of());
} }
public void installModuleJar(CommandSender sender, String name)
{
updateJar(sender, name, true, List.of(), () -> plugin.getApi().scheduler().runGlobal(() ->
{
plugin.getModuleManager().reloadModules();
sendMessage(sender, PlexUtils.messageComponent("moduleRestartRequired"));
}));
}
public void updateModuleJar(CommandSender sender, PlexModule module) public void updateModuleJar(CommandSender sender, PlexModule module)
{ {
PlexModuleFile moduleFile = module.getPlexModuleFile(); PlexModuleFile moduleFile = module.getPlexModuleFile();
@@ -103,6 +112,11 @@ public class UpdateChecker
} }
private void updateJar(CommandSender sender, String name, boolean module, List<String> moduleUpdateUrls) private void updateJar(CommandSender sender, String name, boolean module, List<String> moduleUpdateUrls)
{
updateJar(sender, name, module, moduleUpdateUrls, null);
}
private void updateJar(CommandSender sender, String name, boolean module, List<String> moduleUpdateUrls, Runnable onSuccess)
{ {
try try
{ {
@@ -121,7 +135,7 @@ public class UpdateChecker
: new File(Bukkit.getUpdateFolderFile(), metadata.fileName()); : new File(Bukkit.getUpdateFolderFile(), metadata.fileName());
sendMessage(sender, PlexUtils.messageComponent("updateDownloading", metadata.fileName())); sendMessage(sender, PlexUtils.messageComponent("updateDownloading", metadata.fileName()));
plugin.getApi().scheduler().runAsync(() -> downloadAndInstall(sender, metadata, copyTo)); plugin.getApi().scheduler().runAsync(() -> downloadAndInstall(sender, metadata, copyTo, onSuccess));
} }
catch (UpdateMetadataClient.MetadataException e) catch (UpdateMetadataClient.MetadataException e)
{ {
@@ -174,7 +188,7 @@ public class UpdateChecker
return System.currentTimeMillis() - latestPlexMetadataFailureAtMillis < PLEX_METADATA_FAILURE_CACHE_MILLIS; return System.currentTimeMillis() - latestPlexMetadataFailureAtMillis < PLEX_METADATA_FAILURE_CACHE_MILLIS;
} }
private void downloadAndInstall(CommandSender sender, ArtifactMetadata metadata, File copyTo) private void downloadAndInstall(CommandSender sender, ArtifactMetadata metadata, File copyTo, Runnable onSuccess)
{ {
File parent = copyTo.getParentFile(); File parent = copyTo.getParentFile();
if (parent != null && !parent.exists() && !parent.mkdirs()) if (parent != null && !parent.exists() && !parent.mkdirs())
@@ -190,6 +204,10 @@ public class UpdateChecker
validateDownloadedFile(metadata, temporaryFile); validateDownloadedFile(metadata, temporaryFile);
Files.move(temporaryFile.toPath(), copyTo.toPath(), StandardCopyOption.REPLACE_EXISTING); Files.move(temporaryFile.toPath(), copyTo.toPath(), StandardCopyOption.REPLACE_EXISTING);
sendMessage(sender, PlexUtils.messageComponent("updateDownloaded")); sendMessage(sender, PlexUtils.messageComponent("updateDownloaded"));
if (onSuccess != null)
{
onSuccess.run();
}
} }
catch (IOException e) catch (IOException e)
{ {
+1
View File
@@ -357,3 +357,4 @@ updateMetadataNotFound: "<red>No compatible update is available on the {0} chann
# 0 - The error message # 0 - The error message
updateMetadataError: "<red>There was an error checking update metadata: {0}" updateMetadataError: "<red>There was an error checking update metadata: {0}"
moduleUpdateDisabled: "<yellow>Skipping {0}; module updates are disabled." moduleUpdateDisabled: "<yellow>Skipping {0}; module updates are disabled."
moduleRestartRequired: "<yellow>Module changes applied. Restart the server if module commands do not appear or disappear."