From 125a433ce91727891e6e9322954917b9270e3a1e Mon Sep 17 00:00:00 2001 From: Connor Linfoot Date: Thu, 1 Aug 2024 18:21:32 +0100 Subject: [PATCH 1/4] Implement event registration confirmation and retry --- pom.xml | 2 +- .../modapi/EventSubscriptionHandler.java | 133 ++++++++++++++++++ .../net/hypixel/modapi/HypixelModAPI.java | 35 ++--- .../ClientboundRegisterPacket.java | 59 ++++++++ .../ServerboundRegisterPacket.java | 9 +- .../modapi/packet/TestPacketRoundtrip.java | 3 + 6 files changed, 215 insertions(+), 26 deletions(-) create mode 100644 src/main/java/net/hypixel/modapi/EventSubscriptionHandler.java create mode 100644 src/main/java/net/hypixel/modapi/packet/impl/clientbound/ClientboundRegisterPacket.java diff --git a/pom.xml b/pom.xml index af681ba..93d93e9 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..f9d0940 --- /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: + * + */ +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.isPacketSenderSet()) { + // 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 8337fba..9857947 100644 --- a/src/main/java/net/hypixel/modapi/HypixelModAPI.java +++ b/src/main/java/net/hypixel/modapi/HypixelModAPI.java @@ -17,7 +17,8 @@ import net.hypixel.modapi.serializer.PacketSerializer; import org.jetbrains.annotations.ApiStatus; -import java.util.*; +import java.util.Collection; +import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.function.Predicate; @@ -31,14 +32,14 @@ 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); + private Predicate packetSender = null; private HypixelModAPI() { registerHypixelPackets(); registerEventPackets(); - registerDefaultHandler(); + registerDefaultHandlers(); } private void registerHypixelPackets() { @@ -62,6 +63,7 @@ private void registerHypixelPackets() { .register(); registry.define("hypixel:register") + .clientbound(ClientboundRegisterPacket.class, ClientboundRegisterPacket::new) .serverbound(ServerboundRegisterPacket.class, ServerboundRegisterPacket::new) .register(); } @@ -72,24 +74,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 (packetSender == null) { - // Allow registering events before the mod has fully initialized - return; - } - - if (lastSubscribedEvents.equals(subscribedEvents) && !(alwaysSendIfNotEmpty && !subscribedEvents.isEmpty())) { - return; - } - - Set lastSubscribedEvents = new HashSet<>(subscribedEvents); - if (sendPacket(new ServerboundRegisterPacket(registry, lastSubscribedEvents))) { - this.lastSubscribedEvents = lastSubscribedEvents; - } + boolean isPacketSenderSet() { + return packetSender != null; } @ApiStatus.Internal @@ -175,9 +166,7 @@ public RegisteredHandler createHandler(C } public void subscribeToEventPacket(Class 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..88250fb 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,8 @@ public ServerboundRegisterPacket(PacketSerializer serializer) { protected boolean read(PacketSerializer serializer) { super.read(serializer); + requestIdentifier = serializer.readVarInt(); + int size = serializer.readVarInt(); if (size > MAX_IDENTIFIERS) { throw new IllegalArgumentException("wantedPackets cannot contain more than " + MAX_IDENTIFIERS + " identifiers"); @@ -56,6 +60,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); 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); } From 9f5982f5c2e279511f01f4f4b7fac54e745c4d59 Mon Sep 17 00:00:00 2001 From: Connor Linfoot Date: Thu, 1 Aug 2024 18:24:38 +0100 Subject: [PATCH 2/4] only read identifier if version 2 --- .../packet/impl/serverbound/ServerboundRegisterPacket.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 88250fb..9c7d0c5 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 @@ -41,7 +41,9 @@ public ServerboundRegisterPacket(PacketSerializer serializer) { protected boolean read(PacketSerializer serializer) { super.read(serializer); - requestIdentifier = serializer.readVarInt(); + if (version >= 2) { + requestIdentifier = serializer.readVarInt(); + } int size = serializer.readVarInt(); if (size > MAX_IDENTIFIERS) { From 4623aa88d7aa1ad9b6b141b2a28ebe9528695d4f Mon Sep 17 00:00:00 2001 From: Connor Linfoot Date: Thu, 1 Aug 2024 18:25:14 +0100 Subject: [PATCH 3/4] Add getRequestIdentifier --- .../packet/impl/serverbound/ServerboundRegisterPacket.java | 4 ++++ 1 file changed, 4 insertions(+) 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 9c7d0c5..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 @@ -70,6 +70,10 @@ public void write(PacketSerializer serializer) { } } + public int getRequestIdentifier() { + return requestIdentifier; + } + public Map getSubscribedEvents() { return Collections.unmodifiableMap(subscribedEvents); } From fac4ce9db75f6de2ebdba835ddfca9da005bede0 Mon Sep 17 00:00:00 2001 From: Connor Linfoot Date: Wed, 18 Feb 2026 02:06:40 +0000 Subject: [PATCH 4/4] merge fixes --- .../hypixel/modapi/EventSubscriptionHandler.java | 2 +- src/main/java/net/hypixel/modapi/HypixelModAPI.java | 13 +++---------- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/src/main/java/net/hypixel/modapi/EventSubscriptionHandler.java b/src/main/java/net/hypixel/modapi/EventSubscriptionHandler.java index f9d0940..c2923a4 100644 --- a/src/main/java/net/hypixel/modapi/EventSubscriptionHandler.java +++ b/src/main/java/net/hypixel/modapi/EventSubscriptionHandler.java @@ -60,7 +60,7 @@ void subscribeToEventPacket(String identifier) { } void sendRegisterPacket(boolean alwaysSendIfNotEmpty) { - if (!api.isPacketSenderSet()) { + if (!api.isImplementationSet()) { // Allow registering events before the mod has fully initialized return; } diff --git a/src/main/java/net/hypixel/modapi/HypixelModAPI.java b/src/main/java/net/hypixel/modapi/HypixelModAPI.java index 4390fd2..a29c44b 100644 --- a/src/main/java/net/hypixel/modapi/HypixelModAPI.java +++ b/src/main/java/net/hypixel/modapi/HypixelModAPI.java @@ -39,7 +39,6 @@ public static HypixelModAPI getInstance() { private HypixelModAPI() { registerHypixelPackets(); registerEventPackets(); - registerDefaultHandlers(); } private void registerHypixelPackets() { @@ -79,14 +78,8 @@ private void registerDefaultHandlers() { 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; - } - - boolean isPacketSenderSet() { - return packetSender != null; + boolean isImplementationSet() { + return modImplementation != null; } @ApiStatus.Internal @@ -152,7 +145,7 @@ public void setModImplementation(HypixelModAPIImplementation modImplementation) this.modImplementation = modImplementation; this.modImplementation.onInit(); - registerDefaultHandler(); + registerDefaultHandlers(); } /**