diff options
| author | Aaron <51387595+AzureAaron@users.noreply.github.com> | 2024-08-05 14:26:00 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-08-05 14:26:00 -0400 |
| commit | 088583a5a5fff6758748e152d30de0adbbb12388 (patch) | |
| tree | 536f0c3588c2dc61ca78d30684b57c681477c483 /src/main/java | |
| parent | d99528b34b6f7afed380c6c655ecc461e0082d46 (diff) | |
| download | Skyblocker-088583a5a5fff6758748e152d30de0adbbb12388.tar.gz Skyblocker-088583a5a5fff6758748e152d30de0adbbb12388.tar.bz2 Skyblocker-088583a5a5fff6758748e152d30de0adbbb12388.zip | |
Crystal Waypoints server-sided sharing via WebSocket (#895)
Diffstat (limited to 'src/main/java')
13 files changed, 440 insertions, 9 deletions
diff --git a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java index 6982ad21..4e110e15 100644 --- a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java +++ b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java @@ -60,6 +60,8 @@ import de.hysky.skyblocker.utils.container.ContainerSolverManager; import de.hysky.skyblocker.utils.render.title.TitleContainer; import de.hysky.skyblocker.utils.scheduler.MessageScheduler; import de.hysky.skyblocker.utils.scheduler.Scheduler; +import de.hysky.skyblocker.utils.ws.SkyblockerWebSocket; +import de.hysky.skyblocker.utils.ws.WsStateManager; import net.fabricmc.api.ClientModInitializer; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; import net.fabricmc.loader.api.FabricLoader; @@ -186,6 +188,8 @@ public class SkyblockerMod implements ClientModInitializer { SecretsTracker.init(); ApiAuthentication.init(); ApiUtils.init(); + SkyblockerWebSocket.init(); + WsStateManager.init(); Debug.init(); Kuudra.init(); DojoManager.init(); diff --git a/src/main/java/de/hysky/skyblocker/debug/Debug.java b/src/main/java/de/hysky/skyblocker/debug/Debug.java index 446de66f..f1240a1c 100644 --- a/src/main/java/de/hysky/skyblocker/debug/Debug.java +++ b/src/main/java/de/hysky/skyblocker/debug/Debug.java @@ -29,6 +29,7 @@ public class Debug { private static final boolean DEBUG_ENABLED = Boolean.parseBoolean(System.getProperty("skyblocker.debug", "false")); private static boolean showInvisibleArmorStands = false; + private static boolean webSocketDebug = false; public static boolean debugEnabled() { return DEBUG_ENABLED || FabricLoader.getInstance().isDevelopmentEnvironment(); @@ -38,6 +39,10 @@ public class Debug { return showInvisibleArmorStands; } + public static boolean webSocketDebug() { + return webSocketDebug; + } + public static void init() { if (debugEnabled()) { SnapshotDebug.init(); @@ -46,6 +51,7 @@ public class Debug { .then(ItemUtils.dumpHeldItemCommand()) .then(toggleShowingInvisibleArmorStands()) .then(dumpArmorStandHeadTextures()) + .then(toggleWebSocketDebug()) ))); ScreenEvents.BEFORE_INIT.register((client, screen, scaledWidth, scaledHeight) -> { if (screen instanceof HandledScreen<?> handledScreen) { @@ -76,6 +82,15 @@ public class Debug { return Command.SINGLE_SUCCESS; }); } + + private static LiteralArgumentBuilder<FabricClientCommandSource> toggleWebSocketDebug() { + return literal("toggleWebSocketDebug") + .executes(context -> { + webSocketDebug = !webSocketDebug; + context.getSource().sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.debug.toggledWebSocketDebug", webSocketDebug))); + return Command.SINGLE_SUCCESS; + }); + } private static LiteralArgumentBuilder<FabricClientCommandSource> dumpArmorStandHeadTextures() { return literal("dumpArmorStandHeadTextures") diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dwarven/CrystalsLocationsManager.java b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/CrystalsLocationsManager.java index 22e494ab..8b8a7737 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/dwarven/CrystalsLocationsManager.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/CrystalsLocationsManager.java @@ -6,12 +6,18 @@ import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.logging.LogUtils; import de.hysky.skyblocker.SkyblockerMod; import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.events.SkyblockEvents; import de.hysky.skyblocker.utils.Constants; +import de.hysky.skyblocker.utils.Location; import de.hysky.skyblocker.utils.Utils; import de.hysky.skyblocker.utils.command.argumenttypes.blockpos.ClientBlockPosArgumentType; import de.hysky.skyblocker.utils.command.argumenttypes.blockpos.ClientPosArgument; import de.hysky.skyblocker.utils.scheduler.MessageScheduler; import de.hysky.skyblocker.utils.scheduler.Scheduler; +import de.hysky.skyblocker.utils.ws.WsMessageHandler; +import de.hysky.skyblocker.utils.ws.Service; +import de.hysky.skyblocker.utils.ws.WsStateManager; +import de.hysky.skyblocker.utils.ws.message.CrystalsWaypointMessage; import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents; @@ -60,6 +66,7 @@ public class CrystalsLocationsManager { protected static Map<String, MiningLocationLabel> activeWaypoints = new HashMap<>(); protected static List<String> verifiedWaypoints = new ArrayList<>(); + private static List<MiningLocationLabel.CrystalHollowsLocationsCategory> waypointsSent2Socket = new ArrayList<>(); public static void init() { // Crystal Hollows Waypoints @@ -67,6 +74,7 @@ public class CrystalsLocationsManager { WorldRenderEvents.AFTER_TRANSLUCENT.register(CrystalsLocationsManager::render); ClientReceiveMessageEvents.GAME.register(CrystalsLocationsManager::extractLocationFromMessage); ClientCommandRegistrationCallback.EVENT.register(CrystalsLocationsManager::registerWaypointLocationCommands); + SkyblockEvents.LOCATION_CHANGE.register(CrystalsLocationsManager::onLocationChange); ClientPlayConnectionEvents.JOIN.register((_handler, _sender, _client) -> reset()); // Nucleus Waypoints @@ -127,6 +135,7 @@ public class CrystalsLocationsManager { if (waypointLinkedMessage != null && text.contains(waypointLinkedMessage) && !verifiedWaypoints.contains(waypointName)) { addCustomWaypoint(waypointLocation.getName(), CLIENT.player.getBlockPos()); verifiedWaypoints.add(waypointName); + trySendWaypoint2Socket(waypointLocation); } } } @@ -311,6 +320,14 @@ public class CrystalsLocationsManager { return Command.SINGLE_SUCCESS; } + public static void addCustomWaypointFromSocket(MiningLocationLabel.CrystalHollowsLocationsCategory category, BlockPos pos) { + if (activeWaypoints.containsKey(category.name())) return; + + removeUnknownNear(pos); + MiningLocationLabel waypoint = new MiningLocationLabel(category, pos); + waypointsSent2Socket.add(category); + activeWaypoints.put(category.name(), waypoint); + } protected static void addCustomWaypoint(String waypointName, BlockPos pos) { removeUnknownNear(pos); @@ -335,7 +352,7 @@ public class CrystalsLocationsManager { } } - public static void render(WorldRenderContext context) { + private static void render(WorldRenderContext context) { if (SkyblockerConfigManager.get().mining.crystalsWaypoints.enabled) { for (MiningLocationLabel crystalsWaypoint : activeWaypoints.values()) { crystalsWaypoint.render(context); @@ -343,23 +360,41 @@ public class CrystalsLocationsManager { } } + private static void onLocationChange(Location newLocation) { + if (newLocation == Location.CRYSTAL_HOLLOWS) { + WsStateManager.subscribe(Service.CRYSTAL_WAYPOINTS); + } + } + private static void reset() { activeWaypoints.clear(); verifiedWaypoints.clear(); + waypointsSent2Socket.clear(); } - public static void update() { + private static void update() { if (CLIENT.player == null || CLIENT.getNetworkHandler() == null || !SkyblockerConfigManager.get().mining.crystalsWaypoints.enabled || !Utils.isInCrystalHollows()) { return; } //get if the player is in the crystals String location = Utils.getIslandArea().substring(2); - //if new location and needs waypoint add waypoint - if (!location.equals("Unknown") && WAYPOINT_LOCATIONS.containsKey(location) && !activeWaypoints.containsKey(location)) { - //add waypoint at player location - BlockPos playerLocation = CLIENT.player.getBlockPos(); - addCustomWaypoint(location, playerLocation); + //if new location and needs waypoint add waypoint, and if socket hasn't received waypoint send it + if (!location.equals("Unknown") && WAYPOINT_LOCATIONS.containsKey(location)) { + if (!activeWaypoints.containsKey(location)) { + //add waypoint at player location + BlockPos playerLocation = CLIENT.player.getBlockPos(); + addCustomWaypoint(location, playerLocation); + } + + trySendWaypoint2Socket(WAYPOINT_LOCATIONS.get(location)); + } + } + + private static void trySendWaypoint2Socket(MiningLocationLabel.CrystalHollowsLocationsCategory category) { + if (!waypointsSent2Socket.contains(category)) { + WsMessageHandler.sendMessage(Service.CRYSTAL_WAYPOINTS, new CrystalsWaypointMessage(category, CLIENT.player.getBlockPos())); + waypointsSent2Socket.add(category); } } } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dwarven/MiningLocationLabel.java b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/MiningLocationLabel.java index 3817f6c7..eb40088f 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/dwarven/MiningLocationLabel.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/MiningLocationLabel.java @@ -9,11 +9,14 @@ import net.minecraft.client.MinecraftClient; import net.minecraft.text.Text; import net.minecraft.util.DyeColor; import net.minecraft.util.Formatting; +import net.minecraft.util.StringIdentifiable; import net.minecraft.util.math.BlockPos; import net.minecraft.util.math.Vec3d; import java.awt.*; +import com.mojang.serialization.Codec; + // TODO: Clean up into the waypoint system with a new `DistancedWaypoint` that extends `NamedWaypoint` for this and secret waypoints. public record MiningLocationLabel(Category category, Vec3d centerPos) implements Renderable { public MiningLocationLabel(Category category, BlockPos pos) { @@ -166,7 +169,7 @@ public record MiningLocationLabel(Category category, Vec3d centerPos) implements /** * enum for the different waypoints used int the crystals hud each with a {@link CrystalHollowsLocationsCategory#name} and associated {@link CrystalHollowsLocationsCategory#color} */ - enum CrystalHollowsLocationsCategory implements Category { + public enum CrystalHollowsLocationsCategory implements Category, StringIdentifiable { UNKNOWN("Unknown", Color.WHITE, null), //used when a location is known but what's at the location is not known JUNGLE_TEMPLE("Jungle Temple", new Color(DyeColor.PURPLE.getSignColor()), "[NPC] Kalhuiki Door Guardian:"), MINES_OF_DIVAN("Mines of Divan", Color.GREEN, " Jade Crystal"), @@ -180,6 +183,8 @@ public record MiningLocationLabel(Category category, Vec3d centerPos) implements ODAWA("Odawa", Color.MAGENTA, "[NPC] Odawa:"), KEY_GUARDIAN("Key Guardian", Color.LIGHT_GRAY, null); + public static final Codec<CrystalHollowsLocationsCategory> CODEC = StringIdentifiable.createBasicCodec(CrystalHollowsLocationsCategory::values); + public final Color color; private final String name; private final String linkedMessage; @@ -203,6 +208,11 @@ public record MiningLocationLabel(Category category, Vec3d centerPos) implements public String getLinkedMessage() { return this.linkedMessage; } + + @Override + public String asString() { + return name(); + } } } diff --git a/src/main/java/de/hysky/skyblocker/utils/Http.java b/src/main/java/de/hysky/skyblocker/utils/Http.java index 051bd52e..227cc5a7 100644 --- a/src/main/java/de/hysky/skyblocker/utils/Http.java +++ b/src/main/java/de/hysky/skyblocker/utils/Http.java @@ -27,7 +27,7 @@ import java.util.zip.InflaterInputStream; public class Http { private static final String NAME_2_UUID = "https://api.minecraftservices.com/minecraft/profile/lookup/name/"; private static final String HYPIXEL_PROXY = "https://hysky.de/api/hypixel/v2/"; - private static final String USER_AGENT = "Skyblocker/" + SkyblockerMod.VERSION + " (" + SharedConstants.getGameVersion().getName() + ")"; + public static final String USER_AGENT = "Skyblocker/" + SkyblockerMod.VERSION + " (" + SharedConstants.getGameVersion().getName() + ")"; private static final HttpClient HTTP_CLIENT = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(10)) .followRedirects(Redirect.NORMAL) diff --git a/src/main/java/de/hysky/skyblocker/utils/ws/Payload.java b/src/main/java/de/hysky/skyblocker/utils/ws/Payload.java new file mode 100644 index 00000000..c943f371 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/ws/Payload.java @@ -0,0 +1,16 @@ +package de.hysky.skyblocker.utils.ws; + +import java.util.Optional; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.Dynamic; +import com.mojang.serialization.codecs.RecordCodecBuilder; + +record Payload(Type type, Service service, String serverId, Optional<Dynamic<?>> message) { + static final Codec<Payload> CODEC = RecordCodecBuilder.create(instance -> instance.group( + Type.CODEC.fieldOf("type").forGetter(Payload::type), + Service.CODEC.fieldOf("service").forGetter(Payload::service), + Codec.STRING.fieldOf("serverId").forGetter(Payload::serverId), + Codec.PASSTHROUGH.optionalFieldOf("message").forGetter(Payload::message)) + .apply(instance, Payload::new)); +} diff --git a/src/main/java/de/hysky/skyblocker/utils/ws/Service.java b/src/main/java/de/hysky/skyblocker/utils/ws/Service.java new file mode 100644 index 00000000..f26e90bb --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/ws/Service.java @@ -0,0 +1,16 @@ +package de.hysky.skyblocker.utils.ws; + +import com.mojang.serialization.Codec; + +import net.minecraft.util.StringIdentifiable; + +public enum Service implements StringIdentifiable { + CRYSTAL_WAYPOINTS; + + public static final Codec<Service> CODEC = StringIdentifiable.createBasicCodec(Service::values); + + @Override + public String asString() { + return name(); + } +} diff --git a/src/main/java/de/hysky/skyblocker/utils/ws/SkyblockerWebSocket.java b/src/main/java/de/hysky/skyblocker/utils/ws/SkyblockerWebSocket.java new file mode 100644 index 00000000..be2b979b --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/ws/SkyblockerWebSocket.java @@ -0,0 +1,141 @@ +package de.hysky.skyblocker.utils.ws; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Redirect; +import java.net.http.HttpClient.Version; +import java.net.http.WebSocket; +import java.nio.ByteBuffer; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import org.slf4j.Logger; + +import com.mojang.logging.LogUtils; + +import de.hysky.skyblocker.debug.Debug; +import de.hysky.skyblocker.events.SkyblockEvents; +import de.hysky.skyblocker.utils.ApiAuthentication; +import de.hysky.skyblocker.utils.Http; +import net.minecraft.util.Util; + +public class SkyblockerWebSocket { + private static final Logger LOGGER = LogUtils.getLogger(); + private static final String WS_URL = "wss://ws.hysky.de"; + private static final HttpClient HTTP_CLIENT = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(10)) + .followRedirects(Redirect.NORMAL) + .version(Version.HTTP_2) + .build(); + private static final ExecutorService MESSAGE_SEND_QUEUE = Executors.newSingleThreadExecutor(Thread.ofVirtual() + .name("Skyblocker WebSocket Message Send Queue") + .factory()); + + private static volatile WebSocket socket; + + public static void init() { + SkyblockEvents.JOIN.register(() -> { + if (!isConnectionOpen()) setupSocket(); + }); + } + + private static CompletableFuture<Void> setupSocket() { + return CompletableFuture.runAsync(() -> { + try { + socket = HTTP_CLIENT.newWebSocketBuilder() + .header("Authorization", "Bearer " + Objects.requireNonNull(ApiAuthentication.getToken(), "Token cannot be null")) + .header("User-Agent", Http.USER_AGENT) + .buildAsync(URI.create(WS_URL), new SocketListener()) + .get(); + + LOGGER.info("[Skyblocker WebSocket] Successfully connected to the Skyblocker WebSocket!"); + } catch (Exception e) { + LOGGER.error("[Skyblocker WebSocket] Failed to setup WebSocket connection!", e); + } + }); + } + + private static boolean isConnectionOpen() { + return socket != null && !socket.isInputClosed() && !socket.isOutputClosed(); + } + + static void send(String message) { + if (isConnectionOpen()) { + sendInternal(message); + } else { + setupSocket().thenRun(() -> sendInternal(message)); + } + } + + private static void sendInternal(String message) { + MESSAGE_SEND_QUEUE.submit(() -> { + try { + if (Debug.debugEnabled() && Debug.webSocketDebug()) LOGGER.info("[Skyblocker WebSocket] Sending Message: {}", message); + + socket.sendText(message, true).join(); + } catch (Exception e) { + LOGGER.error("[Skyblocker WebSocket] Failed to send message!", e); + } + }); + } + + private static class SocketListener implements WebSocket.Listener { + private List<CharSequence> parts = new ArrayList<>(); + private CompletableFuture<?> accumulatedMessage = new CompletableFuture<>(); + + @Override + public CompletionStage<?> onText(WebSocket webSocket, CharSequence message, boolean last) { + parts.add(message); + webSocket.request(1); + + if (last) { + //Process message once we've got all the text + handleWholeMessage(parts); + + //Reset state and allow CharSequences to be reclaimed or something? Java WebSockets are very confusing + parts = new ArrayList<>(); + accumulatedMessage.complete(null); + CompletionStage<?> future = accumulatedMessage; + accumulatedMessage = new CompletableFuture<>(); + + return future; + } + + return accumulatedMessage; + } + + @Override + public CompletionStage<?> onPing(WebSocket webSocket, ByteBuffer message) { + if (Debug.debugEnabled() && Debug.webSocketDebug()) LOGGER.info("[Skyblocker WebSocket] Received ping"); + + return WebSocket.Listener.super.onPing(webSocket, message); + } + + @Override + public CompletionStage<?> onClose(WebSocket webSocket, int statusCode, String reason) { + LOGGER.info("[Skyblocker WebSocket] Connection closing. Status Code: {}, Reason: {}", statusCode, reason); + + return WebSocket.Listener.super.onClose(webSocket, statusCode, reason); + } + + @Override + public void onError(WebSocket webSocket, Throwable error) { + LOGGER.error("[Skyblocker WebSocket] Encountered an error and closed the connection!", error); + } + + private void handleWholeMessage(List<CharSequence> parts) { + StringBuilder builder = Util.make(new StringBuilder(), sb -> parts.forEach(sb::append)); + String message = builder.toString(); + + if (Debug.debugEnabled() && Debug.webSocketDebug()) LOGGER.info("[Skyblocker WebSocket] Received Message: {}", message); + + WsMessageHandler.handleMessage(message); + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/utils/ws/Type.java b/src/main/java/de/hysky/skyblocker/utils/ws/Type.java new file mode 100644 index 00000000..c4a0e283 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/ws/Type.java @@ -0,0 +1,26 @@ +package de.hysky.skyblocker.utils.ws; + +import com.mojang.serialization.Codec; + +import net.minecraft.util.StringIdentifiable; + +public enum Type implements StringIdentifiable { + SUBSCRIBE("subscribe"), + INITIAL_MESSAGE("initialMessage"), + PUBLISH("publish"), + RESPONSE("response"), + UNSUBSCRIBE("unsubscribe"); + + public static final Codec<Type> CODEC = StringIdentifiable.createBasicCodec(Type::values); + + private final String id; + + Type(String id) { + this.id = id; + } + + @Override + public String asString() { + return this.id; + } +} diff --git a/src/main/java/de/hysky/skyblocker/utils/ws/WsMessageHandler.java b/src/main/java/de/hysky/skyblocker/utils/ws/WsMessageHandler.java new file mode 100644 index 00000000..d85ccbb8 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/ws/WsMessageHandler.java @@ -0,0 +1,72 @@ +package de.hysky.skyblocker.utils.ws; + +import java.util.Optional; + +import org.slf4j.Logger; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.mojang.logging.LogUtils; +import com.mojang.serialization.Codec; +import com.mojang.serialization.Dynamic; +import com.mojang.serialization.JsonOps; + +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.utils.Utils; +import de.hysky.skyblocker.utils.ws.message.CrystalsWaypointMessage; +import de.hysky.skyblocker.utils.ws.message.Message; + +public class WsMessageHandler { + private static final Logger LOGGER = LogUtils.getLogger(); + + /** + * Used for sending messages to the current channel/server + */ + @SuppressWarnings("unchecked") + public static void sendMessage(Service service, Message<? extends Message<?>> message) { + try { + Codec<Message<?>> codec = (Codec<Message<?>>) message.getCodec(); + Dynamic<JsonElement> dynamic = new Dynamic<>(JsonOps.INSTANCE, codec.encodeStart(JsonOps.INSTANCE, message).getOrThrow()); + + send(Type.PUBLISH, service, Utils.getServer(), Optional.of(dynamic)); + } catch (Exception e) { + LOGGER.info("[Skyblocker WebSocket Message Handler] Failed to encode message! Message: {}", message, e); + } + } + + /** + * Useful for sending simple state updates + */ + static void sendSimple(Type type, Service service, String serverId) { + send(type, service, serverId, Optional.empty()); + } + + private static void send(Type type, Service service, String serverId, Optional<Dynamic<?>> message) { + try { + Payload payload = new Payload(type, service, serverId, message); + JsonObject encoded = Payload.CODEC.encodeStart(JsonOps.INSTANCE, payload).getOrThrow().getAsJsonObject(); + + SkyblockerWebSocket.send(SkyblockerMod.GSON_COMPACT.toJson(encoded)); + } catch (Exception e) { + LOGGER.info("[Skyblocker WebSocket Message Handler] Failed to send message! Type: {}, Service: {}, Message: {}", type, service, message, e); + } + } + + static void handleMessage(String message) { + try { + JsonObject payloadEncoded = JsonParser.parseString(message).getAsJsonObject(); + + //When status is present its usually a response to a packet being sent or some error, we don't need to pay attention to those + if (payloadEncoded.has("type")) { + Payload payload = Payload.CODEC.parse(JsonOps.INSTANCE, payloadEncoded).getOrThrow(); + + switch (payload.service()) { + case Service.CRYSTAL_WAYPOINTS -> CrystalsWaypointMessage.handle(payload.type(), payload.message()); + } + } + } catch (Exception e) { + LOGGER.error("[Skyblocker WebSocket Message Handler] Failed to handle incoming message!", e); + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/utils/ws/WsStateManager.java b/src/main/java/de/hysky/skyblocker/utils/ws/WsStateManager.java new file mode 100644 index 00000000..6715c1f6 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/ws/WsStateManager.java @@ -0,0 +1,39 @@ +package de.hysky.skyblocker.utils.ws; + +import de.hysky.skyblocker.events.SkyblockEvents; +import de.hysky.skyblocker.utils.Utils; +import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; +import it.unimi.dsi.fastutil.objects.ReferenceSet; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; + +public class WsStateManager { + private static final ReferenceSet<Service> SUBSCRIBED_SERVICES = new ReferenceOpenHashSet<>(); + private static String lastServerId = ""; + + public static void init() { + ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> reset()); + ClientPlayConnectionEvents.DISCONNECT.register((handler, client) -> reset()); + } + + private static void reset() { + if (!lastServerId.isEmpty()) { + for (Service service : SUBSCRIBED_SERVICES) { + WsMessageHandler.sendSimple(Type.UNSUBSCRIBE, service, lastServerId); + } + + lastServerId = ""; + } + } + + /** + * @implNote The service must be registered after the {@link ClientPlayConnectionEvents#JOIN} event fires, one good + * place is inside of the {@link SkyblockEvents#LOCATION_CHANGE} event. + */ + public static void subscribe(Service service) { + SUBSCRIBED_SERVICES.add(service); + WsMessageHandler.sendSimple(Type.SUBSCRIBE, service, Utils.getServer()); + + //Update tracked server id + lastServerId = Utils.getServer(); + } +} diff --git a/src/main/java/de/hysky/skyblocker/utils/ws/message/CrystalsWaypointMessage.java b/src/main/java/de/hysky/skyblocker/utils/ws/message/CrystalsWaypointMessage.java new file mode 100644 index 00000000..8e9ed87f --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/ws/message/CrystalsWaypointMessage.java @@ -0,0 +1,49 @@ +package de.hysky.skyblocker.utils.ws.message; + +import java.util.List; +import java.util.Optional; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.Dynamic; +import com.mojang.serialization.codecs.RecordCodecBuilder; + +import de.hysky.skyblocker.skyblock.dwarven.CrystalsLocationsManager; +import de.hysky.skyblocker.skyblock.dwarven.MiningLocationLabel.CrystalHollowsLocationsCategory; +import de.hysky.skyblocker.utils.render.RenderHelper; +import de.hysky.skyblocker.utils.ws.Type; +import net.minecraft.util.math.BlockPos; + +public record CrystalsWaypointMessage(CrystalHollowsLocationsCategory location, BlockPos coordinates) implements Message<CrystalsWaypointMessage> { + private static final Codec<CrystalsWaypointMessage> CODEC = RecordCodecBuilder.create(instance -> instance.group( + CrystalHollowsLocationsCategory.CODEC.fieldOf("name").forGetter(CrystalsWaypointMessage::location), + BlockPos.CODEC.fieldOf("coordinates").forGetter(CrystalsWaypointMessage::coordinates)) + .apply(instance, CrystalsWaypointMessage::new)); + private static final Codec<List<CrystalsWaypointMessage>> LIST_CODEC = CODEC.listOf(); + + public static void handle(Type type, Optional<Dynamic<?>> message) { + switch (type) { + case Type.RESPONSE -> { + CrystalsWaypointMessage waypoint = CODEC.parse(message.get()).getOrThrow(); + + RenderHelper.runOnRenderThread(() -> CrystalsLocationsManager.addCustomWaypointFromSocket(waypoint.location(), waypoint.coordinates())); + } + + case Type.INITIAL_MESSAGE -> { + List<CrystalsWaypointMessage> waypoints = LIST_CODEC.parse(message.get()).getOrThrow(); + + RenderHelper.runOnRenderThread(() -> { + for (CrystalsWaypointMessage waypoint : waypoints) { + CrystalsLocationsManager.addCustomWaypointFromSocket(waypoint.location(), waypoint.coordinates()); + } + }); + } + + default -> {} + } + } + + @Override + public Codec<CrystalsWaypointMessage> getCodec() { + return CODEC; + } +} diff --git a/src/main/java/de/hysky/skyblocker/utils/ws/message/Message.java b/src/main/java/de/hysky/skyblocker/utils/ws/message/Message.java new file mode 100644 index 00000000..2d145ca4 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/ws/message/Message.java @@ -0,0 +1,8 @@ +package de.hysky.skyblocker.utils.ws.message; + +import com.mojang.serialization.Codec; + +public sealed interface Message<T extends Message<T>> permits CrystalsWaypointMessage { + + Codec<T> getCodec(); +} |
