From a48e90f8b8371dd8267cde985a5b9def06ea26e8 Mon Sep 17 00:00:00 2001 From: AriDev <75741608+ArikSquad@users.noreply.github.com> Date: Sat, 8 Aug 2026 16:33:50 +0300 Subject: [PATCH 1/3] refactor(packer): let's trust the folder --- .../net/swofty/packer/HypixelPackServer.java | 147 ++++++++---------- 1 file changed, 63 insertions(+), 84 deletions(-) diff --git a/packer/src/main/java/net/swofty/packer/HypixelPackServer.java b/packer/src/main/java/net/swofty/packer/HypixelPackServer.java index b82168159..7ff13718a 100644 --- a/packer/src/main/java/net/swofty/packer/HypixelPackServer.java +++ b/packer/src/main/java/net/swofty/packer/HypixelPackServer.java @@ -1,101 +1,80 @@ package net.swofty.packer; -import net.swofty.packer.packs.ravengard.RavengardPackDefinition; -import net.swofty.packer.packs.skyblock.SkyblockPackDefinition; import team.unnamed.creative.BuiltResourcePack; -import team.unnamed.creative.server.ResourcePackServer; -import team.unnamed.creative.server.handler.ResourcePackRequestHandler; +import team.unnamed.creative.base.Writable; +import team.unnamed.creative.metadata.pack.FormatVersion; +import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.OutputStream; -import java.nio.charset.StandardCharsets; -import java.util.List; -import java.util.Map; -import java.util.concurrent.Executors; -import java.util.function.Function; -import java.util.stream.Collectors; - -public class HypixelPackServer { - private static final String DEFAULT_HOST = "0.0.0.0"; - private static final int DEFAULT_PORT = 7270; - - static void main(String[] args) throws IOException { - String host = DEFAULT_HOST; - int port = DEFAULT_PORT; - - for (int i = 0; i < args.length - 1; i++) { - switch (args[i]) { - case "-h", "--host" -> host = args[++i]; - case "-p", "--port" -> port = Integer.parseInt(args[++i]); - } - } +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.HexFormat; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +public class HypixelPackBuilder { + private static final FormatVersion FORMAT_VERSION = FormatVersion.of(FormatVersion.FORMAT_26_1); + + private final PackDefinition definition; + + public HypixelPackBuilder(PackDefinition definition) { + this.definition = definition; + } - Map packs = List.of( - RavengardPackDefinition.INSTANCE, - SkyblockPackDefinition.INSTANCE - ).stream() - .map(HypixelPackServer::buildPack) - .collect(Collectors.toMap( - pack -> pack.hash() + ".zip", - Function.identity(), - (first, second) -> first - )); - - ResourcePackServer server = ResourcePackServer.server() - .address(host, port) - .handler(createRequestHandler(packs)) - .executor(Executors.newFixedThreadPool(4)) - .build(); - server.start(); - - System.out.println("Resource pack server started on " + host + ":" + port); - for (String fileName : packs.keySet()) { - System.out.println("Pack URL: http://" + host + ":" + port + "/" + fileName); - } - System.out.println("Press Ctrl+C to stop."); + public BuiltResourcePack build() { + Path packDirectory = Path.of(definition.getPackDirectory()).toAbsolutePath(); - Runtime.getRuntime().addShutdownHook(new Thread(() -> { - System.out.println("Shutting down pack server..."); - server.stop(5); - })); + if (!Files.isDirectory(packDirectory)) { + throw new IllegalStateException( + "Pack directory does not exist: " + packDirectory + ); + } try { - Thread.currentThread().join(); - } catch (InterruptedException ignored) {} + byte[] bytes = zipDirectory(packDirectory); + + MessageDigest digest = MessageDigest.getInstance("SHA-1"); + String hash = HexFormat.of().formatHex(digest.digest(bytes)); + + return BuiltResourcePack.of( + Writable.bytes(bytes), + hash + ); + } catch (IOException e) { + throw new UncheckedIOException("Failed to build resource pack", e); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("SHA-1 is unavailable", e); + } } - private static BuiltResourcePack buildPack(PackDefinition definition) { - System.out.println("Building resource pack '" + definition.getPackName() + "'..."); - System.out.println("Pack directory: " + definition.getPackDirectory()); + private byte[] zipDirectory(Path directory) throws IOException { + ByteArrayOutputStream output = new ByteArrayOutputStream(); - BuiltResourcePack built = new HypixelPackBuilder(definition).build(); - System.out.println("Resource pack built. Hash: " + built.hash()); - return built; - } - - private static ResourcePackRequestHandler createRequestHandler(Map packs) { - return (request, exchange) -> { - String path = exchange.getRequestURI().getPath(); - String fileName = path.substring(path.lastIndexOf('/') + 1); + try (ZipOutputStream zip = new ZipOutputStream(output); + Stream paths = Files.walk(directory)) { - BuiltResourcePack pack = packs.get(fileName); + paths.filter(Files::isRegularFile).forEach(path -> { + String entryName = directory.relativize(path) + .toString() + .replace('\\', '/'); - if (pack == null) { - byte[] response = "Resource pack not found\n".getBytes(StandardCharsets.UTF_8); - exchange.getResponseHeaders().set("Content-Type", "text/plain"); - exchange.sendResponseHeaders(404, response.length); - try (OutputStream output = exchange.getResponseBody()) { - output.write(response); + try { + zip.putNextEntry(new ZipEntry(entryName)); + Files.copy(path, zip); + zip.closeEntry(); + } catch (IOException e) { + throw new UncheckedIOException(e); } - return; - } - - byte[] response = pack.data().toByteArray(); - exchange.getResponseHeaders().set("Content-Type", "application/zip"); - exchange.sendResponseHeaders(200, response.length); - try (OutputStream output = exchange.getResponseBody()) { - output.write(response); - } - }; + }); + } catch (UncheckedIOException e) { + throw e.getCause(); + } + + return output.toByteArray(); } + } From 8a26fab664333b72a8bd46c03169725caeaf2b15 Mon Sep 17 00:00:00 2001 From: ArikSquad <75741608+ArikSquad@users.noreply.github.com> Date: Sat, 8 Aug 2026 19:07:05 +0300 Subject: [PATCH 2/3] feat: official api fix that wrong file thing too --- .../net/swofty/commons/config/Settings.java | 6 + configuration/config.docker.yml | 2 + configuration/config.example.yml | 9 +- .../net/swofty/packer/HypixelPackBuilder.java | 75 +++++++-- .../net/swofty/packer/HypixelPackServer.java | 147 ++++++++++-------- .../java/net/swofty/proxyapi/ProxyPlayer.java | 24 +++ setup/internal/installer/files.go | 2 + .../resourcepack/HypixelResourcePack.java | 7 + .../resourcepack/ResourcePackManager.java | 14 +- .../HypixelPackFormatResolver.java | 43 +++++ .../resourcepack/HypixelSkyblockPackApi.java | 124 +++++++++++++++ .../resourcepack/SkyblockPack.java | 55 ++++++- .../HypixelSkyblockPackApiTest.java | 30 ++++ .../listeners/ListenerPlayerHandler.java | 9 ++ website/docs/docker/setup.md | 2 + website/docs/setup/resource-pack.md | 15 +- 16 files changed, 476 insertions(+), 88 deletions(-) create mode 100644 type.skyblockgeneric/src/main/java/net/swofty/type/skyblockgeneric/resourcepack/HypixelPackFormatResolver.java create mode 100644 type.skyblockgeneric/src/main/java/net/swofty/type/skyblockgeneric/resourcepack/HypixelSkyblockPackApi.java create mode 100644 type.skyblockgeneric/src/test/java/net/swofty/type/skyblockgeneric/resourcepack/HypixelSkyblockPackApiTest.java diff --git a/commons/src/main/java/net/swofty/commons/config/Settings.java b/commons/src/main/java/net/swofty/commons/config/Settings.java index af30a4e01..e4352c52f 100644 --- a/commons/src/main/java/net/swofty/commons/config/Settings.java +++ b/commons/src/main/java/net/swofty/commons/config/Settings.java @@ -57,6 +57,12 @@ public static class LimboSettings { public static class ResourcePackSettings { @Comment("Base URL of the pack server (e.g. http://0.0.0.0:7270)") private String serverUrl = "http://127.0.0.1:7270"; + + @Comment("Whether to use Hypixel's official resource pack API instead of building a local pack") + private boolean useHypixelApi = false; + + @Comment("URL of Hypixel's resource pack metadata API") + private String hypixelApiUrl = "https://api.hypixel.net/v2/resources/packs"; } @Getter diff --git a/configuration/config.docker.yml b/configuration/config.docker.yml index 73d8b1bfd..c04da6425 100644 --- a/configuration/config.docker.yml +++ b/configuration/config.docker.yml @@ -14,5 +14,7 @@ limbo: resource-packs: skyblockpack: server-url: http://127.0.0.1:7270 + use-hypixel-api: false + hypixel-api-url: https://api.hypixel.net/v2/resources/packs ravengard: server-url: http://127.0.0.1:7270 diff --git a/configuration/config.example.yml b/configuration/config.example.yml index 22644649f..813ffa425 100644 --- a/configuration/config.example.yml +++ b/configuration/config.example.yml @@ -24,5 +24,10 @@ integrations: limbo: host-name: 127.0.0.1 port: 65535 -# Resource pack settings keyed by pack name (e.g. testingpack, bedwarspack) -resource-packs: { } +# Resource pack settings keyed by pack name (e.g. testingpack, skyblockpack) +resource-packs: + skyblockpack: + server-url: http://127.0.0.1:7270 + # Use Hypixel's official pack and select its format for each player's version + use-hypixel-api: false + hypixel-api-url: https://api.hypixel.net/v2/resources/packs diff --git a/packer/src/main/java/net/swofty/packer/HypixelPackBuilder.java b/packer/src/main/java/net/swofty/packer/HypixelPackBuilder.java index 971cbde51..ccbdbbe62 100644 --- a/packer/src/main/java/net/swofty/packer/HypixelPackBuilder.java +++ b/packer/src/main/java/net/swofty/packer/HypixelPackBuilder.java @@ -1,19 +1,21 @@ package net.swofty.packer; -import net.kyori.adventure.text.Component; import team.unnamed.creative.BuiltResourcePack; -import team.unnamed.creative.ResourcePack; -import team.unnamed.creative.metadata.pack.FormatVersion; -import team.unnamed.creative.metadata.pack.PackFormat; -import team.unnamed.creative.metadata.pack.PackMeta; -import team.unnamed.creative.serialize.minecraft.MinecraftResourcePackReader; -import team.unnamed.creative.serialize.minecraft.MinecraftResourcePackWriter; +import team.unnamed.creative.base.Writable; -import java.io.File; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.HexFormat; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; public class HypixelPackBuilder { - private static final FormatVersion FORMAT_VERSION = FormatVersion.of(FormatVersion.FORMAT_26_1); - private final PackDefinition definition; public HypixelPackBuilder(PackDefinition definition) { @@ -21,16 +23,55 @@ public HypixelPackBuilder(PackDefinition definition) { } public BuiltResourcePack build() { - File packDirectory = new File(definition.getPackDirectory()).getAbsoluteFile(); - if (!packDirectory.isDirectory()) { - throw new IllegalStateException("Pack directory does not exist: " + packDirectory.getPath()); + Path packDirectory = Path.of(definition.getPackDirectory()).toAbsolutePath(); + + if (!Files.isDirectory(packDirectory)) { + throw new IllegalStateException( + "Pack directory does not exist: " + packDirectory + ); } - ResourcePack pack = MinecraftResourcePackReader.minecraft() - .readFromDirectory(packDirectory); - pack.packMeta(PackMeta.of(PackFormat.format(FORMAT_VERSION, FORMAT_VERSION), Component.text("Hypixel"))); + try { + byte[] bytes = zipDirectory(packDirectory); + + MessageDigest digest = MessageDigest.getInstance("SHA-1"); + String hash = HexFormat.of().formatHex(digest.digest(bytes)); + + return BuiltResourcePack.of( + Writable.bytes(bytes), + hash + ); + } catch (IOException e) { + throw new UncheckedIOException("Failed to build resource pack", e); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("SHA-1 is unavailable", e); + } + } + + private byte[] zipDirectory(Path directory) throws IOException { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + + try (ZipOutputStream zip = new ZipOutputStream(output); + Stream paths = Files.walk(directory)) { + + paths.filter(Files::isRegularFile).forEach(path -> { + String entryName = directory.relativize(path) + .toString() + .replace('\\', '/'); + + try { + zip.putNextEntry(new ZipEntry(entryName)); + Files.copy(path, zip); + zip.closeEntry(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + } catch (UncheckedIOException e) { + throw e.getCause(); + } - return MinecraftResourcePackWriter.minecraft().build(pack); + return output.toByteArray(); } } diff --git a/packer/src/main/java/net/swofty/packer/HypixelPackServer.java b/packer/src/main/java/net/swofty/packer/HypixelPackServer.java index 7ff13718a..b82168159 100644 --- a/packer/src/main/java/net/swofty/packer/HypixelPackServer.java +++ b/packer/src/main/java/net/swofty/packer/HypixelPackServer.java @@ -1,80 +1,101 @@ package net.swofty.packer; +import net.swofty.packer.packs.ravengard.RavengardPackDefinition; +import net.swofty.packer.packs.skyblock.SkyblockPackDefinition; import team.unnamed.creative.BuiltResourcePack; -import team.unnamed.creative.base.Writable; -import team.unnamed.creative.metadata.pack.FormatVersion; +import team.unnamed.creative.server.ResourcePackServer; +import team.unnamed.creative.server.handler.ResourcePackRequestHandler; -import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.UncheckedIOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.HexFormat; -import java.util.stream.Stream; -import java.util.zip.ZipEntry; -import java.util.zip.ZipOutputStream; - -public class HypixelPackBuilder { - private static final FormatVersion FORMAT_VERSION = FormatVersion.of(FormatVersion.FORMAT_26_1); - - private final PackDefinition definition; - - public HypixelPackBuilder(PackDefinition definition) { - this.definition = definition; - } - - public BuiltResourcePack build() { - Path packDirectory = Path.of(definition.getPackDirectory()).toAbsolutePath(); +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executors; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class HypixelPackServer { + private static final String DEFAULT_HOST = "0.0.0.0"; + private static final int DEFAULT_PORT = 7270; + + static void main(String[] args) throws IOException { + String host = DEFAULT_HOST; + int port = DEFAULT_PORT; + + for (int i = 0; i < args.length - 1; i++) { + switch (args[i]) { + case "-h", "--host" -> host = args[++i]; + case "-p", "--port" -> port = Integer.parseInt(args[++i]); + } + } - if (!Files.isDirectory(packDirectory)) { - throw new IllegalStateException( - "Pack directory does not exist: " + packDirectory - ); + Map packs = List.of( + RavengardPackDefinition.INSTANCE, + SkyblockPackDefinition.INSTANCE + ).stream() + .map(HypixelPackServer::buildPack) + .collect(Collectors.toMap( + pack -> pack.hash() + ".zip", + Function.identity(), + (first, second) -> first + )); + + ResourcePackServer server = ResourcePackServer.server() + .address(host, port) + .handler(createRequestHandler(packs)) + .executor(Executors.newFixedThreadPool(4)) + .build(); + server.start(); + + System.out.println("Resource pack server started on " + host + ":" + port); + for (String fileName : packs.keySet()) { + System.out.println("Pack URL: http://" + host + ":" + port + "/" + fileName); } + System.out.println("Press Ctrl+C to stop."); + + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + System.out.println("Shutting down pack server..."); + server.stop(5); + })); try { - byte[] bytes = zipDirectory(packDirectory); - - MessageDigest digest = MessageDigest.getInstance("SHA-1"); - String hash = HexFormat.of().formatHex(digest.digest(bytes)); - - return BuiltResourcePack.of( - Writable.bytes(bytes), - hash - ); - } catch (IOException e) { - throw new UncheckedIOException("Failed to build resource pack", e); - } catch (NoSuchAlgorithmException e) { - throw new IllegalStateException("SHA-1 is unavailable", e); - } + Thread.currentThread().join(); + } catch (InterruptedException ignored) {} } - private byte[] zipDirectory(Path directory) throws IOException { - ByteArrayOutputStream output = new ByteArrayOutputStream(); + private static BuiltResourcePack buildPack(PackDefinition definition) { + System.out.println("Building resource pack '" + definition.getPackName() + "'..."); + System.out.println("Pack directory: " + definition.getPackDirectory()); - try (ZipOutputStream zip = new ZipOutputStream(output); - Stream paths = Files.walk(directory)) { + BuiltResourcePack built = new HypixelPackBuilder(definition).build(); + System.out.println("Resource pack built. Hash: " + built.hash()); + return built; + } - paths.filter(Files::isRegularFile).forEach(path -> { - String entryName = directory.relativize(path) - .toString() - .replace('\\', '/'); + private static ResourcePackRequestHandler createRequestHandler(Map packs) { + return (request, exchange) -> { + String path = exchange.getRequestURI().getPath(); + String fileName = path.substring(path.lastIndexOf('/') + 1); - try { - zip.putNextEntry(new ZipEntry(entryName)); - Files.copy(path, zip); - zip.closeEntry(); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - }); - } catch (UncheckedIOException e) { - throw e.getCause(); - } + BuiltResourcePack pack = packs.get(fileName); - return output.toByteArray(); + if (pack == null) { + byte[] response = "Resource pack not found\n".getBytes(StandardCharsets.UTF_8); + exchange.getResponseHeaders().set("Content-Type", "text/plain"); + exchange.sendResponseHeaders(404, response.length); + try (OutputStream output = exchange.getResponseBody()) { + output.write(response); + } + return; + } + + byte[] response = pack.data().toByteArray(); + exchange.getResponseHeaders().set("Content-Type", "application/zip"); + exchange.sendResponseHeaders(200, response.length); + try (OutputStream output = exchange.getResponseBody()) { + output.write(response); + } + }; } - } diff --git a/proxy.api/src/main/java/net/swofty/proxyapi/ProxyPlayer.java b/proxy.api/src/main/java/net/swofty/proxyapi/ProxyPlayer.java index 9ed8cb57b..91e5c1b51 100644 --- a/proxy.api/src/main/java/net/swofty/proxyapi/ProxyPlayer.java +++ b/proxy.api/src/main/java/net/swofty/proxyapi/ProxyPlayer.java @@ -73,6 +73,30 @@ public CompletableFuture isOnline() { return future; } + public CompletableFuture getVersion() { + CompletableFuture future = new CompletableFuture<>(); + RedisClient.requestProxy(PLAYER_HANDLER, + new PlayerHandlerProtocol.Request(uuid.toString(), PlayerHandlerProtocol.Action.VERSION, Map.of())) + .thenAccept(response -> { + if (!response.success()) { + future.completeExceptionally(new IllegalStateException(response.error())); + return; + } + + Object version = response.data().get("version"); + if (version instanceof Number number) { + future.complete(number.intValue()); + } else { + future.completeExceptionally(new IllegalStateException("Proxy returned no player version")); + } + }) + .exceptionally(error -> { + future.completeExceptionally(error); + return null; + }); + return future; + } + public void runEvent(ProxyUnderstandableEvent event) { RedisClient.requestProxy(PLAYER_HANDLER, new PlayerHandlerProtocol.Request(uuid.toString(), PlayerHandlerProtocol.Action.EVENT, diff --git a/setup/internal/installer/files.go b/setup/internal/installer/files.go index 1c0bab044..f831b0e12 100644 --- a/setup/internal/installer/files.go +++ b/setup/internal/installer/files.go @@ -168,6 +168,8 @@ resource-pack: resource-packs: skyblockpack: server-url: http://127.0.0.1:7270 + use-hypixel-api: false + hypixel-api-url: https://api.hypixel.net/v2/resources/packs ravengard: server-url: http://127.0.0.1:7270 ` diff --git a/type.generic/src/main/java/net/swofty/type/generic/resourcepack/HypixelResourcePack.java b/type.generic/src/main/java/net/swofty/type/generic/resourcepack/HypixelResourcePack.java index 754e4ebf4..43cff6a5c 100644 --- a/type.generic/src/main/java/net/swofty/type/generic/resourcepack/HypixelResourcePack.java +++ b/type.generic/src/main/java/net/swofty/type/generic/resourcepack/HypixelResourcePack.java @@ -5,10 +5,17 @@ import net.swofty.type.generic.user.HypixelPlayer; public interface HypixelResourcePack { + record PackInfo(String url, String hash) { + } + String getPackName(); String getPackUrl(); String getPackHash(); + default PackInfo getPackFor(HypixelPlayer player) { + return new PackInfo(getPackUrl(), getPackHash()); + } + boolean isRequired(); void initialize(); diff --git a/type.generic/src/main/java/net/swofty/type/generic/resourcepack/ResourcePackManager.java b/type.generic/src/main/java/net/swofty/type/generic/resourcepack/ResourcePackManager.java index 57d44b376..4d7744875 100644 --- a/type.generic/src/main/java/net/swofty/type/generic/resourcepack/ResourcePackManager.java +++ b/type.generic/src/main/java/net/swofty/type/generic/resourcepack/ResourcePackManager.java @@ -5,6 +5,7 @@ import net.kyori.adventure.resource.ResourcePackRequest; import net.kyori.adventure.text.Component; import net.minestom.server.entity.Player; +import net.swofty.type.generic.user.HypixelPlayer; import org.tinylog.Logger; import java.net.URI; @@ -37,8 +38,17 @@ public void sendPack(Player player) { * already-received chunk sections permanently unrendered on the client. */ public void sendPackBlocking(Player player, int timeoutSeconds) { - String packUrl = activePack.getPackUrl(); - String packHash = activePack.getPackHash(); + HypixelResourcePack.PackInfo pack = player instanceof HypixelPlayer hypixelPlayer + ? activePack.getPackFor(hypixelPlayer) + : new HypixelResourcePack.PackInfo(activePack.getPackUrl(), activePack.getPackHash()); + + if (pack == null) { + Logger.warn("Resource pack could not be resolved for " + player.getUsername() + ", skipping pack send"); + return; + } + + String packUrl = pack.url(); + String packHash = pack.hash(); if (packUrl == null || packUrl.isEmpty() || packHash == null || packHash.isEmpty()) { Logger.warn("Resource pack URL or hash not configured, skipping pack send for " + player.getUsername()); diff --git a/type.skyblockgeneric/src/main/java/net/swofty/type/skyblockgeneric/resourcepack/HypixelPackFormatResolver.java b/type.skyblockgeneric/src/main/java/net/swofty/type/skyblockgeneric/resourcepack/HypixelPackFormatResolver.java new file mode 100644 index 000000000..de9997b26 --- /dev/null +++ b/type.skyblockgeneric/src/main/java/net/swofty/type/skyblockgeneric/resourcepack/HypixelPackFormatResolver.java @@ -0,0 +1,43 @@ +package net.swofty.type.skyblockgeneric.resourcepack; + +final class HypixelPackFormatResolver { + private HypixelPackFormatResolver() { + } + + static int packFormatForProtocol(int protocolVersion) { + if (protocolVersion > 776) { + return Integer.MAX_VALUE; + } + if (protocolVersion >= 776) { + return 88; + } + if (protocolVersion >= 775) { + return 84; + } + if (protocolVersion >= 774) { + return 75; + } + if (protocolVersion >= 773) { + return 69; + } + if (protocolVersion >= 772) { + return 64; + } + if (protocolVersion >= 771) { + return 63; + } + if (protocolVersion >= 770) { + return 55; + } + if (protocolVersion >= 769) { + return 46; + } + if (protocolVersion >= 768) { + return 42; + } + if (protocolVersion >= 766) { + return 34; + } + return 0; + } +} diff --git a/type.skyblockgeneric/src/main/java/net/swofty/type/skyblockgeneric/resourcepack/HypixelSkyblockPackApi.java b/type.skyblockgeneric/src/main/java/net/swofty/type/skyblockgeneric/resourcepack/HypixelSkyblockPackApi.java new file mode 100644 index 000000000..3325cd9b6 --- /dev/null +++ b/type.skyblockgeneric/src/main/java/net/swofty/type/skyblockgeneric/resourcepack/HypixelSkyblockPackApi.java @@ -0,0 +1,124 @@ +package net.swofty.type.skyblockgeneric.resourcepack; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.Collections; +import java.util.NavigableMap; +import java.util.Objects; +import java.util.TreeMap; + +final class HypixelSkyblockPackApi { + private static final Duration REQUEST_TIMEOUT = Duration.ofSeconds(10); + private static final HttpClient HTTP_CLIENT = HttpClient.newBuilder() + .connectTimeout(REQUEST_TIMEOUT) + .build(); + + private HypixelSkyblockPackApi() { + } + + static Catalog fetch(String apiUrl) throws IOException, InterruptedException { + URI uri = URI.create(apiUrl); + HttpRequest request = HttpRequest.newBuilder(uri) + .timeout(REQUEST_TIMEOUT) + .header("Accept", "application/json") + .GET() + .build(); + HttpResponse response = HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() < 200 || response.statusCode() >= 300) { + throw new IOException("Hypixel API returned HTTP " + response.statusCode()); + } + + return parse(response.body()); + } + + static Catalog parse(String responseBody) { + JSONObject response = new JSONObject(responseBody); + if (!response.optBoolean("success")) { + throw new IllegalStateException("Hypixel API returned an unsuccessful response"); + } + + JSONArray packs = response.optJSONArray("packs"); + if (packs == null) { + throw new IllegalStateException("Hypixel API response did not contain packs"); + } + + for (int i = 0; i < packs.length(); i++) { + JSONObject pack = packs.optJSONObject(i); + if (pack != null && "SkyBlock".equals(pack.optString("id"))) { + return parseCatalog(pack); + } + } + + throw new IllegalStateException("Hypixel API response did not contain the SkyBlock pack"); + } + + private static Catalog parseCatalog(JSONObject pack) { + JSONArray versions = pack.optJSONArray("versions"); + if (versions == null) { + throw new IllegalStateException("Hypixel API response did not contain SkyBlock pack versions"); + } + + NavigableMap byPackFormat = new TreeMap<>(); + for (int i = 0; i < versions.length(); i++) { + JSONObject version = versions.optJSONObject(i); + if (version == null) { + continue; + } + + String url = version.optString("url").trim(); + String hash = version.optString("hash").trim(); + if (!version.has("packFormat") || url.isEmpty() || hash.isEmpty()) { + continue; + } + + int packFormat = version.getInt("packFormat"); + URI.create(url); + byPackFormat.put(packFormat, new Version(packFormat, url, hash)); + } + + if (byPackFormat.isEmpty()) { + throw new IllegalStateException("Hypixel API response did not contain usable SkyBlock pack versions"); + } + + return new Catalog(byPackFormat); + } + + record Version(int packFormat, String url, String hash) { + Version { + Objects.requireNonNull(url, "url"); + Objects.requireNonNull(hash, "hash"); + } + } + + static final class Catalog { + private final NavigableMap versions; + + private Catalog(NavigableMap versions) { + this.versions = Collections.unmodifiableNavigableMap(new TreeMap<>(versions)); + } + + Version latest() { + return versions.lastEntry().getValue(); + } + + Version forProtocol(int protocolVersion) { + int packFormat = HypixelPackFormatResolver.packFormatForProtocol(protocolVersion); + if (packFormat == Integer.MAX_VALUE) { + return latest(); + } + if (packFormat <= 0) { + return null; + } + return versions.get(packFormat); + } + + } +} diff --git a/type.skyblockgeneric/src/main/java/net/swofty/type/skyblockgeneric/resourcepack/SkyblockPack.java b/type.skyblockgeneric/src/main/java/net/swofty/type/skyblockgeneric/resourcepack/SkyblockPack.java index 2b135c1ef..33dded5e6 100644 --- a/type.skyblockgeneric/src/main/java/net/swofty/type/skyblockgeneric/resourcepack/SkyblockPack.java +++ b/type.skyblockgeneric/src/main/java/net/swofty/type/skyblockgeneric/resourcepack/SkyblockPack.java @@ -8,20 +8,48 @@ import org.tinylog.Logger; import team.unnamed.creative.BuiltResourcePack; +import java.util.concurrent.TimeUnit; + public class SkyblockPack implements HypixelResourcePack { private static final SkyblockPackDefinition DEFINITION = SkyblockPackDefinition.INSTANCE; + private final PackInfo defaultPack; + private final HypixelSkyblockPackApi.Catalog officialPacks; + private final String packUrl; private final String packHash; public SkyblockPack(String serverUrl, String hash) { - this.packHash = hash; - this.packUrl = serverUrl + "/" + hash + ".zip"; + this(new PackInfo(serverUrl + "/" + hash + ".zip", hash), null); + } + + private SkyblockPack(PackInfo defaultPack, HypixelSkyblockPackApi.Catalog officialPacks) { + this.defaultPack = defaultPack; + this.officialPacks = officialPacks; + this.packUrl = defaultPack.url(); + this.packHash = defaultPack.hash(); } public static SkyblockPack fromConfig() { Settings.ResourcePackSettings settings = HypixelResourcePack.getConfigFor(DEFINITION.getPackName()); + if (settings.isUseHypixelApi()) { + try { + Logger.info("Loading the official Hypixel SkyBlock resource pack metadata..."); + HypixelSkyblockPackApi.Catalog officialPacks = HypixelSkyblockPackApi.fetch(settings.getHypixelApiUrl()); + HypixelSkyblockPackApi.Version latest = officialPacks.latest(); + PackInfo defaultPack = new PackInfo(latest.url(), latest.hash()); + Logger.info("Loaded official Hypixel SkyBlock resource pack format {} with hash {}", + latest.packFormat(), latest.hash()); + return new SkyblockPack(defaultPack, officialPacks); + } catch (InterruptedException exception) { + Thread.currentThread().interrupt(); + throw new IllegalStateException("Interrupted while loading the official Hypixel SkyBlock resource pack", exception); + } catch (Exception exception) { + throw new IllegalStateException("Failed to load the official Hypixel SkyBlock resource pack", exception); + } + } + Logger.info("Building resource pack '{}' from {}...", DEFINITION.getPackName(), DEFINITION.getPackDirectory()); HypixelPackBuilder builder = new HypixelPackBuilder(DEFINITION); BuiltResourcePack built = builder.build(); @@ -45,6 +73,29 @@ public String getPackHash() { return packHash; } + @Override + public PackInfo getPackFor(HypixelPlayer player) { + if (officialPacks == null) { + return defaultPack; + } + + try { + int protocolVersion = player.asProxyPlayer().getVersion().get(3, TimeUnit.SECONDS); + HypixelSkyblockPackApi.Version version = officialPacks.forProtocol(protocolVersion); + if (version == null) { + Logger.warn("No official Hypixel SkyBlock resource pack is available for protocol version {}", protocolVersion); + return null; + } + return new PackInfo(version.url(), version.hash()); + } catch (InterruptedException exception) { + Thread.currentThread().interrupt(); + Logger.warn("Interrupted while resolving the official Hypixel SkyBlock resource pack for {}", player.getUsername()); + } catch (Exception exception) { + Logger.warn("Failed to resolve the official Hypixel SkyBlock resource pack for {}, using the latest pack", player.getUsername()); + } + return defaultPack; + } + @Override public boolean isRequired() { return true; diff --git a/type.skyblockgeneric/src/test/java/net/swofty/type/skyblockgeneric/resourcepack/HypixelSkyblockPackApiTest.java b/type.skyblockgeneric/src/test/java/net/swofty/type/skyblockgeneric/resourcepack/HypixelSkyblockPackApiTest.java new file mode 100644 index 000000000..64dcb22a7 --- /dev/null +++ b/type.skyblockgeneric/src/test/java/net/swofty/type/skyblockgeneric/resourcepack/HypixelSkyblockPackApiTest.java @@ -0,0 +1,30 @@ +package net.swofty.type.skyblockgeneric.resourcepack; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +class HypixelSkyblockPackApiTest { + @Test + void selectsTheOfficialPackForEachSupportedProtocol() { + HypixelSkyblockPackApi.Catalog catalog = HypixelSkyblockPackApi.parse(""" + { + "success": true, + "packs": [{ + "id": "SkyBlock", + "versions": [ + {"packFormat": 84, "hash": "hash-84", "url": "https://example.test/84.zip"}, + {"packFormat": 88, "hash": "hash-88", "url": "https://example.test/88.zip"}, + {"packFormat": 75, "hash": "hash-75", "url": "https://example.test/75.zip"} + ] + }] + } + """); + + assertEquals("hash-75", catalog.forProtocol(774).hash()); + assertEquals("hash-84", catalog.forProtocol(775).hash()); + assertEquals("hash-88", catalog.forProtocol(776).hash()); + assertNull(catalog.forProtocol(773)); + } +} diff --git a/velocity.extension/src/main/java/net/swofty/velocity/redis/listeners/ListenerPlayerHandler.java b/velocity.extension/src/main/java/net/swofty/velocity/redis/listeners/ListenerPlayerHandler.java index a250e4e01..6868b754b 100644 --- a/velocity.extension/src/main/java/net/swofty/velocity/redis/listeners/ListenerPlayerHandler.java +++ b/velocity.extension/src/main/java/net/swofty/velocity/redis/listeners/ListenerPlayerHandler.java @@ -2,11 +2,13 @@ import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.ServerConnection; +import com.viaversion.viaversion.api.Via; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.serializer.json.JSONComponentSerializer; import net.swofty.commons.ServerType; import net.swofty.commons.StringUtility; import net.swofty.commons.UnderstandableProxyServer; +import net.swofty.commons.config.ConfigProvider; import net.swofty.commons.protocol.RedisProtocol; import net.swofty.commons.protocol.objects.proxy.from.RefreshCoopDataProtocol; import net.swofty.commons.protocol.objects.proxy.from.RunEventProtocol; @@ -59,6 +61,13 @@ public PlayerHandlerProtocol.Response handle(PlayerHandlerProtocol.Request messa Optional potentialServer = player.getCurrentServer(); switch (action) { + case VERSION -> { + int version = player.getProtocolVersion().getProtocol(); + if (ConfigProvider.settings().getIntegrations().isViaVersion()) { + version = Via.getAPI().getPlayerVersion(uuid); + } + return new PlayerHandlerProtocol.Response(Map.of("version", version), true, null); + } case RESOLVE_TRANSFER -> { ServerType type = ServerType.valueOf((String) data.get("type")); if (!GameManager.hasType(type) || !GameManager.isAnyEmpty(type)) { diff --git a/website/docs/docker/setup.md b/website/docs/docker/setup.md index 46345a314..b228393ec 100644 --- a/website/docs/docker/setup.md +++ b/website/docs/docker/setup.md @@ -149,6 +149,8 @@ The pack URL is handed to your client rather than resolved inside the container, resource-packs: skyblockpack: server-url: http://192.0.2.10:7270 + use-hypixel-api: false + hypixel-api-url: https://api.hypixel.net/v2/resources/packs ravengard: server-url: http://192.0.2.10:7270 ``` diff --git a/website/docs/setup/resource-pack.md b/website/docs/setup/resource-pack.md index 83efcbb1b..fd941d449 100644 --- a/website/docs/setup/resource-pack.md +++ b/website/docs/setup/resource-pack.md @@ -51,7 +51,7 @@ On startup, the server logs: ## Configure the Game Server -Set the pack server URL in `configuration/config.yml`: +For locally built packs, set the pack server URL in `configuration/config.yml`: ```yaml resource-packs: @@ -59,10 +59,21 @@ resource-packs: server-url: "http://127.0.0.1:7270" ``` +SkyBlock can use Hypixel's official pack metadata API instead. The selected pack URL is sent directly to each player, using the pack format matching their client version: + +```yaml +resource-packs: + skyblockpack: + use-hypixel-api: true + hypixel-api-url: "https://api.hypixel.net/v2/resources/packs" +``` + Notes: -- Hash is generated automatically at runtime by the pack builder. +- Local pack hashes are generated automatically at runtime by the pack builder; official-pack hashes come from the API. - You do not manually set a `resource-pack-hash` field. +- `use-hypixel-api` uses the original client protocol reported by ViaVersion when it is enabled. +- The client does not download `skyblockpack` from the local pack server when `use-hypixel-api` is enabled. ## Verify In-Game From 7eba2f97f720dc2a9f4abb22c41b0a3f96dca1cc Mon Sep 17 00:00:00 2001 From: ArikSquad <75741608+ArikSquad@users.noreply.github.com> Date: Sat, 8 Aug 2026 19:43:43 +0300 Subject: [PATCH 3/3] feat: add back MinecraftVersion --- .../net/swofty/commons/MinecraftVersion.java | 41 ++++++++++++++++++ .../java/net/swofty/proxyapi/ProxyPlayer.java | 8 +++- .../HypixelPackFormatResolver.java | 43 ------------------- .../resourcepack/HypixelSkyblockPackApi.java | 21 ++++++--- .../resourcepack/SkyblockPack.java | 2 +- .../HypixelSkyblockPackApiTest.java | 30 ------------- .../listeners/ListenerPlayerHandler.java | 8 ++-- 7 files changed, 68 insertions(+), 85 deletions(-) create mode 100644 commons/src/main/java/net/swofty/commons/MinecraftVersion.java delete mode 100644 type.skyblockgeneric/src/main/java/net/swofty/type/skyblockgeneric/resourcepack/HypixelPackFormatResolver.java delete mode 100644 type.skyblockgeneric/src/test/java/net/swofty/type/skyblockgeneric/resourcepack/HypixelSkyblockPackApiTest.java diff --git a/commons/src/main/java/net/swofty/commons/MinecraftVersion.java b/commons/src/main/java/net/swofty/commons/MinecraftVersion.java new file mode 100644 index 000000000..66a9d754f --- /dev/null +++ b/commons/src/main/java/net/swofty/commons/MinecraftVersion.java @@ -0,0 +1,41 @@ +package net.swofty.commons; + +import lombok.Getter; + +@Getter +public enum MinecraftVersion { + MINECRAFT_1_20_5(766, 41.0, 32), + MINECRAFT_1_21(767, 48.0, 34), + MINECRAFT_1_21_2(768, 57.0, 42), + MINECRAFT_1_21_4(769, 61.0, 46), + MINECRAFT_1_21_5(770, 71.0, 55), + MINECRAFT_1_21_6(771, 80.0, 63), + MINECRAFT_1_21_7(772, 81.0, 64), + MINECRAFT_1_21_9(773, 88.0, 69), + MINECRAFT_1_21_11(774, 94.1, 75), + MINECRAFT_26_1(775, 101.1, 84), + MINECRAFT_26_2(776, 107.1, 88); + + private final int protocolVersion; + private final double dataPackVersion; + private final int packVersion; + + MinecraftVersion(int protocolVersion, double dataPackVersion, int packVersion) { + this.protocolVersion = protocolVersion; + this.dataPackVersion = dataPackVersion; + this.packVersion = packVersion; + } + + public static MinecraftVersion byProtocol(int protocolVersion) { + for (MinecraftVersion version : values()) { + if (version.protocolVersion == protocolVersion) { + return version; + } + } + return null; + } + + public static MinecraftVersion latest() { + return MINECRAFT_26_2; + } +} diff --git a/proxy.api/src/main/java/net/swofty/proxyapi/ProxyPlayer.java b/proxy.api/src/main/java/net/swofty/proxyapi/ProxyPlayer.java index 91e5c1b51..00822c66a 100644 --- a/proxy.api/src/main/java/net/swofty/proxyapi/ProxyPlayer.java +++ b/proxy.api/src/main/java/net/swofty/proxyapi/ProxyPlayer.java @@ -73,7 +73,7 @@ public CompletableFuture isOnline() { return future; } - public CompletableFuture getVersion() { + public CompletableFuture getProtocolVersion() { CompletableFuture future = new CompletableFuture<>(); RedisClient.requestProxy(PLAYER_HANDLER, new PlayerHandlerProtocol.Request(uuid.toString(), PlayerHandlerProtocol.Action.VERSION, Map.of())) @@ -83,7 +83,7 @@ public CompletableFuture getVersion() { return; } - Object version = response.data().get("version"); + Object version = response.data().get("protocolVersion"); if (version instanceof Number number) { future.complete(number.intValue()); } else { @@ -97,6 +97,10 @@ public CompletableFuture getVersion() { return future; } + public CompletableFuture getVersion() { + return getProtocolVersion(); + } + public void runEvent(ProxyUnderstandableEvent event) { RedisClient.requestProxy(PLAYER_HANDLER, new PlayerHandlerProtocol.Request(uuid.toString(), PlayerHandlerProtocol.Action.EVENT, diff --git a/type.skyblockgeneric/src/main/java/net/swofty/type/skyblockgeneric/resourcepack/HypixelPackFormatResolver.java b/type.skyblockgeneric/src/main/java/net/swofty/type/skyblockgeneric/resourcepack/HypixelPackFormatResolver.java deleted file mode 100644 index de9997b26..000000000 --- a/type.skyblockgeneric/src/main/java/net/swofty/type/skyblockgeneric/resourcepack/HypixelPackFormatResolver.java +++ /dev/null @@ -1,43 +0,0 @@ -package net.swofty.type.skyblockgeneric.resourcepack; - -final class HypixelPackFormatResolver { - private HypixelPackFormatResolver() { - } - - static int packFormatForProtocol(int protocolVersion) { - if (protocolVersion > 776) { - return Integer.MAX_VALUE; - } - if (protocolVersion >= 776) { - return 88; - } - if (protocolVersion >= 775) { - return 84; - } - if (protocolVersion >= 774) { - return 75; - } - if (protocolVersion >= 773) { - return 69; - } - if (protocolVersion >= 772) { - return 64; - } - if (protocolVersion >= 771) { - return 63; - } - if (protocolVersion >= 770) { - return 55; - } - if (protocolVersion >= 769) { - return 46; - } - if (protocolVersion >= 768) { - return 42; - } - if (protocolVersion >= 766) { - return 34; - } - return 0; - } -} diff --git a/type.skyblockgeneric/src/main/java/net/swofty/type/skyblockgeneric/resourcepack/HypixelSkyblockPackApi.java b/type.skyblockgeneric/src/main/java/net/swofty/type/skyblockgeneric/resourcepack/HypixelSkyblockPackApi.java index 3325cd9b6..c29a2cce6 100644 --- a/type.skyblockgeneric/src/main/java/net/swofty/type/skyblockgeneric/resourcepack/HypixelSkyblockPackApi.java +++ b/type.skyblockgeneric/src/main/java/net/swofty/type/skyblockgeneric/resourcepack/HypixelSkyblockPackApi.java @@ -1,5 +1,6 @@ package net.swofty.type.skyblockgeneric.resourcepack; +import net.swofty.commons.MinecraftVersion; import org.json.JSONArray; import org.json.JSONObject; @@ -110,14 +111,22 @@ Version latest() { } Version forProtocol(int protocolVersion) { - int packFormat = HypixelPackFormatResolver.packFormatForProtocol(protocolVersion); - if (packFormat == Integer.MAX_VALUE) { - return latest(); - } - if (packFormat <= 0) { + MinecraftVersion minecraftVersion = MinecraftVersion.byProtocol(protocolVersion); + if (minecraftVersion == null) { + if (protocolVersion > MinecraftVersion.latest().getProtocolVersion()) { + return latest(); + } return null; } - return versions.get(packFormat); + + Version version = versions.get(minecraftVersion.getPackVersion()); + if (version != null) { + return version; + } + if (minecraftVersion.getPackVersion() > versions.lastKey()) { + return latest(); + } + return null; } } diff --git a/type.skyblockgeneric/src/main/java/net/swofty/type/skyblockgeneric/resourcepack/SkyblockPack.java b/type.skyblockgeneric/src/main/java/net/swofty/type/skyblockgeneric/resourcepack/SkyblockPack.java index 33dded5e6..fdac6c63a 100644 --- a/type.skyblockgeneric/src/main/java/net/swofty/type/skyblockgeneric/resourcepack/SkyblockPack.java +++ b/type.skyblockgeneric/src/main/java/net/swofty/type/skyblockgeneric/resourcepack/SkyblockPack.java @@ -80,7 +80,7 @@ public PackInfo getPackFor(HypixelPlayer player) { } try { - int protocolVersion = player.asProxyPlayer().getVersion().get(3, TimeUnit.SECONDS); + int protocolVersion = player.asProxyPlayer().getProtocolVersion().get(3, TimeUnit.SECONDS); HypixelSkyblockPackApi.Version version = officialPacks.forProtocol(protocolVersion); if (version == null) { Logger.warn("No official Hypixel SkyBlock resource pack is available for protocol version {}", protocolVersion); diff --git a/type.skyblockgeneric/src/test/java/net/swofty/type/skyblockgeneric/resourcepack/HypixelSkyblockPackApiTest.java b/type.skyblockgeneric/src/test/java/net/swofty/type/skyblockgeneric/resourcepack/HypixelSkyblockPackApiTest.java deleted file mode 100644 index 64dcb22a7..000000000 --- a/type.skyblockgeneric/src/test/java/net/swofty/type/skyblockgeneric/resourcepack/HypixelSkyblockPackApiTest.java +++ /dev/null @@ -1,30 +0,0 @@ -package net.swofty.type.skyblockgeneric.resourcepack; - -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; - -class HypixelSkyblockPackApiTest { - @Test - void selectsTheOfficialPackForEachSupportedProtocol() { - HypixelSkyblockPackApi.Catalog catalog = HypixelSkyblockPackApi.parse(""" - { - "success": true, - "packs": [{ - "id": "SkyBlock", - "versions": [ - {"packFormat": 84, "hash": "hash-84", "url": "https://example.test/84.zip"}, - {"packFormat": 88, "hash": "hash-88", "url": "https://example.test/88.zip"}, - {"packFormat": 75, "hash": "hash-75", "url": "https://example.test/75.zip"} - ] - }] - } - """); - - assertEquals("hash-75", catalog.forProtocol(774).hash()); - assertEquals("hash-84", catalog.forProtocol(775).hash()); - assertEquals("hash-88", catalog.forProtocol(776).hash()); - assertNull(catalog.forProtocol(773)); - } -} diff --git a/velocity.extension/src/main/java/net/swofty/velocity/redis/listeners/ListenerPlayerHandler.java b/velocity.extension/src/main/java/net/swofty/velocity/redis/listeners/ListenerPlayerHandler.java index 6868b754b..a788400cc 100644 --- a/velocity.extension/src/main/java/net/swofty/velocity/redis/listeners/ListenerPlayerHandler.java +++ b/velocity.extension/src/main/java/net/swofty/velocity/redis/listeners/ListenerPlayerHandler.java @@ -62,11 +62,13 @@ public PlayerHandlerProtocol.Response handle(PlayerHandlerProtocol.Request messa switch (action) { case VERSION -> { - int version = player.getProtocolVersion().getProtocol(); + int protocolVersion = player.getProtocolVersion().getProtocol(); if (ConfigProvider.settings().getIntegrations().isViaVersion()) { - version = Via.getAPI().getPlayerVersion(uuid); + protocolVersion = Via.getAPI().getPlayerVersion(uuid); } - return new PlayerHandlerProtocol.Response(Map.of("version", version), true, null); + return new PlayerHandlerProtocol.Response(Map.of( + "protocolVersion", protocolVersion, + "version", protocolVersion), true, null); } case RESOLVE_TRANSFER -> { ServerType type = ServerType.valueOf((String) data.get("type"));