ForgePatch/src/main/java/net/minecraftforge/fml/network/FMLHandshakeHandler.java

306 lines
16 KiB
Java

/*
* Minecraft Forge
* Copyright (c) 2016-2020.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation version 2.1
* of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
package net.minecraftforge.fml.network;
import net.minecraft.network.login.ServerLoginNetHandler;
import com.google.common.collect.Multimap;
import net.minecraft.network.NetworkManager;
import net.minecraft.network.PacketBuffer;
import net.minecraft.util.ResourceLocation;
import net.minecraft.util.text.StringTextComponent;
import net.minecraftforge.fml.config.ConfigTracker;
import net.minecraftforge.fml.loading.AdvancedLogMessageAdapter;
import net.minecraftforge.fml.network.simple.SimpleChannel;
import net.minecraftforge.fml.util.ThreeConsumer;
import net.minecraftforge.registries.ForgeRegistry;
import net.minecraftforge.registries.GameData;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.Marker;
import org.apache.logging.log4j.MarkerManager;
import com.google.common.collect.Maps;
import java.util.*;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BiConsumer;
import java.util.function.IntSupplier;
import java.util.function.Supplier;
import static net.minecraftforge.registries.ForgeRegistry.REGISTRIES;
/**
* Instance responsible for handling the overall FML network handshake.
*
* <p>An instance is created during {@link net.minecraft.network.handshake.client.CHandshakePacket} handling, and attached
* to the {@link NetworkManager#channel} via {@link FMLNetworkConstants#FML_HANDSHAKE_HANDLER}.
*
* <p>The {@link FMLNetworkConstants#handshakeChannel} is a {@link SimpleChannel} with standard messages flowing in both directions.
*
* <p>The {@link #loginWrapper} transforms these messages into {@link net.minecraft.network.login.client.CCustomPayloadLoginPacket}
* and {@link net.minecraft.network.login.server.SCustomPayloadLoginPacket} compatible messages, by means of wrapping.
*
* <p>The handshake is ticked {@link #tickLogin(NetworkManager)} from the {@link ServerLoginNetHandler#update()} method,
* utilizing the {@link ServerLoginNetHandler.State#NEGOTIATING} state, which is otherwise unused in vanilla code.
*
* <p>During client to server initiation, on the <em>server</em>, the {@link NetworkEvent.GatherLoginPayloadsEvent} is fired,
* which solicits all registered channels at the {@link NetworkRegistry} for any
* {@link net.minecraftforge.fml.network.NetworkRegistry.LoginPayload} they wish to supply.
*
* <p>The collected {@link net.minecraftforge.fml.network.NetworkRegistry.LoginPayload} are sent, one per tick, via
* the {@link FMLLoginWrapper#wrapPacket(ResourceLocation, PacketBuffer)} mechanism to the incoming client connection. Each
* packet is indexed via {@link net.minecraft.network.login.client.CCustomPayloadLoginPacket#transaction}, which is
* the only mechanism available for tracking request/response pairs.
*
* <p>Each packet sent from the server should be replied by the client, though not necessarily in sent order. The reply
* should contain the index of the server's packet it is replying to. The {@link FMLLoginWrapper} class handles indexing
* replies correctly automatically.
*
* <p>Once all packets have been dispatched, we wait for all replies to be received. Once all replies are received, the
* final login phase will commence.
*/
public class FMLHandshakeHandler {
static final Marker FMLHSMARKER = MarkerManager.getMarker("FMLHANDSHAKE").setParents(FMLNetworkConstants.NETWORK);
private static final Logger LOGGER = LogManager.getLogger();
private static final FMLLoginWrapper loginWrapper = new FMLLoginWrapper();
static {
}
/**
* Create a new handshake instance. Called when connection is first created during the {@link net.minecraft.network.handshake.client.CHandshakePacket}
* handling.
*
* @param manager The network manager for this connection
* @param direction The {@link NetworkDirection} for this connection: {@link NetworkDirection#LOGIN_TO_SERVER} or {@link NetworkDirection#LOGIN_TO_CLIENT}
*/
static void registerHandshake(NetworkManager manager, NetworkDirection direction) {
manager.channel().attr(FMLNetworkConstants.FML_HANDSHAKE_HANDLER).compareAndSet(null, new FMLHandshakeHandler(manager, direction));
}
static boolean tickLogin(NetworkManager networkManager)
{
return networkManager.channel().attr(FMLNetworkConstants.FML_HANDSHAKE_HANDLER).get().tickServer();
}
private List<NetworkRegistry.LoginPayload> messageList;
private List<Integer> sentMessages = new ArrayList<>();
private final NetworkDirection direction;
private final NetworkManager manager;
private int packetPosition;
private Map<ResourceLocation, ForgeRegistry.Snapshot> registrySnapshots;
private Set<ResourceLocation> registriesToReceive;
private Map<ResourceLocation, String> registryHashes;
private FMLHandshakeHandler(NetworkManager networkManager, NetworkDirection side)
{
this.direction = side;
this.manager = networkManager;
if (networkManager.isLocalChannel()) {
this.messageList = NetworkRegistry.gatherLoginPayloads(this.direction, true);
LOGGER.debug(FMLHSMARKER, "Starting local connection.");
} else if (NetworkHooks.getConnectionType(()->this.manager)==ConnectionType.VANILLA) {
this.messageList = Collections.emptyList();
LOGGER.debug(FMLHSMARKER, "Starting new vanilla network connection.");
} else {
this.messageList = NetworkRegistry.gatherLoginPayloads(this.direction, false);
LOGGER.debug(FMLHSMARKER, "Starting new modded network connection. Found {} messages to dispatch.", this.messageList.size());
}
}
/**
* Transforms a two-argument instance method reference into a {@link BiConsumer} based on the {@link #getHandshake(Supplier)} function.
*
* This should only be used for login message types.
*
* @param consumer A two argument instance method reference
* @param <MSG> message type
* @return A {@link BiConsumer} for use in message handling
*/
public static <MSG extends IntSupplier> BiConsumer<MSG, Supplier<NetworkEvent.Context>> biConsumerFor(ThreeConsumer<FMLHandshakeHandler, ? super MSG, ? super Supplier<NetworkEvent.Context>> consumer)
{
return (m, c) -> ThreeConsumer.bindArgs(consumer, m, c).accept(getHandshake(c));
}
/**
* Transforms a two-argument instance method reference into a {@link BiConsumer} {@link #biConsumerFor(ThreeConsumer)}, first calling the {@link #handleIndexedMessage(FMLHandshakeMessages.LoginIndexedMessage, Supplier)}
* method to handle index tracking. Used for client to server replies.
*
* This should only be used for login messages.
*
* @param next The method reference to call after index handling
* @param <MSG> message type
* @return A {@link BiConsumer} for use in message handling
*/
public static <MSG extends IntSupplier> BiConsumer<MSG, Supplier<NetworkEvent.Context>> indexFirst(ThreeConsumer<FMLHandshakeHandler, MSG, Supplier<NetworkEvent.Context>> next)
{
final BiConsumer<MSG, Supplier<NetworkEvent.Context>> loginIndexedMessageSupplierBiConsumer = biConsumerFor(FMLHandshakeHandler::handleIndexedMessage);
return loginIndexedMessageSupplierBiConsumer.andThen(biConsumerFor(next));
}
/**
* Retrieve the handshake from the {@link NetworkEvent.Context}
*
* @param contextSupplier the {@link NetworkEvent.Context}
* @return The handshake handler for the connection
*/
private static FMLHandshakeHandler getHandshake(Supplier<NetworkEvent.Context> contextSupplier) {
return contextSupplier.get().attr(FMLNetworkConstants.FML_HANDSHAKE_HANDLER).get();
}
void handleServerModListOnClient(FMLHandshakeMessages.S2CModList serverModList, Supplier<NetworkEvent.Context> c)
{
LOGGER.debug(FMLHSMARKER, "Logging into server with mod list [{}]", String.join(", ", serverModList.getModList()));
boolean accepted = NetworkRegistry.validateClientChannels(serverModList.getChannels());
c.get().setPacketHandled(true);
if (!accepted) {
LOGGER.error(FMLHSMARKER, "Terminating connection with server, mismatched mod list");
c.get().getNetworkManager().closeChannel(new StringTextComponent("Connection closed - mismatched mod channel list"));
return;
}
FMLNetworkConstants.handshakeChannel.reply(new FMLHandshakeMessages.C2SModListReply(), c.get());
LOGGER.debug(FMLHSMARKER, "Accepted server connection");
// Set the modded marker on the channel so we know we got packets
c.get().getNetworkManager().channel().attr(FMLNetworkConstants.FML_NETVERSION).set(FMLNetworkConstants.NETVERSION);
this.registriesToReceive = new HashSet<>(serverModList.getRegistries());
this.registrySnapshots = Maps.newHashMap();
LOGGER.debug(REGISTRIES, "Expecting {} registries: {}", ()->this.registriesToReceive.size(), ()->this.registriesToReceive);
}
<MSG extends IntSupplier> void handleIndexedMessage(MSG message, Supplier<NetworkEvent.Context> c)
{
LOGGER.debug(FMLHSMARKER, "Received client indexed reply {} of type {}", message.getAsInt(), message.getClass().getName());
boolean removed = this.sentMessages.removeIf(i-> i == message.getAsInt());
if (!removed) {
LOGGER.error(FMLHSMARKER, "Recieved unexpected index {} in client reply", message.getAsInt());
}
}
void handleClientModListOnServer(FMLHandshakeMessages.C2SModListReply clientModList, Supplier<NetworkEvent.Context> c)
{
LOGGER.debug(FMLHSMARKER, "Received client connection with modlist [{}]", String.join(", ", clientModList.getModList()));
boolean accepted = NetworkRegistry.validateServerChannels(clientModList.getChannels());
c.get().setPacketHandled(true);
if (!accepted) {
LOGGER.error(FMLHSMARKER, "Terminating connection with client, mismatched mod list");
c.get().getNetworkManager().closeChannel(new StringTextComponent("Connection closed - mismatched mod channel list"));
return;
}
LOGGER.debug(FMLHSMARKER, "Accepted client connection mod list");
}
void handleRegistryMessage(final FMLHandshakeMessages.S2CRegistry registryPacket, final Supplier<NetworkEvent.Context> contextSupplier){
LOGGER.debug(FMLHSMARKER,"Received registry packet for {}", registryPacket.getRegistryName());
this.registriesToReceive.remove(registryPacket.getRegistryName());
this.registrySnapshots.put(registryPacket.getRegistryName(), registryPacket.getSnapshot());
boolean continueHandshake = true;
if (this.registriesToReceive.isEmpty()) {
continueHandshake = handleRegistryLoading(contextSupplier);
}
// The handshake reply isn't sent until we have processed the message
contextSupplier.get().setPacketHandled(true);
if (!continueHandshake) {
LOGGER.error(FMLHSMARKER, "Connection closed, not continuing handshake");
} else {
FMLNetworkConstants.handshakeChannel.reply(new FMLHandshakeMessages.C2SAcknowledge(), contextSupplier.get());
}
}
private boolean handleRegistryLoading(final Supplier<NetworkEvent.Context> contextSupplier) {
// We use a countdown latch to suspend the network thread pending the client thread processing the registry data
AtomicBoolean successfulConnection = new AtomicBoolean(false);
CountDownLatch block = new CountDownLatch(1);
contextSupplier.get().enqueueWork(() -> {
LOGGER.debug(FMLHSMARKER, "Injecting registry snapshot from server.");
final Multimap<ResourceLocation, ResourceLocation> missingData = GameData.injectSnapshot(registrySnapshots, false, false);
LOGGER.debug(FMLHSMARKER, "Snapshot injected.");
if (!missingData.isEmpty()) {
LOGGER.error(FMLHSMARKER, "Missing registry data for network connection:\n{}", new AdvancedLogMessageAdapter(sb->
missingData.forEach((reg, entry)-> sb.append("\t").append(reg).append(": ").append(entry).append('\n'))));
}
successfulConnection.set(missingData.isEmpty());
block.countDown();
});
LOGGER.debug(FMLHSMARKER, "Waiting for registries to load.");
try {
block.await();
} catch (InterruptedException e) {
Thread.interrupted();
}
if (successfulConnection.get()) {
LOGGER.debug(FMLHSMARKER, "Registry load complete, continuing handshake.");
} else {
LOGGER.error(FMLHSMARKER, "Failed to load registry, closing connection.");
this.manager.closeChannel(new StringTextComponent("Failed to synchronize registry data from server, closing connection"));
}
return successfulConnection.get();
}
void handleClientAck(final FMLHandshakeMessages.C2SAcknowledge msg, final Supplier<NetworkEvent.Context> contextSupplier) {
LOGGER.debug(FMLHSMARKER, "Received acknowledgement from client");
contextSupplier.get().setPacketHandled(true);
}
void handleConfigSync(final FMLHandshakeMessages.S2CConfigData msg, final Supplier<NetworkEvent.Context> contextSupplier) {
LOGGER.debug(FMLHSMARKER, "Received config sync from server");
ConfigTracker.INSTANCE.receiveSyncedConfig(msg, contextSupplier);
contextSupplier.get().setPacketHandled(true);
FMLNetworkConstants.handshakeChannel.reply(new FMLHandshakeMessages.C2SAcknowledge(), contextSupplier.get());
}
/**
* FML will send packets, from Server to Client, from the messages queue until the queue is drained. Each message
* will be indexed, and placed into the "pending acknowledgement" queue.
*
* As indexed packets are received at the server, they will be removed from the "pending acknowledgement" queue.
*
* Once the pending queue is drained, this method returns true - indicating that login processing can proceed to
* the next step.
*
* @return true if there is no more need to tick this login connection.
*/
public boolean tickServer()
{
if (packetPosition < messageList.size()) {
NetworkRegistry.LoginPayload message = messageList.get(packetPosition);
LOGGER.debug(FMLHSMARKER, "Sending ticking packet info '{}' to '{}' sequence {}", message.getMessageContext(), message.getChannelName(), packetPosition);
sentMessages.add(packetPosition);
loginWrapper.sendServerToClientLoginPacket(message.getChannelName(), message.getData(), packetPosition, this.manager);
packetPosition++;
}
// we're done when sentMessages is empty
if (sentMessages.isEmpty() && packetPosition >= messageList.size()-1) {
// clear ourselves - we're done!
this.manager.channel().attr(FMLNetworkConstants.FML_HANDSHAKE_HANDLER).set(null);
LOGGER.debug(FMLHSMARKER, "Handshake complete!");
return true;
}
return false;
}
}