Support remote Geyser Bedrock pack URLs
Build and publish release / Build and publish RemotePackSync (push) Successful in 59s

This commit is contained in:
root
2026-08-02 07:56:04 +02:00
parent b69a0eb244
commit 97c282a389
5 changed files with 73 additions and 26 deletions
+13 -7
View File
@@ -40,7 +40,12 @@ sha1Url: "https://cdn.example.com/server-pack.zip.sha1"
# Seconds between Java SHA-1 and Bedrock pack checks. Minimum: 30. # Seconds between Java SHA-1 and Bedrock pack checks. Minimum: 30.
refreshInterval: 300 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/. # Relative paths are resolved from plugins/RemotePackSync/.
bedrockResourcePack: "bedrock/server-pack.mcpack" 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 ### Bedrock Edition through GeyserMC
- Set `bedrockResourcePack` to a local `.mcpack` or `.zip` file. `.mcpack` is fully supported. - 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 source pack is checked asynchronously at `refreshInterval`. When its SHA-256 changes, RemotePackSync creates a managed copy under `plugins/RemotePackSync/bedrock-managed/`. - 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.
- 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. - 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.
- The original configured pack is never modified. - 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/`.
- Every new Geyser Bedrock connection receives the latest successfully processed pack through Geyser's public `SessionLoadResourcePacksEvent` API. - 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. - 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. - 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. - 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 ## 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 ## License
@@ -57,10 +57,18 @@ public final class BedrockPackManager {
} }
subscribeToGeyser(); subscribeToGeyser();
refreshTask = Bukkit.getScheduler().runTaskTimerAsynchronously( if (newSettings.isRemote()) {
plugin, this::refreshPack, 0L, newSettings.refreshIntervalTicks()); // Geyser's URL codec validates the remote download and obtains its metadata.
plugin.getLogger().info("Bedrock resource-pack refresh enabled every " // Geyser itself downloads remote packs once during its lifecycle, so do not
+ (newSettings.refreshIntervalTicks() / 20L) + " seconds."); // 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() { public void stop() {
@@ -111,6 +119,17 @@ public final class BedrockPackManager {
} }
try { 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(); Path source = activeSettings.sourceFile();
if (!Files.isRegularFile(source)) { if (!Files.isRegularFile(source)) {
throw new IOException("Configured Bedrock pack does not exist or is not a regular file: " + source); throw new IOException("Configured Bedrock pack does not exist or is not a regular file: " + source);
@@ -6,30 +6,46 @@ import java.nio.file.Path;
import java.util.Locale; import java.util.Locale;
import java.util.Optional; import java.util.Optional;
/** Validated configuration for an optional local Bedrock resource pack. */ /** Validated configuration for an optional local or remote Bedrock resource pack. */
public record BedrockPackSettings(Path sourceFile, long refreshIntervalTicks) { public record BedrockPackSettings(Path sourceFile, String remoteUrl, long refreshIntervalTicks) {
private static final long MINIMUM_REFRESH_SECONDS = 30L; private static final long MINIMUM_REFRESH_SECONDS = 30L;
public static Optional<BedrockPackSettings> from(FileConfiguration config, Path dataDirectory) { public static Optional<BedrockPackSettings> from(FileConfiguration config, Path dataDirectory) {
String configuredUrl = config.getString("bedrockResourcePackUrl", "").trim();
String configuredPath = config.getString("bedrockResourcePack", "").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(); return Optional.empty();
} }
Path source = Path.of(configuredPath); Path source = null;
if (!source.isAbsolute()) { if (!configuredPath.isEmpty()) {
source = dataDirectory.resolve(source); 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); long refreshSeconds = config.getLong("refreshInterval", 300L);
if (refreshSeconds < MINIMUM_REFRESH_SECONDS) { if (refreshSeconds < MINIMUM_REFRESH_SECONDS) {
throw new IllegalArgumentException("refreshInterval must be at least " + MINIMUM_REFRESH_SECONDS + " 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;
} }
} }
@@ -23,6 +23,7 @@ public final class RemotePackSyncPlugin extends JavaPlugin implements Listener,
@Override @Override
public void onEnable() { public void onEnable() {
saveDefaultConfig(); saveDefaultConfig();
getConfig().addDefault("bedrockResourcePackUrl", "");
getConfig().addDefault("bedrockResourcePack", ""); getConfig().addDefault("bedrockResourcePack", "");
getConfig().options().copyDefaults(true); getConfig().options().copyDefaults(true);
saveConfig(); saveConfig();
+8 -3
View File
@@ -9,7 +9,12 @@ sha1Url: ""
# Seconds between remote SHA-1 and local Bedrock pack checks. Minimum: 30 seconds. # Seconds between remote SHA-1 and local Bedrock pack checks. Minimum: 30 seconds.
refreshInterval: 300 refreshInterval: 300
# Optional local Bedrock Edition resource pack for players joining through GeyserMC. # Optional remote Bedrock Edition resource-pack URL for players connecting through GeyserMC.
# The path may point to a .mcpack or .zip file. Relative paths are resolved from # This must be a direct HTTP(S) download URL to a .mcpack or .zip pack. The server
# plugins/RemotePackSync/. Leave blank to disable Bedrock pack delivery. # 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: "" bedrockResourcePack: ""