diff --git a/.gitignore b/.gitignore index 5ff6309..d2a93b5 100644 --- a/.gitignore +++ b/.gitignore @@ -4,10 +4,7 @@ target/ !**/src/test/**/target/ ### IntelliJ IDEA ### -.idea/modules.xml -.idea/jarRepositories.xml -.idea/compiler.xml -.idea/libraries/ +.idea/ *.iws *.iml *.ipr @@ -35,4 +32,14 @@ build/ .vscode/ ### Mac OS ### -.DS_Store \ No newline at end of file +.DS_Store + +### Claude / AI ### +.claude/ +codex-build.log + +### Docs ### +docs/plans/ + +### Generated ### +dependency-reduced-pom.xml \ No newline at end of file diff --git a/pom.xml b/pom.xml index bc2f010..4e69ce3 100644 --- a/pom.xml +++ b/pom.xml @@ -5,12 +5,13 @@ com.magmaguy ResourcePackManager - 1.0-SNAPSHOT + 1.7.6 - 22 - 22 + 17 + 17 UTF-8 + 1.18.42 @@ -22,6 +23,11 @@ central https://repo1.maven.org/maven2/ + + magmaguy-repo-releases + MagmaGuy's Repository + https://repo.magmaguy.com/releases + @@ -34,7 +40,7 @@ org.projectlombok lombok - 1.18.34 + ${lombok.version} provided @@ -47,10 +53,41 @@ httpclient5 5.3.1 + + com.magmaguy + MagmaCore + 2.0.0-SNAPSHOT + compile + + + + org.bstats + bstats-bukkit + 3.0.2 + compile + + ResourcePackManager + + + maven-compiler-plugin + 3.13.0 + + 17 + 17 + + + org.projectlombok + lombok + ${lombok.version} + + + + + org.apache.maven.plugins maven-shade-plugin @@ -68,6 +105,10 @@ org.apache.hc com.magmaguy.resourcepackmanager.org.apache.hc + + org.bstats + com.magmaguy.resourcepackmanager.bstats + @@ -75,4 +116,15 @@ + + + magmaguy-repo-snapshots + https://magmaguy.com/snapshots + + + magmaguy-repo-snapshots + MagmaGuy's Repository + https://repo.magmaguy.com/releases + + diff --git a/src/main/java/com/magmaguy/resourcepackmanager/JsonMerger.java b/src/main/java/com/magmaguy/resourcepackmanager/JsonMerger.java deleted file mode 100644 index 0f9afb5..0000000 --- a/src/main/java/com/magmaguy/resourcepackmanager/JsonMerger.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.magmaguy.resourcepackmanager; - -public class JsonMerger { -} diff --git a/src/main/java/com/magmaguy/resourcepackmanager/Logger.java b/src/main/java/com/magmaguy/resourcepackmanager/Logger.java deleted file mode 100644 index 3881571..0000000 --- a/src/main/java/com/magmaguy/resourcepackmanager/Logger.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.magmaguy.resourcepackmanager; - -import org.bukkit.Bukkit; - -public class Logger { - private Logger(){} - public static void warn(String message){ - Bukkit.getLogger().warning("[ResourcePackManager] " + message); - } - - public static void info(String message){ - Bukkit.getLogger().info("[ResourcePackManager] " + message); - } -} diff --git a/src/main/java/com/magmaguy/resourcepackmanager/ResourcePackManager.java b/src/main/java/com/magmaguy/resourcepackmanager/ResourcePackManager.java index 179a3af..d3c44f7 100644 --- a/src/main/java/com/magmaguy/resourcepackmanager/ResourcePackManager.java +++ b/src/main/java/com/magmaguy/resourcepackmanager/ResourcePackManager.java @@ -1,16 +1,43 @@ package com.magmaguy.resourcepackmanager; +import com.magmaguy.magmacore.MagmaCore; +import com.magmaguy.magmacore.command.CommandManager; +import com.magmaguy.magmacore.initialization.PluginInitializationConfig; +import com.magmaguy.magmacore.initialization.PluginInitializationContext; +import com.magmaguy.magmacore.initialization.PluginInitializationState; +import com.magmaguy.magmacore.nightbreak.NightbreakPluginBootstrap; +import com.magmaguy.magmacore.nightbreak.NightbreakPluginHooks; +import com.magmaguy.magmacore.nightbreak.NightbreakPluginSpec; +import com.magmaguy.magmacore.util.Logger; import com.magmaguy.resourcepackmanager.autohost.AutoHost; -import com.magmaguy.resourcepackmanager.commands.CommandManager; +import com.magmaguy.resourcepackmanager.commands.DataComplianceRequestCommand; +import com.magmaguy.resourcepackmanager.commands.ReloadCommand; +import com.magmaguy.resourcepackmanager.config.BlueprintFolder; +import com.magmaguy.resourcepackmanager.itemsadder.ItemsAdderCommand; +import com.magmaguy.resourcepackmanager.itemsadder.ItemsAdderDismissedConfig; +import com.magmaguy.resourcepackmanager.itemsadder.ItemsAdderWarningListener; +import com.magmaguy.resourcepackmanager.config.DataConfig; import com.magmaguy.resourcepackmanager.config.DefaultConfig; -import com.magmaguy.resourcepackmanager.mixer.Mix; +import com.magmaguy.resourcepackmanager.config.compatibleplugins.CompatiblePluginConfig; +import com.magmaguy.resourcepackmanager.config.compatibleplugins.CompatiblePluginConfigFields; +import com.magmaguy.resourcepackmanager.playermanager.PlayerManager; +import com.magmaguy.resourcepackmanager.thirdparty.ThirdPartyResourcePack; +import org.bstats.bukkit.Metrics; import org.bukkit.Bukkit; -import org.bukkit.plugin.Plugin; +import org.bukkit.event.HandlerList; import org.bukkit.plugin.java.JavaPlugin; +import java.io.File; + public class ResourcePackManager extends JavaPlugin { - public static Plugin plugin; + public static final NightbreakPluginSpec NIGHTBREAK_PLUGIN_SPEC = new NightbreakPluginSpec( + "ResourcePackManager", "resourcepackmanager", "resourcepackmanager.*", + "resourcepackmanager.setup", "resourcepackmanager.initialize", + "", "Reloaded ResourcePackManager.", + false, false, false); + + public static JavaPlugin plugin; @Override public void onEnable() { @@ -20,17 +47,111 @@ public void onEnable() { " | /\\__ \\ _/ |\\/| / _` | ' \\/ _` / _` / -_) '_|\n" + " |_|_\\|___/_| |_| |_\\__,_|_||_\\__,_\\__, \\___|_| \n" + " |___/ "); - Logger.info("Enabling ResourcePackManager v." + this.getDescription().getVersion()); + Bukkit.getLogger().info("ResourcePackManager v." + this.getDescription().getVersion()); plugin = this; - DefaultConfig.initializeConfig(); - Mix.initialize(); - AutoHost.initialize(); - new CommandManager(this); + NightbreakPluginBootstrap.startInitialization(this, + new PluginInitializationConfig("ResourcePackManager", null, 10), + NIGHTBREAK_PLUGIN_SPEC, + new NightbreakPluginHooks() { + @Override + public void asyncInitialization(PluginInitializationContext initializationContext) { + ResourcePackManager.this.asyncInitialization(initializationContext); + } + + @Override + public void syncInitialization(PluginInitializationContext initializationContext) { + ResourcePackManager.this.syncInitialization(initializationContext); + } + + @Override + public void onInitializationSuccess() { + Logger.info("ResourcePackManager fully initialized!"); + } + + @Override + public void onInitializationFailure(Throwable throwable) { + throwable.printStackTrace(); + } + }); + } + + @Override + public void onLoad() { + MagmaCore.createInstance(this); } @Override public void onDisable() { + MagmaCore.requestInitializationShutdown(this); + if (MagmaCore.getInitializationState(this.getName()) == PluginInitializationState.INITIALIZING) { + Logger.info("Disabling ResourcePackManager during initialization"); + ThirdPartyResourcePack.shutdown(); + AutoHost.shutdown(); + HandlerList.unregisterAll(this); + MagmaCore.shutdown(this); + return; + } Logger.info("Disabling ResourcePackManager"); + ThirdPartyResourcePack.shutdown(); AutoHost.shutdown(); + HandlerList.unregisterAll(this); + MagmaCore.shutdown(this); + } + + private void asyncInitialization(PluginInitializationContext initializationContext) { + initializationContext.step("Data Config"); + new DataConfig(); + + initializationContext.step("Default Config"); + new DefaultConfig(); + + initializationContext.step("ItemsAdder Config"); + new ItemsAdderDismissedConfig(); + + initializationContext.step("Mixer Folder"); + File mixerFolder = new File(getDataFolder(), "mixer"); + if (!mixerFolder.exists()) { + mixerFolder.mkdirs(); + } + + initializationContext.step("Blueprint Folder"); + BlueprintFolder.initialize(); + + initializationContext.step("Compatible Plugins"); + new CompatiblePluginConfig(); + + initializationContext.step("Pack Integrations"); + for (CompatiblePluginConfigFields compatiblePluginConfigFields : CompatiblePluginConfig.getCompatiblePlugins().values()) { + if (!compatiblePluginConfigFields.isEnabled()) continue; + ThirdPartyResourcePack.initializeThirdPartyResourcePack(compatiblePluginConfigFields); + } + } + + private void syncInitialization(PluginInitializationContext initializationContext) { + initializationContext.step("Change Watchdog"); + ThirdPartyResourcePack.startResourcePackChangeWatchdog(); + + initializationContext.step("Event Listeners"); + if (DefaultConfig.isAutoHost()) { + Bukkit.getPluginManager().registerEvents(new PlayerManager(), this); + } + Bukkit.getPluginManager().registerEvents(new ItemsAdderWarningListener(), this); + + initializationContext.step("Commands"); + CommandManager commandManager = new CommandManager(this, "resourcepackmanager"); + NightbreakPluginBootstrap.registerStandardCommands(this, + commandManager, + NIGHTBREAK_PLUGIN_SPEC, + player -> Logger.sendMessage(player, "&eResourcePackManager has no setup menu. Edit config files in plugins/ResourcePackManager/ and use &6/resourcepackmanager reload&e."), + sender -> ReloadCommand.reloadPlugin(sender)); + commandManager.registerCommand(new ReloadCommand()); + commandManager.registerCommand(new DataComplianceRequestCommand()); + commandManager.registerCommand(new ItemsAdderCommand()); + + initializationContext.step("Metrics"); + new Metrics(this, 22867); + + initializationContext.step("Version Check"); + MagmaCore.checkVersionUpdate("118574", "https://www.spigotmc.org/resources/resource-pack-manager.118574/"); } -} \ No newline at end of file +} diff --git a/src/main/java/com/magmaguy/resourcepackmanager/api/ResourcePackManagerAPI.java b/src/main/java/com/magmaguy/resourcepackmanager/api/ResourcePackManagerAPI.java new file mode 100644 index 0000000..7695329 --- /dev/null +++ b/src/main/java/com/magmaguy/resourcepackmanager/api/ResourcePackManagerAPI.java @@ -0,0 +1,82 @@ +package com.magmaguy.resourcepackmanager.api; + +import com.magmaguy.resourcepackmanager.commands.ReloadCommand; +import com.magmaguy.resourcepackmanager.thirdparty.ThirdPartyResourcePack; +import org.bukkit.Bukkit; + +import java.util.HashMap; + +public class ResourcePackManagerAPI { + public static HashMap thirdPartyResourcePackHashMap = new HashMap<>(); + + /** + * Registers a resource pack with the ResourcePackManager. + * Either localPath or url must be provided (non-null), but not both. + * + * @param pluginName The name of the plugin as it appears in the plugin list. Case-sensitive. + * @param localPath The relative path to the resource pack file (zipped or folder) from the plugins directory, or null if using URL. + * @param url The URL to download the resource pack from, or null if using local path. + * @param encrypts Whether the pack can be encrypted by the plugin. Currently does nothing. + * @param distributes Whether the plugin can distribute the pack. Currently does nothing. + * @param zips Whether the resource pack is already zipped. If false, ResourcePackManager will zip it. + * @param reloadCommand The reload command of the plugin adding a pack. Currently does nothing. + */ + public static void registerResourcePack(String pluginName, + String localPath, + String url, + boolean encrypts, + boolean distributes, + boolean zips, + String reloadCommand) { + thirdPartyResourcePackHashMap.put(pluginName, + new ThirdPartyResourcePack(pluginName, localPath, url, zips, false, reloadCommand)); + } + + /** + * Registers a local resource pack with the ResourcePackManager. + * + * @param pluginName The name of the plugin as it appears in the plugin list. Case-sensitive. + * @param localPath The relative path to the resource pack file (zipped or folder) from the plugins directory. + * @param encrypts Whether the pack can be encrypted by the plugin. Currently does nothing. + * @param distributes Whether the plugin can distribute the pack. Currently does nothing. + * @param zips Whether the resource pack is already zipped. If false, ResourcePackManager will zip it. + * @param reloadCommand The reload command of the plugin adding a pack. Currently does nothing. + */ + public static void registerLocalResourcePack(String pluginName, + String localPath, + boolean encrypts, + boolean distributes, + boolean zips, + String reloadCommand) { + thirdPartyResourcePackHashMap.put(pluginName, + new ThirdPartyResourcePack(pluginName, localPath, null, zips, false, reloadCommand)); + } + + /** + * Registers a remote resource pack with the ResourcePackManager. + * The resource pack will be downloaded from the provided URL. + * + * @param pluginName The name of the plugin as it appears in the plugin list. Case-sensitive. + * @param url The URL to download the resource pack from. + * @param encrypts Whether the pack can be encrypted by the plugin. Currently does nothing. + * @param distributes Whether the plugin can distribute the pack. Currently does nothing. + * @param zips Whether the resource pack from the URL is already zipped. If false, ResourcePackManager will zip it. + * @param reloadCommand The reload command of the plugin adding a pack. Currently does nothing. + */ + public static void registerRemoteResourcePack(String pluginName, + String url, + boolean encrypts, + boolean distributes, + boolean zips, + String reloadCommand) { + thirdPartyResourcePackHashMap.put(pluginName, + new ThirdPartyResourcePack(pluginName, null, url, zips, false, reloadCommand)); + } + + /** + * Reloads the plugin, thereby redoing everything necessary to merge and host the resource pack + */ + public static void reloadResourcePack() { + ReloadCommand.reloadPlugin(Bukkit.getConsoleSender()); + } +} \ No newline at end of file diff --git a/src/main/java/com/magmaguy/resourcepackmanager/autohost/AutoHost.java b/src/main/java/com/magmaguy/resourcepackmanager/autohost/AutoHost.java index a520e44..fc2996a 100644 --- a/src/main/java/com/magmaguy/resourcepackmanager/autohost/AutoHost.java +++ b/src/main/java/com/magmaguy/resourcepackmanager/autohost/AutoHost.java @@ -1,99 +1,389 @@ package com.magmaguy.resourcepackmanager.autohost; -import com.magmaguy.resourcepackmanager.Logger; +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.magmaguy.magmacore.util.Logger; import com.magmaguy.resourcepackmanager.ResourcePackManager; +import com.magmaguy.resourcepackmanager.config.DataConfig; import com.magmaguy.resourcepackmanager.config.DefaultConfig; import com.magmaguy.resourcepackmanager.mixer.Mix; +import com.magmaguy.resourcepackmanager.utils.ServerVersionHelper; +import lombok.Getter; +import lombok.Setter; import org.apache.hc.client5.http.classic.methods.HttpPost; +import org.apache.hc.client5.http.config.ConnectionConfig; +import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; import org.apache.hc.client5.http.impl.classic.HttpClients; +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; +import org.apache.hc.client5.http.io.HttpClientConnectionManager; import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.HttpEntity; import org.apache.hc.core5.http.io.entity.EntityUtils; -import org.apache.hc.core5.http.io.entity.FileEntity; +import org.apache.hc.core5.util.Timeout; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; import org.bukkit.scheduler.BukkitRunnable; import org.bukkit.scheduler.BukkitTask; -import java.io.*; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.util.UUID; public class AutoHost { - private static final String hostURL = "http://localhost:3000/sha1"; - private static final String stillAliveURL = "http://localhost:3000/still_alive"; - private static final String rspURL = "http://localhost:3000/upload"; + private static final String finalURL = "https://magmaguy.com/rsp/"; + // Consistent UUID for identifying ResourcePackManager's pack when using multiple resource packs + private static final UUID RESOURCE_PACK_UUID = UUID.fromString("a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d"); + @Setter + private static boolean done = false; +// private static final String finalURL = "https://localhost:50000/"; + private static BukkitTask keepAlive = null; + @Getter + private static String rspUUID = null; + @Setter + private static boolean firstUpload = true; + + // Timeout settings for HTTP requests (in seconds) + private static final int DEFAULT_CONNECT_TIMEOUT = 30; + private static final int DEFAULT_SOCKET_TIMEOUT = 60; + private static final int UPLOAD_SOCKET_TIMEOUT = 300; // 5 minutes for file uploads private AutoHost() { } + /** + * Creates an HTTP client with custom timeouts suitable for regular requests. + */ + private static CloseableHttpClient createHttpClient() { + return createHttpClient(DEFAULT_SOCKET_TIMEOUT); + } + + /** + * Creates an HTTP client with custom timeouts. + * @param socketTimeoutSeconds The socket (read) timeout in seconds + */ + private static CloseableHttpClient createHttpClient(int socketTimeoutSeconds) { + ConnectionConfig connectionConfig = ConnectionConfig.custom() + .setConnectTimeout(Timeout.ofSeconds(DEFAULT_CONNECT_TIMEOUT)) + .setSocketTimeout(Timeout.ofSeconds(socketTimeoutSeconds)) + .build(); + + HttpClientConnectionManager connectionManager = PoolingHttpClientConnectionManagerBuilder.create() + .setDefaultConnectionConfig(connectionConfig) + .build(); + + RequestConfig requestConfig = RequestConfig.custom() + .setConnectionRequestTimeout(Timeout.ofSeconds(DEFAULT_CONNECT_TIMEOUT)) + .setResponseTimeout(Timeout.ofSeconds(socketTimeoutSeconds)) + .build(); + + return HttpClients.custom() + .setConnectionManager(connectionManager) + .setDefaultRequestConfig(requestConfig) + .build(); + } + + public static void sendResourcePack(Player player) { + if (rspUUID == null || !done) return; + Logger.info("Sending resource pack to " + player.getName()); + + String url = finalURL + rspUUID; + byte[] hash = Mix.getFinalSHA1Bytes(); + String prompt = DefaultConfig.getResourcePackPrompt(); + boolean force = DefaultConfig.isForceResourcePack(); + + if (ServerVersionHelper.supportsMultipleResourcePacks()) { + // 1.20.3+ supports multiple resource packs - use addResourcePack to coexist with other plugins + player.addResourcePack(RESOURCE_PACK_UUID, url, hash, prompt, force); + } else { + // Older versions - use setResourcePack (replaces any existing packs) + player.setResourcePack(url, hash, prompt, force); + } + } + public static void initialize() { if (!DefaultConfig.isAutoHost()) return; if (Mix.getFinalResourcePack() == null) return; + Logger.info("Starting autohost!"); + firstUpload = true; + done = false; + rspUUID = null; + if (keepAlive != null) keepAlive.cancel(); + keepAlive = new BukkitRunnable() { + int counter = 0; + @Override public void run() { - try { - if (!sendSHA1(Mix.getFinalSHA1(), hostURL)) { - uploadFile(Mix.getFinalResourcePack(), rspURL); + if (rspUUID != null) { + counter = 0; + try { + sendStillAlive(); + } catch (Exception e) { + rspUUID = null; + Logger.warn("Failed to autohost resource pack!"); + e.printStackTrace(); + } + } else { + checkFileExistence(); + if (rspUUID == null && counter % 10 == 0) { + Logger.warn("Failed to connect to remote server to autohost the resource pack!"); } - sendStillAlive(stillAliveURL); - } catch (Exception e) { - Logger.warn("Failed to autohost resource pack!"); - e.printStackTrace(); + counter++; } } - }.runTaskTimerAsynchronously(ResourcePackManager.plugin, 0, 3 * 24 * 60 * 60 * 20L); + }.runTaskTimerAsynchronously(ResourcePackManager.plugin, 0, 6 * 60 * 60 * 20L); + } + + private static void checkFileExistence() { + initializeLink(); + if (rspUUID == null) { + Logger.info("No resource pack found on the server! Uploading resource pack to the server..."); + return; + } + if (!sendSHA1()) uploadFile(); + else { + //Case if the remote server already has the resource pack + done = true; + if (firstUpload) { + //Recover from a reload by sending the pack to online players + for (Player player : Bukkit.getOnlinePlayers()) + AutoHost.sendResourcePack(player); + } + firstUpload = false; + } + } + + public static void initializeLink() { + try (CloseableHttpClient httpClient = createHttpClient()) { + HttpPost httpPost = new HttpPost(finalURL + "initialize"); + MultipartEntityBuilder builder = MultipartEntityBuilder.create(); + builder.addTextBody("uuid", DataConfig.getRspUUID(), ContentType.TEXT_PLAIN.withCharset(StandardCharsets.UTF_8)); + httpPost.setEntity(builder.build()); + + try (CloseableHttpResponse response = httpClient.execute(httpPost)) { + String responseString = EntityUtils.toString(response.getEntity()); + int statusCode = response.getCode(); + + if (statusCode >= 200 && statusCode < 300) { + // Success - parse JSON response + try { + Gson gson = new Gson(); + JsonObject jsonResponse = gson.fromJson(responseString, JsonObject.class); + + // Check if response indicates success + if (jsonResponse.has("success") && jsonResponse.get("success").getAsBoolean()) { + rspUUID = jsonResponse.get("uuid").getAsString(); + DataConfig.setRspUUID(rspUUID); + Logger.info("Server initialized successfully: " + jsonResponse.get("message").getAsString()); + } else { + // Server returned error in success status code + Logger.warn("Server returned error in response: " + responseString); + rspUUID = null; + } + } catch (Exception e) { + // JSON parsing failed - validate if it looks like a UUID before using it + String trimmedResponse = responseString.trim(); + try { + UUID.fromString(trimmedResponse); + rspUUID = trimmedResponse; + DataConfig.setRspUUID(rspUUID); + Logger.info("Server initialized with UUID: " + rspUUID); + } catch (IllegalArgumentException ignored) { + Logger.warn("Invalid response format from server: " + responseString); + rspUUID = null; + } + } + } else { + // Error - parse and log detailed error message + handleErrorResponse(responseString, statusCode, "initialization"); + rspUUID = null; + } + } catch (Exception e) { + Logger.warn("Failed to communicate with remote server!"); + e.printStackTrace(); + rspUUID = null; + } + } catch (Exception e) { + rspUUID = null; + Logger.warn("Failed remote server initialization."); + e.printStackTrace(); + } } - public static void uploadFile(File file, String url) throws IOException { - try (CloseableHttpClient httpClient = HttpClients.createDefault()) { - HttpPost uploadFile = new HttpPost(url); - // Use FileEntity for streaming large files - FileEntity fileEntity = new FileEntity(file, ContentType.APPLICATION_OCTET_STREAM); - uploadFile.setEntity(fileEntity); + public static void uploadFile() { + Logger.info("Uploading resource!"); + + try (CloseableHttpClient httpClient = createHttpClient(UPLOAD_SOCKET_TIMEOUT)) { + HttpPost uploadFile = new HttpPost(finalURL + "upload"); + + MultipartEntityBuilder builder = MultipartEntityBuilder.create(); + builder.addTextBody("uuid", rspUUID, ContentType.TEXT_PLAIN.withCharset(StandardCharsets.UTF_8)); + builder.addBinaryBody("file", Mix.getFinalResourcePack(), ContentType.APPLICATION_OCTET_STREAM, Mix.getFinalResourcePack().getName()); + + uploadFile.setEntity(builder.build()); try (CloseableHttpResponse response = httpClient.execute(uploadFile)) { String responseString = EntityUtils.toString(response.getEntity()); - System.out.println("Response from server: " + responseString); + int statusCode = response.getCode(); + + if (statusCode >= 200 && statusCode < 300) { + Logger.info("Uploaded resource pack for automatic hosting! url: " + finalURL + rspUUID); + done = true; + if (firstUpload) { + //Recover from a reload by sending the pack to online players + for (Player player : Bukkit.getOnlinePlayers()) + AutoHost.sendResourcePack(player); + } + firstUpload = false; + } else { + // Handle detailed error messages from server + handleErrorResponse(responseString, statusCode, "upload"); + } } catch (Exception e) { - throw new RuntimeException(e); + Logger.warn("Failed to communicate with remote server during upload!"); + e.printStackTrace(); } + } catch (IOException e) { + throw new RuntimeException(e); } } - private static Boolean sendSHA1(String stringData, String url) throws IOException { - try (CloseableHttpClient httpClient = HttpClients.createDefault()) { - HttpPost httpPost = new HttpPost(url); + private static boolean sendSHA1() { + try (CloseableHttpClient httpClient = createHttpClient()) { + HttpPost httpPost = new HttpPost(finalURL + "sha1"); MultipartEntityBuilder builder = MultipartEntityBuilder.create(); - builder.addTextBody("stringData", stringData, ContentType.TEXT_PLAIN.withCharset(StandardCharsets.UTF_8)); - httpPost.setEntity(builder.build()); + builder.addTextBody("uuid", rspUUID, ContentType.TEXT_PLAIN.withCharset(StandardCharsets.UTF_8)); + builder.addTextBody("sha1", Mix.getFinalSHA1(), ContentType.TEXT_PLAIN.withCharset(StandardCharsets.UTF_8)); + + HttpEntity entity = builder.build(); + httpPost.setEntity(entity); try (CloseableHttpResponse response = httpClient.execute(httpPost)) { String responseString = EntityUtils.toString(response.getEntity()); - System.out.println("Response from server: " + responseString); - return Boolean.parseBoolean(responseString.trim()); + int statusCode = response.getCode(); + + if (statusCode >= 200 && statusCode < 300) { + try { + // Parse JSON response + Gson gson = new Gson(); + JsonObject jsonResponse = gson.fromJson(responseString, JsonObject.class); + + // Check if response indicates success + if (jsonResponse.has("success") && jsonResponse.get("success").getAsBoolean()) { + boolean uploadNeeded = jsonResponse.get("uploadNeeded").getAsBoolean(); + + if (!uploadNeeded) { + Logger.info("Remote server already has this resource pack!"); + done = true; + if (firstUpload) { + //Recover from a reload by sending the pack to online players + for (Player player : Bukkit.getOnlinePlayers()) + AutoHost.sendResourcePack(player); + } + firstUpload = false; + } + return !uploadNeeded; // Return true if no upload needed + } else { + // Server returned error in success status code + Logger.warn("Server returned error in SHA1 response: " + responseString); + return false; + } + } catch (Exception e) { + // Fallback to boolean parsing (backward compatibility) + String trimmedResponse = responseString.trim(); + if (trimmedResponse.equals("true") || trimmedResponse.equals("false")) { + boolean result = Boolean.valueOf(trimmedResponse); + Logger.info("Remote server already has this resource pack! Response: " + responseString); + return result; + } else { + Logger.warn("Invalid SHA1 response format from server: " + responseString); + return false; + } + } + } else { + // Handle error response + handleErrorResponse(responseString, statusCode, "SHA1 check"); + return false; + } } catch (Exception e) { - Logger.warn("Failed to communicate with remote server!"); + Logger.warn("Failed to communicate with remote server during SHA1 check!"); e.printStackTrace(); - return null; + return false; } + } catch (Exception e) { + Logger.warn("Failed to create HTTP client for SHA1 check!"); + e.printStackTrace(); + return false; } } - private static void sendStillAlive(String url) throws IOException { - try (CloseableHttpClient httpClient = HttpClients.createDefault()) { - HttpPost httpPost = new HttpPost(url); + private static void sendStillAlive() throws IOException { + try (CloseableHttpClient httpClient = createHttpClient()) { + HttpPost httpPost = new HttpPost(finalURL + "still_alive"); MultipartEntityBuilder builder = MultipartEntityBuilder.create(); - builder.addTextBody("status", "alive", ContentType.TEXT_PLAIN.withCharset(StandardCharsets.UTF_8)); + builder.addTextBody("uuid", rspUUID); httpPost.setEntity(builder.build()); try (CloseableHttpResponse response = httpClient.execute(httpPost)) { String responseString = EntityUtils.toString(response.getEntity()); - System.out.println("Response from server: " + responseString); + int statusCode = response.getCode(); + + if (statusCode >= 200 && statusCode < 300) { + // Success - optionally log the success message + // Logger.info("Still alive ping successful"); + } else { + // Handle error - this might indicate session expired + handleErrorResponse(responseString, statusCode, "still alive"); + // Reset UUID to trigger re-initialization + rspUUID = null; + } + } catch (Exception e) { + Logger.warn("Failed to communicate with remote server during still alive ping!"); + e.printStackTrace(); + } + } + } + + public static void dataComplianceRequest() throws IOException { + try (CloseableHttpClient httpClient = createHttpClient()) { + HttpPost httpPost = new HttpPost(finalURL + "data_compliance"); + + MultipartEntityBuilder builder = MultipartEntityBuilder.create(); + builder.addTextBody("uuid", rspUUID); + httpPost.setEntity(builder.build()); + + try (CloseableHttpResponse response = httpClient.execute(httpPost)) { + HttpEntity responseEntity = response.getEntity(); + + if (responseEntity != null) { + // Save the response as a zip file + File zipFile = new File(ResourcePackManager.plugin.getDataFolder().getAbsolutePath() + File.separatorChar + "data_compliance" + File.separatorChar + "data.zip"); + if (!zipFile.getParentFile().exists()) zipFile.mkdirs(); + if (zipFile.exists()) zipFile.delete(); + zipFile.createNewFile(); + try (FileOutputStream outStream = new FileOutputStream(zipFile)) { + responseEntity.writeTo(outStream); + } + InputStream inputStream = ResourcePackManager.plugin.getResource("ReadMe.md"); + + File readMe = new File(ResourcePackManager.plugin.getDataFolder().getAbsolutePath() + File.separatorChar + "data_compliance" + File.separatorChar + "ReadMe.md"); + if (!readMe.exists()) readMe.createNewFile(); + + // Copy the InputStream to the file + Files.copy(inputStream, readMe.toPath(), StandardCopyOption.REPLACE_EXISTING); + } } catch (Exception e) { Logger.warn("Failed to communicate with remote server!"); e.printStackTrace(); @@ -103,5 +393,55 @@ private static void sendStillAlive(String url) throws IOException { public static void shutdown() { if (keepAlive != null) keepAlive.cancel(); + done = false; + rspUUID = null; + } + + private static void handleErrorResponse(String responseString, int statusCode, String operation) { + try { + Gson gson = new Gson(); + JsonObject errorResponse = gson.fromJson(responseString, JsonObject.class); + + if (errorResponse.has("error")) { + JsonObject error = errorResponse.getAsJsonObject("error"); + String errorCode = error.get("code").getAsString(); + String errorMessage = error.get("message").getAsString(); + String errorType = error.get("type").getAsString(); + + Logger.warn("=== Resource Pack " + operation.toUpperCase() + " ERROR ==="); + Logger.warn("Error Code: " + errorCode); + Logger.warn("Error Type: " + errorType); + Logger.warn("Message: " + errorMessage); + Logger.warn("HTTP Status: " + statusCode); + Logger.warn("====================================="); + + // Handle specific error types + switch (errorCode) { + case "MISSING_REQUIRED_FILES": + Logger.warn("Your resource pack structure is incorrect!"); + Logger.warn("Make sure pack.png and pack.mcmeta are in the root of your zip file."); + break; + case "FILE_TOO_LARGE": + Logger.warn("Your resource pack is too large! Please reduce the file size."); + break; + case "INVALID_FILE_FORMAT": + Logger.warn("Your resource pack file is corrupted or not a valid zip file."); + break; + case "SESSION_NOT_FOUND": + Logger.warn("Server session expired. Will attempt to reinitialize..."); + rspUUID = null; // Trigger re-initialization + break; + case "SERVER_UNAVAILABLE": + Logger.warn("Remote server is temporarily unavailable. Will retry later."); + break; + } + } else { + // Fallback for non-JSON error responses + Logger.warn("Server error during " + operation + " (HTTP " + statusCode + "): " + responseString); + } + } catch (Exception e) { + // Fallback if JSON parsing fails + Logger.warn("Server error during " + operation + " (HTTP " + statusCode + "): " + responseString); + } } } diff --git a/src/main/java/com/magmaguy/resourcepackmanager/commands/AdvancedCommand.java b/src/main/java/com/magmaguy/resourcepackmanager/commands/AdvancedCommand.java deleted file mode 100644 index a6a651a..0000000 --- a/src/main/java/com/magmaguy/resourcepackmanager/commands/AdvancedCommand.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.magmaguy.resourcepackmanager.commands; - -import org.bukkit.command.CommandSender; - -import java.util.ArrayList; -import java.util.List; - -public abstract class AdvancedCommand { - public final List aliases; - public final String description; - public final String permission; - public final boolean onlyForPlayers; - public final boolean enabled = true; - public final String usage; - - public AdvancedCommand(List aliases, String description, String permission, boolean onlyForPlayers, String usage) { - this.aliases = aliases; - this.description = description; - this.permission = permission; - this.onlyForPlayers = onlyForPlayers; - this.usage = usage; - } - - public abstract void execute(CommandSender sender, String[] arguments); - - public abstract List onTabComplete(CommandSender commandSender, org.bukkit.command.Command command, String label, String[] args); - - protected List trimSuggestions(List suggestions, String input) { - if (input.isEmpty()) return suggestions; - List newList = new ArrayList<>(); - for (String suggestion : suggestions) - if (suggestion.contains(input)) - newList.add(suggestion); - return newList; - } -} diff --git a/src/main/java/com/magmaguy/resourcepackmanager/commands/CommandManager.java b/src/main/java/com/magmaguy/resourcepackmanager/commands/CommandManager.java deleted file mode 100644 index 520ffff..0000000 --- a/src/main/java/com/magmaguy/resourcepackmanager/commands/CommandManager.java +++ /dev/null @@ -1,103 +0,0 @@ -package com.magmaguy.resourcepackmanager.commands; - -import org.bukkit.command.Command; -import org.bukkit.command.CommandExecutor; -import org.bukkit.command.CommandSender; -import org.bukkit.command.TabCompleter; -import org.bukkit.entity.Player; -import org.bukkit.plugin.java.JavaPlugin; - -import java.util.ArrayList; -import java.util.List; - -public class CommandManager implements CommandExecutor, TabCompleter { - public final List commands = new ArrayList<>(); - - public CommandManager(JavaPlugin javaPlugin) { - javaPlugin.getCommand("resourcepackmanager").setExecutor(this); - registerCommands(); - } - - private void registerCommands() { - registerCommand(new ReloadCommand()); - } - - public void registerCommand(AdvancedCommand command) { - commands.add(command); - } - - public void unregisterCommand(Command command) { - commands.remove(command); - } - - - private void sendMessage(CommandSender commandSender, String message) { - commandSender.sendMessage("[ResourcePackManager] " + message); - } - - @Override - public boolean onCommand(CommandSender commandSender, Command cmd, String label, String[] args) { - if (args.length == 0) { - sendMessage(commandSender, "Valid commands:"); - commands.forEach(command -> commandSender.sendMessage(command.usage)); - return true; - } - - for (AdvancedCommand command : commands) { - // We don't want to execute other commands or ones that are disabled - if (!(command.aliases.contains(args[0]) && command.enabled)) { - continue; - } - - if (command.onlyForPlayers && !(commandSender instanceof Player)) { - // Must be a player - commandSender.sendMessage("[ResourcePackManager] This command must be run as a player!"); - return false; - } - - if (!((commandSender.hasPermission(command.permission) || - command.permission.equalsIgnoreCase("") || - command.permission.equalsIgnoreCase("resourcepackmanager.")) && - command.enabled)) { - // No permissions - commandSender.sendMessage("[ResourcePackManager] You do not have the permission to run this command!"); - return false; - } - - command.execute(commandSender, args); - return true; - } - // Unknown command message - commandSender.sendMessage("[ResourcePackManager] Unknown command!"); - return false; - } - - @Override - public List onTabComplete(CommandSender commandSender, Command cmd, String label, String[] args) { - // Handle the tab completion if it's a sub-command. - if (args.length == 1) { - List result = new ArrayList<>(); - for (AdvancedCommand command : commands) { - for (String alias : command.aliases) { - if (alias.toLowerCase().startsWith(args[0].toLowerCase()) && ( - command.enabled && (commandSender.hasPermission(command.permission) - || command.permission.equalsIgnoreCase("") || command.permission - .equalsIgnoreCase("resourcepackmanager.")))) { - result.add(alias); - } - } - } - return result; - } - - // Let the sub-command handle the tab completion - for (AdvancedCommand command : commands) { - if (command.aliases.contains(args[0]) && (command.enabled && ( - commandSender.hasPermission(command.permission) || command.permission.equalsIgnoreCase("") - || command.permission.equalsIgnoreCase("resourcepackmanager.")))) { - return command.onTabComplete(commandSender, cmd, label, args); - } - } - return null; - } -} diff --git a/src/main/java/com/magmaguy/resourcepackmanager/commands/DataComplianceRequestCommand.java b/src/main/java/com/magmaguy/resourcepackmanager/commands/DataComplianceRequestCommand.java new file mode 100644 index 0000000..a1c7fcc --- /dev/null +++ b/src/main/java/com/magmaguy/resourcepackmanager/commands/DataComplianceRequestCommand.java @@ -0,0 +1,41 @@ +package com.magmaguy.resourcepackmanager.commands; + +import com.magmaguy.magmacore.command.AdvancedCommand; +import com.magmaguy.magmacore.command.CommandData; +import com.magmaguy.magmacore.util.Logger; +import com.magmaguy.resourcepackmanager.ResourcePackManager; +import com.magmaguy.resourcepackmanager.autohost.AutoHost; +import org.bukkit.scheduler.BukkitRunnable; + +import java.util.List; + +public class DataComplianceRequestCommand extends AdvancedCommand { + + public DataComplianceRequestCommand() { + super(List.of("data_compliance_request")); + setDescription("Downloads a copy of all data associated to this server from the autohoster"); + setPermission("resourcepackmanager.*"); + setUsage("/rspm data_compliance_request"); + } + + @Override + public void execute(CommandData commandData) { + if (AutoHost.getRspUUID() == null) { + Logger.sendMessage(commandData.getCommandSender(), "Seems like the auto-hoster is either disabled or not working, no data is stored in remote servers because no connection to remote servers is established"); + return; + } + + new BukkitRunnable() { + @Override + public void run() { + try { + AutoHost.dataComplianceRequest(); + Logger.sendMessage(commandData.getCommandSender(), "Data compliance request completed, check ~/plugins/ResourcePackManager/data_compliance to see all data the remote server has stored for this server."); + } catch (Exception e) { + Logger.sendMessage(commandData.getCommandSender(), "Failed to request data, check console for error logs!"); + e.printStackTrace(); + } + } + }.runTaskAsynchronously(ResourcePackManager.plugin); + } +} diff --git a/src/main/java/com/magmaguy/resourcepackmanager/commands/ReloadCommand.java b/src/main/java/com/magmaguy/resourcepackmanager/commands/ReloadCommand.java index 7da4f08..c40163f 100644 --- a/src/main/java/com/magmaguy/resourcepackmanager/commands/ReloadCommand.java +++ b/src/main/java/com/magmaguy/resourcepackmanager/commands/ReloadCommand.java @@ -1,30 +1,29 @@ package com.magmaguy.resourcepackmanager.commands; -import com.magmaguy.resourcepackmanager.Logger; +import com.magmaguy.magmacore.command.AdvancedCommand; +import com.magmaguy.magmacore.command.CommandData; +import com.magmaguy.magmacore.util.Logger; import com.magmaguy.resourcepackmanager.ResourcePackManager; -import org.bukkit.command.Command; import org.bukkit.command.CommandSender; import java.util.List; public class ReloadCommand extends AdvancedCommand { public ReloadCommand() { - super(List.of("reload"), "Reloads the plugin", "*", false, "/fmm reload"); + super(List.of("reload")); + setDescription("Reloads the plugin"); + setPermission("resourcepackmanager.*"); + setUsage("/rspm reload"); } public static void reloadPlugin(CommandSender sender) { ResourcePackManager.plugin.onDisable(); ResourcePackManager.plugin.onEnable(); - sender.sendMessage("[ResourcePackManager] Reloaded plugin!"); + Logger.sendMessage(sender, "Reloaded the plugin!"); } @Override - public void execute(CommandSender sender, String[] arguments) { - reloadPlugin(sender); - } - - @Override - public List onTabComplete(CommandSender commandSender, Command command, String label, String[] args) { - return null; + public void execute(CommandData commandData) { + reloadPlugin(commandData.getCommandSender()); } } \ No newline at end of file diff --git a/src/main/java/com/magmaguy/resourcepackmanager/config/BlueprintFolder.java b/src/main/java/com/magmaguy/resourcepackmanager/config/BlueprintFolder.java new file mode 100644 index 0000000..a30cd90 --- /dev/null +++ b/src/main/java/com/magmaguy/resourcepackmanager/config/BlueprintFolder.java @@ -0,0 +1,44 @@ +package com.magmaguy.resourcepackmanager.config; + +import com.magmaguy.magmacore.util.Logger; +import com.magmaguy.magmacore.util.ZipFile; +import com.magmaguy.resourcepackmanager.ResourcePackManager; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; + +public class BlueprintFolder { + private BlueprintFolder() { + } + + public static void initialize() { + Logger.info("Creating blueprint folder"); + File blueprintDirectory = new File(ResourcePackManager.plugin.getDataFolder().getAbsolutePath() + File.separatorChar + "blueprint"); + if (!blueprintDirectory.exists()) blueprintDirectory.mkdir(); + Logger.info("Copying image"); + File imageFile = new File(blueprintDirectory.getAbsolutePath() + File.separatorChar + "pack.png"); + if (!imageFile.exists()) { + try (InputStream inputStream = ResourcePackManager.plugin.getResource("pack.png")) { + Files.copy(inputStream, imageFile.toPath()); + } catch (IOException e) { + e.printStackTrace(); + } + } + Logger.info("Copying mcmeta"); + File mcmetaFile = new File(blueprintDirectory.getAbsolutePath() + File.separatorChar + "pack.mcmeta"); + if (!mcmetaFile.exists()) { + try (InputStream inputStream = ResourcePackManager.plugin.getResource("pack.mcmeta")) { + Files.copy(inputStream, mcmetaFile.toPath()); + } catch (IOException e) { + e.printStackTrace(); + } + } + try { + ZipFile.ZipUtility.zip(blueprintDirectory, blueprintDirectory.getAbsolutePath() + File.separatorChar + "blueprint.zip"); + } catch (Exception e) { + Logger.warn("Failed to zip blueprint resource pack!"); + } + } +} diff --git a/src/main/java/com/magmaguy/resourcepackmanager/config/ConfigurationEngine.java b/src/main/java/com/magmaguy/resourcepackmanager/config/ConfigurationEngine.java deleted file mode 100644 index 7e6c8a0..0000000 --- a/src/main/java/com/magmaguy/resourcepackmanager/config/ConfigurationEngine.java +++ /dev/null @@ -1,139 +0,0 @@ -package com.magmaguy.resourcepackmanager.config; - -import com.magmaguy.resourcepackmanager.Logger; -import com.magmaguy.resourcepackmanager.ResourcePackManager; -import org.bukkit.Bukkit; -import org.bukkit.configuration.file.FileConfiguration; -import org.bukkit.configuration.file.YamlConfiguration; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; -import java.util.List; - -public class ConfigurationEngine { - - public static File fileCreator(String path, String fileName) { - File file = new File(ResourcePackManager.plugin.getDataFolder().getPath() + "/" + path + "/", fileName); - return fileCreator(file); - } - - public static File fileCreator(String fileName) { - File file = new File(ResourcePackManager.plugin.getDataFolder().getPath(), fileName); - return fileCreator(file); - } - - public static File fileCreator(File file) { - - if (!file.exists()) - try { - file.getParentFile().mkdirs(); - file.createNewFile(); - } catch (IOException ex) { - Bukkit.getLogger().warning("[EliteMobs] Error generating the plugin file: " + file.getName()); - } - - return file; - - } - - public static FileConfiguration fileConfigurationCreator(File file) { - try { - return YamlConfiguration.loadConfiguration(new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8)); - } catch (Exception exception) { - Logger.warn("Failed to read configuration from file " + file.getName()); - return null; - } - } - - public static void fileSaverCustomValues(FileConfiguration fileConfiguration, File file) { - fileConfiguration.options().copyDefaults(true); - - try { - fileConfiguration.save(file); - } catch (IOException e) { - e.printStackTrace(); - } - - } - - public static void fileSaverOnlyDefaults(FileConfiguration fileConfiguration, File file) { - fileConfiguration.options().copyDefaults(true); - UnusedNodeHandler.clearNodes(fileConfiguration); - - try { - fileConfiguration.save(file); - } catch (IOException e) { - e.printStackTrace(); - } - - } - - private static void setComments(FileConfiguration fileConfiguration, String key, List comments) { - fileConfiguration.setComments(key, comments); - } - - public static Boolean setBoolean(FileConfiguration fileConfiguration, String key, boolean defaultValue) { - fileConfiguration.addDefault(key, defaultValue); - return fileConfiguration.getBoolean(key); - } - - public static Boolean setBoolean(List comments, FileConfiguration fileConfiguration, String key, boolean defaultValue) { - boolean value = setBoolean(fileConfiguration, key, defaultValue); - setComments(fileConfiguration, key, comments); - return value; - } - - - public static int setInt(FileConfiguration fileConfiguration, String key, int defaultValue) { - fileConfiguration.addDefault(key, defaultValue); - return fileConfiguration.getInt(key); - } - - public static int setInt(List comments, FileConfiguration fileConfiguration, String key, int defaultValue) { - int value = setInt(fileConfiguration, key, defaultValue); - setComments(fileConfiguration, key, comments); - return value; - } - - public static double setDouble(FileConfiguration fileConfiguration, String key, double defaultValue) { - fileConfiguration.addDefault(key, defaultValue); - return fileConfiguration.getDouble(key); - } - - public static double setDouble(List comments, FileConfiguration fileConfiguration, String key, double defaultValue) { - double value = setDouble(fileConfiguration, key, defaultValue); - setComments(fileConfiguration, key, comments); - return value; - } - - public static boolean writeValue(Object value, File file, FileConfiguration fileConfiguration, String path) { - fileConfiguration.set(path, value); - try { - fileSaverCustomValues(fileConfiguration, file); - } catch (Exception exception) { - Logger.warn("Failed to write value for " + path + " in file " + file.getName()); - return false; - } - return true; - } - - public static void removeValue(File file, FileConfiguration fileConfiguration, String path) { - writeValue(null, file, fileConfiguration, path); - } - - public static List setList(File file, FileConfiguration fileConfiguration, String key, List defaultValue) { - fileConfiguration.addDefault(key, defaultValue); - return fileConfiguration.getList(key); - } - - public static List setList(List comment, File file, FileConfiguration fileConfiguration, String key, List defaultValue) { - List value = setList(file, fileConfiguration, key, defaultValue); - setComments(fileConfiguration, key, comment); - return value; - } - - -} diff --git a/src/main/java/com/magmaguy/resourcepackmanager/config/DataConfig.java b/src/main/java/com/magmaguy/resourcepackmanager/config/DataConfig.java new file mode 100644 index 0000000..d8b25a0 --- /dev/null +++ b/src/main/java/com/magmaguy/resourcepackmanager/config/DataConfig.java @@ -0,0 +1,95 @@ +package com.magmaguy.resourcepackmanager.config; + +import com.magmaguy.magmacore.config.ConfigurationFile; +import com.magmaguy.magmacore.util.Logger; + +import java.util.UUID; + +public class DataConfig extends ConfigurationFile { + private static DataConfig instance = null; + + public DataConfig() { + super("data.yml"); + instance = this; + } + + public static String getRspUUID() { + String uuid = instance.getFileConfiguration().getString("uuid"); + + // If UUID exists but is invalid, clear it and return null + if (uuid != null && !uuid.isEmpty() && !isValidUUID(uuid)) { + Logger.warn("Invalid UUID found in config file: " + uuid + ". Deleting invalid UUID from file."); + instance.clearInvalidUUID(); + return null; + } + + // Return null for empty/null UUIDs instead of empty string + return (uuid == null || uuid.isEmpty()) ? "" : uuid; + } + + public static void setRspUUID(String rspUUID) { + // Validate UUID if not null/empty + if (rspUUID != null && !rspUUID.isEmpty() && !isValidUUID(rspUUID)) { + throw new IllegalArgumentException("Invalid UUID format: " + rspUUID + + ". UUID must be in format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"); + } + + // Store null as null, not empty string + instance.getFileConfiguration().set("uuid", rspUUID); + try { + instance.getFileConfiguration().save(instance.file); + if (rspUUID != null && !rspUUID.isEmpty()) { + Logger.info("Successfully saved UUID: " + rspUUID); + } else { + Logger.info("Successfully cleared UUID from config."); + } + } catch (Exception e) { + Logger.warn("Failed to save uuid!"); + e.printStackTrace(); + } + } + + /** + * Clears invalid UUID from config file + */ + private void clearInvalidUUID() { + getFileConfiguration().set("uuid", null); + try { + getFileConfiguration().save(file); + Logger.info("Invalid UUID cleared from config file."); + } catch (Exception e) { + Logger.warn("Failed to clear invalid UUID from config!"); + e.printStackTrace(); + } + } + + /** + * Validates if a string is a proper UUID format + * @param uuidString The string to validate + * @return true if valid UUID format, false otherwise + */ + private static boolean isValidUUID(String uuidString) { + if (uuidString == null || uuidString.trim().isEmpty()) { + return false; + } + + try { + // Use Java's built-in UUID validation + UUID.fromString(uuidString.trim()); + return true; + } catch (IllegalArgumentException e) { + return false; + } + } + + @Override + public void initializeValues() { + // Validate existing UUID on initialization and clear if invalid + String existingUUID = getFileConfiguration().getString("uuid"); + if (existingUUID != null && !existingUUID.isEmpty() && !isValidUUID(existingUUID)) { + Logger.warn("Invalid UUID found in config file during initialization: " + existingUUID); + Logger.warn("Deleting invalid UUID from file. A new one will be generated on next server connection."); + clearInvalidUUID(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/magmaguy/resourcepackmanager/config/DefaultConfig.java b/src/main/java/com/magmaguy/resourcepackmanager/config/DefaultConfig.java index 702aea3..b5c9050 100644 --- a/src/main/java/com/magmaguy/resourcepackmanager/config/DefaultConfig.java +++ b/src/main/java/com/magmaguy/resourcepackmanager/config/DefaultConfig.java @@ -1,38 +1,77 @@ package com.magmaguy.resourcepackmanager.config; +import com.magmaguy.magmacore.config.ConfigurationEngine; +import com.magmaguy.magmacore.config.ConfigurationFile; import lombok.Getter; -import org.bukkit.configuration.file.FileConfiguration; -import java.io.File; import java.util.List; -public class DefaultConfig { - private static File file = null; - private static FileConfiguration fileConfiguration = null; +public class DefaultConfig extends ConfigurationFile { + @Getter private static List priorityOrder; @Getter private static boolean autoHost; + @Getter + private static boolean forceResourcePack; + @Getter + private static boolean autoMixOnStartup; + @Getter + private static String resourcePackPrompt; + @Getter + private static String resourcePackRerouting; + - public static void initializeConfig() { - file = ConfigurationEngine.fileCreator("config.yml"); - fileConfiguration = ConfigurationEngine.fileConfigurationCreator(file); + public DefaultConfig() { + super("config.yml"); + } + @Override + public void initializeValues() { priorityOrder = ConfigurationEngine.setList( List.of( "Sets the list, from highest priority (top) to lowest priority (bottom), in which the resource" + " packs will automatically resolve merge conflicts.", "The defaults use plugin names. If you manually added your own resource pack in the mixer folder to be merged in, add its exact filename, including .zip in the name"), - file, fileConfiguration, "priorityOrder", - List.of("ResourcePackManager", "EliteMobs", "FreeMinecraftModels", "ModelEngine", "ItemsAdder", "Nova", "Oraxen")); + fileConfiguration, "priorityOrder", + List.of( + "ResourcePackManager", + "EliteMobs", + "FreeMinecraftModels", + "ModelEngine", + "Nova", + "ItemsAdder", + "Oraxen", + "BetterHUD", + "ValhallaMMO", + "MMOInventory", + "vane-core", + "RealisticSurvival")); autoHost = ConfigurationEngine.setBoolean( List.of("Automatically host the resource pack on MagmaGuy's servers", "These servers cost money to keep running. There is no guarantee this will be an option forever."), fileConfiguration, "autoHost", true); - ConfigurationEngine.fileSaverOnlyDefaults(fileConfiguration, file); + forceResourcePack = ConfigurationEngine.setBoolean( + List.of("Sets whether the resource pack use will be forced to clients"), + fileConfiguration, "forceResourcePack", false); + autoMixOnStartup = ConfigurationEngine.setBoolean( + List.of( + "Enable automatic resource pack mixing when the server starts.", + "If disabled, ResourcePackManager will wait for a resource pack change or manual reload before mixing."), + fileConfiguration, "autoMixOnStartup", true); + resourcePackPrompt = ConfigurationEngine.setString( + List.of("Sets whether the resource pack use will be forced to clients"), + fileConfiguration, "resourcePackPrompt", "Use recommended resource pack?"); + resourcePackRerouting = ConfigurationEngine.setString( + List.of( + "OPTIONAL: Copy the merged directory to a custom directory location. Useful for unusual setups, like people trying to host with a different plugin.", + "If you are hosting with a different plugin make sure to disable Auto-hosting here!", + "This will use the plugin directory as the base directory.", + "As an example, if you wanted to target ResourcePackManager's output folder, you'd do:", + "ResourcePackManage/output", + "If you don't know what any of what is written here means, just don't touch this setting!"), + fileConfiguration, "resourcePackRerouting", ""); } - - } diff --git a/src/main/java/com/magmaguy/resourcepackmanager/config/UnusedNodeHandler.java b/src/main/java/com/magmaguy/resourcepackmanager/config/UnusedNodeHandler.java deleted file mode 100644 index e05eb13..0000000 --- a/src/main/java/com/magmaguy/resourcepackmanager/config/UnusedNodeHandler.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.magmaguy.resourcepackmanager.config; - -import com.magmaguy.resourcepackmanager.Logger; -import org.bukkit.Bukkit; -import org.bukkit.configuration.Configuration; -import org.bukkit.configuration.file.FileConfiguration; - -public class UnusedNodeHandler { - - private UnusedNodeHandler() { - } - - public static Configuration clearNodes(FileConfiguration configuration) { - - for (String actual : configuration.getKeys(false)) { - boolean keyExists = false; - for (String defaults : configuration.getDefaults().getKeys(true)) - if (actual.equals(defaults)) { - keyExists = true; - break; - } - - if (!keyExists) { - configuration.set(actual, null); - Bukkit.getLogger().warning(actual); - Logger.warn("Deleting unused config values."); - } - } - return configuration; - } -} diff --git a/src/main/java/com/magmaguy/resourcepackmanager/config/compatibleplugins/CompatiblePluginConfig.java b/src/main/java/com/magmaguy/resourcepackmanager/config/compatibleplugins/CompatiblePluginConfig.java new file mode 100644 index 0000000..b1223fe --- /dev/null +++ b/src/main/java/com/magmaguy/resourcepackmanager/config/compatibleplugins/CompatiblePluginConfig.java @@ -0,0 +1,19 @@ +package com.magmaguy.resourcepackmanager.config.compatibleplugins; + +import com.magmaguy.magmacore.config.CustomConfig; +import lombok.Getter; + +import java.util.HashMap; +import java.util.Map; + +public class CompatiblePluginConfig extends CustomConfig { + @Getter + private static Map compatiblePlugins = new HashMap<>(); + + public CompatiblePluginConfig() { + super("compatible_plugins", "com.magmaguy.resourcepackmanager.config.compatibleplugins.premade", CompatiblePluginConfigFields.class); + compatiblePlugins = new HashMap<>(); + for (String key : super.getCustomConfigFieldsHashMap().keySet()) + compatiblePlugins.put(key, (CompatiblePluginConfigFields) super.getCustomConfigFieldsHashMap().get(key)); + } +} diff --git a/src/main/java/com/magmaguy/resourcepackmanager/config/compatibleplugins/CompatiblePluginConfigFields.java b/src/main/java/com/magmaguy/resourcepackmanager/config/compatibleplugins/CompatiblePluginConfigFields.java new file mode 100644 index 0000000..0d13720 --- /dev/null +++ b/src/main/java/com/magmaguy/resourcepackmanager/config/compatibleplugins/CompatiblePluginConfigFields.java @@ -0,0 +1,43 @@ +package com.magmaguy.resourcepackmanager.config.compatibleplugins; + +import com.magmaguy.magmacore.config.CustomConfigFields; +import lombok.Getter; +import lombok.Setter; + +public class CompatiblePluginConfigFields extends CustomConfigFields { + + @Getter + @Setter + private String pluginName = "placeholder"; + @Getter + @Setter + private String url = null; + @Getter + @Setter + private String localPath = null; + @Getter + @Setter + private boolean zips = true; + @Getter + @Setter + private String reloadCommand; + @Getter + @Setter + private boolean cluster = false; + + public CompatiblePluginConfigFields(String filename, boolean isEnabled) { + super(filename, isEnabled); + } + + @Override + public void processConfigFields() { + this.isEnabled = processBoolean("isEnabled", isEnabled, isEnabled, true); + this.pluginName = processString("pluginName", pluginName, pluginName, true); + this.url = processString("url", url, url, true); + this.zips = processBoolean("zips", zips, zips, true); + this.reloadCommand = processString("reloadCommand", reloadCommand, reloadCommand, true); + this.localPath = processString("localPath", localPath, localPath, true); + this.cluster = processBoolean("cluster", cluster, cluster, true); + + } +} diff --git a/src/main/java/com/magmaguy/resourcepackmanager/config/compatibleplugins/premade/BackpackPlus.java b/src/main/java/com/magmaguy/resourcepackmanager/config/compatibleplugins/premade/BackpackPlus.java new file mode 100644 index 0000000..d829860 --- /dev/null +++ b/src/main/java/com/magmaguy/resourcepackmanager/config/compatibleplugins/premade/BackpackPlus.java @@ -0,0 +1,13 @@ +package com.magmaguy.resourcepackmanager.config.compatibleplugins.premade; + +import com.magmaguy.resourcepackmanager.config.compatibleplugins.CompatiblePluginConfigFields; + +import java.io.File; + +public class BackpackPlus extends CompatiblePluginConfigFields { + public BackpackPlus() { + super("backpack_plus", true); + setPluginName("BackpackPlus"); + setLocalPath("BackpackPlus" + File.separatorChar + "pack" + File.separatorChar + "resourcepack.zip"); + } +} diff --git a/src/main/java/com/magmaguy/resourcepackmanager/config/compatibleplugins/premade/BetterHUD.java b/src/main/java/com/magmaguy/resourcepackmanager/config/compatibleplugins/premade/BetterHUD.java new file mode 100644 index 0000000..a25d3fe --- /dev/null +++ b/src/main/java/com/magmaguy/resourcepackmanager/config/compatibleplugins/premade/BetterHUD.java @@ -0,0 +1,14 @@ +package com.magmaguy.resourcepackmanager.config.compatibleplugins.premade; + +import com.magmaguy.resourcepackmanager.config.compatibleplugins.CompatiblePluginConfigFields; + +import java.io.File; + +public class BetterHUD extends CompatiblePluginConfigFields { + public BetterHUD() { + super("better_hud", true); + setPluginName("BetterHUD"); + setLocalPath("BetterHUD" + File.separatorChar + "build.zip"); + setReloadCommand("betterhud reload"); + } +} diff --git a/src/main/java/com/magmaguy/resourcepackmanager/config/compatibleplugins/premade/EliteMobs.java b/src/main/java/com/magmaguy/resourcepackmanager/config/compatibleplugins/premade/EliteMobs.java new file mode 100644 index 0000000..27deed7 --- /dev/null +++ b/src/main/java/com/magmaguy/resourcepackmanager/config/compatibleplugins/premade/EliteMobs.java @@ -0,0 +1,16 @@ +package com.magmaguy.resourcepackmanager.config.compatibleplugins.premade; + +import com.magmaguy.resourcepackmanager.config.compatibleplugins.CompatiblePluginConfigFields; + +import java.io.File; + +public class EliteMobs extends CompatiblePluginConfigFields { + public EliteMobs() { + super("elitemobs", true); + setPluginName("EliteMobs"); + setLocalPath("EliteMobs" + File.separatorChar + "resource_pack"); + setReloadCommand("elitemobs reload"); + setCluster(true); + setZips(false); + } +} \ No newline at end of file diff --git a/src/main/java/com/magmaguy/resourcepackmanager/config/compatibleplugins/premade/FreeMinecraftModels.java b/src/main/java/com/magmaguy/resourcepackmanager/config/compatibleplugins/premade/FreeMinecraftModels.java new file mode 100644 index 0000000..44ab1f0 --- /dev/null +++ b/src/main/java/com/magmaguy/resourcepackmanager/config/compatibleplugins/premade/FreeMinecraftModels.java @@ -0,0 +1,14 @@ +package com.magmaguy.resourcepackmanager.config.compatibleplugins.premade; + +import com.magmaguy.resourcepackmanager.config.compatibleplugins.CompatiblePluginConfigFields; + +import java.io.File; + +public class FreeMinecraftModels extends CompatiblePluginConfigFields { + public FreeMinecraftModels() { + super("free_minecraft_models", true); + setPluginName("FreeMinecraftModels"); + setLocalPath("FreeMinecraftModels" + File.separatorChar + "output" + File.separatorChar + "FreeMinecraftModels.zip"); + setReloadCommand("freeminecraftmodels reload"); + } +} \ No newline at end of file diff --git a/src/main/java/com/magmaguy/resourcepackmanager/config/compatibleplugins/premade/InfiniteVehicles.java b/src/main/java/com/magmaguy/resourcepackmanager/config/compatibleplugins/premade/InfiniteVehicles.java new file mode 100644 index 0000000..18f2587 --- /dev/null +++ b/src/main/java/com/magmaguy/resourcepackmanager/config/compatibleplugins/premade/InfiniteVehicles.java @@ -0,0 +1,13 @@ +package com.magmaguy.resourcepackmanager.config.compatibleplugins.premade; + +import com.magmaguy.resourcepackmanager.config.compatibleplugins.CompatiblePluginConfigFields; + +import java.io.File; + +public class InfiniteVehicles extends CompatiblePluginConfigFields { + public InfiniteVehicles() { + super("infinite_vehicles", true); + setPluginName("InfiniteVehicles"); + setLocalPath("InfiniteVehicles" + File.separatorChar + "InfiniteModelPack.zip"); + } +} \ No newline at end of file diff --git a/src/main/java/com/magmaguy/resourcepackmanager/config/compatibleplugins/premade/ItemsAdder.java b/src/main/java/com/magmaguy/resourcepackmanager/config/compatibleplugins/premade/ItemsAdder.java new file mode 100644 index 0000000..5629222 --- /dev/null +++ b/src/main/java/com/magmaguy/resourcepackmanager/config/compatibleplugins/premade/ItemsAdder.java @@ -0,0 +1,13 @@ +package com.magmaguy.resourcepackmanager.config.compatibleplugins.premade; + +import com.magmaguy.resourcepackmanager.config.compatibleplugins.CompatiblePluginConfigFields; + +import java.io.File; + +public class ItemsAdder extends CompatiblePluginConfigFields { + public ItemsAdder() { + super("items_adder", true); + setPluginName("ItemsAdder"); + setLocalPath("ItemsAdder" + File.separatorChar + "output" + File.separatorChar + "generated.zip"); + } +} \ No newline at end of file diff --git a/src/main/java/com/magmaguy/resourcepackmanager/config/compatibleplugins/premade/MMOInventory.java b/src/main/java/com/magmaguy/resourcepackmanager/config/compatibleplugins/premade/MMOInventory.java new file mode 100644 index 0000000..d357c0d --- /dev/null +++ b/src/main/java/com/magmaguy/resourcepackmanager/config/compatibleplugins/premade/MMOInventory.java @@ -0,0 +1,11 @@ +package com.magmaguy.resourcepackmanager.config.compatibleplugins.premade; + +import com.magmaguy.resourcepackmanager.config.compatibleplugins.CompatiblePluginConfigFields; + +public class MMOInventory extends CompatiblePluginConfigFields { + public MMOInventory() { + super("mmo_inventory", true); + setPluginName("MMOInventory"); + setUrl("https://www.dropbox.com/s/1lftxmzh0q4b5yu/mmoinv_rp_3.zip?dl=1"); + } +} \ No newline at end of file diff --git a/src/main/java/com/magmaguy/resourcepackmanager/config/compatibleplugins/premade/MegaBlockSurvivors.java b/src/main/java/com/magmaguy/resourcepackmanager/config/compatibleplugins/premade/MegaBlockSurvivors.java new file mode 100644 index 0000000..be92a15 --- /dev/null +++ b/src/main/java/com/magmaguy/resourcepackmanager/config/compatibleplugins/premade/MegaBlockSurvivors.java @@ -0,0 +1,14 @@ +package com.magmaguy.resourcepackmanager.config.compatibleplugins.premade; + +import com.magmaguy.resourcepackmanager.config.compatibleplugins.CompatiblePluginConfigFields; + +import java.io.File; + +public class MegaBlockSurvivors extends CompatiblePluginConfigFields { + public MegaBlockSurvivors() { + super("megablock_survivors", true); + setPluginName("MegaBlockSurvivors"); + setLocalPath("MegaBlockSurvivors" + File.separatorChar + "resourcepack"); + setZips(false); + } +} \ No newline at end of file diff --git a/src/main/java/com/magmaguy/resourcepackmanager/config/compatibleplugins/premade/ModelEngine.java b/src/main/java/com/magmaguy/resourcepackmanager/config/compatibleplugins/premade/ModelEngine.java new file mode 100644 index 0000000..d16a585 --- /dev/null +++ b/src/main/java/com/magmaguy/resourcepackmanager/config/compatibleplugins/premade/ModelEngine.java @@ -0,0 +1,14 @@ +package com.magmaguy.resourcepackmanager.config.compatibleplugins.premade; + +import com.magmaguy.resourcepackmanager.config.compatibleplugins.CompatiblePluginConfigFields; + +import java.io.File; + +public class ModelEngine extends CompatiblePluginConfigFields { + public ModelEngine() { + super("model_engine", true); + setPluginName("ModelEngine"); + setLocalPath("ModelEngine" + File.separatorChar + "resource pack.zip"); + setReloadCommand("meg reload"); + } +} \ No newline at end of file diff --git a/src/main/java/com/magmaguy/resourcepackmanager/config/compatibleplugins/premade/Nexo.java b/src/main/java/com/magmaguy/resourcepackmanager/config/compatibleplugins/premade/Nexo.java new file mode 100644 index 0000000..552fc7b --- /dev/null +++ b/src/main/java/com/magmaguy/resourcepackmanager/config/compatibleplugins/premade/Nexo.java @@ -0,0 +1,13 @@ +package com.magmaguy.resourcepackmanager.config.compatibleplugins.premade; + +import com.magmaguy.resourcepackmanager.config.compatibleplugins.CompatiblePluginConfigFields; + +import java.io.File; + +public class Nexo extends CompatiblePluginConfigFields { + public Nexo() { + super("nexo", true); + setPluginName("Nexo"); + setLocalPath("Nexo" + File.separatorChar + "pack" + File.separatorChar + "pack.zip"); + } +} diff --git a/src/main/java/com/magmaguy/resourcepackmanager/config/compatibleplugins/premade/Nova.java b/src/main/java/com/magmaguy/resourcepackmanager/config/compatibleplugins/premade/Nova.java new file mode 100644 index 0000000..ce68b14 --- /dev/null +++ b/src/main/java/com/magmaguy/resourcepackmanager/config/compatibleplugins/premade/Nova.java @@ -0,0 +1,13 @@ +package com.magmaguy.resourcepackmanager.config.compatibleplugins.premade; + +import com.magmaguy.resourcepackmanager.config.compatibleplugins.CompatiblePluginConfigFields; + +import java.io.File; + +public class Nova extends CompatiblePluginConfigFields { + public Nova() { + super("nova", true); + setPluginName("Nova"); + setLocalPath("Nova" + File.separatorChar + "resource_pack" + File.separatorChar + "ResourcePack.zip"); + } +} \ No newline at end of file diff --git a/src/main/java/com/magmaguy/resourcepackmanager/config/compatibleplugins/premade/Oraxen.java b/src/main/java/com/magmaguy/resourcepackmanager/config/compatibleplugins/premade/Oraxen.java new file mode 100644 index 0000000..68ca0a5 --- /dev/null +++ b/src/main/java/com/magmaguy/resourcepackmanager/config/compatibleplugins/premade/Oraxen.java @@ -0,0 +1,13 @@ +package com.magmaguy.resourcepackmanager.config.compatibleplugins.premade; + +import com.magmaguy.resourcepackmanager.config.compatibleplugins.CompatiblePluginConfigFields; + +import java.io.File; + +public class Oraxen extends CompatiblePluginConfigFields { + public Oraxen() { + super("oraxen", true); + setPluginName("Oraxen"); + setLocalPath("Oraxen" + File.separatorChar + "pack" + File.separatorChar + "pack.zip"); + } +} \ No newline at end of file diff --git a/src/main/java/com/magmaguy/resourcepackmanager/config/compatibleplugins/premade/RealisticSurvival.java b/src/main/java/com/magmaguy/resourcepackmanager/config/compatibleplugins/premade/RealisticSurvival.java new file mode 100644 index 0000000..774f6f6 --- /dev/null +++ b/src/main/java/com/magmaguy/resourcepackmanager/config/compatibleplugins/premade/RealisticSurvival.java @@ -0,0 +1,11 @@ +package com.magmaguy.resourcepackmanager.config.compatibleplugins.premade; + +import com.magmaguy.resourcepackmanager.config.compatibleplugins.CompatiblePluginConfigFields; + +public class RealisticSurvival extends CompatiblePluginConfigFields { + public RealisticSurvival() { + super("realistic_survival", true); + setPluginName("RealisticSurvival"); + setUrl("https://www.dropbox.com/scl/fi/5ggc7t20bfxmrzsbb1kok/Realistic-Survival-RP-1.2.8-RELEASE.zip?rlkey=55llpn4zj146usfmm8okf1ier&dl=1"); + } +} \ No newline at end of file diff --git a/src/main/java/com/magmaguy/resourcepackmanager/config/compatibleplugins/premade/ResourcePackManager.java b/src/main/java/com/magmaguy/resourcepackmanager/config/compatibleplugins/premade/ResourcePackManager.java new file mode 100644 index 0000000..b69b50a --- /dev/null +++ b/src/main/java/com/magmaguy/resourcepackmanager/config/compatibleplugins/premade/ResourcePackManager.java @@ -0,0 +1,14 @@ +package com.magmaguy.resourcepackmanager.config.compatibleplugins.premade; + +import com.magmaguy.resourcepackmanager.config.compatibleplugins.CompatiblePluginConfigFields; + +import java.io.File; + +public class ResourcePackManager extends CompatiblePluginConfigFields { + public ResourcePackManager() { + super("resource_pack_manager", true); + setPluginName("ResourcePackManager"); + setLocalPath("ResourcePackManager" + File.separatorChar + "blueprint" + File.separatorChar + "blueprint.zip"); + setReloadCommand("rspm reload"); + } +} \ No newline at end of file diff --git a/src/main/java/com/magmaguy/resourcepackmanager/config/compatibleplugins/premade/ValhallaMMO.java b/src/main/java/com/magmaguy/resourcepackmanager/config/compatibleplugins/premade/ValhallaMMO.java new file mode 100644 index 0000000..e4a506b --- /dev/null +++ b/src/main/java/com/magmaguy/resourcepackmanager/config/compatibleplugins/premade/ValhallaMMO.java @@ -0,0 +1,14 @@ +package com.magmaguy.resourcepackmanager.config.compatibleplugins.premade; + +import com.magmaguy.resourcepackmanager.config.compatibleplugins.CompatiblePluginConfigFields; + +import java.io.File; + +public class ValhallaMMO extends CompatiblePluginConfigFields { + public ValhallaMMO() { + super("valhalla_mmo", true); + setPluginName("ValhallaMMO"); + setLocalPath("ValhallaMMO" + File.separatorChar + "resourcepack"); + setZips(false); + } +} \ No newline at end of file diff --git a/src/main/java/com/magmaguy/resourcepackmanager/config/compatibleplugins/premade/VaneCore.java b/src/main/java/com/magmaguy/resourcepackmanager/config/compatibleplugins/premade/VaneCore.java new file mode 100644 index 0000000..0b445ce --- /dev/null +++ b/src/main/java/com/magmaguy/resourcepackmanager/config/compatibleplugins/premade/VaneCore.java @@ -0,0 +1,11 @@ +package com.magmaguy.resourcepackmanager.config.compatibleplugins.premade; + +import com.magmaguy.resourcepackmanager.config.compatibleplugins.CompatiblePluginConfigFields; + +public class VaneCore extends CompatiblePluginConfigFields { + public VaneCore() { + super("vane_core", true); + setPluginName("vane-core"); + setLocalPath("vane-resource-pack.zip"); + } +} \ No newline at end of file diff --git a/src/main/java/com/magmaguy/resourcepackmanager/itemsadder/ItemsAdderCommand.java b/src/main/java/com/magmaguy/resourcepackmanager/itemsadder/ItemsAdderCommand.java new file mode 100644 index 0000000..f3107c3 --- /dev/null +++ b/src/main/java/com/magmaguy/resourcepackmanager/itemsadder/ItemsAdderCommand.java @@ -0,0 +1,138 @@ +package com.magmaguy.resourcepackmanager.itemsadder; + +import com.magmaguy.magmacore.command.AdvancedCommand; +import com.magmaguy.magmacore.command.CommandData; +import com.magmaguy.magmacore.command.arguments.ListStringCommandArgument; +import com.magmaguy.magmacore.util.Logger; +import com.magmaguy.resourcepackmanager.ResourcePackManager; +import com.magmaguy.resourcepackmanager.commands.ReloadCommand; +import org.bukkit.Bukkit; +import org.bukkit.command.CommandSender; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Player; +import org.bukkit.scheduler.BukkitRunnable; + +import java.io.File; +import java.util.List; + +/** + * Command to handle ItemsAdder configuration and warning dismissal. + */ +public class ItemsAdderCommand extends AdvancedCommand { + + public ItemsAdderCommand() { + super(List.of("itemsadder")); + setDescription("Configure ItemsAdder integration"); + addArgument("action", new ListStringCommandArgument(List.of("configure", "dismiss"), "")); + setPermission("resourcepackmanager.*"); + setUsage("/rspm itemsadder "); + } + + @Override + public void execute(CommandData commandData) { + CommandSender sender = commandData.getCommandSender(); + + String action = commandData.getStringArgument("action"); + if (action == null || action.isEmpty()) { + Logger.sendMessage(sender, "&cUsage: /rspm itemsadder "); + return; + } + + switch (action.toLowerCase()) { + case "configure": + handleConfigure(sender); + break; + case "dismiss": + handleDismiss(sender); + break; + default: + Logger.sendMessage(sender, "&cUnknown action. Use: /rspm itemsadder "); + } + } + + /** + * Handle the configure action - modifies ItemsAdder config and reloads plugins. + */ + private void handleConfigure(CommandSender sender) { + if (!ItemsAdderDetector.isItemsAdderInstalled()) { + Logger.sendMessage(sender, "&cItemsAdder is not installed!"); + return; + } + + if (ItemsAdderDetector.isItemsAdderHosting()) { + Logger.sendMessage(sender, "&eItemsAdder is already configured to host its own resource pack."); + Logger.sendMessage(sender, "&7If you want ResourcePackManager to host instead, please manually disable ItemsAdder's hosting."); + return; + } + + File configFile = ItemsAdderDetector.getItemsAdderConfigFile(); + if (configFile == null || !configFile.exists()) { + Logger.sendMessage(sender, "&cCould not find ItemsAdder config.yml!"); + return; + } + + try { + YamlConfiguration config = YamlConfiguration.loadConfiguration(configFile); + + // Set no-host enabled + config.set("resource-pack.hosting.no-host.enabled", true); + + // Disable all protections + config.set("resource-pack.zip.protect-file-from-unzip.protection_1", false); + config.set("resource-pack.zip.protect-file-from-unzip.protection_2", false); + config.set("resource-pack.zip.protect-file-from-unzip.protection_3", false); + + // Save the config + config.save(configFile); + + Logger.sendMessage(sender, "&aItemsAdder configuration updated successfully!"); + Logger.sendMessage(sender, "&7- Enabled no-host mode"); + Logger.sendMessage(sender, "&7- Disabled file protections"); + Logger.sendMessage(sender, ""); + Logger.sendMessage(sender, "&eReloading ItemsAdder..."); + + // Reload ItemsAdder first, then RSPM + new BukkitRunnable() { + @Override + public void run() { + // Reload ItemsAdder + try { + Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "iazip"); + Logger.sendMessage(sender, "&aItemsAdder reloaded!"); + } catch (Exception e) { + Logger.sendMessage(sender, "&cFailed to reload ItemsAdder: " + e.getMessage()); + Logger.sendMessage(sender, "&7Try running /iazip manually."); + } + + // Schedule RSPM reload after ItemsAdder has time to regenerate + new BukkitRunnable() { + @Override + public void run() { + Logger.sendMessage(sender, "&eReloading ResourcePackManager..."); + ReloadCommand.reloadPlugin(sender); + Logger.sendMessage(sender, "&aConfiguration complete! ResourcePackManager is now hosting the merged resource pack."); + } + }.runTaskLater(ResourcePackManager.plugin, 100L); // 5 seconds to let IA regenerate + } + }.runTaskLater(ResourcePackManager.plugin, 20L); // 1 second delay + + } catch (Exception e) { + Logger.sendMessage(sender, "&cFailed to update ItemsAdder config: " + e.getMessage()); + e.printStackTrace(); + } + } + + /** + * Handle the dismiss action - permanently dismisses the warning for the player. + */ + private void handleDismiss(CommandSender sender) { + if (!(sender instanceof Player player)) { + Logger.sendMessage(sender, "&cThis command can only be used by players!"); + return; + } + + ItemsAdderDismissedConfig.setDismissed(player.getUniqueId(), true); + Logger.sendMessage(sender, "&aItemsAdder configuration warning has been dismissed permanently."); + Logger.sendMessage(sender, "&7You can run &e/rspm itemsadder configure &7at any time to set it up."); + } +} diff --git a/src/main/java/com/magmaguy/resourcepackmanager/itemsadder/ItemsAdderDetector.java b/src/main/java/com/magmaguy/resourcepackmanager/itemsadder/ItemsAdderDetector.java new file mode 100644 index 0000000..f4fd414 --- /dev/null +++ b/src/main/java/com/magmaguy/resourcepackmanager/itemsadder/ItemsAdderDetector.java @@ -0,0 +1,145 @@ +package com.magmaguy.resourcepackmanager.itemsadder; + +import com.magmaguy.magmacore.util.Logger; +import com.magmaguy.resourcepackmanager.ResourcePackManager; +import org.bukkit.Bukkit; +import org.bukkit.configuration.file.YamlConfiguration; + +import java.io.File; + +/** + * Detects ItemsAdder installation and checks its hosting configuration. + */ +public class ItemsAdderDetector { + + private ItemsAdderDetector() { + } + + /** + * Check if ItemsAdder plugin is installed and enabled. + * @return true if ItemsAdder is installed and enabled + */ + public static boolean isItemsAdderInstalled() { + return Bukkit.getPluginManager().isPluginEnabled("ItemsAdder"); + } + + /** + * Check if ItemsAdder is currently configured to host its own resource pack. + * This checks all built-in hosting methods: self-host, external-host, and lobfile. + * @return true if ItemsAdder is hosting via any method + */ + public static boolean isItemsAdderHosting() { + if (!isItemsAdderInstalled()) return false; + + File configFile = getItemsAdderConfigFile(); + if (configFile == null || !configFile.exists()) { + Logger.warn("Could not find ItemsAdder config.yml"); + return false; + } + + try { + YamlConfiguration config = YamlConfiguration.loadConfiguration(configFile); + + // Check self-host + boolean selfHostEnabled = config.getBoolean("resource-pack.hosting.self-host.enabled", false); + if (selfHostEnabled) { + Logger.info("ItemsAdder is configured with self-host enabled"); + return true; + } + + // Check external-host + String externalHostUrl = config.getString("resource-pack.hosting.external-host.url", ""); + if (externalHostUrl != null && !externalHostUrl.isEmpty() && !externalHostUrl.equals("http://example.com/resourcepack.zip")) { + Logger.info("ItemsAdder is configured with external-host URL: " + externalHostUrl); + return true; + } + + // Check lobfile hosting + boolean lobfileEnabled = config.getBoolean("resource-pack.hosting.lobfile.enabled", false); + if (lobfileEnabled) { + Logger.info("ItemsAdder is configured with lobfile hosting enabled"); + return true; + } + + // Check if no-host is enabled (this means ItemsAdder is NOT hosting) + boolean noHostEnabled = config.getBoolean("resource-pack.hosting.no-host.enabled", false); + if (noHostEnabled) { + Logger.info("ItemsAdder has no-host enabled - not hosting"); + return false; + } + + // Default: if nothing is explicitly enabled, ItemsAdder is not hosting + return false; + + } catch (Exception e) { + Logger.warn("Failed to read ItemsAdder config: " + e.getMessage()); + return false; + } + } + + /** + * Check if ItemsAdder has encryption/protection enabled. + * @return true if any protection is enabled + */ + public static boolean hasProtectionEnabled() { + if (!isItemsAdderInstalled()) return false; + + File configFile = getItemsAdderConfigFile(); + if (configFile == null || !configFile.exists()) return false; + + try { + YamlConfiguration config = YamlConfiguration.loadConfiguration(configFile); + + boolean protection1 = config.getBoolean("resource-pack.zip.protect-file-from-unzip.protection_1", false); + boolean protection2 = config.getBoolean("resource-pack.zip.protect-file-from-unzip.protection_2", false); + boolean protection3 = config.getBoolean("resource-pack.zip.protect-file-from-unzip.protection_3", false); + + return protection1 || protection2 || protection3; + + } catch (Exception e) { + Logger.warn("Failed to check ItemsAdder protection settings: " + e.getMessage()); + return false; + } + } + + /** + * Get the ItemsAdder config.yml file. + * @return the config file, or null if not found + */ + public static File getItemsAdderConfigFile() { + File pluginsFolder = ResourcePackManager.plugin.getDataFolder().getParentFile(); + return new File(pluginsFolder, "ItemsAdder" + File.separatorChar + "config.yml"); + } + + /** + * Check if ItemsAdder needs configuration for ResourcePackManager to host. + * Returns true if ItemsAdder is installed but not set up for external hosting. + * @return true if ItemsAdder needs to be configured + */ + public static boolean needsConfiguration() { + if (!isItemsAdderInstalled()) return false; + + // If ItemsAdder is hosting via any method, don't warn + if (isItemsAdderHosting()) return false; + + // If no-host is already enabled, check if protections need to be disabled + File configFile = getItemsAdderConfigFile(); + if (configFile == null || !configFile.exists()) return false; + + try { + YamlConfiguration config = YamlConfiguration.loadConfiguration(configFile); + boolean noHostEnabled = config.getBoolean("resource-pack.hosting.no-host.enabled", false); + + // If no-host is enabled but protections are still on, needs configuration + if (noHostEnabled && hasProtectionEnabled()) { + return true; + } + + // If no-host is not enabled, needs configuration + return !noHostEnabled; + + } catch (Exception e) { + return false; + } + } +} diff --git a/src/main/java/com/magmaguy/resourcepackmanager/itemsadder/ItemsAdderDismissedConfig.java b/src/main/java/com/magmaguy/resourcepackmanager/itemsadder/ItemsAdderDismissedConfig.java new file mode 100644 index 0000000..3438fc8 --- /dev/null +++ b/src/main/java/com/magmaguy/resourcepackmanager/itemsadder/ItemsAdderDismissedConfig.java @@ -0,0 +1,71 @@ +package com.magmaguy.resourcepackmanager.itemsadder; + +import com.magmaguy.magmacore.config.ConfigurationFile; +import com.magmaguy.magmacore.util.Logger; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +/** + * Stores which players have dismissed the ItemsAdder configuration warning. + */ +public class ItemsAdderDismissedConfig extends ConfigurationFile { + + private static ItemsAdderDismissedConfig instance = null; + + public ItemsAdderDismissedConfig() { + super("itemsadder_dismissed.yml"); + instance = this; + } + + /** + * Check if a player has dismissed the warning. + * @param playerUUID the player's UUID + * @return true if the player has dismissed the warning + */ + public static boolean hasDismissed(UUID playerUUID) { + if (instance == null) return false; + + List dismissed = instance.getFileConfiguration().getStringList("dismissed"); + return dismissed.contains(playerUUID.toString()); + } + + /** + * Set whether a player has dismissed the warning. + * @param playerUUID the player's UUID + * @param dismissed true to dismiss, false to un-dismiss + */ + public static void setDismissed(UUID playerUUID, boolean dismissed) { + if (instance == null) return; + + List dismissedList = new ArrayList<>(instance.getFileConfiguration().getStringList("dismissed")); + + if (dismissed && !dismissedList.contains(playerUUID.toString())) { + dismissedList.add(playerUUID.toString()); + } else if (!dismissed) { + dismissedList.remove(playerUUID.toString()); + } + + instance.getFileConfiguration().set("dismissed", dismissedList); + try { + instance.getFileConfiguration().save(instance.file); + } catch (Exception e) { + Logger.warn("Failed to save ItemsAdder dismissed config!"); + e.printStackTrace(); + } + } + + @Override + public void initializeValues() { + // Ensure the dismissed list exists + if (!getFileConfiguration().contains("dismissed")) { + getFileConfiguration().set("dismissed", new ArrayList()); + try { + getFileConfiguration().save(file); + } catch (Exception e) { + Logger.warn("Failed to initialize ItemsAdder dismissed config!"); + } + } + } +} diff --git a/src/main/java/com/magmaguy/resourcepackmanager/itemsadder/ItemsAdderWarningListener.java b/src/main/java/com/magmaguy/resourcepackmanager/itemsadder/ItemsAdderWarningListener.java new file mode 100644 index 0000000..8ec6fad --- /dev/null +++ b/src/main/java/com/magmaguy/resourcepackmanager/itemsadder/ItemsAdderWarningListener.java @@ -0,0 +1,90 @@ +package com.magmaguy.resourcepackmanager.itemsadder; + +import com.magmaguy.magmacore.util.ChatColorConverter; +import com.magmaguy.resourcepackmanager.ResourcePackManager; +import net.md_5.bungee.api.chat.ClickEvent; +import net.md_5.bungee.api.chat.ComponentBuilder; +import net.md_5.bungee.api.chat.HoverEvent; +import net.md_5.bungee.api.chat.TextComponent; +import net.md_5.bungee.api.chat.hover.content.Text; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.scheduler.BukkitRunnable; + +/** + * Listens for OP player joins and warns them if ItemsAdder needs configuration. + */ +public class ItemsAdderWarningListener implements Listener { + + @EventHandler + public void onPlayerJoin(PlayerJoinEvent event) { + Player player = event.getPlayer(); + + // Only warn OP players + if (!player.isOp()) return; + + // Check if this player has dismissed the warning + if (ItemsAdderDismissedConfig.hasDismissed(player.getUniqueId())) return; + + // Check if ItemsAdder needs configuration + if (!ItemsAdderDetector.needsConfiguration()) return; + + // Delay the warning slightly to let the player fully join + new BukkitRunnable() { + @Override + public void run() { + if (!player.isOnline()) return; + + sendWarning(player); + } + }.runTaskLater(ResourcePackManager.plugin, 60L); // 3 seconds after join + } + + /** + * Send the warning title and chat message to the player. + */ + private void sendWarning(Player player) { + // Send title/subtitle + player.sendTitle( + ChatColorConverter.convert("&c&lItemsAdder Detected"), + ChatColorConverter.convert("&eResource pack not configured - check chat!"), + 10, 70, 20 + ); + + // Send chat message with explanation + player.sendMessage(""); + player.sendMessage(ChatColorConverter.convert("&8&m----------------------------------------")); + player.sendMessage(ChatColorConverter.convert("&c&lItemsAdder Configuration Warning")); + player.sendMessage(ChatColorConverter.convert("&8&m----------------------------------------")); + player.sendMessage(""); + player.sendMessage(ChatColorConverter.convert("&eItemsAdder has been detected but is not currently")); + player.sendMessage(ChatColorConverter.convert("&econfigured to let ResourcePackManager host the resource pack.")); + player.sendMessage(""); + player.sendMessage(ChatColorConverter.convert("&7For ResourcePackManager to merge and host the resource pack,")); + player.sendMessage(ChatColorConverter.convert("&7ItemsAdder needs to have hosting disabled and protections off.")); + player.sendMessage(""); + + // Create clickable buttons + // Option 1: Configure automatically + TextComponent configureButton = new TextComponent(ChatColorConverter.convert("&a&l[Configure Automatically]")); + configureButton.setClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/rspm itemsadder configure")); + configureButton.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, + new Text(ChatColorConverter.convert("&aClick to automatically configure ItemsAdder\n&7This will:\n&7- Enable no-host mode\n&7- Disable file protections\n&7- Reload both plugins")))); + + // Option 2: Dismiss permanently + TextComponent dismissButton = new TextComponent(ChatColorConverter.convert("&c&l[Dismiss Permanently]")); + dismissButton.setClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/rspm itemsadder dismiss")); + dismissButton.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, + new Text(ChatColorConverter.convert("&cClick to dismiss this warning permanently\n&7You won't see this warning again")))); + + // Send the buttons + TextComponent space = new TextComponent(" "); + player.spigot().sendMessage(configureButton, space, dismissButton); + + player.sendMessage(""); + player.sendMessage(ChatColorConverter.convert("&8&m----------------------------------------")); + player.sendMessage(""); + } +} diff --git a/src/main/java/com/magmaguy/resourcepackmanager/mixer/Mix.java b/src/main/java/com/magmaguy/resourcepackmanager/mixer/Mix.java index 608d64e..364d11d 100644 --- a/src/main/java/com/magmaguy/resourcepackmanager/mixer/Mix.java +++ b/src/main/java/com/magmaguy/resourcepackmanager/mixer/Mix.java @@ -2,15 +2,16 @@ import com.google.gson.*; import com.google.gson.stream.JsonReader; -import com.magmaguy.resourcepackmanager.Logger; +import com.magmaguy.magmacore.util.Logger; +import com.magmaguy.magmacore.util.ZipFile; import com.magmaguy.resourcepackmanager.ResourcePackManager; -import com.magmaguy.resourcepackmanager.thirdparty.EliteMobs; -import com.magmaguy.resourcepackmanager.thirdparty.FreeMinecraftModels; -import com.magmaguy.resourcepackmanager.thirdparty.ModelEngine; +import com.magmaguy.resourcepackmanager.api.ResourcePackManagerAPI; +import com.magmaguy.resourcepackmanager.autohost.AutoHost; +import com.magmaguy.resourcepackmanager.config.DefaultConfig; import com.magmaguy.resourcepackmanager.thirdparty.ThirdPartyResourcePack; import com.magmaguy.resourcepackmanager.utils.SHA1Generator; -import com.magmaguy.resourcepackmanager.utils.ZipFile; import lombok.Getter; +import org.bukkit.scheduler.BukkitRunnable; import java.io.File; import java.io.FileReader; @@ -18,34 +19,54 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.attribute.BasicFileAttributeView; -import java.nio.file.attribute.BasicFileAttributes; -import java.nio.file.attribute.FileTime; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.TimeUnit; +import java.util.*; public class Mix { private static final String resourcePackName = "ResourcePackManager_RSP"; - private static List thirdPartyResourcePacks; + private static List resourcePacks; + private static List orderedResourcePacks; @Getter private static File finalResourcePack; @Getter private static String finalSHA1; + @Getter + private static byte[] finalSHA1Bytes; + private static File mixerFolder; + private static List collisionLog; private Mix() { } - public static void initialize() { + /** + * Mixes resource packs asynchronously. Use this from the main thread. + */ + public static void mixResourcePacksAsync() { + new BukkitRunnable() { + @Override + public void run() { + mixResourcePacks(); + } + }.runTaskAsynchronously(ResourcePackManager.plugin); + } + + /** + * Mixes resource packs synchronously. Only call this from an async context to avoid blocking the main thread. + */ + public static void mixResourcePacks() { + Logger.info("Starting resource pack mixing..."); + collisionLog = new ArrayList<>(); if (!initializeDefaultPluginFolders()) return; initializeThirdPartyResourcePacks(); - cloneToOutPutAndUnzip(); + cloneToOutputAndUnzip(); createOutputDefaultElements(); + writeCollisionLog(); + Logger.info("Resource pack mixing complete."); + AutoHost.initialize(); } private static boolean initializeDefaultPluginFolders() { try { - File mixerFolder = new File(ResourcePackManager.plugin.getDataFolder().getAbsolutePath() + File.separatorChar + "mixer"); + mixerFolder = new File(ResourcePackManager.plugin.getDataFolder().getAbsolutePath() + File.separatorChar + "mixer"); if (!mixerFolder.exists()) mixerFolder.mkdir(); File outputFolder = getOutputFolder(); @@ -58,48 +79,113 @@ private static boolean initializeDefaultPluginFolders() { } } + /** + * Builds the ordered list of resource packs to merge, sorted by configured priority. + * Packs are copied in this order during merging — higher priority packs go first, + * and their files are preserved when lower priority packs collide with them. + */ private static void initializeThirdPartyResourcePacks() { - ArrayList tempList = new ArrayList<>(); - EliteMobs eliteMobs = new EliteMobs(); - if (eliteMobs.isEnabled()) - tempList.add(eliteMobs); - FreeMinecraftModels freeMinecraftModels = new FreeMinecraftModels(); - if (freeMinecraftModels.isEnabled()) - tempList.add(freeMinecraftModels); - ModelEngine modelEngine = new ModelEngine(); - if (modelEngine.isEnabled()) - tempList.add(modelEngine); - //todo: add the rest - thirdPartyResourcePacks = new ArrayList<>(); - for (int i = 0; i < tempList.size(); i++) { - for (ThirdPartyResourcePack thirdPartyResourcePack : tempList) { - if (thirdPartyResourcePack.getPriority() == i) { - thirdPartyResourcePacks.add(thirdPartyResourcePack); - tempList.remove(thirdPartyResourcePack); - break; - } + orderedResourcePacks = new ArrayList<>(); + resourcePacks = new ArrayList<>(); + List priorityOrder = DefaultConfig.getPriorityOrder(); + + // Collect all enabled packs from both the static set and API registrations + List allPacks = new ArrayList<>(ThirdPartyResourcePack.thirdPartyResourcePacks); + allPacks.addAll(ResourcePackManagerAPI.thirdPartyResourcePackHashMap.values()); + + // Build a unified map of (file -> priority) for sorting + Map filePriorities = new HashMap<>(); + Set registeredFilenames = new HashSet<>(); + + for (ThirdPartyResourcePack pack : allPacks) { + if (!pack.isEnabled() || pack.getMixerResourcePack() == null) continue; + registeredFilenames.add(pack.getMixerResourcePack().getName()); + filePriorities.put(pack.getMixerResourcePack(), + pack.getPriority() >= 0 ? pack.getPriority() : Integer.MAX_VALUE); + } + + // Add custom zip files from the mixer folder (user-provided packs not tied to a plugin) + File[] mixerContents = mixerFolder.listFiles(); + if (mixerContents != null) { + for (File file : mixerContents) { + if (file.isDirectory() || !file.getName().endsWith(".zip")) continue; + if (registeredFilenames.contains(file.getName())) continue; + // Check priority list by both filename and name without .zip + int prio = priorityOrder.indexOf(file.getName()); + if (prio < 0) prio = priorityOrder.indexOf(file.getName().replace(".zip", "")); + filePriorities.put(file, prio >= 0 ? prio : Integer.MAX_VALUE); } } - thirdPartyResourcePacks.addAll(tempList); + + // Sort all packs by priority (lower index = higher priority = copied first = wins collisions) + filePriorities.entrySet().stream() + .sorted(Map.Entry.comparingByValue()) + .forEach(entry -> { + resourcePacks.add(entry.getKey()); + orderedResourcePacks.add(entry.getKey().getName().replace(".zip", "")); + }); } - private static void cloneToOutPutAndUnzip() { - thirdPartyResourcePacks.forEach(thirdPartyResourcePack -> { + private static void cloneToOutputAndUnzip() { + resourcePacks.forEach(resourcePack -> { try { - File file = new File(ResourcePackManager.plugin.getDataFolder().getAbsolutePath() + File.separatorChar + "output" + File.separatorChar + thirdPartyResourcePack.getMixerResourcePack().getName().replace(".zip", "")); - ZipFile.unzip(thirdPartyResourcePack.getMixerResourcePack(), file); - stripDirectoryMetadata(file); + if (resourcePack == null) { + Logger.warn("A resource pack was null by the time it was meant to be unzipped!"); + return; + } + File outputDir = new File(ResourcePackManager.plugin.getDataFolder().getAbsolutePath() + File.separatorChar + "output" + File.separatorChar + resourcePack.getName().replace(".zip", "")); + // Pre-create the output directory to ensure getCanonicalPath() works correctly in the unzip security check + if (!outputDir.exists()) outputDir.mkdirs(); + ZipFile.unzip(resourcePack, outputDir); } catch (Exception e) { - Logger.warn("Failed to extract file " + thirdPartyResourcePack.getMixerResourcePack().getAbsolutePath() + " ! The file might be encrypted."); - e.printStackTrace(); + if (resourcePack == null) + Logger.warn("Failed to extract resource pack! The file might be encrypted. This pack will be skipped."); + else { + Logger.warn("Failed to extract resource pack " + resourcePack.getName() + " - the file might be encrypted or the plugin distributes its own pack. This pack will be skipped."); + Logger.warn("Error details: " + e.getMessage()); + } } }); + + // Also copy any directories from mixer folder (from cluster processing) directly to output + copyClusterDirectoriesToOutput(); + } + + private static void copyClusterDirectoriesToOutput() { + if (mixerFolder == null || !mixerFolder.exists()) return; + File[] mixerContents = mixerFolder.listFiles(); + if (mixerContents == null) return; + + for (File file : mixerContents) { + // Only process directories (cluster content like 'assets') + if (!file.isDirectory()) continue; + // Skip the output folder if it somehow ends up here + if (file.getName().equals("output")) continue; + + // Copy the directory to a wrapper folder in output + // This ensures the structure is: output/cluster_assets/assets/... + File outputWrapper = new File(getOutputFolder().getPath() + File.separatorChar + "cluster_" + file.getName()); + if (!outputWrapper.exists()) outputWrapper.mkdir(); + + try { + recursivelyCopyDirectory(file, outputWrapper); + // Track this for the merge process + if (!orderedResourcePacks.contains("cluster_" + file.getName())) { + orderedResourcePacks.add("cluster_" + file.getName()); + } + } catch (Exception e) { + Logger.warn("Failed to copy cluster directory " + file.getName() + " to output folder"); + e.printStackTrace(); + } + } } private static void createOutputDefaultElements() { + //Clear old resource pack if (getOutputResourcePackFolder().exists()) { recursivelyDeleteDirectory(getOutputResourcePackFolder()); } + //Make sure new resource pack exists try { getOutputResourcePackFolder().mkdir(); } catch (Exception e) { @@ -107,26 +193,56 @@ private static void createOutputDefaultElements() { throw new RuntimeException(e); } - for (File file : getOutputFolder().listFiles()) { - if (file.getName().equals(resourcePackName)) continue; + List orderedFiles = new ArrayList<>(); + for (String filename : orderedResourcePacks) { + orderedFiles.add(new File(getOutputFolder().getPath() + File.separatorChar + filename)); + } + + for (File file : orderedFiles) { + if (file.getName().equals(resourcePackName + ".zip")) continue; + if (!file.exists()) { + // Pack likely failed to extract (possibly encrypted) - skip gracefully + continue; + } if (!file.isDirectory()) { if (file.getName().endsWith(".zip")) continue; Logger.warn("Somehow a non-folder file made its way to the output folder! This isn't good. File: " + file.getAbsolutePath()); continue; } - for (File subFile : file.listFiles()) { + File[] subFiles = file.listFiles(); + if (subFiles == null) continue; + for (File subFile : subFiles) { recursivelyCopyDirectory(subFile, getOutputResourcePackFolder()); } } + if (!ZipFile.zip(getOutputResourcePackFolder(), getOutputResourcePackFolder().getPath() + ".zip")) { Logger.warn("Failed to zip merged resource pack!"); return; } + + if (!DefaultConfig.getResourcePackRerouting().isEmpty() && !DefaultConfig.getResourcePackRerouting().isBlank()) { + try { + File rerouteFolder = new File(ResourcePackManager.plugin.getDataFolder().getParentFile().getAbsolutePath() + File.separatorChar + DefaultConfig.getResourcePackRerouting()); + if (!rerouteFolder.exists()) { + Logger.warn("Failed to reroute zipped file to " + rerouteFolder.getAbsolutePath() + " because that folder does not exist!"); + } else if (!rerouteFolder.isDirectory()) { + Logger.warn("Failed to reroute zipped file to " + rerouteFolder.getAbsolutePath() + " because that is a file and not a folder!"); + } else if (!ZipFile.zip(getOutputResourcePackFolder(), rerouteFolder.getPath() + File.separatorChar + resourcePackName + ".zip")) { + Logger.warn("Failed to zip merged resource pack into reroute directory!"); + return; + } + } catch (Exception e) { + Logger.warn("Failed to reroute zipped file to " + DefaultConfig.getResourcePackRerouting()); + } + } + for (File file : getOutputFolder().listFiles()) { if (file.getName().equals(resourcePackName + ".zip")) { finalResourcePack = file; try { finalSHA1 = SHA1Generator.sha1CodeString(finalResourcePack); + finalSHA1Bytes = SHA1Generator.sha1CodeByteArray(finalResourcePack); } catch (Exception e) { Logger.warn("Failed to get SHA1 from zipped resource pack!"); finalResourcePack = null; @@ -135,6 +251,27 @@ private static void createOutputDefaultElements() { } recursivelyDeleteDirectory(file); } + +// // ADD THE BEDROCK CONVERSION CALL HERE - AFTER finalResourcePack IS SET: +// generateBedrockResourcePack(); + } + + public static boolean loadExistingFinalResourcePack() { + if (finalResourcePack != null) return true; + File possiblePack = new File(getOutputFolder().getAbsolutePath() + File.separatorChar + resourcePackName + ".zip"); + if (!possiblePack.exists()) return false; + try { + finalResourcePack = possiblePack; + finalSHA1 = SHA1Generator.sha1CodeString(finalResourcePack); + finalSHA1Bytes = SHA1Generator.sha1CodeByteArray(finalResourcePack); + return true; + } catch (Exception e) { + Logger.warn("Failed to load existing merged resource pack from disk."); + finalResourcePack = null; + finalSHA1 = null; + finalSHA1Bytes = null; + return false; + } } private static File getOutputFolder() { @@ -145,7 +282,7 @@ private static File getOutputResourcePackFolder() { return new File(getOutputFolder().getAbsolutePath() + File.separatorChar + resourcePackName); } - private static void recursivelyDeleteDirectory(File directory) { + public static void recursivelyDeleteDirectory(File directory) { if (directory.isDirectory()) { for (File file : directory.listFiles()) { recursivelyDeleteDirectory(file); @@ -154,21 +291,21 @@ private static void recursivelyDeleteDirectory(File directory) { Files.delete(directory.toPath()); } catch (Exception e) { Logger.warn("Failed to delete directory " + directory.getPath()); -// e.printStackTrace(); } } else { try { Files.delete(directory.toPath()); } catch (IOException e) { Logger.warn("Failed to delete file " + directory.getPath()); -// e.printStackTrace(); } } } - private static void recursivelyCopyDirectory(File source, File target) { + public static void recursivelyCopyDirectory(File source, File target) { if (source.isDirectory()) { - target = new File(target.getAbsolutePath() + File.separatorChar + source.getName()); + String sourceName = source.getName(); + + target = new File(target.getAbsolutePath() + File.separatorChar + sourceName); target.mkdir(); for (File file : source.listFiles()) { recursivelyCopyDirectory(file, target); @@ -179,7 +316,9 @@ private static void recursivelyCopyDirectory(File source, File target) { resolveFileCollision(source, Path.of(target.getPath() + File.separatorChar + source.getName()).toFile()); return; } - Files.copy(source.toPath(), Path.of(target.getPath() + File.separatorChar + source.getName())); + Path targetPath = Path.of(target.getPath() + File.separatorChar + source.getName()); + targetPath.getParent().toFile().mkdirs(); + Files.copy(source.toPath(), targetPath); } catch (IOException e) { Logger.warn("Failed to copy file"); throw new RuntimeException(e); @@ -187,56 +326,118 @@ private static void recursivelyCopyDirectory(File source, File target) { } } - private static void resolveFileCollision(File sourceFile, File targetFile) throws IOException { + public static void resolveFileCollision(File sourceFile, File targetFile) throws IOException { + // pack.mcmeta needs overlay entries merged from all packs + if (targetFile.getName().equals("pack.mcmeta")) { + mergePackMcmeta(sourceFile, targetFile); + return; + } + if (!targetFile.getName().endsWith(".json")) { - //If the file isn't .json then it can't be merged, only replaced (such as with .png). - //No further action is needed here since files are transferred over in priority order - Logger.info("Hard collision for file " + targetFile.getPath() + " detected! Auto-resolved based on highest priority."); + // Non-JSON: higher priority pack already placed this file, keep it + logCollision("Kept (higher priority): " + targetFile.getPath()); return; } - FileReader sourceFileReader = new FileReader(sourceFile); - FileReader targetFileReader = new FileReader(targetFile); + if (!isMergeableJsonFile(targetFile)) { + // Non-mergeable JSON (models, blockstates, etc.): higher priority version takes precedence + logCollision("Kept (higher priority, non-mergeable JSON): " + targetFile.getPath()); + return; + } - JsonObject json1 = null; - // Read JSON files - try { - json1 = JsonParser.parseReader(sourceFileReader).getAsJsonObject(); + JsonObject json1 = readJsonFile(sourceFile); + JsonObject json2 = readJsonFile(targetFile); + + if (json1 == null && json2 == null) { + Logger.warn("Both JSON files unreadable during merge, skipping: " + targetFile.getPath()); + return; + } + if (json1 == null) return; + if (json2 == null) { + Files.copy(sourceFile.toPath(), targetFile.toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING); + logCollision("Replaced (unreadable target JSON): " + targetFile.getPath()); + return; + } + + // Route to format-specific merge where needed + JsonObject mergedJson; + if (isItemsFile(targetFile)) { + mergedJson = mergeItemsModels(json1, json2); + } else if (targetFile.getName().equals("sounds.json")) { + mergedJson = mergeSoundsJson(json1, json2); + } else { + mergedJson = mergeJsonObjects(json1, json2); + } + + // Post-process: sort overrides in legacy item model files by custom_model_data + if (isLegacyItemModel(targetFile) && mergedJson.has("overrides")) { + sortModelOverrides(mergedJson); + } + + try (FileWriter writer = new FileWriter(targetFile)) { + new Gson().toJson(mergedJson, writer); + } + + logCollision("Merged: " + targetFile.getPath()); + } + + private static JsonObject readJsonFile(File file) { + try (FileReader reader = new FileReader(file)) { + return JsonParser.parseReader(reader).getAsJsonObject(); } catch (Exception e) { - Logger.warn("Malformed JSON for " + sourceFile.getAbsolutePath() + " !"); - try { - JsonReader jsonReader = new JsonReader(sourceFileReader); + Logger.warn("Malformed JSON: " + file.getAbsolutePath()); + try (FileReader reader = new FileReader(file); + JsonReader jsonReader = new JsonReader(reader)) { jsonReader.setStrictness(Strictness.LENIENT); - json1 = JsonParser.parseReader(jsonReader).getAsJsonObject(); - Logger.info(JsonParser.parseReader(jsonReader).getAsString()); + return JsonParser.parseReader(jsonReader).getAsJsonObject(); } catch (Exception ex) { - Logger.warn("Your JSON " + sourceFile.getAbsolutePath() + " is so broken even lenient won't let me read it!"); + Logger.warn("Unreadable JSON: " + file.getAbsolutePath()); + return null; } } - JsonObject json2 = JsonParser.parseReader(targetFileReader).getAsJsonObject(); + } - sourceFileReader.close(); - targetFileReader.close(); + /** + * Checks if a JSON file is designed to be merged (content can be combined). + * Files like sounds.json, lang files, atlases, fonts, and vanilla item model overrides can be merged. + * Custom model files, blockstates, equipment layers, etc. have fixed-size arrays that break when concatenated. + */ + private static boolean isMergeableJsonFile(File file) { + String path = file.getPath().replace("\\", "/"); + String fileName = file.getName(); + + // sounds.json files should be merged + if (fileName.equals("sounds.json")) { + return true; + } - // Merge JSON objects - JsonObject mergedJson = mergeJsonObjects(json1, json2); + // Language files should be merged + if (path.contains("/lang/") || path.contains("/languages/")) { + return true; + } - FileWriter targetFileWriter = new FileWriter(targetFile); + // Vanilla item model overrides should be merged (for custom model data) + if (path.contains("/minecraft/models/item/")) { + return true; + } - // Write merged JSON to a file - try (FileWriter file = targetFileWriter) { - new Gson().toJson(mergedJson, file); + // Atlas files should be merged (sources array) + if (path.contains("/atlases/")) { + return true; } - targetFileWriter.close(); + // Font files should be merged (providers array) + if (path.contains("/font/")) { + return true; + } - Logger.info("File " + targetFile.getName() + " successfully auto-merged!"); - } + // 1.21.4+ item model definitions should be merged (range_dispatch entries, select cases) + if (path.contains("/items/")) { + return true; + } - private static void stripDirectoryMetadata(File file) throws IOException { - if (!file.isDirectory()) return; - for (File listFile : file.listFiles()) - stripDirectoryMetadata(listFile); + // All other JSON files (custom models, blockstates, equipment layers, etc.) should not be merged + return false; } public static JsonObject mergeJsonObjects(JsonObject json1, JsonObject json2) { @@ -280,4 +481,352 @@ private static JsonArray mergeJsonArrays(JsonArray array1, JsonArray array2) { return mergedArray; } + + /** + * Writes the collision log to a file in the plugin's config folder. + * Only keeps the latest log, no history. + */ + private static void writeCollisionLog() { + if (collisionLog == null || collisionLog.isEmpty()) return; + + File logFile = new File(ResourcePackManager.plugin.getDataFolder().getAbsolutePath() + File.separatorChar + "collision_log.txt"); + try (FileWriter writer = new FileWriter(logFile, false)) { + writer.write("Resource Pack Collision Log\n"); + writer.write("Generated: " + java.time.LocalDateTime.now() + "\n"); + writer.write("================================================\n\n"); + for (String entry : collisionLog) { + writer.write(entry + "\n"); + } + } catch (IOException e) { + Logger.warn("Failed to write collision log file."); + } + } + + private static void logCollision(String message) { + if (collisionLog != null) { + collisionLog.add(message); + } + } + + private static void mergePackMcmeta(File sourceFile, File targetFile) throws IOException { + JsonObject source = readJsonFile(sourceFile); + JsonObject target = readJsonFile(targetFile); + + if (source == null) return; + if (target == null) { + Files.copy(sourceFile.toPath(), targetFile.toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING); + return; + } + + // Take the highest pack_format + if (source.has("pack") && target.has("pack")) { + JsonObject sourcePack = source.getAsJsonObject("pack"); + JsonObject targetPack = target.getAsJsonObject("pack"); + if (sourcePack.has("pack_format") && targetPack.has("pack_format")) { + int sourceFormat = sourcePack.get("pack_format").getAsInt(); + int targetFormat = targetPack.get("pack_format").getAsInt(); + targetPack.addProperty("pack_format", Math.max(sourceFormat, targetFormat)); + } + } + + // Merge supported_formats to widest range + if (source.has("supported_formats") && target.has("supported_formats")) { + JsonArray sourceFormats = source.getAsJsonArray("supported_formats"); + JsonArray targetFormats = target.getAsJsonArray("supported_formats"); + if (sourceFormats.size() >= 2 && targetFormats.size() >= 2) { + int min = Math.min(sourceFormats.get(0).getAsInt(), targetFormats.get(0).getAsInt()); + int max = Math.max(sourceFormats.get(1).getAsInt(), targetFormats.get(1).getAsInt()); + JsonArray merged = new JsonArray(); + merged.add(min); + merged.add(max); + target.add("supported_formats", merged); + } + } else if (source.has("supported_formats") && !target.has("supported_formats")) { + target.add("supported_formats", source.get("supported_formats")); + } + + // Merge overlay entries from both packs + JsonArray mergedEntries = new JsonArray(); + + if (target.has("overlays")) { + JsonObject targetOverlays = target.getAsJsonObject("overlays"); + if (targetOverlays.has("entries")) { + mergedEntries.addAll(targetOverlays.getAsJsonArray("entries")); + } + } + if (source.has("overlays")) { + JsonObject sourceOverlays = source.getAsJsonObject("overlays"); + if (sourceOverlays.has("entries")) { + Set existingDirs = new HashSet<>(); + for (JsonElement e : mergedEntries) { + if (e.isJsonObject() && e.getAsJsonObject().has("directory")) { + existingDirs.add(e.getAsJsonObject().get("directory").getAsString()); + } + } + for (JsonElement e : sourceOverlays.getAsJsonArray("entries")) { + if (e.isJsonObject()) { + String dir = e.getAsJsonObject().has("directory") + ? e.getAsJsonObject().get("directory").getAsString() : ""; + if (!existingDirs.contains(dir)) { + mergedEntries.add(e); + } + } + } + } + } + + if (mergedEntries.size() > 0) { + // Defensive normalization: ensure overlay entries have min_format/max_format fields. + // Starting with resource pack format 65 (Minecraft 1.21.9+), overlay entries MUST include + // min_format and max_format as separate fields — the old "formats" field alone is no longer + // sufficient. If an overlay's format range covers 65+, the client rejects entries missing + // these fields with: "declares support for version newer than 64, but is missing mandatory + // fields min_format and max_format". + // This is NOT an RSPM bug — the source packs (e.g. ModelEngine) are generating overlay entries + // without these fields. Ideally those packs should fix their own pack.mcmeta output. + // We patch it here because RSPM gets the bug reports when the merged pack fails to load. + normalizeOverlayEntries(mergedEntries); + + JsonObject overlays = new JsonObject(); + overlays.add("entries", mergedEntries); + target.add("overlays", overlays); + } + + // Preserve any non-standard top-level keys from source (e.g. "sodium" with ignored_shaders) + for (String key : source.keySet()) { + if (!target.has(key)) { + target.add(key, source.get(key)); + } + } + + try (FileWriter writer = new FileWriter(targetFile)) { + new Gson().toJson(target, writer); + } + + logCollision("Merged pack.mcmeta: " + targetFile.getPath()); + } + + /** + * Patches overlay entries that are missing min_format/max_format fields. + * See comment at call site for full rationale — this works around third-party packs + * that haven't updated their pack.mcmeta to the 1.21.9+ overlay format. + */ + private static void normalizeOverlayEntries(JsonArray entries) { + for (JsonElement element : entries) { + if (!element.isJsonObject()) continue; + JsonObject entry = element.getAsJsonObject(); + if (entry.has("min_format") && entry.has("max_format")) continue; + if (!entry.has("formats")) continue; + + int min, max; + JsonElement formats = entry.get("formats"); + if (formats.isJsonArray()) { + JsonArray arr = formats.getAsJsonArray(); + if (arr.size() < 2) continue; + min = arr.get(0).getAsInt(); + max = arr.get(1).getAsInt(); + } else if (formats.isJsonObject()) { + JsonObject obj = formats.getAsJsonObject(); + if (!obj.has("min_inclusive") || !obj.has("max_inclusive")) continue; + min = obj.get("min_inclusive").getAsInt(); + max = obj.get("max_inclusive").getAsInt(); + } else if (formats.isJsonPrimitive()) { + min = max = formats.getAsInt(); + } else { + continue; + } + + if (!entry.has("min_format")) entry.addProperty("min_format", min); + if (!entry.has("max_format")) entry.addProperty("max_format", max); + } + } + + private static boolean isLegacyItemModel(File file) { + return file.getPath().replace("\\", "/").contains("/minecraft/models/item/"); + } + + private static boolean isItemsFile(File file) { + String path = file.getPath().replace("\\", "/"); + return path.contains("/items/") && !path.contains("/models/item/"); + } + + private static void sortModelOverrides(JsonObject modelJson) { + JsonArray overrides = modelJson.getAsJsonArray("overrides"); + if (overrides == null || overrides.size() <= 1) return; + + List sorted = new ArrayList<>(); + for (JsonElement e : overrides) sorted.add(e); + + sorted.sort((a, b) -> { + int cmdA = getCustomModelData(a); + int cmdB = getCustomModelData(b); + return Integer.compare(cmdA, cmdB); + }); + + JsonArray sortedArray = new JsonArray(); + for (JsonElement e : sorted) sortedArray.add(e); + modelJson.add("overrides", sortedArray); + } + + private static int getCustomModelData(JsonElement override) { + try { + return override.getAsJsonObject() + .getAsJsonObject("predicate") + .get("custom_model_data").getAsInt(); + } catch (Exception e) { + return Integer.MAX_VALUE; + } + } + + private static JsonObject mergeItemsModels(JsonObject source, JsonObject target) { + if (!source.has("model") || !target.has("model")) { + return mergeJsonObjects(source, target); + } + + JsonObject sourceModel = source.getAsJsonObject("model"); + JsonObject targetModel = target.getAsJsonObject("model"); + + String sourceType = sourceModel.has("type") ? sourceModel.get("type").getAsString().replace("minecraft:", "") : ""; + String targetType = targetModel.has("type") ? targetModel.get("type").getAsString().replace("minecraft:", "") : ""; + + if (sourceType.equals("range_dispatch") && targetType.equals("range_dispatch")) { + mergeRangeDispatchEntries(sourceModel, targetModel); + target.add("model", targetModel); + for (String key : source.keySet()) { + if (!key.equals("model") && !target.has(key)) { + target.add(key, source.get(key)); + } + } + return target; + } + + if (sourceType.equals("select") && targetType.equals("select")) { + String sourceProp = sourceModel.has("property") ? sourceModel.get("property").getAsString() : ""; + String targetProp = targetModel.has("property") ? targetModel.get("property").getAsString() : ""; + if (sourceProp.equals(targetProp)) { + mergeSelectCases(sourceModel, targetModel); + target.add("model", targetModel); + for (String key : source.keySet()) { + if (!key.equals("model") && !target.has(key)) { + target.add(key, source.get(key)); + } + } + return target; + } + } + + // Incompatible types: higher priority (target) wins + return mergeJsonObjects(source, target); + } + + private static void mergeRangeDispatchEntries(JsonObject sourceModel, JsonObject targetModel) { + JsonArray sourceEntries = sourceModel.has("entries") ? sourceModel.getAsJsonArray("entries") : new JsonArray(); + JsonArray targetEntries = targetModel.has("entries") ? targetModel.getAsJsonArray("entries") : new JsonArray(); + + // Collect all entries, target (higher priority) wins on threshold conflicts + Map entryMap = new LinkedHashMap<>(); + for (JsonElement e : sourceEntries) { + double threshold = e.getAsJsonObject().has("threshold") + ? e.getAsJsonObject().get("threshold").getAsDouble() : 0; + entryMap.put(threshold, e); + } + for (JsonElement e : targetEntries) { + double threshold = e.getAsJsonObject().has("threshold") + ? e.getAsJsonObject().get("threshold").getAsDouble() : 0; + entryMap.put(threshold, e); + } + + List> sorted = new ArrayList<>(entryMap.entrySet()); + sorted.sort(Comparator.comparingDouble(Map.Entry::getKey)); + + JsonArray merged = new JsonArray(); + for (Map.Entry entry : sorted) { + merged.add(entry.getValue()); + } + + targetModel.add("entries", merged); + } + + private static void mergeSelectCases(JsonObject sourceModel, JsonObject targetModel) { + JsonArray sourceCases = sourceModel.has("cases") ? sourceModel.getAsJsonArray("cases") : new JsonArray(); + JsonArray targetCases = targetModel.has("cases") ? targetModel.getAsJsonArray("cases") : new JsonArray(); + + Map caseMap = new LinkedHashMap<>(); + for (JsonElement e : sourceCases) { + String when = e.getAsJsonObject().has("when") + ? e.getAsJsonObject().get("when").getAsString() : ""; + caseMap.put(when, e); + } + for (JsonElement e : targetCases) { + String when = e.getAsJsonObject().has("when") + ? e.getAsJsonObject().get("when").getAsString() : ""; + caseMap.put(when, e); + } + + JsonArray merged = new JsonArray(); + for (JsonElement e : caseMap.values()) { + merged.add(e); + } + + targetModel.add("cases", merged); + } + + private static JsonObject mergeSoundsJson(JsonObject source, JsonObject target) { + JsonObject merged = new JsonObject(); + + // Start with all source (lower priority) events + for (String key : source.keySet()) { + merged.add(key, source.get(key)); + } + + // Apply target (higher priority) events + for (String key : target.keySet()) { + JsonElement targetEvent = target.get(key); + if (!merged.has(key)) { + merged.add(key, targetEvent); + continue; + } + + if (targetEvent.isJsonObject()) { + JsonObject targetObj = targetEvent.getAsJsonObject(); + boolean replace = targetObj.has("replace") && targetObj.get("replace").getAsBoolean(); + + if (replace) { + merged.add(key, targetEvent); + } else { + JsonObject sourceObj = merged.get(key).isJsonObject() + ? merged.get(key).getAsJsonObject() : new JsonObject(); + JsonObject mergedEvent = new JsonObject(); + + JsonArray mergedSounds = new JsonArray(); + if (sourceObj.has("sounds")) { + mergedSounds.addAll(sourceObj.getAsJsonArray("sounds")); + } + if (targetObj.has("sounds")) { + mergedSounds.addAll(targetObj.getAsJsonArray("sounds")); + } + mergedEvent.add("sounds", mergedSounds); + + for (String prop : sourceObj.keySet()) { + if (!prop.equals("sounds") && !prop.equals("replace")) { + mergedEvent.add(prop, sourceObj.get(prop)); + } + } + for (String prop : targetObj.keySet()) { + if (!prop.equals("sounds") && !prop.equals("replace")) { + mergedEvent.add(prop, targetObj.get(prop)); + } + } + + merged.add(key, mergedEvent); + } + } else { + merged.add(key, targetEvent); + } + } + + return merged; + } + } + diff --git a/src/main/java/com/magmaguy/resourcepackmanager/playermanager/PlayerManager.java b/src/main/java/com/magmaguy/resourcepackmanager/playermanager/PlayerManager.java new file mode 100644 index 0000000..d75e125 --- /dev/null +++ b/src/main/java/com/magmaguy/resourcepackmanager/playermanager/PlayerManager.java @@ -0,0 +1,14 @@ +package com.magmaguy.resourcepackmanager.playermanager; + +import com.magmaguy.resourcepackmanager.autohost.AutoHost; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; + +public class PlayerManager implements Listener { + + @EventHandler + public void onPlayerJoin(PlayerJoinEvent event) { + AutoHost.sendResourcePack(event.getPlayer()); + } +} diff --git a/src/main/java/com/magmaguy/resourcepackmanager/thirdparty/EliteMobs.java b/src/main/java/com/magmaguy/resourcepackmanager/thirdparty/EliteMobs.java deleted file mode 100644 index 6a69bb7..0000000 --- a/src/main/java/com/magmaguy/resourcepackmanager/thirdparty/EliteMobs.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.magmaguy.resourcepackmanager.thirdparty; - -import lombok.Getter; - -import java.io.File; - -public class EliteMobs extends ThirdPartyResourcePack { - public EliteMobs() { - super("EliteMobs", - "EliteMobs" + File.separatorChar + "exports" + File.separatorChar + "elitemobs_resource_pack.zip", - false, - false, - "elitemobs reload"); - } -} diff --git a/src/main/java/com/magmaguy/resourcepackmanager/thirdparty/FreeMinecraftModels.java b/src/main/java/com/magmaguy/resourcepackmanager/thirdparty/FreeMinecraftModels.java deleted file mode 100644 index 7697c26..0000000 --- a/src/main/java/com/magmaguy/resourcepackmanager/thirdparty/FreeMinecraftModels.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.magmaguy.resourcepackmanager.thirdparty; - -import java.io.File; - -public class FreeMinecraftModels extends ThirdPartyResourcePack { - public FreeMinecraftModels() { - super("FreeMinecraftModels", - "FreeMinecraftModels" + File.separatorChar + "output" + File.separatorChar + "FreeMinecraftModels.zip", - false, - false, - "freeminecraftmodels reload"); - } -} diff --git a/src/main/java/com/magmaguy/resourcepackmanager/thirdparty/GeneratorInterface.java b/src/main/java/com/magmaguy/resourcepackmanager/thirdparty/GeneratorInterface.java deleted file mode 100644 index 7f99791..0000000 --- a/src/main/java/com/magmaguy/resourcepackmanager/thirdparty/GeneratorInterface.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.magmaguy.resourcepackmanager.thirdparty; - -import java.nio.file.Path; - -public interface GeneratorInterface { - public void decrypt(); - public void unpublish(); - public void cloneResourcePackFile(); - public void reload(); -} diff --git a/src/main/java/com/magmaguy/resourcepackmanager/thirdparty/ItemsAdder.java b/src/main/java/com/magmaguy/resourcepackmanager/thirdparty/ItemsAdder.java deleted file mode 100644 index 3edad18..0000000 --- a/src/main/java/com/magmaguy/resourcepackmanager/thirdparty/ItemsAdder.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.magmaguy.resourcepackmanager.thirdparty; - -public class ItemsAdder { -} diff --git a/src/main/java/com/magmaguy/resourcepackmanager/thirdparty/ModelEngine.java b/src/main/java/com/magmaguy/resourcepackmanager/thirdparty/ModelEngine.java deleted file mode 100644 index e3125e9..0000000 --- a/src/main/java/com/magmaguy/resourcepackmanager/thirdparty/ModelEngine.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.magmaguy.resourcepackmanager.thirdparty; - -import java.io.File; - -public class ModelEngine extends ThirdPartyResourcePack { - public ModelEngine() { - super("ModelEngine", - "ModelEngine" + File.separatorChar + "resource pack.zip", - false, - false, - "meg reload"); - } -} \ No newline at end of file diff --git a/src/main/java/com/magmaguy/resourcepackmanager/thirdparty/Nova.java b/src/main/java/com/magmaguy/resourcepackmanager/thirdparty/Nova.java deleted file mode 100644 index 0494ff6..0000000 --- a/src/main/java/com/magmaguy/resourcepackmanager/thirdparty/Nova.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.magmaguy.resourcepackmanager.thirdparty; - -public class Nova { -} diff --git a/src/main/java/com/magmaguy/resourcepackmanager/thirdparty/Oraxen.java b/src/main/java/com/magmaguy/resourcepackmanager/thirdparty/Oraxen.java deleted file mode 100644 index 3d409ac..0000000 --- a/src/main/java/com/magmaguy/resourcepackmanager/thirdparty/Oraxen.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.magmaguy.resourcepackmanager.thirdparty; - -public class Oraxen { -} diff --git a/src/main/java/com/magmaguy/resourcepackmanager/thirdparty/ThirdPartyResourcePack.java b/src/main/java/com/magmaguy/resourcepackmanager/thirdparty/ThirdPartyResourcePack.java index 59e6c2f..65d5d39 100644 --- a/src/main/java/com/magmaguy/resourcepackmanager/thirdparty/ThirdPartyResourcePack.java +++ b/src/main/java/com/magmaguy/resourcepackmanager/thirdparty/ThirdPartyResourcePack.java @@ -1,53 +1,339 @@ package com.magmaguy.resourcepackmanager.thirdparty; -import com.magmaguy.resourcepackmanager.Logger; +import com.magmaguy.magmacore.util.Logger; +import com.magmaguy.magmacore.util.ZipFile; import com.magmaguy.resourcepackmanager.ResourcePackManager; +import com.magmaguy.resourcepackmanager.mixer.Mix; import com.magmaguy.resourcepackmanager.config.DefaultConfig; +import com.magmaguy.resourcepackmanager.config.compatibleplugins.CompatiblePluginConfigFields; import com.magmaguy.resourcepackmanager.utils.SHA1Generator; import lombok.Getter; +import lombok.Setter; +import org.apache.hc.client5.http.classic.methods.HttpGet; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; +import org.apache.hc.client5.http.impl.classic.HttpClients; +import org.apache.hc.core5.http.HttpEntity; import org.bukkit.Bukkit; +import org.bukkit.scheduler.BukkitRunnable; +import org.bukkit.scheduler.BukkitTask; +import java.io.BufferedInputStream; import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; +import java.util.HashSet; +import java.util.Objects; -public class ThirdPartyResourcePack implements GeneratorInterface { +public class ThirdPartyResourcePack { + public static HashSet thirdPartyResourcePacks = new HashSet<>(); + + @Getter + private final String pluginName; + @Getter + private final String mixerFilename; + private final String localPath; + private final String url; @Getter - private final File file; - private final boolean encrypts; - private final boolean distributes; - private final String reloadCommand; + private File file = null; + private boolean zips; + private boolean cluster; + private String reloadCommand; @Getter private boolean isEnabled; private String SHA1; - private boolean resourcePackUpdated = false; @Getter private File mixerResourcePack = null; @Getter private int priority = -1; + @Getter + private boolean done = false; + + private int ticksWithoutChange = 0; + private boolean consideredStable = false; + @Setter + private boolean stableResourcePackSent = false; + + public ThirdPartyResourcePack(String pluginName, String localPath, String url, boolean zips, boolean cluster, String reloadCommand) { + this.pluginName = pluginName; + this.mixerFilename = pluginName + "_resource_pack.zip"; + this.url = url; + this.localPath = localPath; - public ThirdPartyResourcePack(String pluginName, String path, boolean encrypts, boolean distributes, String reloadCommand) { isEnabled = Bukkit.getPluginManager().isPluginEnabled(pluginName); - if (isEnabled) - Logger.info("Initializing " + pluginName + "'s resource pack"); - this.file = new File(ResourcePackManager.plugin.getDataFolder().getParentFile().toPath().toString() + File.separatorChar + path); - if (!file.exists()) { - Logger.warn("Found " + pluginName + " but could not find resource pack at location " + file.getPath() + " ! ResourcePackManager will not be able to merge the resource pack from this plugin."); + if (!isEnabled) { + done = true; + return; + } + + if (localPath != null && !processLocal(localPath)) { + done = true; + return; + } else if (localPath == null && url == null) { + Logger.warn("Plugin " + pluginName + " has no resource pack path specified! ResourcePackManager will not be able to merge the resource pack from this plugin."); isEnabled = false; + done = true; + return; } - this.encrypts = encrypts; - this.distributes = distributes; + this.reloadCommand = reloadCommand; - if (isEnabled) SHA1 = getSHA1(file); - process(); + this.zips = zips; + this.cluster = cluster; + + // If this is a cluster, process each resource pack in the folder + if (cluster) { + processCluster(); + } else if (!zips) { + zipThirdPartyPack(); + } + + if (localPath != null && !cluster) SHA1 = getSHA1(file); + + // Check if source file matches existing mixer file - if so, no changes occurred and it's stable + if (!cluster && localPath != null) { + File existingMixerFile = getTarget().toFile(); + if (existingMixerFile.exists()) { + String existingMixerSHA1 = getSHA1(existingMixerFile); + if (Objects.equals(SHA1, existingMixerSHA1)) { + consideredStable = true; + } + } + } + + if (!cluster) process(); + if (DefaultConfig.getPriorityOrder().contains(pluginName)) priority = DefaultConfig.getPriorityOrder().indexOf(pluginName); + + thirdPartyResourcePacks.add(this); + done = true; + } + + private static BukkitTask resourcePackChangeWatcher = null; + private static boolean initialStartup = true; + + /** + * Checks whether a monitored plugin has finished its Magmacore initialization. + * Uses System properties published by each plugin's shaded Magmacore instance. + */ + private static boolean isPluginInitialized(String pluginName) { + String state = System.getProperty("magmacore.init." + pluginName); + // If no state is published, the plugin doesn't use Magmacore — treat as ready + if (state == null) return true; + return "INITIALIZED".equals(state); + } + + public static void startResourcePackChangeWatchdog() { + if (resourcePackChangeWatcher != null) { + resourcePackChangeWatcher.cancel(); + } + resourcePackChangeWatcher = new BukkitRunnable() { + private boolean allPluginsReady = false; + + @Override + public void run() { + // Phase 1: Wait for all monitored plugins to finish initializing + if (!allPluginsReady) { + for (ThirdPartyResourcePack thirdPartyResourcePack : thirdPartyResourcePacks) { + if (!thirdPartyResourcePack.isEnabled) continue; + if (!isPluginInitialized(thirdPartyResourcePack.pluginName)) { + return; // Still waiting — check again next tick + } + } + allPluginsReady = true; + Logger.info("All monitored plugins are initialized. Starting resource pack stability checks."); + } + + // Check if any monitored plugin has gone back to initializing (reload detected) + for (ThirdPartyResourcePack thirdPartyResourcePack : thirdPartyResourcePacks) { + if (!thirdPartyResourcePack.isEnabled) continue; + if (!isPluginInitialized(thirdPartyResourcePack.pluginName)) { + Logger.info("Plugin " + thirdPartyResourcePack.pluginName + " is reloading. Pausing resource pack processing."); + allPluginsReady = false; + // Reset stability for all packs since a reload may change them + for (ThirdPartyResourcePack pack : thirdPartyResourcePacks) { + pack.consideredStable = false; + pack.stableResourcePackSent = false; + pack.ticksWithoutChange = 0; + } + return; + } + } + + // Phase 2: SHA1 stability checks (same logic, but no arbitrary extra delay) + boolean readyToSend = true; + boolean stableAlreadySent = true; + for (ThirdPartyResourcePack thirdPartyResourcePack : thirdPartyResourcePacks) { + if (!thirdPartyResourcePack.isEnabled || thirdPartyResourcePack.file == null) continue; + // Cluster packs have a directory as their source file — can't hash, skip SHA1 check + if (thirdPartyResourcePack.cluster) continue; + if (!Objects.equals(thirdPartyResourcePack.getSHA1(thirdPartyResourcePack.file), thirdPartyResourcePack.SHA1)) { + thirdPartyResourcePack.ticksWithoutChange = 0; + thirdPartyResourcePack.SHA1 = thirdPartyResourcePack.getSHA1(thirdPartyResourcePack.file); + if (thirdPartyResourcePack.consideredStable) { + thirdPartyResourcePack.consideredStable = false; + thirdPartyResourcePack.stableResourcePackSent = false; + Logger.info("Resource pack for " + thirdPartyResourcePack.pluginName + " has changed, considering it unstable."); + } + } + if (!thirdPartyResourcePack.stableResourcePackSent) stableAlreadySent = false; + if (!thirdPartyResourcePack.consideredStable) readyToSend = false; + if (thirdPartyResourcePack.consideredStable) continue; + thirdPartyResourcePack.ticksWithoutChange++; + if (thirdPartyResourcePack.ticksWithoutChange == 3) { + thirdPartyResourcePack.consideredStable = true; + Logger.info("Resource pack for " + thirdPartyResourcePack.pluginName + " has not changed for 3 seconds, considering it stable."); + } + } + + if (!stableAlreadySent && readyToSend) { + if (!DefaultConfig.isAutoMixOnStartup() && initialStartup) { + Logger.info("Automatic resource pack mixing on startup is disabled. Waiting for resource pack changes or manual reload before mixing."); + tagAsResourcePackSent(); + initialStartup = false; + return; + } + + notifyResourcePackSending(); + tagAsResourcePackSent(); + initialStartup = false; + Logger.info("Sending resource pack now."); + Bukkit.getScheduler().runTaskAsynchronously(ResourcePackManager.plugin, Mix::mixResourcePacks); + } + } + }.runTaskTimerAsynchronously(ResourcePackManager.plugin, 20, 20); + + if (!DefaultConfig.isAutoMixOnStartup()) { + if (Mix.loadExistingFinalResourcePack()) { + Logger.info("autoMixOnStartup is disabled, reusing the last merged resource pack from disk."); + AutoHost.initialize(); + } else { + Logger.info("autoMixOnStartup is disabled and no existing merged resource pack was found. Waiting for changes."); + } + } + } + + public static void tagAsResourcePackSent(){ + for (ThirdPartyResourcePack thirdPartyResourcePack : thirdPartyResourcePacks) { + thirdPartyResourcePack.stableResourcePackSent = true; + } + } + + private static void notifyResourcePackSending() { + String message = "&eAll resource packs are stable. Mixing and sending now."; + Logger.info("All resource packs are stable. Mixing and sending now."); + // Notify all online OPs + for (org.bukkit.entity.Player player : Bukkit.getOnlinePlayers()) { + if (player.isOp()) { + player.sendMessage(com.magmaguy.magmacore.util.ChatColorConverter.convert(message)); + } + } + } + + public static void shutdown() { + thirdPartyResourcePacks.clear(); + if (resourcePackChangeWatcher != null) { + resourcePackChangeWatcher.cancel(); + resourcePackChangeWatcher = null; + } + } + + public static void initializeThirdPartyResourcePack(CompatiblePluginConfigFields compatiblePluginConfigFields) { + new ThirdPartyResourcePack( + compatiblePluginConfigFields.getPluginName(), + compatiblePluginConfigFields.getLocalPath(), + compatiblePluginConfigFields.getUrl(), + compatiblePluginConfigFields.isZips(), + compatiblePluginConfigFields.isCluster(), + compatiblePluginConfigFields.getReloadCommand()); + } + + private void zipThirdPartyPack() { + ZipFile.zip(file, getTarget().toString()); + file = new File(getTarget().toUri()); + mixerResourcePack = file; + } + + private void processCluster() { + if (!file.isDirectory()) { + Logger.warn("Cluster path for " + pluginName + " is not a directory: " + file.getPath()); + isEnabled = false; + return; + } + + File[] clusterContents = file.listFiles(); + if (clusterContents == null || clusterContents.length == 0) { + Logger.warn("Cluster directory for " + pluginName + " is empty: " + file.getPath()); + isEnabled = false; + return; + } + + File mixerDir = new File(ResourcePackManager.plugin.getDataFolder().toString() + File.separatorChar + "mixer"); + if (!mixerDir.exists()) mixerDir.mkdir(); + + // Merge all cluster sub-packs into a temporary directory + File clusterTemp = new File(mixerDir.getPath() + File.separatorChar + pluginName + "_cluster_temp"); + if (clusterTemp.exists()) Mix.recursivelyDeleteDirectory(clusterTemp); + clusterTemp.mkdir(); + + Logger.info("Processing cluster for " + pluginName + " with " + clusterContents.length + " resource packs"); + + for (File resourcePackFolder : clusterContents) { + if (!resourcePackFolder.isDirectory()) { + Logger.info("Skipping non-directory in cluster: " + resourcePackFolder.getName()); + continue; + } + + File[] resourcePackContents = resourcePackFolder.listFiles(); + if (resourcePackContents == null) continue; + + for (File contentFolder : resourcePackContents) { + if (!contentFolder.isDirectory()) continue; + + try { + Mix.recursivelyCopyDirectory(contentFolder, clusterTemp); + } catch (Exception e) { + Logger.warn("Failed to copy " + contentFolder.getPath() + " to cluster temp"); + e.printStackTrace(); + } + } + } + + // Zip the merged cluster content so it participates in priority ordering like any other pack + File targetZip = getTarget().toFile(); + if (ZipFile.zip(clusterTemp, targetZip.getAbsolutePath())) { + mixerResourcePack = targetZip; + Logger.info("Created merged cluster pack: " + targetZip.getAbsolutePath()); + } else { + Logger.warn("Failed to zip merged cluster for " + pluginName); + isEnabled = false; + } + + // Clean up temp directory + Mix.recursivelyDeleteDirectory(clusterTemp); + Logger.info("Finished processing cluster for " + pluginName); + } + + private boolean processLocal(String localPath) { + this.file = new File(ResourcePackManager.plugin.getDataFolder().getParentFile().toPath().toString() + File.separatorChar + localPath); + + if (!file.exists()) { + Logger.warn("Found " + pluginName + " but could not find resource pack at location " + file.getPath() + " ! ResourcePackManager will not be able to merge the resource pack from this plugin."); + isEnabled = false; + return false; + } + + return true; } public void process() { if (!isEnabled) return; - if (mixerCloneExists()) { + + //Check if a copy already exists in the mixer folder and if it is up-to-date + if (localPath != null && mixerCloneExists()) { if (getSHA1(new File(getTarget().toUri())).equals(SHA1)) { mixerResourcePack = getTarget().toFile(); return; @@ -56,8 +342,7 @@ public void process() { getTarget().toFile().delete(); } } - if (encrypts) decrypt(); - if (distributes) unpublish(); + cloneResourcePackFile(); } @@ -70,30 +355,82 @@ private String getSHA1(File file) { } } - @Override - public void decrypt() { - //Implementation depends on extended classes + public void cloneResourcePackFile() { + if (localPath != null) cloneLocalRSP(); + else cloneRemoteRSP(); } - @Override - public void unpublish() { - //Implementation depends on extended classes + private void cloneLocalRSP() { + if (!zips) return; + File mixerFolder = new File(ResourcePackManager.plugin.getDataFolder().toString(), "mixer"); + if (!mixerFolder.exists()) mixerFolder.mkdirs(); + attemptClone(1); } - @Override - public void cloneResourcePackFile() { + private void attemptClone(int attempt) { try { Logger.info("Cloning resource pack from " + file.toPath()); mixerResourcePack = Files.copy(Path.of(file.getAbsolutePath()), Path.of(getTarget().toAbsolutePath().toString()), StandardCopyOption.REPLACE_EXISTING).toFile(); + } catch (java.nio.file.FileSystemException e) { + if (attempt < 5) { + Logger.warn("Resource pack file is locked (attempt " + attempt + "/5), retrying in " + (attempt * 10) + " ticks..."); + Bukkit.getScheduler().runTaskLaterAsynchronously(ResourcePackManager.plugin, () -> attemptClone(attempt + 1), attempt * 10L); + } else { + Logger.warn("Failed to clone resource pack from " + file.getPath() + " after 5 attempts — file is still locked by another process."); + e.printStackTrace(); + } } catch (Exception e) { Logger.warn("Failed to clone resource pack from " + file.getPath() + " to the mixer folder!"); e.printStackTrace(); } + } + + public void cloneRemoteRSP() { + Logger.info("Getting resource pack from remote URL! This is not ideal but not optional for some plugins. URL: " + url); + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + HttpGet httpGet = new HttpGet(url); + try (CloseableHttpResponse response = httpClient.execute(httpGet)) { + int statusCode = response.getCode(); + Logger.info("Response status code: " + statusCode); + + if (statusCode == 200) { + HttpEntity responseEntity = response.getEntity(); + if (responseEntity != null) { + // Save the response as a zip file + File zipFile = getTarget().toFile(); + if (zipFile.exists()) { + Logger.info("Target file exists, deleting it: " + zipFile.getAbsolutePath()); + zipFile.delete(); + } + zipFile.createNewFile(); - resourcePackUpdated = true; + try (InputStream inStream = new BufferedInputStream(responseEntity.getContent()); + FileOutputStream outStream = new FileOutputStream(zipFile)) { + + byte[] buffer = new byte[4096]; + int bytesRead; + while ((bytesRead = inStream.read(buffer)) != -1) { + outStream.write(buffer, 0, bytesRead); + } + + Logger.info("Successfully downloaded the resource pack to " + zipFile.getAbsolutePath()); + } catch (Exception e) { + Logger.warn("Failed to write resource pack from remote!"); + } + } else { + Logger.warn("Response entity is null"); + } + } else { + Logger.warn("Unexpected response status: " + statusCode); + } + } catch (Exception e) { + Logger.warn("Failed to communicate with remote server when downloading resource pack for plugin " + pluginName + "!"); + } + } catch (Exception e) { + Logger.warn("Failed to connect to url " + url + " to download resource pack for plugin " + pluginName); + } } - @Override public void reload() { Bukkit.dispatchCommand(Bukkit.getConsoleSender(), reloadCommand); } @@ -103,6 +440,6 @@ private boolean mixerCloneExists() { } private Path getTarget() { - return Path.of(ResourcePackManager.plugin.getDataFolder().toString(), "mixer", file.getName()); + return Path.of(ResourcePackManager.plugin.getDataFolder().toString(), "mixer", mixerFilename); } } diff --git a/src/main/java/com/magmaguy/resourcepackmanager/utils/SHA1Generator.java b/src/main/java/com/magmaguy/resourcepackmanager/utils/SHA1Generator.java index c1d4edb..d6b21a0 100644 --- a/src/main/java/com/magmaguy/resourcepackmanager/utils/SHA1Generator.java +++ b/src/main/java/com/magmaguy/resourcepackmanager/utils/SHA1Generator.java @@ -1,8 +1,11 @@ package com.magmaguy.resourcepackmanager.utils; +import com.magmaguy.magmacore.util.Logger; + import java.io.File; import java.io.FileInputStream; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.security.DigestInputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; @@ -13,11 +16,30 @@ public static String sha1CodeString(File file) throws IOException, NoSuchAlgorit try (FileInputStream fileInputStream = new FileInputStream(file); DigestInputStream digestInputStream = new DigestInputStream(fileInputStream, MessageDigest.getInstance("SHA-1"))) { byte[] bytes = new byte[1024]; - MessageDigest digest = null; - //read all file content - while (digestInputStream.read(bytes) > 0) digest = digestInputStream.getMessageDigest(); - byte[] resultByteArry = digest.digest(); + while (digestInputStream.read(bytes) > 0) ; + byte[] resultByteArry = digestInputStream.getMessageDigest().digest(); + return bytesToHexString(resultByteArry); + } + } + + public static String sha1CodeString(String text) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-1"); + byte[] bytes = text.getBytes(StandardCharsets.UTF_8); + byte[] resultByteArry = digest.digest(bytes); return bytesToHexString(resultByteArry); + } catch (NoSuchAlgorithmException e) { + Logger.warn("Failed to find SHA-1 algorithm."); + } + return null; + } + + public static byte[] sha1CodeByteArray(File file) throws IOException, NoSuchAlgorithmException { + try (FileInputStream fileInputStream = new FileInputStream(file); + DigestInputStream digestInputStream = new DigestInputStream(fileInputStream, MessageDigest.getInstance("SHA-1"))) { + byte[] bytes = new byte[1024]; + while (digestInputStream.read(bytes) > 0) ; + return digestInputStream.getMessageDigest().digest(); } } diff --git a/src/main/java/com/magmaguy/resourcepackmanager/utils/ServerVersionHelper.java b/src/main/java/com/magmaguy/resourcepackmanager/utils/ServerVersionHelper.java new file mode 100644 index 0000000..c42a214 --- /dev/null +++ b/src/main/java/com/magmaguy/resourcepackmanager/utils/ServerVersionHelper.java @@ -0,0 +1,40 @@ +package com.magmaguy.resourcepackmanager.utils; + +import org.bukkit.Bukkit; + +public class ServerVersionHelper { + private static final int majorVersion; + private static final int minorVersion; + private static final boolean supportsMultipleResourcePacks; + + static { + String version = Bukkit.getBukkitVersion(); // e.g., "1.20.4-R0.1-SNAPSHOT" or "26.1-R0.1-SNAPSHOT" + String[] parts = version.split("-")[0].split("\\."); + + if (parts[0].equals("1")) { + // Legacy format: 1.MAJOR.MINOR + majorVersion = Integer.parseInt(parts[1]); + minorVersion = parts.length >= 3 ? Integer.parseInt(parts[2]) : 0; + } else { + // New year.drop format: MAJOR.MINOR (e.g. 26.1) + majorVersion = Integer.parseInt(parts[0]); + minorVersion = parts.length >= 2 ? Integer.parseInt(parts[1]) : 0; + } + + // addResourcePack was added in 1.20.3 (majorVersion=20, minorVersion>=3) + // All versions >= 26 support it + supportsMultipleResourcePacks = majorVersion > 20 || (majorVersion == 20 && minorVersion >= 3); + } + + public static boolean supportsMultipleResourcePacks() { + return supportsMultipleResourcePacks; + } + + public static int getMajorVersion() { + return majorVersion; + } + + public static int getMinorVersion() { + return minorVersion; + } +} diff --git a/src/main/java/com/magmaguy/resourcepackmanager/utils/ZipFile.java b/src/main/java/com/magmaguy/resourcepackmanager/utils/ZipFile.java deleted file mode 100644 index 757dee4..0000000 --- a/src/main/java/com/magmaguy/resourcepackmanager/utils/ZipFile.java +++ /dev/null @@ -1,155 +0,0 @@ -package com.magmaguy.resourcepackmanager.utils; - -import com.magmaguy.resourcepackmanager.Logger; - -import java.io.*; -import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; -import java.util.zip.ZipOutputStream; - -public class ZipFile { - private ZipFile() { - } - - public static boolean zip(File directory, String targetZipPath) { - if (!directory.exists()) { - Logger.warn("Failed to zip directory " + directory.getPath() + " because it does not exist!"); - return false; - } - - try { - ZipUtility.zip(directory, targetZipPath); - return true; - } catch (IOException e) { - e.printStackTrace(); - return false; - } - } - - public static File unzip(File zippedFile, File destinationUnzippedFile) throws IOException { - byte[] buffer = new byte[1024]; - ZipInputStream zipInputStream = new ZipInputStream(new FileInputStream(zippedFile)); - ZipEntry zipEntry = zipInputStream.getNextEntry(); - while (zipEntry != null) { - File newFile = newFile(destinationUnzippedFile, zipEntry); - if (zipEntry.isDirectory()) { - if (!newFile.isDirectory() && !newFile.mkdirs()) { - throw new IOException("Failed to create directory " + newFile); - } - } else { - // Fix for Windows-created archives - File parent = newFile.getParentFile(); - if (!parent.isDirectory() && !parent.mkdirs()) { - throw new IOException("Failed to create directory " + parent); - } - - // Write file content - FileOutputStream fileOutputStream = new FileOutputStream(newFile); - int len; - while ((len = zipInputStream.read(buffer)) > 0) { - fileOutputStream.write(buffer, 0, len); - } - fileOutputStream.close(); - } - newFile.setLastModified(zipEntry.getTime()); - zipEntry = zipInputStream.getNextEntry(); - } - zipInputStream.closeEntry(); - zipInputStream.close(); - return destinationUnzippedFile; - } - - private static File newFile(File destinationDir, ZipEntry zipEntry) throws IOException { - File destFile = new File(destinationDir, zipEntry.getName()); - - String destDirPath = destinationDir.getCanonicalPath(); - String destFilePath = destFile.getCanonicalPath(); - - if (!destFilePath.startsWith(destDirPath + File.separatorChar)) { - throw new IOException("Entry is outside of the target dir: " + zipEntry.getName()); - } - - return destFile; - } - - public static class ZipUtility { - /** - * A constant for buffer size used to read/write data - */ - private static final int BUFFER_SIZE = 4096; - - /** - * Compresses a list of files to a destination zip file - * - * @param file File to zip - * @param destZipFile The path of the destination zip file - * @throws FileNotFoundException - * @throws IOException - */ - public static void zip(File file, String destZipFile) throws FileNotFoundException, IOException { - FileOutputStream fileOutputStream = new FileOutputStream(destZipFile); - ZipOutputStream zos = new ZipOutputStream(fileOutputStream); - // This slight tweak avoids making the directory zipped be in the zipped file when what we are looking for is to - // zip the contents of the directory, outside of the directory itself - if (file.isDirectory()) { - for (File file1 : file.listFiles()) { - if (file1.isDirectory()) - zipDirectory(file1, file1.getName(), zos); - else - zipFile(file1, zos); - } - } else { - zipFile(file, zos); - } - zos.flush(); - zos.close(); - fileOutputStream.close(); - } - - /** - * Adds a directory to the current zip output stream - * - * @param folder the directory to be added - * @param parentFolder the path of parent directory - * @param zos the current zip output stream - * @throws FileNotFoundException - * @throws IOException - */ - private static void zipDirectory(File folder, String parentFolder, ZipOutputStream zos) throws FileNotFoundException, IOException { - for (File file : folder.listFiles()) { - if (file.isDirectory()) { - zipDirectory(file, parentFolder + "/" + file.getName(), zos); - continue; - } - ZipEntry zipEntry = new ZipEntry(parentFolder + "/" + file.getName()); - zippedySplit(zos, file, zipEntry); - } - } - - /** - * Adds a file to the current zip output stream - * - * @param file the file to be added - * @param zos the current zip output stream - * @throws FileNotFoundException - * @throws IOException - */ - private static void zipFile(File file, ZipOutputStream zos) throws FileNotFoundException, IOException { - ZipEntry zipEntry = new ZipEntry(file.getName()); - zippedySplit(zos, file, zipEntry); - } - - private static void zippedySplit(ZipOutputStream zos, File file, ZipEntry zipEntry) throws IOException { - zipEntry.setTime(0L); - zos.putNextEntry(zipEntry); - BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file)); - byte[] bytesIn = new byte[BUFFER_SIZE]; - int read; - while ((read = bis.read(bytesIn)) != -1) { - zos.write(bytesIn, 0, read); - } - zos.closeEntry(); - bis.close(); - } - } -} diff --git a/src/main/resources/ReadMe.md b/src/main/resources/ReadMe.md new file mode 100644 index 0000000..15939ec --- /dev/null +++ b/src/main/resources/ReadMe.md @@ -0,0 +1,74 @@ +### ResourcePackManager Data Policy and Compliance + +**ResourcePackManager**, developed by MagmaGuy for the Nightbreak game studio, includes an optional auto-host feature +that temporarily hosts resource pack data on a remote server. + +As of this writing, the hosted data is fully anonymous and serves the sole purpose of simplifying the distribution of +resource packs to clients of servers utilizing this service. Future versions of this document may be updated to reflect +any changes in data policy and other related matters. + +This system complies with: + +- **Directive 2000/31/EC** of the European Parliament and of the Council of 8 June 2000 +- **Regulation (EU) 2022/2065** of the European Parliament and of the Council of 19 October 2022 + +For data hosting transparency and compliance with these and other European norms, it is possible to obtain all data +related to a server via the `/resourcepackmanager data_compliance_request` command. This command packages a full copy of +all files and data associated with the requesting server. + +To request the takedown of your server's data, contact MagmaGuy at `magmaguy/at\nightbreak.io` (replace `/at\ ` with +`@`). However, due to the system's design, data is only retained for up to 24h after a server using ResourcePackManager +shuts down, making email requests largely unnecessary. Nonetheless, the option remains available to ensure full +compliance with European norms. + +### Data Handling by ResourcePackManager and Nightbreak Servers + +1. **Resource Pack Creation** + - ResourcePackManager aggregates resource packs on your server into a single zipped file containing all custom + content. + +2. **Initialization Request** + - An initialization request is sent to remote servers, creating a `.txt` file with a random UUID. This file can be + obtained via the `/resourcepackmanager data_compliance_request`. + +3. **SHA1 Request** + - ResourcePackManager transmits the SHA1 code of your resource pack to the remote server, which is saved in the + `.txt` file. + +4. **File Transmission** + - The zipped resource pack file is sent to the remote server, assigned the same UUID as the `.txt` file. This file + can be obtained through `/resourcepackmanager data_compliance_request` and verified to be identical to the + original in your output folder, as it is not modified by the Nightbreak servers. + +5. **"Still Alive" Ping** + - ResourcePackManager sends a "still alive" ping every 6 hours, transmitting the UUID to the server, which updates + the timestamp in the `.txt` file. + - If no "still alive" ping is received for over 24 hours, all data associated with that UUID (the `.txt` file + and the resource pack) is deleted from the Nightbreak servers. + +### Data Policy + +- **Pseudonymous Identification:** Nightbreak assigns a random UUID to your server's files each time the server reboots, + ensuring no IP address or identifiable information is stored unless users manually add such information to their + resource packs. +- **No Download Logging:** Nightbreak does not log any data related to download requests by Minecraft clients. +- **No Data Sales:** Data uploaded to Nightbreak is not, has never been, and will never be sold. +- **Compliance with Takedown Requests:** Nightbreak will comply with takedown requests from both server administrators + and law enforcement agencies. +- **Automatic Data Removal:** All data associated with your server is automatically removed 24 hours after your server + shuts down and is reuploaded on every restart for as long as ResourcePackManager is in use and using the auto-host + feature. + +### Terms of service + +As of writing this, the hosting service is provided for free for all users of ResourcePackManager. + +It is the user's responsibility to ensure that the data uploaded to servers is not illegal and complies with any Mojang +TOS as defined in their EULA. + +Abusing the service to host material other than resource packages may result in a permanent denial of service for the +offending IP. + +The service may, at any time, cease or be modified in such a way that makes old versions unable to connect to it. + +We reserve the right to unilaterally terminate this service at any time and for any reason. \ No newline at end of file diff --git a/src/main/resources/pack.mcmeta b/src/main/resources/pack.mcmeta new file mode 100644 index 0000000..31d8f2e --- /dev/null +++ b/src/main/resources/pack.mcmeta @@ -0,0 +1,6 @@ +{ + "pack": { + "pack_format": 34, + "description": "= ResourcePackManagerRSP =" + } +} \ No newline at end of file diff --git a/src/main/resources/pack.png b/src/main/resources/pack.png new file mode 100644 index 0000000..485d0af Binary files /dev/null and b/src/main/resources/pack.png differ diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index b02ba46..e12c4aa 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,17 +1,27 @@ name: ResourcePackManager main: com.magmaguy.resourcepackmanager.ResourcePackManager -version: 1.0.0-SNAPSHOT +version: 1.7.6 api-version: 1.14 +author: MagmaGuy softdepend: -- EliteMobs -- FreeMinecraftModels -- ModelEngine -- Nova -- Oraxen -- ItemsAdder + - EliteMobs + - FreeMinecraftModels + - ModelEngine + - Nova + - Oraxen + - ItemsAdder + - BackpackPlus + - BetterHUD + - InfiniteVehicles + - MegaBlockSurvivors + - MMOInventory + - Nexo + - ValhallaMMO + - vane-core + - RealisticSurvival commands: resourcepackmanager: aliases: - rspm - description: "Main command" \ No newline at end of file + description: "Main command"