diff --git a/src/main/java/net/minecraftforge/event/entity/player/PlayerNegotiationEvent.java b/src/main/java/net/minecraftforge/event/entity/player/PlayerNegotiationEvent.java
new file mode 100644
index 0000000000..d1940a9828
--- /dev/null
+++ b/src/main/java/net/minecraftforge/event/entity/player/PlayerNegotiationEvent.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) Forge Development LLC and contributors
+ * SPDX-License-Identifier: LGPL-2.1-only
+ */
+
+package net.minecraftforge.event.entity.player;
+
+import com.mojang.authlib.GameProfile;
+import net.minecraft.network.Connection;
+import net.minecraftforge.common.MinecraftForge;
+import net.minecraftforge.eventbus.api.Event;
+
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Future;
+
+/**
+ * This event is fired on the server when a connection has started the Forge handshake,
+ * Forge will wait for all enqueued work to be completed before proceeding further with the login process.
+ *
+ * This event can be used to delay the player login until any necessary work such as preloading user data has completed.
+ *
+ * This event is fired on the {@link MinecraftForge#EVENT_BUS}.
+ */
+public class PlayerNegotiationEvent extends Event
+{
+
+ private final Connection connection;
+ private final GameProfile profile;
+ private final List> futures;
+
+ public PlayerNegotiationEvent(Connection connection, GameProfile profile, List> futures)
+ {
+ this.connection = connection;
+ this.profile = profile;
+ this.futures = futures;
+ }
+
+ /**
+ * Enqueue work to be completed asynchronously before the login proceeds.
+ */
+ public void enqueueWork(Runnable runnable)
+ {
+ enqueueWork(CompletableFuture.runAsync(runnable));
+ }
+
+ /**
+ * Enqueue work to be completed asynchronously before the login proceeds.
+ */
+ public void enqueueWork(Future future)
+ {
+ futures.add(future);
+ }
+
+ public Connection getConnection()
+ {
+ return connection;
+ }
+
+ public GameProfile getProfile()
+ {
+ return profile;
+ }
+}
diff --git a/src/main/java/net/minecraftforge/network/HandshakeHandler.java b/src/main/java/net/minecraftforge/network/HandshakeHandler.java
index 04636b675c..48f81ca64f 100644
--- a/src/main/java/net/minecraftforge/network/HandshakeHandler.java
+++ b/src/main/java/net/minecraftforge/network/HandshakeHandler.java
@@ -6,6 +6,7 @@
package net.minecraftforge.network;
import com.google.common.collect.Multimap;
+import com.mojang.authlib.GameProfile;
import net.minecraft.network.Connection;
import net.minecraft.network.protocol.handshake.ClientIntentionPacket;
import net.minecraft.network.protocol.login.ClientboundCustomQueryPacket;
@@ -13,7 +14,9 @@ import net.minecraft.network.protocol.login.ServerboundCustomQueryPacket;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.network.chat.TextComponent;
import net.minecraft.server.network.ServerLoginPacketListenerImpl;
+import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.common.util.LogMessageAdapter;
+import net.minecraftforge.event.entity.player.PlayerNegotiationEvent;
import net.minecraftforge.network.simple.SimpleChannel;
import net.minecraftforge.registries.ForgeRegistry;
import net.minecraftforge.registries.GameData;
@@ -25,7 +28,10 @@ import org.apache.logging.log4j.MarkerManager;
import com.google.common.collect.Maps;
import java.util.*;
+import java.util.concurrent.CancellationException;
import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BiConsumer;
import java.util.function.IntSupplier;
@@ -99,6 +105,8 @@ public class HandshakeHandler
private Map registrySnapshots;
private Set registriesToReceive;
private Map registryHashes;
+ private boolean negotiationStarted = false;
+ private final List> pendingFutures = new ArrayList<>();
private HandshakeHandler(Connection networkManager, NetworkDirection side)
{
@@ -281,6 +289,13 @@ public class HandshakeHandler
*/
public boolean tickServer()
{
+ if (!negotiationStarted) {
+ GameProfile profile = ((ServerLoginPacketListenerImpl) manager.getPacketListener()).gameProfile;
+ PlayerNegotiationEvent event = new PlayerNegotiationEvent(manager, profile, pendingFutures);
+ MinecraftForge.EVENT_BUS.post(event);
+ negotiationStarted = true;
+ }
+
if (packetPosition < messageList.size()) {
NetworkRegistry.LoginPayload message = messageList.get(packetPosition);
@@ -290,8 +305,24 @@ public class HandshakeHandler
packetPosition++;
}
+ pendingFutures.removeIf(future -> {
+ if (!future.isDone()) {
+ return false;
+ }
+
+ try {
+ future.get();
+ } catch (ExecutionException ex) {
+ LOGGER.error("Error during negotiation", ex.getCause());
+ } catch (CancellationException | InterruptedException ex) {
+ // no-op
+ }
+
+ return true;
+ });
+
// we're done when sentMessages is empty
- if (sentMessages.isEmpty() && packetPosition >= messageList.size()-1) {
+ if (sentMessages.isEmpty() && packetPosition >= messageList.size()-1 && pendingFutures.isEmpty()) {
// clear ourselves - we're done!
this.manager.channel().attr(NetworkConstants.FML_HANDSHAKE_HANDLER).set(null);
LOGGER.debug(FMLHSMARKER, "Handshake complete!");
diff --git a/src/main/resources/META-INF/accesstransformer.cfg b/src/main/resources/META-INF/accesstransformer.cfg
index f2198035bc..753a4b9df8 100644
--- a/src/main/resources/META-INF/accesstransformer.cfg
+++ b/src/main/resources/META-INF/accesstransformer.cfg
@@ -271,6 +271,7 @@ public net.minecraft.server.level.ServerLevel m_142646_()Lnet/minecraft/world/le
public net.minecraft.server.level.ServerPlayer f_8940_ # containerCounter
public net.minecraft.server.level.ServerPlayer m_143399_(Lnet/minecraft/world/inventory/AbstractContainerMenu;)V # initMenu
public net.minecraft.server.level.ServerPlayer m_9217_()V # nextContainerCounter
+public net.minecraft.server.network.ServerLoginPacketListenerImpl f_10021_ # gameProfile
public net.minecraft.server.packs.AbstractPackResources f_10203_ # file
public net.minecraft.server.packs.resources.FallbackResourceManager f_10599_ # fallbacks
public net.minecraft.tags.Tag$BuilderEntry (Lnet/minecraft/tags/Tag$Entry;Ljava/lang/String;)V # constructor
diff --git a/src/test/java/net/minecraftforge/debug/entity/player/PlayerNegotiationEventTest.java b/src/test/java/net/minecraftforge/debug/entity/player/PlayerNegotiationEventTest.java
new file mode 100644
index 0000000000..0ff420de13
--- /dev/null
+++ b/src/test/java/net/minecraftforge/debug/entity/player/PlayerNegotiationEventTest.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) Forge Development LLC and contributors
+ * SPDX-License-Identifier: LGPL-2.1-only
+ */
+
+package net.minecraftforge.debug.entity.player;
+
+import net.minecraftforge.event.entity.player.PlayerNegotiationEvent;
+import net.minecraftforge.eventbus.api.SubscribeEvent;
+import net.minecraftforge.fml.common.Mod;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+/**
+ * Tests {@link PlayerNegotiationEvent} by listening for and then enqueuing work to be done asynchronously,
+ * details regarding the work execution is printed out and exceptions are thrown to ensure proper handling.
+ */
+@Mod("player_negotiation_event_test")
+@Mod.EventBusSubscriber()
+public class PlayerNegotiationEventTest
+{
+ private static final boolean ENABLE = false;
+ private static final Logger LOGGER = LogManager.getLogger(PlayerNegotiationEventTest.class);
+
+ @SubscribeEvent
+ public static void onPlayerNegotiation(PlayerNegotiationEvent event)
+ {
+ if (!ENABLE) return;
+ LOGGER.info("{} ({})[{}] started negotiation", event.getProfile().getName(), event.getProfile().getId(), event.getConnection().getRemoteAddress());
+ event.enqueueWork(() -> {
+ // This log message should be printed on the fork-join-pool.
+ LOGGER.info("Hello from {}", Thread.currentThread().getName());
+ });
+ event.enqueueWork(() -> {
+ // This exception should be logged by Forge.
+ throw new RuntimeException("Test Exception from PlayerNegotiationEventTest");
+ });
+ }
+}
diff --git a/src/test/resources/META-INF/mods.toml b/src/test/resources/META-INF/mods.toml
index 7622b41a64..5dd3dbc55f 100644
--- a/src/test/resources/META-INF/mods.toml
+++ b/src/test/resources/META-INF/mods.toml
@@ -218,5 +218,7 @@ license="LGPL v2.1"
modId="server_world_creation_test"
[[mods]]
modId="valid_railshape_test"
+[[mods]]
+ modId="player_negotiation_event_test"
# ADD ABOVE THIS LINE