diff --git a/pom.xml b/pom.xml
index 3623c9f..cfda79c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
net.hypixel
mod-api
- dev-SNAPSHOT
+ registerv2-SNAPSHOT
8
diff --git a/src/main/java/net/hypixel/modapi/EventSubscriptionHandler.java b/src/main/java/net/hypixel/modapi/EventSubscriptionHandler.java
new file mode 100644
index 0000000..c2923a4
--- /dev/null
+++ b/src/main/java/net/hypixel/modapi/EventSubscriptionHandler.java
@@ -0,0 +1,133 @@
+package net.hypixel.modapi;
+
+import net.hypixel.modapi.error.BuiltinErrorReason;
+import net.hypixel.modapi.error.ErrorReason;
+import net.hypixel.modapi.handler.ClientboundPacketHandler;
+import net.hypixel.modapi.handler.ErrorHandler;
+import net.hypixel.modapi.packet.impl.clientbound.ClientboundRegisterPacket;
+import net.hypixel.modapi.packet.impl.serverbound.ServerboundRegisterPacket;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Deals with all the logic surrounding event subscription.
+ *
+ * Some things to note:
+ *
+ * - The last sent set of events are always the state stored on the server, so the whole events list has to be sent.
+ * - The server has a small cooldown between sending registration packets, this can result in receiving a RATE_LIMITED response if this cooldown is hit.
+ * - Because of the above, we attempt to batch subscriptions, so when we first get a subscribe call we wait before sending the packet in-case more follow.
+ * - If we are ever RATE_LIMITED, we will attempt to send the registration again shortly after.
+ *
+ */
+class EventSubscriptionHandler implements ClientboundPacketHandler, ErrorHandler {
+ private static final ScheduledExecutorService SCHEDULER = Executors.newSingleThreadScheduledExecutor(r -> {
+ Thread thread = new Thread(r, "HypixelEventSubscriptionHandler");
+ thread.setDaemon(true);
+ return thread;
+ });
+ private static final long DEFAULT_RESEND_DELAY_MS = 5_000L;
+
+ private final HypixelModAPI api;
+
+ // Indicates we are pending to send the registration shortly
+ private final AtomicBoolean sendingScheduled = new AtomicBoolean(false);
+
+ // Indicates we are waiting for a response to the registration, and we should not send another until we get one (or timeout)
+ private final AtomicBoolean pendingResponse = new AtomicBoolean(false);
+ // The current packet identifier, used to match responses to requests
+ private final AtomicInteger packetIdentifier = new AtomicInteger(0);
+
+ private final Set subscribedEvents = ConcurrentHashMap.newKeySet();
+ private Set lastSubscribedEvents = Collections.emptySet();
+
+ EventSubscriptionHandler(HypixelModAPI api) {
+ this.api = api;
+ }
+
+ void subscribeToEventPacket(String identifier) {
+ if (subscribedEvents.add(identifier)) {
+ sendRegisterPacket(false);
+ }
+ }
+
+ void sendRegisterPacket(boolean alwaysSendIfNotEmpty) {
+ if (!api.isImplementationSet()) {
+ // Allow registering events before the mod has fully initialized
+ return;
+ }
+
+ if (lastSubscribedEvents.equals(subscribedEvents) && !(alwaysSendIfNotEmpty && !subscribedEvents.isEmpty())) {
+ return;
+ }
+
+ if (!sendingScheduled.compareAndSet(false, true)) {
+ return;
+ }
+
+ // We wait 500ms before sending the packet, this gives further subscribe calls a chance to batch (such as mods registering them during server-join)
+ SCHEDULER.schedule(() -> {
+ sendingScheduled.set(false);
+ actuallySendRegisterPacket();
+ }, 500, TimeUnit.MILLISECONDS);
+ }
+
+ void actuallySendRegisterPacket() {
+ if (!pendingResponse.compareAndSet(false, true)) {
+ // We are already waiting for a response, so we should instead schedule to update it again later
+ scheduleResend(DEFAULT_RESEND_DELAY_MS);
+ return;
+ }
+
+ Set lastSubscribedEvents = new HashSet<>(subscribedEvents);
+ int requestIdentifier = packetIdentifier.incrementAndGet();
+
+ if (api.sendPacket(new ServerboundRegisterPacket(api.getRegistry(), requestIdentifier, lastSubscribedEvents))) {
+ this.lastSubscribedEvents = lastSubscribedEvents;
+
+ // Schedule a 5s timeout for getting a response on the registration
+ SCHEDULER.schedule(() -> {
+ if (pendingResponse.compareAndSet(true, false)) {
+ // If we timed out, try again in 5s
+ scheduleResend(DEFAULT_RESEND_DELAY_MS);
+ }
+ }, 5, TimeUnit.SECONDS);
+ } else {
+ // If we failed to send the packet, unset the pending response flag
+ pendingResponse.set(false);
+ }
+ }
+
+ @Override
+ public void handle(ClientboundRegisterPacket packet) {
+ int requestIdentifier = packet.getRequestIdentifier();
+ if (requestIdentifier != packetIdentifier.get()) {
+ throw new IllegalStateException("Received a response for an unknown request identifier: " + requestIdentifier);
+ }
+
+ pendingResponse.set(false);
+ }
+
+ @Override
+ public void onError(ErrorReason reason) {
+ pendingResponse.set(false);
+
+ if (reason != BuiltinErrorReason.RATE_LIMITED) {
+ throw new IllegalStateException("Failed to send register packet: " + reason);
+ }
+
+ scheduleResend(DEFAULT_RESEND_DELAY_MS);
+ }
+
+ private void scheduleResend(long delayMs) {
+ SCHEDULER.schedule(this::actuallySendRegisterPacket, delayMs, TimeUnit.MILLISECONDS);
+ }
+}
diff --git a/src/main/java/net/hypixel/modapi/HypixelModAPI.java b/src/main/java/net/hypixel/modapi/HypixelModAPI.java
index c2a542c..a29c44b 100644
--- a/src/main/java/net/hypixel/modapi/HypixelModAPI.java
+++ b/src/main/java/net/hypixel/modapi/HypixelModAPI.java
@@ -18,7 +18,8 @@
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;
-import java.util.*;
+import java.util.Collection;
+import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
@@ -31,8 +32,7 @@ public static HypixelModAPI getInstance() {
private final PacketRegistry registry = new PacketRegistry();
private final Map>> handlers = new ConcurrentHashMap<>();
- private final Set subscribedEvents = ConcurrentHashMap.newKeySet();
- private Set lastSubscribedEvents = Collections.emptySet();
+ private final EventSubscriptionHandler eventSubscriptionHandler = new EventSubscriptionHandler(this);
@Nullable
private HypixelModAPIImplementation modImplementation;
@@ -62,6 +62,7 @@ private void registerHypixelPackets() {
.register();
registry.define("hypixel:register")
+ .clientbound(ClientboundRegisterPacket.class, ClientboundRegisterPacket::new)
.serverbound(ServerboundRegisterPacket.class, ServerboundRegisterPacket::new)
.register();
}
@@ -72,24 +73,13 @@ private void registerEventPackets() {
.register();
}
- private void registerDefaultHandler() {
- createHandler(ClientboundHelloPacket.class, p -> sendRegisterPacket(true));
+ private void registerDefaultHandlers() {
+ createHandler(ClientboundRegisterPacket.class, eventSubscriptionHandler).onError(eventSubscriptionHandler);
+ createHandler(ClientboundHelloPacket.class, p -> eventSubscriptionHandler.sendRegisterPacket(true));
}
- private void sendRegisterPacket(boolean alwaysSendIfNotEmpty) {
- if (modImplementation == null || !modImplementation.isConnectedToHypixel()) {
- // Allow registering events when not connected to Hypixel
- return;
- }
-
- if (lastSubscribedEvents.equals(subscribedEvents) && !(alwaysSendIfNotEmpty && !subscribedEvents.isEmpty())) {
- return;
- }
-
- Set lastSubscribedEvents = new HashSet<>(subscribedEvents);
- if (sendPacket(new ServerboundRegisterPacket(registry, lastSubscribedEvents))) {
- this.lastSubscribedEvents = lastSubscribedEvents;
- }
+ boolean isImplementationSet() {
+ return modImplementation != null;
}
@ApiStatus.Internal
@@ -155,7 +145,7 @@ public void setModImplementation(HypixelModAPIImplementation modImplementation)
this.modImplementation = modImplementation;
this.modImplementation.onInit();
- registerDefaultHandler();
+ registerDefaultHandlers();
}
/**
@@ -182,9 +172,7 @@ public RegisteredHandler createHandler(C
}
public void subscribeToEventPacket(Class extends EventPacket> packet) {
- if (subscribedEvents.add(getRegistry().getIdentifier(packet))) {
- sendRegisterPacket(false);
- }
+ eventSubscriptionHandler.subscribeToEventPacket(getRegistry().getIdentifier(packet));
}
/**
diff --git a/src/main/java/net/hypixel/modapi/packet/impl/clientbound/ClientboundRegisterPacket.java b/src/main/java/net/hypixel/modapi/packet/impl/clientbound/ClientboundRegisterPacket.java
new file mode 100644
index 0000000..94f5518
--- /dev/null
+++ b/src/main/java/net/hypixel/modapi/packet/impl/clientbound/ClientboundRegisterPacket.java
@@ -0,0 +1,59 @@
+package net.hypixel.modapi.packet.impl.clientbound;
+
+import net.hypixel.modapi.serializer.PacketSerializer;
+import org.jetbrains.annotations.ApiStatus;
+
+/**
+ * Is sent as a response to the {@link net.hypixel.modapi.packet.impl.serverbound.ServerboundRegisterPacket} when successfully registered.
+ */
+@ApiStatus.Internal
+public class ClientboundRegisterPacket extends ClientboundVersionedPacket {
+ private static final int CURRENT_VERSION = 2;
+
+ private int requestIdentifier;
+
+ public ClientboundRegisterPacket(int version, int requestIdentifier) {
+ super(version);
+ this.requestIdentifier = requestIdentifier;
+ if (version != CURRENT_VERSION) {
+ // Version 1 never existed, so we don't support sending it
+ throw new IllegalArgumentException("Invalid version: " + version);
+ }
+ }
+
+ public ClientboundRegisterPacket(PacketSerializer serializer) {
+ super(serializer);
+ }
+
+ @Override
+ protected boolean read(PacketSerializer serializer) {
+ if (!super.read(serializer)) {
+ return false;
+ }
+
+ this.requestIdentifier = serializer.readVarInt();
+ return true;
+ }
+
+ @Override
+ public void write(PacketSerializer serializer) {
+ super.write(serializer);
+ serializer.writeVarInt(requestIdentifier);
+ }
+
+ @Override
+ protected int getLatestVersion() {
+ return CURRENT_VERSION;
+ }
+
+ public int getRequestIdentifier() {
+ return requestIdentifier;
+ }
+
+ @Override
+ public String toString() {
+ return "ClientboundRegisterPacket{" +
+ "requestIdentifier=" + requestIdentifier +
+ "} " + super.toString();
+ }
+}
diff --git a/src/main/java/net/hypixel/modapi/packet/impl/serverbound/ServerboundRegisterPacket.java b/src/main/java/net/hypixel/modapi/packet/impl/serverbound/ServerboundRegisterPacket.java
index 24af97f..a305f87 100644
--- a/src/main/java/net/hypixel/modapi/packet/impl/serverbound/ServerboundRegisterPacket.java
+++ b/src/main/java/net/hypixel/modapi/packet/impl/serverbound/ServerboundRegisterPacket.java
@@ -18,12 +18,14 @@
public class ServerboundRegisterPacket extends ServerboundVersionedPacket {
private static final int MAX_IDENTIFIER_LENGTH = 20;
private static final int MAX_IDENTIFIERS = 5;
- private static final int CURRENT_VERSION = 1;
+ private static final int CURRENT_VERSION = 2;
+ private int requestIdentifier;
private Map subscribedEvents;
- public ServerboundRegisterPacket(PacketRegistry registry, Set subscribedEventIdentifiers) {
+ public ServerboundRegisterPacket(PacketRegistry registry, int requestIdentifier, Set subscribedEventIdentifiers) {
super(CURRENT_VERSION);
+ this.requestIdentifier = requestIdentifier;
this.subscribedEvents = registry.getEventVersions(subscribedEventIdentifiers);
if (subscribedEvents.size() > MAX_IDENTIFIERS) {
@@ -39,6 +41,10 @@ public ServerboundRegisterPacket(PacketSerializer serializer) {
protected boolean read(PacketSerializer serializer) {
super.read(serializer);
+ if (version >= 2) {
+ requestIdentifier = serializer.readVarInt();
+ }
+
int size = serializer.readVarInt();
if (size > MAX_IDENTIFIERS) {
throw new IllegalArgumentException("wantedPackets cannot contain more than " + MAX_IDENTIFIERS + " identifiers");
@@ -56,6 +62,7 @@ protected boolean read(PacketSerializer serializer) {
public void write(PacketSerializer serializer) {
super.write(serializer);
+ serializer.writeVarInt(requestIdentifier);
serializer.writeVarInt(subscribedEvents.size());
for (Map.Entry entry : subscribedEvents.entrySet()) {
serializer.writeString(entry.getKey(), MAX_IDENTIFIER_LENGTH);
@@ -63,6 +70,10 @@ public void write(PacketSerializer serializer) {
}
}
+ public int getRequestIdentifier() {
+ return requestIdentifier;
+ }
+
public Map getSubscribedEvents() {
return Collections.unmodifiableMap(subscribedEvents);
}
diff --git a/src/test/java/net/hypixel/modapi/packet/TestPacketRoundtrip.java b/src/test/java/net/hypixel/modapi/packet/TestPacketRoundtrip.java
index c736017..5b28eca 100644
--- a/src/test/java/net/hypixel/modapi/packet/TestPacketRoundtrip.java
+++ b/src/test/java/net/hypixel/modapi/packet/TestPacketRoundtrip.java
@@ -3,6 +3,7 @@
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import net.hypixel.modapi.HypixelModAPI;
+import net.hypixel.modapi.packet.impl.serverbound.ServerboundRegisterPacket;
import net.hypixel.modapi.serializer.PacketSerializer;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.params.ParameterizedTest;
@@ -39,6 +40,8 @@ private static Stream packetProvider() {
return HypixelModAPI.getInstance().getRegistry().getRegistrations().stream()
.filter(registration -> registration.getServerboundClazz() != null)
.filter(registration -> registration.getClientboundClazz() != null)
+ // Exclude register packet as it's not a normal packet and doesn't have an empty constructor
+ .filter(registration -> !registration.getServerboundClazz().equals(ServerboundRegisterPacket.class))
.map(Arguments::of);
}