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