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: + * + */ +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 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); }