diff --git a/README.md b/README.md index d793a0c..6e18b5f 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,12 @@ sha1Url: "https://cdn.example.com/server-pack.zip.sha1" # Seconds between Java SHA-1 and Bedrock pack checks. Minimum: 30. refreshInterval: 300 -# Optional local Bedrock Edition pack for GeyserMC players. Accepts .mcpack or .zip. +# Preferred: remote Bedrock pack URL for GeyserMC players. +# Must be a direct .mcpack/.zip download with Content-Type: application/zip +# and an exact Content-Length. +bedrockResourcePackUrl: "https://example.com/packs/server-pack.mcpack" + +# Optional local alternative; use only when bedrockResourcePackUrl is blank. # Relative paths are resolved from plugins/RemotePackSync/. bedrockResourcePack: "bedrock/server-pack.mcpack" ``` @@ -67,11 +72,12 @@ The SHA-1 response must contain a 40-character hexadecimal SHA-1. Both of these ### Bedrock Edition through GeyserMC -- Set `bedrockResourcePack` to a local `.mcpack` or `.zip` file. `.mcpack` is fully supported. -- The source pack is checked asynchronously at `refreshInterval`. When its SHA-256 changes, RemotePackSync creates a managed copy under `plugins/RemotePackSync/bedrock-managed/`. -- The managed copy receives a deterministic new header UUID derived from the updated source SHA-256. This makes Bedrock clients treat a changed pack as a new pack, while Geyser calculates and supplies the required final pack hash and size. -- The original configured pack is never modified. -- Every new Geyser Bedrock connection receives the latest successfully processed pack through Geyser's public `SessionLoadResourcePacksEvent` API. +- Preferred remote mode: set `bedrockResourcePackUrl` to a direct `.mcpack` or `.zip` download URL. RemotePackSync creates a Geyser `UrlPackCodec` and registers it through the public `SessionLoadResourcePacksEvent` API, so Geyser supplies the URL to connecting Bedrock clients. +- The remote server must return `Content-Type: application/zip` and an exact `Content-Length`. Redirects are allowed only when they ultimately resolve to a valid direct download. The URL should be stable and should expose a changed ETag or URL when the pack changes. +- Geyser validates/downloads remote URL packs during its lifecycle. Use `/remotepacksync reload` after changing the URL or after replacing a pack at the same URL; players already connected must reconnect. +- Local alternative: set `bedrockResourcePack` to a `.mcpack` or `.zip` file. The source is checked asynchronously at `refreshInterval`; changes receive a deterministic manifest UUID and are managed under `plugins/RemotePackSync/bedrock-managed/`. +- The original configured local pack is never modified. +- Every new Geyser Bedrock connection receives the latest successfully registered pack. - The pack must be a valid Bedrock pack containing `manifest.json` at the archive root. Geyser does **not** convert Java packs into Bedrock packs. - Bedrock's protocol only negotiates packs while a client connects. Players already connected when the file changes must reconnect to receive the new version; RemotePackSync cannot hot-push a replacement to an active Bedrock session. - If Geyser is absent, Java Edition resource-pack operation continues normally and Bedrock delivery stays disabled. @@ -83,7 +89,7 @@ The SHA-1 response must contain a 40-character hexadecimal SHA-1. Both of these ## Compatibility -RemotePackSync compiles against `org.spigotmc:spigot-api:26.2-R0.1-SNAPSHOT` and Geyser API `2.11.0-SNAPSHOT`, targeting Java 25 as required by Minecraft/Spigot 26.2. Java packs are sent through Bukkit's stable `Player#setResourcePack(String, byte[])` API; Bedrock packs use Geyser's public pack/session API. Geyser is a soft dependency, so Java-only servers remain supported. +RemotePackSync compiles against `org.spigotmc:spigot-api:26.2-R0.1-SNAPSHOT` and Geyser API `2.11.0-SNAPSHOT`, targeting Java 25 as required by Minecraft/Spigot 26.2. Java packs are sent through Bukkit's stable `Player#setResourcePack(String, byte[])` API; Bedrock packs use Geyser's public `UrlPackCodec`/pack-session API. Geyser is a soft dependency, so Java-only servers remain supported. ## License diff --git a/src/main/java/io/github/lf1/remotepacksync/BedrockPackManager.java b/src/main/java/io/github/lf1/remotepacksync/BedrockPackManager.java index bc87a20..9d49f6d 100644 --- a/src/main/java/io/github/lf1/remotepacksync/BedrockPackManager.java +++ b/src/main/java/io/github/lf1/remotepacksync/BedrockPackManager.java @@ -57,10 +57,18 @@ public final class BedrockPackManager { } subscribeToGeyser(); - refreshTask = Bukkit.getScheduler().runTaskTimerAsynchronously( - plugin, this::refreshPack, 0L, newSettings.refreshIntervalTicks()); - plugin.getLogger().info("Bedrock resource-pack refresh enabled every " - + (newSettings.refreshIntervalTicks() / 20L) + " seconds."); + if (newSettings.isRemote()) { + // Geyser's URL codec validates the remote download and obtains its metadata. + // Geyser itself downloads remote packs once during its lifecycle, so do not + // repeatedly recreate the URL codec on a timer. + refreshTask = Bukkit.getScheduler().runTaskAsynchronously(plugin, this::refreshPack); + plugin.getLogger().info("Bedrock remote resource-pack URL configured. Geyser will fetch it for new connections."); + } else { + refreshTask = Bukkit.getScheduler().runTaskTimerAsynchronously( + plugin, this::refreshPack, 0L, newSettings.refreshIntervalTicks()); + plugin.getLogger().info("Bedrock local resource-pack refresh enabled every " + + (newSettings.refreshIntervalTicks() / 20L) + " seconds."); + } } public void stop() { @@ -111,6 +119,17 @@ public final class BedrockPackManager { } try { + if (activeSettings.isRemote()) { + ResourcePack resourcePack = ResourcePack.create(PackCodec.url(activeSettings.remoteUrl())); + if (activeSettings != settings) { + return; + } + currentPack.set(new Snapshot(null, resourcePack)); + plugin.getLogger().info("Bedrock remote resource-pack URL validated and registered: " + + activeSettings.remoteUrl()); + return; + } + Path source = activeSettings.sourceFile(); if (!Files.isRegularFile(source)) { throw new IOException("Configured Bedrock pack does not exist or is not a regular file: " + source); diff --git a/src/main/java/io/github/lf1/remotepacksync/BedrockPackSettings.java b/src/main/java/io/github/lf1/remotepacksync/BedrockPackSettings.java index 27506f1..4c8f32b 100644 --- a/src/main/java/io/github/lf1/remotepacksync/BedrockPackSettings.java +++ b/src/main/java/io/github/lf1/remotepacksync/BedrockPackSettings.java @@ -6,30 +6,46 @@ import java.nio.file.Path; import java.util.Locale; import java.util.Optional; -/** Validated configuration for an optional local Bedrock resource pack. */ -public record BedrockPackSettings(Path sourceFile, long refreshIntervalTicks) { +/** Validated configuration for an optional local or remote Bedrock resource pack. */ +public record BedrockPackSettings(Path sourceFile, String remoteUrl, long refreshIntervalTicks) { private static final long MINIMUM_REFRESH_SECONDS = 30L; public static Optional from(FileConfiguration config, Path dataDirectory) { + String configuredUrl = config.getString("bedrockResourcePackUrl", "").trim(); String configuredPath = config.getString("bedrockResourcePack", "").trim(); - if (configuredPath.isEmpty()) { + if (!configuredUrl.isEmpty() && !configuredPath.isEmpty()) { + throw new IllegalArgumentException("Configure only one of bedrockResourcePackUrl or bedrockResourcePack."); + } + if (configuredUrl.isEmpty() && configuredPath.isEmpty()) { return Optional.empty(); } - Path source = Path.of(configuredPath); - if (!source.isAbsolute()) { - source = dataDirectory.resolve(source); + Path source = null; + if (!configuredPath.isEmpty()) { + source = Path.of(configuredPath); + if (!source.isAbsolute()) { + source = dataDirectory.resolve(source); + } + source = source.normalize(); + String fileName = source.getFileName().toString().toLowerCase(Locale.ROOT); + if (!fileName.endsWith(".mcpack") && !fileName.endsWith(".zip")) { + throw new IllegalArgumentException("bedrockResourcePack must reference a .mcpack or .zip file."); + } + } else { + if (!configuredUrl.startsWith("https://") && !configuredUrl.startsWith("http://")) { + throw new IllegalArgumentException("bedrockResourcePackUrl must be an HTTP(S) direct download URL."); + } } - source = source.normalize(); - String fileName = source.getFileName().toString().toLowerCase(Locale.ROOT); - if (!fileName.endsWith(".mcpack") && !fileName.endsWith(".zip")) { - throw new IllegalArgumentException("bedrockResourcePack must reference a .mcpack or .zip file."); - } long refreshSeconds = config.getLong("refreshInterval", 300L); if (refreshSeconds < MINIMUM_REFRESH_SECONDS) { throw new IllegalArgumentException("refreshInterval must be at least " + MINIMUM_REFRESH_SECONDS + " seconds."); } - return Optional.of(new BedrockPackSettings(source, Math.multiplyExact(refreshSeconds, 20L))); + return Optional.of(new BedrockPackSettings(source, configuredUrl.isEmpty() ? null : configuredUrl, + Math.multiplyExact(refreshSeconds, 20L))); + } + + public boolean isRemote() { + return remoteUrl != null; } } diff --git a/src/main/java/io/github/lf1/remotepacksync/RemotePackSyncPlugin.java b/src/main/java/io/github/lf1/remotepacksync/RemotePackSyncPlugin.java index aa739d5..4bcf9fd 100644 --- a/src/main/java/io/github/lf1/remotepacksync/RemotePackSyncPlugin.java +++ b/src/main/java/io/github/lf1/remotepacksync/RemotePackSyncPlugin.java @@ -23,6 +23,7 @@ public final class RemotePackSyncPlugin extends JavaPlugin implements Listener, @Override public void onEnable() { saveDefaultConfig(); + getConfig().addDefault("bedrockResourcePackUrl", ""); getConfig().addDefault("bedrockResourcePack", ""); getConfig().options().copyDefaults(true); saveConfig(); diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 9a66633..b85971e 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -9,7 +9,12 @@ sha1Url: "" # Seconds between remote SHA-1 and local Bedrock pack checks. Minimum: 30 seconds. refreshInterval: 300 -# Optional local Bedrock Edition resource pack for players joining through GeyserMC. -# The path may point to a .mcpack or .zip file. Relative paths are resolved from -# plugins/RemotePackSync/. Leave blank to disable Bedrock pack delivery. +# Optional remote Bedrock Edition resource-pack URL for players connecting through GeyserMC. +# This must be a direct HTTP(S) download URL to a .mcpack or .zip pack. The server +# must return Content-Type: application/zip and an exact Content-Length. +# Geyser passes this URL to Bedrock clients using its URL pack codec. +bedrockResourcePackUrl: "" + +# Optional local alternative. Configure this only when bedrockResourcePackUrl is blank. +# Relative paths are resolved from plugins/RemotePackSync/. bedrockResourcePack: ""