diff --git a/api/src/main/java/dev/plex/command/PlexCommand.java b/api/src/main/java/dev/plex/command/PlexCommand.java index 083d583..e8422ba 100644 --- a/api/src/main/java/dev/plex/command/PlexCommand.java +++ b/api/src/main/java/dev/plex/command/PlexCommand.java @@ -3,6 +3,7 @@ package dev.plex.command; import com.mojang.brigadier.tree.LiteralCommandNode; import dev.plex.api.PlexApi; import dev.plex.command.source.RequiredCommandSource; +import dev.plex.module.PlexModule; import io.papermc.paper.command.brigadier.CommandSourceStack; import java.util.List; @@ -37,6 +38,15 @@ public interface PlexCommand { } + /** + * Supplies the owning module to commands that need module-owned resources. + * + * @param module owning module + */ + default void bindModule(PlexModule module) + { + } + /** * Returns the primary command name. * diff --git a/api/src/main/java/dev/plex/command/SimplePlexCommand.java b/api/src/main/java/dev/plex/command/SimplePlexCommand.java index 44283e4..92f8f87 100644 --- a/api/src/main/java/dev/plex/command/SimplePlexCommand.java +++ b/api/src/main/java/dev/plex/command/SimplePlexCommand.java @@ -13,6 +13,7 @@ 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 dev.plex.module.PlexModule; import io.papermc.paper.command.brigadier.CommandSourceStack; import io.papermc.paper.command.brigadier.Commands; import java.util.Collection; @@ -42,6 +43,7 @@ public abstract class SimplePlexCommand implements PlexCommand { private final CommandSpec commandSpec; private PlexApi api; + private PlexModule module; protected SimplePlexCommand(CommandSpec commandSpec) { @@ -65,6 +67,12 @@ public abstract class SimplePlexCommand implements PlexCommand this.api = api; } + @Override + public final void bindModule(PlexModule module) + { + this.module = module; + } + @Override public final LiteralCommandNode buildCommand() { @@ -124,7 +132,7 @@ public abstract class SimplePlexCommand implements PlexCommand { return true; } - throw new CommandFailException(api().messages().messageString("noPermissionNode", permission)); + throw new CommandFailException(messageString("noPermissionNode", permission)); } protected boolean silentCheckPermission(CommandSender sender, String permission) @@ -154,16 +162,28 @@ public abstract class SimplePlexCommand implements PlexCommand protected Component messageComponent(String key, Object... objects) { + if (module != null) + { + return module.messageComponent(key, objects); + } return api().messages().messageComponent(key, objects); } protected Component messageComponent(String key, Component... objects) { + if (module != null) + { + return module.messageComponent(key, objects); + } return api().messages().messageComponent(key, objects); } protected String messageString(String key, Object... objects) { + if (module != null) + { + return module.messageString(key, objects); + } return api().messages().messageString(key, objects); } diff --git a/api/src/main/java/dev/plex/module/PlexModule.java b/api/src/main/java/dev/plex/module/PlexModule.java index 7b372f5..f0cc58b 100644 --- a/api/src/main/java/dev/plex/module/PlexModule.java +++ b/api/src/main/java/dev/plex/module/PlexModule.java @@ -1,7 +1,9 @@ package dev.plex.module; import dev.plex.api.PlexApi; +import dev.plex.api.config.ModuleConfiguration; import dev.plex.command.PlexCommand; +import dev.plex.config.ModuleConfig; import java.io.File; import java.io.IOException; @@ -9,10 +11,10 @@ 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 net.kyori.adventure.text.Component; import org.apache.logging.log4j.Logger; import org.bukkit.event.HandlerList; import org.bukkit.event.Listener; @@ -31,6 +33,7 @@ public abstract class PlexModule private final List listeners = new ArrayList<>(); private PlexApi api; + private ModuleConfiguration messages; private PlexModuleFile plexModuleFile; private File dataFolder; private Logger logger; @@ -158,41 +161,6 @@ public abstract class PlexModule .orElse(null); } - /** - * Adds a default message if the message key is not already configured. - * - * @param message message key - * @param initValue default value to write - */ - 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()); - } - } - - /** - * Adds a default message and comments if the message key is not already configured. - * - * @param message message key - * @param initValue default value to write - * @param comments comments to write above the message key - */ - 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()); - } - } - /** * Opens a resource from this module's class loader. * @@ -270,6 +238,90 @@ public abstract class PlexModule return logger; } + /** + * Loads this module's message file. + * + * @param from resource path to copy defaults from + */ + public void loadMessages(String from) + { + loadMessages(from, "messages.yml"); + } + + /** + * Loads this module's message file. + * + * @param from resource path to copy defaults from + * @param to destination file path relative to the module data folder + */ + public void loadMessages(String from, String to) + { + messages = new ModuleConfig(this, from, to); + messages.load(); + } + + /** + * Returns this module's loaded messages, if any. + * + * @return module messages, or {@code null} when this module has no messages + */ + @Nullable + public ModuleConfiguration messages() + { + return messages; + } + + /** + * Resolves a module message into a component, falling back to Plex messages. + * + * @param entry message key + * @param objects replacement values + * @return resolved component + */ + public Component messageComponent(String entry, Object... objects) + { + return api.messages().miniMessage(messageString(entry, objects)); + } + + /** + * Resolves a module message into a component using component replacements. + * + * @param entry message key + * @param objects component replacement values + * @return resolved component + */ + public Component messageComponent(String entry, Component... objects) + { + Component component = api.messages().miniMessage(messageString(entry)); + for (int i = 0; i < objects.length; i++) + { + int finalI = i; + component = component.replaceText(builder -> builder.matchLiteral("{" + finalI + "}").replacement(objects[finalI]).build()); + } + return component; + } + + /** + * Resolves a module message into a string, falling back to Plex messages. + * + * @param entry message key + * @param objects replacement values + * @return resolved message string + */ + public String messageString(String entry, Object... objects) + { + String message = messages == null ? null : messages.getString(entry); + if (message == null) + { + return api.messages().messageString(entry, objects); + } + for (int i = 0; i < objects.length; i++) + { + message = message.replace("{" + i + "}", String.valueOf(objects[i])); + } + return message; + } + /** * Sets the Plex API facade for this module. * @@ -287,6 +339,7 @@ public abstract class PlexModule { command.bindApi(api); } + command.bindModule(this); } /** diff --git a/api/src/main/java/dev/plex/module/PlexModuleFile.java b/api/src/main/java/dev/plex/module/PlexModuleFile.java index bffd976..9fc61ff 100644 --- a/api/src/main/java/dev/plex/module/PlexModuleFile.java +++ b/api/src/main/java/dev/plex/module/PlexModuleFile.java @@ -14,6 +14,8 @@ public final class PlexModuleFile private final int apiCompatibility; private List libraries = List.of(); private List repositories = List.of(); + private boolean updaterEnabled = true; + private List updateUrls = List.of(); /** * Creates module metadata. @@ -122,4 +124,47 @@ public final class PlexModuleFile { this.repositories = List.copyOf(repositories); } + + /** + * Returns whether Plex should include this module in module update commands. + * + * @return {@code true} when module updates are enabled + */ + public boolean isUpdaterEnabled() + { + return updaterEnabled; + } + + /** + * Sets whether Plex should include this module in module update commands. + * + * @param updaterEnabled whether module updates are enabled + */ + public void setUpdaterEnabled(boolean updaterEnabled) + { + this.updaterEnabled = updaterEnabled; + } + + /** + * Returns custom updater base URLs declared by the module. + * + *

An empty list means Plex should use the built-in first-party updater + * endpoints.

+ * + * @return custom updater base URLs + */ + public List getUpdateUrls() + { + return updateUrls; + } + + /** + * Sets custom updater base URLs declared by the module. + * + * @param updateUrls custom updater base URLs + */ + public void setUpdateUrls(List updateUrls) + { + this.updateUrls = updateUrls == null ? List.of() : List.copyOf(updateUrls); + } } diff --git a/server/src/main/java/dev/plex/command/impl/PlexCMD.java b/server/src/main/java/dev/plex/command/impl/PlexCMD.java index 6a160bb..62b3695 100644 --- a/server/src/main/java/dev/plex/command/impl/PlexCMD.java +++ b/server/src/main/java/dev/plex/command/impl/PlexCMD.java @@ -124,7 +124,7 @@ public class PlexCMD extends ServerCommand } for (PlexModule module : plugin.getModuleManager().getModules()) { - plugin.getUpdateChecker().updateJar(sender, module.getPlexModuleFile().getName(), true); + plugin.getUpdateChecker().updateModuleJar(sender, module); } plugin.getModuleManager().reloadModules(); return context.mmString("All modules updated and reloaded!"); diff --git a/server/src/main/java/dev/plex/module/ModuleManager.java b/server/src/main/java/dev/plex/module/ModuleManager.java index 901bc5f..068b19c 100644 --- a/server/src/main/java/dev/plex/module/ModuleManager.java +++ b/server/src/main/java/dev/plex/module/ModuleManager.java @@ -14,6 +14,7 @@ import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -95,10 +96,22 @@ public class ModuleManager .map(id -> internalModuleConfig.getConfigurationSection("repositories").getString(id, "")) .filter(repository -> !repository.isBlank()) .toList(); + boolean updaterEnabled = internalModuleConfig.getBoolean("updater.enabled", true); + List updateUrls = new ArrayList<>(); + String updateUrl = internalModuleConfig.getString("updater.url", ""); + if (!updateUrl.isBlank()) + { + updateUrls.add(updateUrl); + } + updateUrls.addAll(internalModuleConfig.getStringList("updater.urls").stream() + .filter(url -> !url.isBlank()) + .toList()); PlexModuleFile plexModuleFile = new PlexModuleFile(name, main, description, version, apiCompatibility); plexModuleFile.setLibraries(libraries); plexModuleFile.setRepositories(repositories); + plexModuleFile.setUpdaterEnabled(updaterEnabled); + plexModuleFile.setUpdateUrls(updateUrls); Class module = (Class) Class.forName(main, true, loader); PlexModule plexModule = module.getConstructor().newInstance(); diff --git a/server/src/main/java/dev/plex/updater/UpdateMetadataClient.java b/server/src/main/java/dev/plex/updater/UpdateMetadataClient.java index b2cbf83..63e77d9 100644 --- a/server/src/main/java/dev/plex/updater/UpdateMetadataClient.java +++ b/server/src/main/java/dev/plex/updater/UpdateMetadataClient.java @@ -15,7 +15,7 @@ import java.util.Optional; public final class UpdateMetadataClient { - private static final List BASE_URLS = List.of("https://updater.plex.us.org", "https://plex-updater.com"); + private static final List DEFAULT_BASE_URLS = List.of("https://updater.plex.us.org", "https://plex-updater.com"); private final Gson gson = new Gson(); private final UpdateChannel channel; @@ -38,9 +38,14 @@ public final class UpdateMetadataClient } public ArtifactMetadata fetchModuleLatest(String moduleName, int apiCompatibility) throws MetadataException + { + return fetchModuleLatest(moduleName, apiCompatibility, List.of()); + } + + public ArtifactMetadata fetchModuleLatest(String moduleName, int apiCompatibility, List baseUrls) throws MetadataException { String path = "/api/v1/projects/" + encodePathSegment(moduleName) + "/channels/" + channel.id() + "/latest/api/" + apiCompatibility + ".json"; - ArtifactMetadata metadata = fetch(path); + ArtifactMetadata metadata = fetch(path, baseUrls); Optional validationError = metadata.validateModule(channel, moduleName, apiCompatibility); if (validationError.isPresent()) { @@ -50,10 +55,15 @@ public final class UpdateMetadataClient } private ArtifactMetadata fetch(String path) throws MetadataException + { + return fetch(path, DEFAULT_BASE_URLS); + } + + private ArtifactMetadata fetch(String path, List baseUrls) throws MetadataException { MetadataException notFound = null; MetadataException failure = null; - for (String baseUrl : BASE_URLS) + for (String baseUrl : normalizeBaseUrls(baseUrls)) { try { @@ -133,6 +143,17 @@ public final class UpdateMetadataClient return URLEncoder.encode(value, StandardCharsets.UTF_8).replace("+", "%20"); } + private static List normalizeBaseUrls(List baseUrls) + { + List urls = baseUrls == null || baseUrls.isEmpty() ? DEFAULT_BASE_URLS : baseUrls; + return urls.stream() + .map(String::trim) + .filter(url -> !url.isBlank()) + .map(url -> url.endsWith("/") ? url.substring(0, url.length() - 1) : url) + .distinct() + .toList(); + } + public static final class MetadataException extends Exception { private final boolean notFound; diff --git a/server/src/main/java/dev/plex/util/UpdateChecker.java b/server/src/main/java/dev/plex/util/UpdateChecker.java index 7322aca..c2aa032 100644 --- a/server/src/main/java/dev/plex/util/UpdateChecker.java +++ b/server/src/main/java/dev/plex/util/UpdateChecker.java @@ -1,6 +1,8 @@ package dev.plex.util; import dev.plex.Plex; +import dev.plex.module.PlexModule; +import dev.plex.module.PlexModuleFile; import dev.plex.updater.ArtifactMetadata; import dev.plex.updater.UpdateChannel; import dev.plex.updater.UpdateMetadataClient; @@ -17,6 +19,7 @@ import java.security.DigestInputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.HexFormat; +import java.util.List; import net.kyori.adventure.text.Component; import org.bukkit.Bukkit; @@ -84,11 +87,27 @@ public class UpdateChecker } public void updateJar(CommandSender sender, String name, boolean module) + { + updateJar(sender, name, module, List.of()); + } + + public void updateModuleJar(CommandSender sender, PlexModule module) + { + PlexModuleFile moduleFile = module.getPlexModuleFile(); + if (!moduleFile.isUpdaterEnabled()) + { + sendMessage(sender, PlexUtils.messageComponent("moduleUpdateDisabled", moduleFile.getName())); + return; + } + updateJar(sender, moduleFile.getName(), true, moduleFile.getUpdateUrls()); + } + + private void updateJar(CommandSender sender, String name, boolean module, List moduleUpdateUrls) { try { ArtifactMetadata metadata = module - ? metadataClient.fetchModuleLatest(name, plugin.getApi().compatibility().version()) + ? metadataClient.fetchModuleLatest(name, plugin.getApi().compatibility().version(), moduleUpdateUrls) : fetchLatestPlexMetadata(false); if (!module && metadata.matchesCurrentBuild(plugin.getPluginMeta().getVersion(), BuildInfo.getNumber(), BuildInfo.getCommit())) diff --git a/server/src/main/resources/messages.yml b/server/src/main/resources/messages.yml index 85fef2a..fcc537c 100644 --- a/server/src/main/resources/messages.yml +++ b/server/src/main/resources/messages.yml @@ -356,3 +356,4 @@ updateDownloadFailed: "Something went wrong while downloading {0}. Please c updateMetadataNotFound: "No compatible update is available on the {0} channel." # 0 - The error message updateMetadataError: "There was an error checking update metadata: {0}" +moduleUpdateDisabled: "Skipping {0}; module updates are disabled."