diff --git a/fabric-mod-protocol-api-v1/build.gradle b/fabric-mod-protocol-api-v1/build.gradle
new file mode 100644
index 0000000000..60da64c69a
--- /dev/null
+++ b/fabric-mod-protocol-api-v1/build.gradle
@@ -0,0 +1,3 @@
+version = getSubprojectVersion(project)
+
+moduleDependencies(project, ['fabric-api-base', 'fabric-networking-api-v1'])
diff --git a/fabric-mod-protocol-api-v1/src/client/java/net/fabricmc/fabric/api/client/modprotocol/v1/ClientModProtocolLookup.java b/fabric-mod-protocol-api-v1/src/client/java/net/fabricmc/fabric/api/client/modprotocol/v1/ClientModProtocolLookup.java
new file mode 100644
index 0000000000..51a37200b4
--- /dev/null
+++ b/fabric-mod-protocol-api-v1/src/client/java/net/fabricmc/fabric/api/client/modprotocol/v1/ClientModProtocolLookup.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.api.client.modprotocol.v1;
+
+import it.unimi.dsi.fastutil.objects.Object2IntMap;
+
+import net.minecraft.client.network.ClientCommonNetworkHandler;
+import net.minecraft.network.ClientConnection;
+import net.minecraft.server.network.ServerCommonNetworkHandler;
+import net.minecraft.util.Identifier;
+
+import net.fabricmc.fabric.api.modprotocol.v1.ModProtocol;
+import net.fabricmc.fabric.api.modprotocol.v1.ModProtocolIds;
+import net.fabricmc.fabric.impl.modprotocol.RemoteProtocolStorage;
+
+/**
+ * Utility methods allowing to get protocol versions supported by the server.
+ *
+ *
Protocol identifiers can be any valid {@link Identifier}. The default is {@code mod:(mod ID)}.
+ * @see ModProtocolIds
+ */
+public final class ClientModProtocolLookup {
+ /**
+ * A protocol version returned by {@code getSupportedProtocol} methods, when the server doesn't support the requested protocol.
+ */
+ public static final int UNSUPPORTED = -1;
+ private ClientModProtocolLookup() { }
+
+ /**
+ * Gets the protocol version supported by the server.
+ *
+ * @param handler the network handler connected to the server
+ * @param protocolId protocol's id
+ * @return the protocol version supported by the server
+ */
+ public static int getSupportedProtocol(ClientCommonNetworkHandler handler, Identifier protocolId) {
+ return RemoteProtocolStorage.getProtocol(handler, protocolId);
+ }
+
+ /**
+ * Gets the protocol version supported by the server.
+ *
+ * @param connection the ClientConnection connected to the server
+ * @param protocolId protocol's id
+ * @return the protocol version supported by the server
+ */
+ public static int getSupportedProtocol(ClientConnection connection, Identifier protocolId) {
+ return RemoteProtocolStorage.getProtocol(connection, protocolId);
+ }
+
+ /**
+ * Gets the protocol version supported by the server.
+ *
+ * @param handler the network handler connected to the server
+ * @param protocol protocol to check against
+ * @return the protocol version supported by the server
+ */
+ public static int getSupportedProtocol(ClientCommonNetworkHandler handler, ModProtocol protocol) {
+ return RemoteProtocolStorage.getProtocol(handler, protocol.id());
+ }
+
+ /**
+ * Gets the protocol version supported by the server.
+ *
+ * @param connection the ClientConnection connected to the server
+ * @param protocol protocol to check against
+ * @return the protocol version supported by the server
+ */
+ public static int getSupportedProtocol(ClientConnection connection, ModProtocol protocol) {
+ return RemoteProtocolStorage.getProtocol(connection, protocol.id());
+ }
+
+ /**
+ * Gets all protocols supported by the server.
+ *
+ * @param handler the network handler connected to the server
+ * @return the map of protocols to the versions supported by the server
+ */
+ public static Object2IntMap getAllSupportedProtocols(ServerCommonNetworkHandler handler) {
+ return RemoteProtocolStorage.getMap(handler);
+ }
+
+ /**
+ * Gets all protocols supported by the server.
+ *
+ * @param connection the ClientConnection connected to the server
+ * @return the map of protocols to the versions supported by the server
+ */
+ public static Object2IntMap getAllSupportedProtocols(ClientConnection connection) {
+ return RemoteProtocolStorage.getMap(connection);
+ }
+}
diff --git a/fabric-mod-protocol-api-v1/src/client/java/net/fabricmc/fabric/api/client/modprotocol/v1/package-info.java b/fabric-mod-protocol-api-v1/src/client/java/net/fabricmc/fabric/api/client/modprotocol/v1/package-info.java
new file mode 100644
index 0000000000..f39de72ade
--- /dev/null
+++ b/fabric-mod-protocol-api-v1/src/client/java/net/fabricmc/fabric/api/client/modprotocol/v1/package-info.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * The Mod Protocol API (client side), version 1.
+ *
+ * See {@link net.fabricmc.fabric.api.modprotocol.v1}
+ */
+
+@ApiStatus.Experimental
+package net.fabricmc.fabric.api.client.modprotocol.v1;
+
+import org.jetbrains.annotations.ApiStatus;
diff --git a/fabric-mod-protocol-api-v1/src/client/java/net/fabricmc/fabric/impl/modprotocol/client/ClientModProtocolInit.java b/fabric-mod-protocol-api-v1/src/client/java/net/fabricmc/fabric/impl/modprotocol/client/ClientModProtocolInit.java
new file mode 100644
index 0000000000..c3716930be
--- /dev/null
+++ b/fabric-mod-protocol-api-v1/src/client/java/net/fabricmc/fabric/impl/modprotocol/client/ClientModProtocolInit.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.impl.modprotocol.client;
+
+import java.util.HashMap;
+
+import net.minecraft.util.Identifier;
+
+import net.fabricmc.api.ClientModInitializer;
+import net.fabricmc.fabric.api.client.networking.v1.ClientConfigurationNetworking;
+import net.fabricmc.fabric.impl.modprotocol.ModProtocolImpl;
+import net.fabricmc.fabric.impl.modprotocol.ModProtocolInit;
+import net.fabricmc.fabric.impl.modprotocol.ModProtocolManager;
+import net.fabricmc.fabric.impl.modprotocol.RemoteProtocolStorage;
+import net.fabricmc.fabric.impl.modprotocol.payload.ModProtocolRequestS2CPayload;
+import net.fabricmc.fabric.impl.modprotocol.payload.ModProtocolResponseC2SPayload;
+
+public final class ClientModProtocolInit implements ClientModInitializer {
+ public void onInitializeClient() {
+ ClientConfigurationNetworking.registerGlobalReceiver(ModProtocolRequestS2CPayload.ID, (payload, context) -> {
+ var map = new HashMap(payload.modProtocol().size());
+
+ for (ModProtocolImpl protocol : payload.modProtocol()) {
+ map.put(protocol.id(), protocol);
+ }
+
+ ModProtocolManager.ValidationResult validate = ModProtocolManager.validateClient(map);
+
+ if (validate.isSuccess()) {
+ ((RemoteProtocolStorage) context.networkHandler()).fabric$setRemoteProtocol(validate.supportedProtocols());
+ context.responseSender().sendPacket(new ModProtocolResponseC2SPayload(validate.supportedProtocols()));
+ return;
+ }
+
+ var b = new StringBuilder();
+ b.append("Disconnected due to mismatched protocols!").append('\n');
+ b.append("Missing entries:").append('\n');
+ ModProtocolManager.appendTextEntries(validate.missing(), ModProtocolManager.LOCAL_MOD_PROTOCOLS_BY_ID, -1, text -> b.append(" - ").append(text.getString()));
+
+ context.responseSender().disconnect(ModProtocolManager.constructMessage(validate.missing(), ModProtocolManager.LOCAL_MOD_PROTOCOLS_BY_ID));
+ ModProtocolInit.LOGGER.warn(b.toString());
+ });
+ }
+}
diff --git a/fabric-mod-protocol-api-v1/src/client/java/net/fabricmc/fabric/mixin/modprotocol/client/ClientCommonNetworkHandlerMixin.java b/fabric-mod-protocol-api-v1/src/client/java/net/fabricmc/fabric/mixin/modprotocol/client/ClientCommonNetworkHandlerMixin.java
new file mode 100644
index 0000000000..33cca449e6
--- /dev/null
+++ b/fabric-mod-protocol-api-v1/src/client/java/net/fabricmc/fabric/mixin/modprotocol/client/ClientCommonNetworkHandlerMixin.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.mixin.modprotocol.client;
+
+import it.unimi.dsi.fastutil.objects.Object2IntMap;
+import org.spongepowered.asm.mixin.Final;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Shadow;
+
+import net.minecraft.client.network.ClientCommonNetworkHandler;
+import net.minecraft.network.ClientConnection;
+import net.minecraft.util.Identifier;
+
+import net.fabricmc.fabric.impl.modprotocol.RemoteProtocolStorage;
+
+@Mixin(ClientCommonNetworkHandler.class)
+public class ClientCommonNetworkHandlerMixin implements RemoteProtocolStorage {
+ @Shadow
+ @Final
+ protected ClientConnection connection;
+
+ @Override
+ public Object2IntMap fabric$getRemoteProtocol() {
+ return ((RemoteProtocolStorage) this.connection).fabric$getRemoteProtocol();
+ }
+
+ @Override
+ public void fabric$setRemoteProtocol(Object2IntMap protocol) {
+ ((RemoteProtocolStorage) this.connection).fabric$setRemoteProtocol(protocol);
+ }
+}
diff --git a/fabric-mod-protocol-api-v1/src/client/java/net/fabricmc/fabric/mixin/modprotocol/client/ClientConfigurationNetworkHandlerMixin.java b/fabric-mod-protocol-api-v1/src/client/java/net/fabricmc/fabric/mixin/modprotocol/client/ClientConfigurationNetworkHandlerMixin.java
new file mode 100644
index 0000000000..96bac544b5
--- /dev/null
+++ b/fabric-mod-protocol-api-v1/src/client/java/net/fabricmc/fabric/mixin/modprotocol/client/ClientConfigurationNetworkHandlerMixin.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.mixin.modprotocol.client;
+
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.network.ClientCommonNetworkHandler;
+import net.minecraft.client.network.ClientConfigurationNetworkHandler;
+import net.minecraft.client.network.ClientConnectionState;
+import net.minecraft.network.ClientConnection;
+import net.minecraft.network.packet.s2c.config.SelectKnownPacksS2CPacket;
+
+import net.fabricmc.fabric.impl.modprotocol.ModProtocolManager;
+import net.fabricmc.fabric.impl.modprotocol.RemoteProtocolStorage;
+
+@Mixin(ClientConfigurationNetworkHandler.class)
+public abstract class ClientConfigurationNetworkHandlerMixin extends ClientCommonNetworkHandler {
+ protected ClientConfigurationNetworkHandlerMixin(MinecraftClient client, ClientConnection connection, ClientConnectionState connectionState) {
+ super(client, connection, connectionState);
+ }
+
+ @Inject(method = "onSelectKnownPacks", at = @At("HEAD"), cancellable = true)
+ private void preventJoiningIncompatibleServers(SelectKnownPacksS2CPacket packet, CallbackInfo ci) {
+ if (((RemoteProtocolStorage) this.connection).fabric$getRemoteProtocol() == null && !ModProtocolManager.SERVER_REQUIRED.isEmpty()) {
+ this.client.execute(() -> this.connection.disconnect(ModProtocolManager.constructMessage(ModProtocolManager.SERVER_REQUIRED, ModProtocolManager.LOCAL_MOD_PROTOCOLS_BY_ID)));
+ ci.cancel();
+ }
+ }
+}
diff --git a/fabric-mod-protocol-api-v1/src/client/resources/fabric-mod-protocol-api-v1.client.mixins.json b/fabric-mod-protocol-api-v1/src/client/resources/fabric-mod-protocol-api-v1.client.mixins.json
new file mode 100644
index 0000000000..13c1d91ba3
--- /dev/null
+++ b/fabric-mod-protocol-api-v1/src/client/resources/fabric-mod-protocol-api-v1.client.mixins.json
@@ -0,0 +1,14 @@
+{
+ "required": true,
+ "package": "net.fabricmc.fabric.mixin.modprotocol.client",
+ "compatibilityLevel": "JAVA_17",
+ "mixins": [
+ "ClientCommonNetworkHandlerMixin"
+ ],
+ "injectors": {
+ "defaultRequire": 1
+ },
+ "client": [
+ "ClientConfigurationNetworkHandlerMixin"
+ ]
+}
diff --git a/fabric-mod-protocol-api-v1/src/main/java/net/fabricmc/fabric/api/modprotocol/v1/ModProtocol.java b/fabric-mod-protocol-api-v1/src/main/java/net/fabricmc/fabric/api/modprotocol/v1/ModProtocol.java
new file mode 100644
index 0000000000..8f2bf4ab20
--- /dev/null
+++ b/fabric-mod-protocol-api-v1/src/main/java/net/fabricmc/fabric/api/modprotocol/v1/ModProtocol.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.api.modprotocol.v1;
+
+import it.unimi.dsi.fastutil.ints.IntList;
+import org.jetbrains.annotations.ApiStatus;
+
+import net.minecraft.util.Identifier;
+
+/**
+ * Interface representing registered ModProtocol. Can be used for further lookups.
+ */
+@ApiStatus.NonExtendable
+public interface ModProtocol {
+ /**
+ * @return Identifier associated with this Mod Protocol
+ */
+ Identifier id();
+ /**
+ * @return Display name of this protocol
+ */
+ String name();
+ /**
+ * @return Display version of this protocol
+ */
+ String version();
+ /**
+ * @return Protocol versions supported by this protocol
+ */
+ IntList protocol();
+ /**
+ * @return Client requirement of this protocol
+ */
+ boolean requireClient();
+ /**
+ * @return Server requirement of this protocol
+ */
+ boolean requireServer();
+}
diff --git a/fabric-mod-protocol-api-v1/src/main/java/net/fabricmc/fabric/api/modprotocol/v1/ModProtocolIds.java b/fabric-mod-protocol-api-v1/src/main/java/net/fabricmc/fabric/api/modprotocol/v1/ModProtocolIds.java
new file mode 100644
index 0000000000..ccdada3882
--- /dev/null
+++ b/fabric-mod-protocol-api-v1/src/main/java/net/fabricmc/fabric/api/modprotocol/v1/ModProtocolIds.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.api.modprotocol.v1;
+
+import net.minecraft.util.Identifier;
+
+/**
+ * Utility methods allowing to create Identifiers targeting default protocols.
+ */
+public final class ModProtocolIds {
+ public static final String MOD = "mod";
+ public static final String SPECIAL = "special";
+ public static final String FEATURE = "feature";
+ private ModProtocolIds() { }
+
+ public static Identifier mod(String modId) {
+ return Identifier.of(MOD, modId);
+ }
+
+ public static Identifier special(String path) {
+ return Identifier.of(SPECIAL, path);
+ }
+
+ public static Identifier feature(String path) {
+ return Identifier.of(FEATURE, path);
+ }
+}
diff --git a/fabric-mod-protocol-api-v1/src/main/java/net/fabricmc/fabric/api/modprotocol/v1/ModProtocolRegistry.java b/fabric-mod-protocol-api-v1/src/main/java/net/fabricmc/fabric/api/modprotocol/v1/ModProtocolRegistry.java
new file mode 100644
index 0000000000..9471a364e0
--- /dev/null
+++ b/fabric-mod-protocol-api-v1/src/main/java/net/fabricmc/fabric/api/modprotocol/v1/ModProtocolRegistry.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.api.modprotocol.v1;
+
+import java.util.Collection;
+import java.util.Collections;
+
+import it.unimi.dsi.fastutil.ints.IntList;
+import org.jetbrains.annotations.Nullable;
+
+import net.minecraft.util.Identifier;
+
+import net.fabricmc.fabric.impl.modprotocol.ModProtocolImpl;
+import net.fabricmc.fabric.impl.modprotocol.ModProtocolManager;
+
+/**
+ * Utility methods allowing to lookup or register new protocols.
+ *
+ * While lookup can be done anytime, registration is only possible before registries are frozen.
+ */
+public final class ModProtocolRegistry {
+ private ModProtocolRegistry() { }
+
+ /**
+ * Allows to get a locally registered protocol.
+ * @param identifier protocol's id
+ * @return the requested ModProtocol or {@code null} if not found
+ */
+ @Nullable
+ public static ModProtocol get(Identifier identifier) {
+ return ModProtocolManager.LOCAL_MOD_PROTOCOLS_BY_ID.get(identifier);
+ }
+
+ /**
+ * @return All registered protocols
+ */
+ public static Collection getAll() {
+ return Collections.unmodifiableCollection(ModProtocolManager.LOCAL_MOD_PROTOCOLS);
+ }
+
+ /**
+ * Registers new mod protocol with its own unique settings.
+ *
+ * @param identifier the identifier of protocol
+ * @param name display name in protocol, shown if it's missing
+ * @param version display version of the protocol, shown if it's missing
+ * @param protocol list of protocol versions
+ * @param requireClient marks protocol as required on client
+ * @param requireServer marks protocol as required on server
+ * @return registered Mod Protocol
+ */
+ public static ModProtocol register(Identifier identifier, String name, String version, IntList protocol, boolean requireClient, boolean requireServer) {
+ return ModProtocolManager.add(null, new ModProtocolImpl(identifier, name, version, IntList.of(protocol.toIntArray()), requireClient, requireServer));
+ }
+
+ /**
+ * Allows to customize priority of namespaces when displaying missing protocols.
+ * @param firstNamespace namespace that should display first
+ * @param secondNamespace namespace that should display second
+ * @return true if change occurred, false if it didn't
+ */
+ public static boolean addDisplayOrdering(String firstNamespace, String secondNamespace) {
+ return ModProtocolManager.registerOrder(firstNamespace, secondNamespace);
+ }
+}
diff --git a/fabric-mod-protocol-api-v1/src/main/java/net/fabricmc/fabric/api/modprotocol/v1/ServerModProtocolLookup.java b/fabric-mod-protocol-api-v1/src/main/java/net/fabricmc/fabric/api/modprotocol/v1/ServerModProtocolLookup.java
new file mode 100644
index 0000000000..63557d79de
--- /dev/null
+++ b/fabric-mod-protocol-api-v1/src/main/java/net/fabricmc/fabric/api/modprotocol/v1/ServerModProtocolLookup.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.api.modprotocol.v1;
+
+import it.unimi.dsi.fastutil.objects.Object2IntMap;
+
+import net.minecraft.network.ClientConnection;
+import net.minecraft.server.network.ServerCommonNetworkHandler;
+import net.minecraft.server.network.ServerPlayerEntity;
+import net.minecraft.util.Identifier;
+
+import net.fabricmc.fabric.impl.modprotocol.RemoteProtocolStorage;
+
+/**
+ * Utility methods allowing to get protocol versions supported by the player.
+ *
+ * Protocol identifier's can be any valid identifier, through by default mods defining it will use "mod" namespace and path equal to its id.
+ * See {@link ModProtocolIds} for more information.
+ */
+public final class ServerModProtocolLookup {
+ /**
+ * A protocol version returned by {@code getSupportedProtocol} methods, when the player doesn't support the requested protocol.
+ */
+ public static final int UNSUPPORTED = -1;
+
+ private ServerModProtocolLookup() {
+ }
+
+ /**
+ * Gets protocol version supported by the player.
+ *
+ * @param player the player
+ * @param protocolId protocol's id
+ * @return Protocol version supported by the player
+ */
+ public static int getSupportedProtocol(ServerPlayerEntity player, Identifier protocolId) {
+ return RemoteProtocolStorage.getProtocol(player.networkHandler, protocolId);
+ }
+
+ /**
+ * Gets protocol version supported by the player.
+ *
+ * @param handler the network handler owned by the player you want to check protocol for
+ * @param protocolId protocol's id
+ * @return Protocol version supported by the player
+ */
+ public static int getSupportedProtocol(ServerCommonNetworkHandler handler, Identifier protocolId) {
+ return RemoteProtocolStorage.getProtocol(handler, protocolId);
+ }
+
+ /**
+ * Gets protocol version supported by the server.
+ *
+ * @param connection the ClientConnection connected to the server
+ * @param protocolId protocol's id
+ * @return Protocol version supported by the server
+ */
+ public static int getSupportedProtocol(ClientConnection connection, Identifier protocolId) {
+ return RemoteProtocolStorage.getProtocol(connection, protocolId);
+ }
+
+ /**
+ * Gets protocol version supported by the player.
+ *
+ * @param player the player
+ * @param protocol protocol to check against
+ * @return Protocol version supported by the player
+ */
+ public static int getSupportedProtocol(ServerPlayerEntity player, ModProtocol protocol) {
+ return RemoteProtocolStorage.getProtocol(player.networkHandler, protocol.id());
+ }
+
+ /**
+ * Gets protocol version supported by the player.
+ *
+ * @param handler the network handler owned by the player you want to check protocol for
+ * @param protocol protocol to check against
+ * @return Protocol version supported by the player
+ */
+ public static int getSupportedProtocol(ServerCommonNetworkHandler handler, ModProtocol protocol) {
+ return RemoteProtocolStorage.getProtocol(handler, protocol.id());
+ }
+
+ /**
+ * Gets protocol version supported by the server.
+ *
+ * @param connection the ClientConnection connected to the server.
+ * @param protocol protocol to check against
+ * @return Protocol version supported by the server
+ */
+ public static int getSupportedProtocol(ClientConnection connection, ModProtocol protocol) {
+ return RemoteProtocolStorage.getProtocol(connection, protocol.id());
+ }
+
+ /**
+ * Gets all protocols supported by the player.
+ *
+ * @param player the player
+ * @return Map of protocols supported by the player
+ */
+ public static Object2IntMap getAllSupportedProtocols(ServerPlayerEntity player) {
+ return RemoteProtocolStorage.getMap(player.networkHandler);
+ }
+
+ /**
+ * Gets all protocols supported by the player.
+ *
+ * @param handler the network handler owned by the player you want to check protocol for
+ * @return Map of protocols supported by the player
+ */
+ public static Object2IntMap getAllSupportedProtocols(ServerCommonNetworkHandler handler) {
+ return RemoteProtocolStorage.getMap(handler);
+ }
+
+ /**
+ * Gets all protocols supported by the player.
+ *
+ * @param connection the ClientConnection connected to the server
+ * @return Map of protocols supported by the player
+ */
+ public static Object2IntMap getAllSupportedProtocols(ClientConnection connection) {
+ return RemoteProtocolStorage.getMap(connection);
+ }
+}
diff --git a/fabric-mod-protocol-api-v1/src/main/java/net/fabricmc/fabric/api/modprotocol/v1/package-info.java b/fabric-mod-protocol-api-v1/src/main/java/net/fabricmc/fabric/api/modprotocol/v1/package-info.java
new file mode 100644
index 0000000000..7b5a75821e
--- /dev/null
+++ b/fabric-mod-protocol-api-v1/src/main/java/net/fabricmc/fabric/api/modprotocol/v1/package-info.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * The Mod Protocol API, version 1.
+ *
+ * Mod Protocol is an additional syncing system allowing mods to define their protocol, with simple utilities
+ * allowing to check supported version and use it for networking or safeguards against using mismatched/incompatible
+ * mod versions on client and server. Exact configuration can differ from mod to mod.
+ *
+ * The mod protocol can be defined in two ways:
+ *
+ * - fabric.mod.json
+ * - This is the simplest way to define it. Can be useful when you don't need to change it depending on mods configuration
+ * or external dependencies. It is set within custom field of that file under "fabric:mod_protocol" key.
+ *
+ * It can be defined in multiple ways:
+ *
+ * - "fabric:mod_protocol": 1
+ * This will automatically use mods id with "mod" namespace as the protocol identifier, as a single supported protocol version,
+ * with display name and version being copied form mods metadata. It also marks the protocol as required on both client and server.
+ *
+ * - "fabric:mod_protocol": {
+ * "protocol": [1, 2],
+ * "id: "custom:id",
+ * "name": "Mod Name",
+ * "version": "v1.2.3",
+ * "require_client": false,
+ * "require_server": true
+ * }
+ * Full object. Only required value is "protocol", which can be set directly for single version or as an array for multiple.
+ * "id" is the protocols identifier, which can have any namespace and path, as long as it's valid.
+ * It is optional and defaults to an ID with "mod" namespace and path equal to mod's id.
+ *
+ * "name" is a name displayed if protocol doesn't match. It's optional and by default it uses one from mod's metadata.
+ * "version" is a version displayed if protocol doesn't match. It's optional and by default it uses one from mod's metadata.
+ * "require_client" controls if clients without this protocol can join the server, defaults to true, preventing joining
+ * "require_server" controls if clients can join servers without this mod, defaults to true, preventing joining
+ *
+ * - "fabric:mod_protocol": [{
+ * "protocol": [1, 2],
+ * "id: "custom:id",
+ * "name": "Mod Name",
+ * "version": "v1.2.3",
+ * "require_client": false,
+ * "require_server": true
+ * }]
+ * Array of full objects. Allows to define multiple versions of the protocol. The inner objects use the same format as single-full object format,
+ * with main exception being that fields "id", "name" and "version" aren't defaulted and need to be always set
+ *
+ *
+ * - {@link net.fabricmc.fabric.api.modprotocol.v1.ModProtocolRegistry}
+ * - This is the simplest way to define it. Can be useful when you don't need to change it depending on mods configuration
+ * or external dependencies. It is set within custom field of that file under "fabric:mod_protocol" key.
+ *
+ *
+ *
+ */
+@ApiStatus.Experimental
+package net.fabricmc.fabric.api.modprotocol.v1;
+
+import org.jetbrains.annotations.ApiStatus;
diff --git a/fabric-mod-protocol-api-v1/src/main/java/net/fabricmc/fabric/impl/modprotocol/ModProtocolHolder.java b/fabric-mod-protocol-api-v1/src/main/java/net/fabricmc/fabric/impl/modprotocol/ModProtocolHolder.java
new file mode 100644
index 0000000000..e783a9dc94
--- /dev/null
+++ b/fabric-mod-protocol-api-v1/src/main/java/net/fabricmc/fabric/impl/modprotocol/ModProtocolHolder.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.impl.modprotocol;
+
+import java.util.List;
+
+import org.jetbrains.annotations.Nullable;
+
+import net.minecraft.server.ServerMetadata;
+
+public interface ModProtocolHolder {
+ static ModProtocolHolder of(ServerMetadata input) {
+ return (ModProtocolHolder) (Object) input;
+ }
+
+ @Nullable
+ List fabric$getModProtocol();
+ void fabric$setModProtocol(List protocol);
+}
diff --git a/fabric-mod-protocol-api-v1/src/main/java/net/fabricmc/fabric/impl/modprotocol/ModProtocolImpl.java b/fabric-mod-protocol-api-v1/src/main/java/net/fabricmc/fabric/impl/modprotocol/ModProtocolImpl.java
new file mode 100644
index 0000000000..4e13786391
--- /dev/null
+++ b/fabric-mod-protocol-api-v1/src/main/java/net/fabricmc/fabric/impl/modprotocol/ModProtocolImpl.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.impl.modprotocol;
+
+import java.util.List;
+import java.util.stream.IntStream;
+
+import com.mojang.serialization.Codec;
+import com.mojang.serialization.codecs.RecordCodecBuilder;
+import it.unimi.dsi.fastutil.ints.IntList;
+
+import net.minecraft.network.PacketByteBuf;
+import net.minecraft.network.codec.PacketCodec;
+import net.minecraft.util.Identifier;
+
+import net.fabricmc.fabric.api.modprotocol.v1.ModProtocol;
+
+public record ModProtocolImpl(Identifier id, String name, String version, IntList protocol, boolean requireClient, boolean requireServer) implements ModProtocol {
+ public static final int UNSUPPORTED = -1;
+ public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group(
+ Identifier.CODEC.fieldOf("id").forGetter(ModProtocolImpl::id),
+ Codec.STRING.fieldOf("name").forGetter(ModProtocolImpl::name),
+ Codec.STRING.fieldOf("version").forGetter(ModProtocolImpl::version),
+ Codec.INT_STREAM.xmap(x -> IntList.of(x.toArray()), x -> IntStream.of(x.toIntArray())).fieldOf("protocol").forGetter(ModProtocolImpl::protocol),
+ Codec.BOOL.fieldOf("require_client").forGetter(ModProtocolImpl::requireClient),
+ Codec.BOOL.fieldOf("require_server").forGetter(ModProtocolImpl::requireServer)
+ ).apply(instance, ModProtocolImpl::new));
+ public static final Codec> LIST_CODEC = CODEC.listOf();
+ public static final PacketCodec PACKET_CODEC = PacketCodec.ofStatic(ModProtocolImpl::encode, ModProtocolImpl::decode);
+
+ private static ModProtocolImpl decode(PacketByteBuf buf) {
+ Identifier id = buf.readIdentifier();
+ String name = buf.readString();
+ String version = buf.readString();
+ IntList protocols = IntList.of(buf.readIntArray());
+ byte b = buf.readByte();
+ boolean requireClient = (b & 0b10) != 0;
+ boolean requireServer = (b & 0b01) != 0;
+ return new ModProtocolImpl(id, name, version, protocols, requireClient, requireServer);
+ }
+
+ private static void encode(PacketByteBuf buf, ModProtocolImpl protocol) {
+ buf.writeIdentifier(protocol.id);
+ buf.writeString(protocol.name);
+ buf.writeString(protocol.version);
+ buf.writeIntArray(protocol.protocol.toIntArray());
+ buf.writeByte((protocol.requireClient ? 0b10 : 0) | (protocol.requireServer ? 0b01 : 0));
+ }
+
+ public int getHighestVersion(IntList list) {
+ int value = UNSUPPORTED;
+ int size = list.size();
+
+ for (int i = 0; i < size; i++) {
+ int proto = list.getInt(i);
+
+ if (this.protocol.contains(proto) && value < proto) {
+ value = proto;
+ }
+ }
+
+ return value;
+ }
+
+ public boolean syncWithServerMetadata() {
+ return this.requireClient() || this.requireServer();
+ }
+}
diff --git a/fabric-mod-protocol-api-v1/src/main/java/net/fabricmc/fabric/impl/modprotocol/ModProtocolInit.java b/fabric-mod-protocol-api-v1/src/main/java/net/fabricmc/fabric/impl/modprotocol/ModProtocolInit.java
new file mode 100644
index 0000000000..8019b52aef
--- /dev/null
+++ b/fabric-mod-protocol-api-v1/src/main/java/net/fabricmc/fabric/impl/modprotocol/ModProtocolInit.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.impl.modprotocol;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import net.minecraft.util.Identifier;
+
+import net.fabricmc.api.ModInitializer;
+import net.fabricmc.fabric.api.event.Event;
+import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry;
+import net.fabricmc.fabric.api.networking.v1.ServerConfigurationConnectionEvents;
+import net.fabricmc.fabric.api.networking.v1.ServerConfigurationNetworking;
+import net.fabricmc.fabric.impl.modprotocol.payload.ModProtocolRequestS2CPayload;
+import net.fabricmc.fabric.impl.modprotocol.payload.ModProtocolResponseC2SPayload;
+
+public final class ModProtocolInit implements ModInitializer {
+ public static final String MOD_ID = "fabric-mod-protocol-api-v1";
+ public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID);
+ public static boolean frozen = false;
+
+ public void onInitialize() {
+ Identifier phase = Identifier.of("fabric", "mod_protocol");
+ Identifier registrySync = Identifier.of("fabric", "registry_sync");
+
+ ServerConfigurationConnectionEvents.BEFORE_CONFIGURE.addPhaseOrdering(phase, Event.DEFAULT_PHASE);
+ ServerConfigurationConnectionEvents.BEFORE_CONFIGURE.addPhaseOrdering(phase, registrySync);
+ ServerConfigurationConnectionEvents.BEFORE_CONFIGURE.register(phase, ModProtocolManager::setupClient);
+
+ PayloadTypeRegistry.configurationC2S().register(ModProtocolResponseC2SPayload.ID, ModProtocolResponseC2SPayload.PACKET_CODEC);
+ PayloadTypeRegistry.configurationS2C().register(ModProtocolRequestS2CPayload.ID, ModProtocolRequestS2CPayload.PACKET_CODEC);
+ ServerConfigurationNetworking.registerGlobalReceiver(ModProtocolResponseC2SPayload.ID, (payload, context) -> {
+ ((RemoteProtocolStorage) context.networkHandler()).fabric$setRemoteProtocol(payload.supported());
+ context.networkHandler().completeTask(ModProtocolManager.SyncConfigurationTask.KEY);
+ });
+
+ ModProtocolManager.collectModProtocols();
+ }
+}
diff --git a/fabric-mod-protocol-api-v1/src/main/java/net/fabricmc/fabric/impl/modprotocol/ModProtocolLocator.java b/fabric-mod-protocol-api-v1/src/main/java/net/fabricmc/fabric/impl/modprotocol/ModProtocolLocator.java
new file mode 100644
index 0000000000..fa15d54d87
--- /dev/null
+++ b/fabric-mod-protocol-api-v1/src/main/java/net/fabricmc/fabric/impl/modprotocol/ModProtocolLocator.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.impl.modprotocol;
+
+import java.util.function.BiConsumer;
+
+import it.unimi.dsi.fastutil.ints.IntArrayList;
+import it.unimi.dsi.fastutil.ints.IntList;
+
+import net.minecraft.util.Identifier;
+
+import net.fabricmc.loader.api.FabricLoader;
+import net.fabricmc.loader.api.ModContainer;
+import net.fabricmc.loader.api.metadata.CustomValue;
+import net.fabricmc.loader.api.metadata.ModMetadata;
+
+public class ModProtocolLocator {
+ public static void provide(BiConsumer consumer) {
+ for (ModContainer mod : FabricLoader.getInstance().getAllMods()) {
+ create(mod, consumer);
+ }
+ }
+
+ private static void create(ModContainer container, BiConsumer consumer) {
+ ModMetadata meta = container.getMetadata();
+ CustomValue definition = meta.getCustomValue("fabric:mod_protocol");
+
+ if (definition == null) {
+ return;
+ }
+
+ if (definition.getType() == CustomValue.CvType.ARRAY) {
+ for (CustomValue entry : definition.getAsArray()) {
+ consumer.accept(container, decodeFullDefinition(entry, meta, true));
+ }
+ } else if (definition.getType() == CustomValue.CvType.NUMBER) {
+ consumer.accept(container, new ModProtocolImpl(Identifier.of("mod", meta.getId()), meta.getName(), meta.getVersion().getFriendlyString(), IntList.of(definition.getAsNumber().intValue()), true, true));
+ } else {
+ consumer.accept(container, decodeFullDefinition(definition, meta, false));
+ }
+ }
+
+ public static ModProtocolImpl decodeFullDefinition(CustomValue entry, ModMetadata meta, boolean requireFullData) {
+ if (entry.getType() != CustomValue.CvType.OBJECT) {
+ throw new RuntimeException("Mod Protocol entry provided by '" + meta.getId() + "' is not valid!");
+ }
+
+ CustomValue.CvObject object = entry.getAsObject();
+ Identifier id;
+ String name;
+ String version;
+ boolean requiredClient;
+ boolean requiredServer;
+ IntList protocols = new IntArrayList();
+
+ CustomValue idField = object.get("id");
+ CustomValue nameField = object.get("name");
+ CustomValue versionField = object.get("version");
+ CustomValue protocolField = object.get("protocol");
+ CustomValue requiredClientField = object.get("require_client");
+ CustomValue requiredServerField = object.get("require_server");
+
+ if (!requireFullData && idField == null) {
+ id = Identifier.of("mod", meta.getId());
+ } else if (idField != null && idField.getType() == CustomValue.CvType.STRING) {
+ id = Identifier.of(idField.getAsString());
+ } else {
+ throw new RuntimeException("Mod Protocol entry provided by '" + meta.getId() + "' is not valid!");
+ }
+
+ if (protocolField != null && protocolField.getType() == CustomValue.CvType.NUMBER) {
+ protocols.add(protocolField.getAsNumber().intValue());
+ } else if (protocolField != null && protocolField.getType() == CustomValue.CvType.ARRAY) {
+ for (CustomValue value : protocolField.getAsArray()) {
+ if (value.getType() == CustomValue.CvType.NUMBER) {
+ protocols.add(value.getAsNumber().intValue());
+ } else {
+ throw new RuntimeException("Mod Protocol entry provided by '" + meta.getId() + "' is not valid!");
+ }
+ }
+ } else {
+ throw new RuntimeException("Mod Protocol entry provided by '" + meta.getId() + "' is not valid!");
+ }
+
+ if (!requireFullData && nameField == null) {
+ name = meta.getName();
+ } else if (nameField != null && nameField.getType() == CustomValue.CvType.STRING) {
+ name = nameField.getAsString();
+ } else {
+ throw new RuntimeException("Mod Protocol entry provided by '" + meta.getId() + "' is not valid!");
+ }
+
+ if (!requireFullData && versionField == null) {
+ version = meta.getName();
+ } else if (versionField != null && versionField.getType() == CustomValue.CvType.STRING) {
+ version = versionField.getAsString();
+ } else {
+ throw new RuntimeException("Mod Protocol entry provided by '" + meta.getId() + "' is not valid!");
+ }
+
+ if (requiredClientField == null) {
+ requiredClient = true;
+ } else if (requiredClientField.getType() == CustomValue.CvType.BOOLEAN) {
+ requiredClient = requiredClientField.getAsBoolean();
+ } else {
+ throw new RuntimeException("Mod Protocol entry provided by '" + meta.getId() + "' is not valid!");
+ }
+
+ if (requiredServerField == null) {
+ requiredServer = true;
+ } else if (requiredServerField.getType() == CustomValue.CvType.BOOLEAN) {
+ requiredServer = requiredServerField.getAsBoolean();
+ } else {
+ throw new RuntimeException("Mod Protocol entry provided by '" + meta.getId() + "' is not valid!");
+ }
+
+ return new ModProtocolImpl(id, name, version, IntList.of(protocols.toIntArray()), requiredClient, requiredServer);
+ }
+}
diff --git a/fabric-mod-protocol-api-v1/src/main/java/net/fabricmc/fabric/impl/modprotocol/ModProtocolManager.java b/fabric-mod-protocol-api-v1/src/main/java/net/fabricmc/fabric/impl/modprotocol/ModProtocolManager.java
new file mode 100644
index 0000000000..3d22b9fb14
--- /dev/null
+++ b/fabric-mod-protocol-api-v1/src/main/java/net/fabricmc/fabric/impl/modprotocol/ModProtocolManager.java
@@ -0,0 +1,233 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.impl.modprotocol;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+
+import it.unimi.dsi.fastutil.objects.Object2IntMap;
+import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
+import org.jetbrains.annotations.Nullable;
+
+import net.minecraft.network.packet.Packet;
+import net.minecraft.network.packet.s2c.common.CustomPayloadS2CPacket;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.network.ServerConfigurationNetworkHandler;
+import net.minecraft.server.network.ServerPlayerConfigurationTask;
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import net.minecraft.util.Identifier;
+
+import net.fabricmc.fabric.api.networking.v1.ServerConfigurationNetworking;
+import net.fabricmc.fabric.impl.modprotocol.payload.ModProtocolRequestS2CPayload;
+import net.fabricmc.loader.api.ModContainer;
+
+public final class ModProtocolManager {
+ public static final List NAMESPACE_PRIORITY = new ArrayList<>(List.of("special", "mod", "feature"));
+ public static final Comparator MOD_PROTOCOL_COMPARATOR = Comparator.comparingInt(x -> {
+ int out = NAMESPACE_PRIORITY.indexOf(x.id().getNamespace());
+ return out == -1 ? NAMESPACE_PRIORITY.size() : out;
+ }).thenComparing(ModProtocolImpl::id);
+
+ public static final Map LOCAL_MOD_PROTOCOLS_BY_ID = new HashMap<>();
+ public static final List LOCAL_MOD_PROTOCOLS = new ArrayList<>();
+ public static final List PING_SYNCED_PROTOCOLS = new ArrayList<>();
+ public static final List CLIENT_REQUIRED = new ArrayList<>();
+ public static final List SERVER_REQUIRED = new ArrayList<>();
+
+ public static void setupClient(ServerConfigurationNetworkHandler handler, MinecraftServer server) {
+ if (!ServerConfigurationNetworking.canSend(handler, ModProtocolRequestS2CPayload.ID)) {
+ if (CLIENT_REQUIRED.isEmpty()) {
+ return;
+ } else {
+ handler.disconnect(constructMessage(new ArrayList<>(CLIENT_REQUIRED), Map.of()));
+ }
+ }
+
+ handler.addTask(new SyncConfigurationTask());
+ }
+
+ public static Text constructMessage(List missingProtocols, Map localProtocols) {
+ MutableText text = Text.empty();
+ text.append(TextUtil.translatable("text.fabric.mod_protocol.mismatched.title").formatted(Formatting.GOLD)).append("\n");
+ text.append(TextUtil.translatable("text.fabric.mod_protocol.mismatched.desc").formatted(Formatting.YELLOW)).append("\n\n");
+ text.append(TextUtil.translatable("text.fabric.mod_protocol.mismatched.entries.title").formatted(Formatting.RED)).append("\n");
+ appendTextEntries(missingProtocols, localProtocols, 6, text::append);
+ return text;
+ }
+
+ public static void appendTextEntries(List missingProtocols, Map localProtocols, int limit, Consumer consumer) {
+ missingProtocols.sort(MOD_PROTOCOL_COMPARATOR);
+
+ if (limit == -1) {
+ limit = missingProtocols.size();
+ }
+
+ int size = Math.min(limit, missingProtocols.size());
+
+ for (int i = 0; i < size; i++) {
+ ModProtocolImpl protocol = missingProtocols.get(i);
+ ModProtocolImpl local = localProtocols.get(protocol.id());
+ Text localVersion = local == null ? TextUtil.translatable("text.fabric.mod_protocol.missing").formatted(Formatting.DARK_RED)
+ : Text.literal(local.version()).formatted(Formatting.YELLOW);
+ Text remoteVersion = local == protocol ? TextUtil.translatable("text.fabric.mod_protocol.missing").formatted(Formatting.DARK_RED)
+ : Text.literal(protocol.version()).formatted(Formatting.YELLOW);
+
+ MutableText text = TextUtil.translatable("text.fabric.mod_protocol.entry",
+ Text.literal(protocol.name()).formatted(Formatting.WHITE), localVersion, remoteVersion).formatted(Formatting.GRAY);
+
+ if (i + 1 < size) {
+ text.append("\n");
+ }
+
+ consumer.accept(text);
+ }
+
+ if (limit < missingProtocols.size()) {
+ consumer.accept(Text.literal("\n").append(TextUtil.translatable("text.fabric.mod_protocol.and_x_more", missingProtocols.size() - size).formatted(Formatting.GRAY, Formatting.ITALIC)));
+ }
+ }
+
+ public static ValidationResult validateClient(Map received) {
+ return validate(received, LOCAL_MOD_PROTOCOLS_BY_ID, SERVER_REQUIRED);
+ }
+
+ public static ValidationResult validate(Map received, Map localById, List requiredRemote) {
+ var supported = new Object2IntOpenHashMap();
+ var missingLocal = new ArrayList();
+ var missingRemote = new ArrayList();
+
+ for (ModProtocolImpl modProtocol : received.values()) {
+ ModProtocolImpl local = localById.get(modProtocol.id());
+
+ if (local != null) {
+ int version = local.getHighestVersion(modProtocol.protocol());
+
+ if (version != -1) {
+ supported.put(modProtocol.id(), version);
+ } else if (modProtocol.requireClient()) {
+ missingLocal.add(modProtocol);
+ }
+ } else if (modProtocol.requireClient()) {
+ missingLocal.add(modProtocol);
+ }
+ }
+
+ for (ModProtocolImpl modProtocol : requiredRemote) {
+ ModProtocolImpl remote = received.get(modProtocol.id());
+
+ if (remote == null) {
+ missingRemote.add(modProtocol);
+ }
+ }
+
+ return new ValidationResult(supported, missingLocal, missingRemote);
+ }
+
+ public static void collectModProtocols() {
+ ModProtocolLocator.provide(ModProtocolManager::add);
+ }
+
+ public static ModProtocolImpl add(@Nullable ModContainer container, ModProtocolImpl protocol) {
+ if (LOCAL_MOD_PROTOCOLS_BY_ID.containsKey(protocol.id())) {
+ if (container != null) {
+ ModProtocolInit.LOGGER.warn("Found duplicate protocol id '{}' provided by mod '{}'", protocol.id(), container.getMetadata().getId());
+ } else {
+ ModProtocolInit.LOGGER.warn("Found duplicate protocol id '{}' registered by a mod!'", protocol.id(), new RuntimeException());
+ }
+
+ return LOCAL_MOD_PROTOCOLS_BY_ID.get(protocol.id());
+ }
+
+ LOCAL_MOD_PROTOCOLS_BY_ID.put(protocol.id(), protocol);
+ LOCAL_MOD_PROTOCOLS.add(protocol);
+
+ if (protocol.requireClient()) {
+ CLIENT_REQUIRED.add(protocol);
+ }
+
+ if (protocol.requireServer()) {
+ SERVER_REQUIRED.add(protocol);
+ }
+
+ if (protocol.syncWithServerMetadata()) {
+ PING_SYNCED_PROTOCOLS.add(protocol);
+ }
+
+ return null;
+ }
+
+ @SuppressWarnings("ConstantValue")
+ public static boolean registerOrder(String firstNamespace, String secondNamespace) {
+ if (firstNamespace.equals(secondNamespace)) {
+ return false;
+ }
+
+ int firstIndex = NAMESPACE_PRIORITY.indexOf(firstNamespace);
+ int secondIndex = NAMESPACE_PRIORITY.indexOf(secondNamespace);
+
+ if (firstIndex != -1 && secondIndex != -1) {
+ if (firstIndex > secondIndex) {
+ ModProtocolInit.LOGGER.warn("Protocol '{}' is already set to display after '{}'!", firstNamespace, secondNamespace);
+ return false;
+ }
+
+ return true;
+ } else if (firstIndex == -1) {
+ NAMESPACE_PRIORITY.add(secondIndex, firstNamespace);
+ } else if (secondIndex == -1) {
+ NAMESPACE_PRIORITY.add(firstIndex + 1, secondNamespace);
+ } else {
+ NAMESPACE_PRIORITY.add(firstNamespace);
+ NAMESPACE_PRIORITY.add(secondNamespace);
+ }
+
+ return true;
+ }
+
+ public static class SyncConfigurationTask implements ServerPlayerConfigurationTask {
+ public static final Key KEY = new Key("fabric:mod_protocol_sync");
+
+ @Override
+ public void sendPacket(Consumer> sender) {
+ sender.accept(new CustomPayloadS2CPacket(new ModProtocolRequestS2CPayload(LOCAL_MOD_PROTOCOLS)));
+ }
+
+ @Override
+ public Key getKey() {
+ return KEY;
+ }
+ }
+
+ public record ValidationResult(Object2IntMap supportedProtocols, List missingLocal, List missingRemote) {
+ public boolean isSuccess() {
+ return missingLocal.isEmpty() && missingRemote.isEmpty();
+ }
+
+ public List missing() {
+ var arr = new ArrayList();
+ arr.addAll(missingLocal);
+ arr.addAll(missingRemote);
+ return arr;
+ }
+ }
+}
diff --git a/fabric-mod-protocol-api-v1/src/main/java/net/fabricmc/fabric/impl/modprotocol/RemoteProtocolStorage.java b/fabric-mod-protocol-api-v1/src/main/java/net/fabricmc/fabric/impl/modprotocol/RemoteProtocolStorage.java
new file mode 100644
index 0000000000..860bdd1c2d
--- /dev/null
+++ b/fabric-mod-protocol-api-v1/src/main/java/net/fabricmc/fabric/impl/modprotocol/RemoteProtocolStorage.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.impl.modprotocol;
+
+import it.unimi.dsi.fastutil.objects.Object2IntMap;
+import it.unimi.dsi.fastutil.objects.Object2IntMaps;
+import org.jetbrains.annotations.Nullable;
+
+import net.minecraft.util.Identifier;
+
+public interface RemoteProtocolStorage {
+ static int getProtocol(Object object, Identifier identifier) {
+ if (object instanceof RemoteProtocolStorage storage) {
+ Object2IntMap map = storage.fabric$getRemoteProtocol();
+
+ if (map != null) {
+ return map.getOrDefault(identifier, -1);
+ }
+ }
+
+ return -1;
+ }
+
+ static Object2IntMap getMap(Object object) {
+ if (object instanceof RemoteProtocolStorage storage) {
+ Object2IntMap map = storage.fabric$getRemoteProtocol();
+
+ if (map != null) {
+ return Object2IntMaps.unmodifiable(map);
+ }
+ }
+
+ return Object2IntMaps.emptyMap();
+ }
+
+ @Nullable
+ Object2IntMap fabric$getRemoteProtocol();
+ void fabric$setRemoteProtocol(Object2IntMap protocol);
+}
diff --git a/fabric-mod-protocol-api-v1/src/main/java/net/fabricmc/fabric/impl/modprotocol/TextUtil.java b/fabric-mod-protocol-api-v1/src/main/java/net/fabricmc/fabric/impl/modprotocol/TextUtil.java
new file mode 100644
index 0000000000..d9d13c1a3c
--- /dev/null
+++ b/fabric-mod-protocol-api-v1/src/main/java/net/fabricmc/fabric/impl/modprotocol/TextUtil.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.impl.modprotocol;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+
+import net.fabricmc.loader.api.FabricLoader;
+import net.fabricmc.loader.api.ModContainer;
+
+public class TextUtil {
+ private static final Map FALLBACK_TRANSLATIONS = new HashMap<>();
+
+ static {
+ try {
+ Optional container = FabricLoader.getInstance().getModContainer(ModProtocolInit.MOD_ID);
+ Optional path = container.get().findPath("assets/fabric-mod-protocol-api-v1/lang/en_us.json");
+ JsonObject lang = JsonParser.parseString(Files.readString(path.get())).getAsJsonObject();
+
+ for (String key : lang.keySet()) {
+ FALLBACK_TRANSLATIONS.put(key, lang.get(key).getAsString());
+ }
+ } catch (Throwable e) {
+ ModProtocolInit.LOGGER.error("Failed to load translation file!", e);
+ }
+ }
+
+ public static MutableText translatable(String key, Object... args) {
+ return Text.translatableWithFallback(key, FALLBACK_TRANSLATIONS.get(key), args);
+ }
+
+ public static MutableText translatable(String key) {
+ return Text.translatableWithFallback(key, FALLBACK_TRANSLATIONS.get(key));
+ }
+}
diff --git a/fabric-mod-protocol-api-v1/src/main/java/net/fabricmc/fabric/impl/modprotocol/payload/ModProtocolRequestS2CPayload.java b/fabric-mod-protocol-api-v1/src/main/java/net/fabricmc/fabric/impl/modprotocol/payload/ModProtocolRequestS2CPayload.java
new file mode 100644
index 0000000000..d170f00d94
--- /dev/null
+++ b/fabric-mod-protocol-api-v1/src/main/java/net/fabricmc/fabric/impl/modprotocol/payload/ModProtocolRequestS2CPayload.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.impl.modprotocol.payload;
+
+import java.util.List;
+
+import net.minecraft.network.PacketByteBuf;
+import net.minecraft.network.codec.PacketCodec;
+import net.minecraft.network.codec.PacketCodecs;
+import net.minecraft.network.packet.CustomPayload;
+import net.minecraft.util.Identifier;
+
+import net.fabricmc.fabric.impl.modprotocol.ModProtocolImpl;
+
+public record ModProtocolRequestS2CPayload(List modProtocol) implements CustomPayload {
+ public static final Id ID = new Id<>(Identifier.of("fabric", "mod_protocol_request"));
+ public static final PacketCodec PACKET_CODEC = ModProtocolImpl.PACKET_CODEC.collect(PacketCodecs.toList())
+ .xmap(ModProtocolRequestS2CPayload::new, ModProtocolRequestS2CPayload::modProtocol);
+
+ @Override
+ public Id extends CustomPayload> getId() {
+ return ID;
+ }
+}
diff --git a/fabric-mod-protocol-api-v1/src/main/java/net/fabricmc/fabric/impl/modprotocol/payload/ModProtocolResponseC2SPayload.java b/fabric-mod-protocol-api-v1/src/main/java/net/fabricmc/fabric/impl/modprotocol/payload/ModProtocolResponseC2SPayload.java
new file mode 100644
index 0000000000..b16f9cdf0b
--- /dev/null
+++ b/fabric-mod-protocol-api-v1/src/main/java/net/fabricmc/fabric/impl/modprotocol/payload/ModProtocolResponseC2SPayload.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.impl.modprotocol.payload;
+
+import it.unimi.dsi.fastutil.objects.Object2IntMap;
+import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
+
+import net.minecraft.network.PacketByteBuf;
+import net.minecraft.network.codec.PacketCodec;
+import net.minecraft.network.codec.PacketCodecs;
+import net.minecraft.network.packet.CustomPayload;
+import net.minecraft.util.Identifier;
+
+public record ModProtocolResponseC2SPayload(Object2IntMap supported) implements CustomPayload {
+ public static final Id ID = new Id<>(Identifier.of("fabric", "mod_protocol/response"));
+ public static final PacketCodec PACKET_CODEC =
+ PacketCodecs.map(ModProtocolResponseC2SPayload::createMap, Identifier.PACKET_CODEC, PacketCodecs.INTEGER)
+ .xmap(ModProtocolResponseC2SPayload::new, ModProtocolResponseC2SPayload::supported).cast();
+
+ private static Object2IntMap createMap(int i) {
+ return new Object2IntOpenHashMap<>(i);
+ }
+
+ @Override
+ public Id extends CustomPayload> getId() {
+ return ID;
+ }
+}
diff --git a/fabric-mod-protocol-api-v1/src/main/java/net/fabricmc/fabric/mixin/modprotocol/ClientConnectionMixin.java b/fabric-mod-protocol-api-v1/src/main/java/net/fabricmc/fabric/mixin/modprotocol/ClientConnectionMixin.java
new file mode 100644
index 0000000000..822ebb9dfd
--- /dev/null
+++ b/fabric-mod-protocol-api-v1/src/main/java/net/fabricmc/fabric/mixin/modprotocol/ClientConnectionMixin.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.mixin.modprotocol;
+
+import it.unimi.dsi.fastutil.objects.Object2IntMap;
+import org.jetbrains.annotations.Nullable;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Unique;
+
+import net.minecraft.network.ClientConnection;
+import net.minecraft.util.Identifier;
+
+import net.fabricmc.fabric.impl.modprotocol.RemoteProtocolStorage;
+
+@Mixin(ClientConnection.class)
+public class ClientConnectionMixin implements RemoteProtocolStorage {
+ @Unique
+ @Nullable
+ private Object2IntMap remoteProtocol = null;
+
+ @Override
+ public Object2IntMap fabric$getRemoteProtocol() {
+ return this.remoteProtocol;
+ }
+
+ @Override
+ public void fabric$setRemoteProtocol(Object2IntMap protocol) {
+ this.remoteProtocol = protocol;
+ }
+}
diff --git a/fabric-mod-protocol-api-v1/src/main/java/net/fabricmc/fabric/mixin/modprotocol/MinecraftServerMixin.java b/fabric-mod-protocol-api-v1/src/main/java/net/fabricmc/fabric/mixin/modprotocol/MinecraftServerMixin.java
new file mode 100644
index 0000000000..c7fd9b2b7b
--- /dev/null
+++ b/fabric-mod-protocol-api-v1/src/main/java/net/fabricmc/fabric/mixin/modprotocol/MinecraftServerMixin.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.mixin.modprotocol;
+
+import com.llamalad7.mixinextras.injector.ModifyReturnValue;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.ServerMetadata;
+
+import net.fabricmc.fabric.impl.modprotocol.ModProtocolHolder;
+import net.fabricmc.fabric.impl.modprotocol.ModProtocolManager;
+
+@Mixin(MinecraftServer.class)
+public class MinecraftServerMixin {
+ @ModifyReturnValue(method = "createMetadata", at = @At("RETURN"))
+ private ServerMetadata addModProtocol(ServerMetadata original) {
+ if (!ModProtocolManager.PING_SYNCED_PROTOCOLS.isEmpty()) {
+ ModProtocolHolder.of(original).fabric$setModProtocol(ModProtocolManager.PING_SYNCED_PROTOCOLS);
+ }
+
+ return original;
+ }
+}
diff --git a/fabric-mod-protocol-api-v1/src/main/java/net/fabricmc/fabric/mixin/modprotocol/RegistriesMixin.java b/fabric-mod-protocol-api-v1/src/main/java/net/fabricmc/fabric/mixin/modprotocol/RegistriesMixin.java
new file mode 100644
index 0000000000..affb107254
--- /dev/null
+++ b/fabric-mod-protocol-api-v1/src/main/java/net/fabricmc/fabric/mixin/modprotocol/RegistriesMixin.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.mixin.modprotocol;
+
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+import net.minecraft.registry.Registries;
+
+import net.fabricmc.fabric.impl.modprotocol.ModProtocolInit;
+
+@Mixin(Registries.class)
+public class RegistriesMixin {
+ @Inject(method = "freezeRegistries", at = @At("TAIL"))
+ private static void onRegistryFrozen(CallbackInfo ci) {
+ ModProtocolInit.frozen = true;
+ }
+}
diff --git a/fabric-mod-protocol-api-v1/src/main/java/net/fabricmc/fabric/mixin/modprotocol/ServerCommonNetworkHandlerMixin.java b/fabric-mod-protocol-api-v1/src/main/java/net/fabricmc/fabric/mixin/modprotocol/ServerCommonNetworkHandlerMixin.java
new file mode 100644
index 0000000000..9b07f101c3
--- /dev/null
+++ b/fabric-mod-protocol-api-v1/src/main/java/net/fabricmc/fabric/mixin/modprotocol/ServerCommonNetworkHandlerMixin.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.mixin.modprotocol;
+
+import it.unimi.dsi.fastutil.objects.Object2IntMap;
+import org.spongepowered.asm.mixin.Final;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Shadow;
+
+import net.minecraft.network.ClientConnection;
+import net.minecraft.server.network.ServerCommonNetworkHandler;
+import net.minecraft.util.Identifier;
+
+import net.fabricmc.fabric.impl.modprotocol.RemoteProtocolStorage;
+
+@Mixin(ServerCommonNetworkHandler.class)
+public class ServerCommonNetworkHandlerMixin implements RemoteProtocolStorage {
+ @Shadow
+ @Final
+ protected ClientConnection connection;
+
+ @Override
+ public Object2IntMap fabric$getRemoteProtocol() {
+ return ((RemoteProtocolStorage) this.connection).fabric$getRemoteProtocol();
+ }
+
+ @Override
+ public void fabric$setRemoteProtocol(Object2IntMap protocol) {
+ ((RemoteProtocolStorage) this.connection).fabric$setRemoteProtocol(protocol);
+ }
+}
diff --git a/fabric-mod-protocol-api-v1/src/main/java/net/fabricmc/fabric/mixin/modprotocol/ServerMetadataMixin.java b/fabric-mod-protocol-api-v1/src/main/java/net/fabricmc/fabric/mixin/modprotocol/ServerMetadataMixin.java
new file mode 100644
index 0000000000..c2c4a0833f
--- /dev/null
+++ b/fabric-mod-protocol-api-v1/src/main/java/net/fabricmc/fabric/mixin/modprotocol/ServerMetadataMixin.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.mixin.modprotocol;
+
+import java.util.List;
+
+import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
+import com.mojang.datafixers.util.Pair;
+import com.mojang.serialization.Codec;
+import com.mojang.serialization.DataResult;
+import com.mojang.serialization.DynamicOps;
+import org.jetbrains.annotations.Nullable;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Unique;
+import org.spongepowered.asm.mixin.injection.At;
+
+import net.minecraft.server.ServerMetadata;
+
+import net.fabricmc.fabric.impl.modprotocol.ModProtocolHolder;
+import net.fabricmc.fabric.impl.modprotocol.ModProtocolImpl;
+
+@Mixin(ServerMetadata.class)
+public class ServerMetadataMixin implements ModProtocolHolder {
+ @Unique
+ @Nullable
+ private List modProtocol;
+
+ @Override
+ public List fabric$getModProtocol() {
+ return this.modProtocol;
+ }
+
+ @Override
+ public void fabric$setModProtocol(List protocol) {
+ this.modProtocol = protocol;
+ }
+
+ @ModifyExpressionValue(method = "", at = @At(value = "INVOKE", target = "Lcom/mojang/serialization/codecs/RecordCodecBuilder;create(Ljava/util/function/Function;)Lcom/mojang/serialization/Codec;"))
+ private static Codec extendCodec(Codec original) {
+ return new Codec<>() {
+ @Override
+ public DataResult> decode(DynamicOps ops, T input) {
+ DataResult> decoded = original.decode(ops, input);
+
+ if (decoded.isSuccess()) {
+ DataResult protocol = ops.get(input, "fabric:mod_protocol");
+
+ if (protocol.isSuccess()) {
+ DataResult, T>> result = ModProtocolImpl.LIST_CODEC.decode(ops, protocol.getOrThrow());
+
+ if (result.isSuccess()) {
+ ModProtocolHolder.of(decoded.getOrThrow().getFirst()).fabric$setModProtocol(result.getOrThrow().getFirst());
+ }
+ }
+ }
+
+ return decoded;
+ }
+
+ @Override
+ public DataResult encode(ServerMetadata input, DynamicOps ops, T prefix) {
+ DataResult encode = original.encode(input, ops, prefix);
+
+ if (encode.isSuccess() && ModProtocolHolder.of(input).fabric$getModProtocol() != null) {
+ DataResult protocol = ModProtocolImpl.LIST_CODEC.encodeStart(ops, ModProtocolHolder.of(input).fabric$getModProtocol());
+
+ if (protocol.isSuccess()) {
+ encode = ops.mergeToMap(encode.getOrThrow(), ops.createString("fabric:mod_protocol"), protocol.getOrThrow());
+ }
+ }
+
+ return encode;
+ }
+ };
+ }
+}
diff --git a/fabric-mod-protocol-api-v1/src/main/resources/assets/fabric-mod-protocol-api-v1/icon.png b/fabric-mod-protocol-api-v1/src/main/resources/assets/fabric-mod-protocol-api-v1/icon.png
new file mode 100644
index 0000000000..2931efbf61
Binary files /dev/null and b/fabric-mod-protocol-api-v1/src/main/resources/assets/fabric-mod-protocol-api-v1/icon.png differ
diff --git a/fabric-mod-protocol-api-v1/src/main/resources/assets/fabric-mod-protocol-api-v1/lang/en_us.json b/fabric-mod-protocol-api-v1/src/main/resources/assets/fabric-mod-protocol-api-v1/lang/en_us.json
new file mode 100644
index 0000000000..02b67a2a94
--- /dev/null
+++ b/fabric-mod-protocol-api-v1/src/main/resources/assets/fabric-mod-protocol-api-v1/lang/en_us.json
@@ -0,0 +1,8 @@
+{
+ "text.fabric.mod_protocol.mismatched.title": "Required protocols are mismatched!",
+ "text.fabric.mod_protocol.mismatched.desc": "This usually means mods on your client don't match with the server!",
+ "text.fabric.mod_protocol.mismatched.entries.title": "== Mismatched entries ==",
+ "text.fabric.mod_protocol.and_x_more": "...and %s more...",
+ "text.fabric.mod_protocol.missing": "Missing!",
+ "text.fabric.mod_protocol.entry": "%s (Local: %s, Remote: %s)"
+}
diff --git a/fabric-mod-protocol-api-v1/src/main/resources/fabric-mod-protocol-api-v1.mixins.json b/fabric-mod-protocol-api-v1/src/main/resources/fabric-mod-protocol-api-v1.mixins.json
new file mode 100644
index 0000000000..af5ef83df3
--- /dev/null
+++ b/fabric-mod-protocol-api-v1/src/main/resources/fabric-mod-protocol-api-v1.mixins.json
@@ -0,0 +1,15 @@
+{
+ "required": true,
+ "package": "net.fabricmc.fabric.mixin.modprotocol",
+ "compatibilityLevel": "JAVA_17",
+ "mixins": [
+ "ClientConnectionMixin",
+ "MinecraftServerMixin",
+ "RegistriesMixin",
+ "ServerCommonNetworkHandlerMixin",
+ "ServerMetadataMixin"
+ ],
+ "injectors": {
+ "defaultRequire": 1
+ }
+}
diff --git a/fabric-mod-protocol-api-v1/src/main/resources/fabric.mod.json b/fabric-mod-protocol-api-v1/src/main/resources/fabric.mod.json
new file mode 100644
index 0000000000..2cf3c76866
--- /dev/null
+++ b/fabric-mod-protocol-api-v1/src/main/resources/fabric.mod.json
@@ -0,0 +1,42 @@
+{
+ "schemaVersion": 1,
+ "id": "fabric-mod-protocol-api-v1",
+ "name": "Fabric Mod Protocol API (v1)",
+ "description": "Versioned network protocol sync, validation and requirement system.",
+ "version": "${version}",
+ "environment": "*",
+ "license": "Apache-2.0",
+ "icon": "assets/fabric-mod-protocol-api-v1/icon.png",
+ "contact": {
+ "homepage": "https://fabricmc.net",
+ "irc": "irc://irc.esper.net:6667/fabric",
+ "issues": "https://github.com/FabricMC/fabric/issues",
+ "sources": "https://github.com/FabricMC/fabric"
+ },
+ "authors": [
+ "FabricMC"
+ ],
+ "entrypoints": {
+ "main": [
+ "net.fabricmc.fabric.impl.modprotocol.ModProtocolInit"
+ ],
+ "client": [
+ "net.fabricmc.fabric.impl.modprotocol.client.ClientModProtocolInit"
+ ]
+ },
+ "mixins": [
+ "fabric-mod-protocol-api-v1.mixins.json",
+ {
+ "config": "fabric-mod-protocol-api-v1.client.mixins.json",
+ "environment": "client"
+ }
+ ],
+ "depends": {
+ "fabricloader": ">=0.15.11",
+ "fabric-api-base": "*",
+ "fabric-networking-api-v1": "*"
+ },
+ "custom": {
+ "fabric-api:module-lifecycle": "experimental"
+ }
+}
diff --git a/fabric-mod-protocol-api-v1/src/test/java/net/fabricmc/fabric/test/modprotocol/unit/VersionMatchingTests.java b/fabric-mod-protocol-api-v1/src/test/java/net/fabricmc/fabric/test/modprotocol/unit/VersionMatchingTests.java
new file mode 100644
index 0000000000..1ec71c4a69
--- /dev/null
+++ b/fabric-mod-protocol-api-v1/src/test/java/net/fabricmc/fabric/test/modprotocol/unit/VersionMatchingTests.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.test.modprotocol.unit;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+
+public class VersionMatchingTests {
+ @BeforeAll
+ static void beforeAll() { }
+
+ @BeforeEach
+ void setUp() { }
+}
diff --git a/fabric-mod-protocol-api-v1/src/testmod/java/net/fabricmc/fabric/test/modprotocol/ModProtocolTestmods.java b/fabric-mod-protocol-api-v1/src/testmod/java/net/fabricmc/fabric/test/modprotocol/ModProtocolTestmods.java
new file mode 100644
index 0000000000..b9c7760e94
--- /dev/null
+++ b/fabric-mod-protocol-api-v1/src/testmod/java/net/fabricmc/fabric/test/modprotocol/ModProtocolTestmods.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.test.modprotocol;
+
+import it.unimi.dsi.fastutil.ints.IntList;
+import it.unimi.dsi.fastutil.objects.Object2IntMap;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import net.minecraft.util.Identifier;
+
+import net.fabricmc.api.ModInitializer;
+import net.fabricmc.fabric.api.modprotocol.v1.ModProtocolIds;
+import net.fabricmc.fabric.api.modprotocol.v1.ModProtocolRegistry;
+import net.fabricmc.fabric.api.modprotocol.v1.ServerModProtocolLookup;
+import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
+import net.fabricmc.fabric.impl.modprotocol.ModProtocolLocator;
+import net.fabricmc.loader.api.FabricLoader;
+import net.fabricmc.loader.api.ModContainer;
+import net.fabricmc.loader.api.metadata.CustomValue;
+
+public final class ModProtocolTestmods implements ModInitializer {
+ public static final String ID = "fabric-mod-protocol-api-v1-testmod";
+ public static final Logger LOGGER = LoggerFactory.getLogger(ID);
+
+ public static Identifier id(String name) {
+ return Identifier.of(ID, name);
+ }
+
+ public void onInitialize() {
+ ModContainer modContainer = FabricLoader.getInstance().getModContainer(ID).get();
+
+ ModProtocolRegistry.register(ModProtocolIds.special("test_modification"), "Hello there", "1.12.2", IntList.of(1, 2, 3), true, false);
+ ModProtocolRegistry.register(ModProtocolIds.special("test_modification2"), "Test2", "1.0", IntList.of(1), false, true);
+ ModProtocolRegistry.register(ModProtocolIds.special("test_modification3"), "Test3", "2.0", IntList.of(2), true, true);
+ ModProtocolRegistry.register(ModProtocolIds.special("test_modification4"), "Test4", "2.0", IntList.of(2), true, true);
+ //ModProtocolRegistry.register(ModProtocolIds.special("test_modification5"), "Test5", "1.0", IntList.of(1), true, true);
+ ModProtocolRegistry.register(ModProtocolIds.special("test_modification6"), "Test6", "1.0", IntList.of(1), true, true);
+ ModProtocolRegistry.register(ModProtocolIds.special("test_modification7"), "Test7", "1.0", IntList.of(1), true, true);
+
+ CustomValue testificate = modContainer.getMetadata().getCustomValue("test_fabric:mod_protocol");
+ CustomValue defaulted = modContainer.getMetadata().getCustomValue("test2_fabric:mod_protocol");
+
+ ServerPlayConnectionEvents.JOIN.register(((handler, sender, server) -> {
+ Object2IntMap protocols = ServerModProtocolLookup.getAllSupportedProtocols(handler);
+ LOGGER.info("Protocols supported by {}", handler.getDebugProfile().getName());
+
+ for (Object2IntMap.Entry entry : protocols.object2IntEntrySet()) {
+ LOGGER.info(" - {}: {}", entry.getKey(), entry.getIntValue());
+ }
+ }));
+
+ LOGGER.info("Parser full array-like, {}", ModProtocolLocator.decodeFullDefinition(testificate, modContainer.getMetadata(), true));
+ LOGGER.info("Parser default, no defaults: {}", ModProtocolLocator.decodeFullDefinition(testificate, modContainer.getMetadata(), false));
+ LOGGER.info("Parser default, with defaults: {}", ModProtocolLocator.decodeFullDefinition(defaulted, modContainer.getMetadata(), false));
+ }
+}
diff --git a/fabric-mod-protocol-api-v1/src/testmod/resources/assets/fabric-networking-api-v1-testmod/lang/en_us.json b/fabric-mod-protocol-api-v1/src/testmod/resources/assets/fabric-networking-api-v1-testmod/lang/en_us.json
new file mode 100644
index 0000000000..e8773318a1
--- /dev/null
+++ b/fabric-mod-protocol-api-v1/src/testmod/resources/assets/fabric-networking-api-v1-testmod/lang/en_us.json
@@ -0,0 +1,5 @@
+{
+ "key.category.fabric-networking-api-v1-testmod": "Fabric Network Test",
+ "key.fabric-networking-api-v1-testmod.test": "Send test packet",
+ "key.fabric-networking-api-v1-testmod.open": "Open channel tester"
+}
diff --git a/fabric-mod-protocol-api-v1/src/testmod/resources/fabric.mod.json b/fabric-mod-protocol-api-v1/src/testmod/resources/fabric.mod.json
new file mode 100644
index 0000000000..e75a481e0c
--- /dev/null
+++ b/fabric-mod-protocol-api-v1/src/testmod/resources/fabric.mod.json
@@ -0,0 +1,30 @@
+{
+ "schemaVersion": 1,
+ "id": "fabric-mod-protocol-api-v1-testmod",
+ "name": "Fabric Mod Protocol API (v1) Test Mod",
+ "version": "1.0.0",
+ "environment": "*",
+ "license": "Apache-2.0",
+ "depends": {
+ "fabric-mod-protocol-api-v1": "*"
+ },
+ "entrypoints": {
+ "main": [
+ "net.fabricmc.fabric.test.modprotocol.ModProtocolTestmods"
+ ]
+ },
+ "custom": {
+ "fabric:mod_protocol": 5,
+ "test_fabric:mod_protocol": {
+ "name": "Name",
+ "id": "special:id",
+ "protocol": [0, 1, 2, 3],
+ "version": "Tater",
+ "require_client": false,
+ "require_server": false
+ },
+ "test2_fabric:mod_protocol": {
+ "protocol": [0, 1, 2, 3]
+ }
+ }
+}
diff --git a/fabric-networking-api-v1/src/client/java/net/fabricmc/fabric/api/client/networking/v1/ClientConfigurationNetworking.java b/fabric-networking-api-v1/src/client/java/net/fabricmc/fabric/api/client/networking/v1/ClientConfigurationNetworking.java
index 4bf48a9948..ac8b0cb663 100644
--- a/fabric-networking-api-v1/src/client/java/net/fabricmc/fabric/api/client/networking/v1/ClientConfigurationNetworking.java
+++ b/fabric-networking-api-v1/src/client/java/net/fabricmc/fabric/api/client/networking/v1/ClientConfigurationNetworking.java
@@ -23,6 +23,7 @@
import org.jetbrains.annotations.Nullable;
import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.network.ClientConfigurationNetworkHandler;
import net.minecraft.network.packet.CustomPayload;
import net.minecraft.util.Identifier;
import net.minecraft.util.thread.ThreadExecutor;
@@ -280,6 +281,11 @@ public interface Context {
*/
MinecraftClient client();
+ /**
+ * @return The ClientConfigurationNetworkHandler instance
+ */
+ ClientConfigurationNetworkHandler networkHandler();
+
/**
* @return The packet sender
*/
diff --git a/fabric-networking-api-v1/src/client/java/net/fabricmc/fabric/impl/networking/client/ClientConfigurationNetworkAddon.java b/fabric-networking-api-v1/src/client/java/net/fabricmc/fabric/impl/networking/client/ClientConfigurationNetworkAddon.java
index d391223bb3..17cbcab37b 100644
--- a/fabric-networking-api-v1/src/client/java/net/fabricmc/fabric/impl/networking/client/ClientConfigurationNetworkAddon.java
+++ b/fabric-networking-api-v1/src/client/java/net/fabricmc/fabric/impl/networking/client/ClientConfigurationNetworkAddon.java
@@ -44,7 +44,7 @@ public final class ClientConfigurationNetworkAddon extends ClientCommonNetworkAd
public ClientConfigurationNetworkAddon(ClientConfigurationNetworkHandler handler, MinecraftClient client) {
super(ClientNetworkingImpl.CONFIGURATION, ((ClientCommonNetworkHandlerAccessor) handler).getConnection(), "ClientPlayNetworkAddon for " + ((ClientConfigurationNetworkHandlerAccessor) handler).getProfile().getName(), handler, client);
- this.context = new ContextImpl(client, this);
+ this.context = new ContextImpl(client, handler, this);
// Must register pending channels via lateinit
this.registerPendingChannels((ChannelInfoHolder) this.connection, NetworkPhase.CONFIGURATION);
@@ -128,9 +128,10 @@ public ChannelInfoHolder getChannelInfoHolder() {
return (ChannelInfoHolder) ((ClientCommonNetworkHandlerAccessor) handler).getConnection();
}
- private record ContextImpl(MinecraftClient client, PacketSender responseSender) implements ClientConfigurationNetworking.Context {
+ private record ContextImpl(MinecraftClient client, ClientConfigurationNetworkHandler networkHandler, PacketSender responseSender) implements ClientConfigurationNetworking.Context {
private ContextImpl {
Objects.requireNonNull(client, "client");
+ Objects.requireNonNull(networkHandler, "networkHandler");
Objects.requireNonNull(responseSender, "responseSender");
}
}
diff --git a/fabric-networking-api-v1/src/client/java/net/fabricmc/fabric/impl/networking/client/ClientLoginNetworkAddon.java b/fabric-networking-api-v1/src/client/java/net/fabricmc/fabric/impl/networking/client/ClientLoginNetworkAddon.java
index e050c0326c..f4e46131aa 100644
--- a/fabric-networking-api-v1/src/client/java/net/fabricmc/fabric/impl/networking/client/ClientLoginNetworkAddon.java
+++ b/fabric-networking-api-v1/src/client/java/net/fabricmc/fabric/impl/networking/client/ClientLoginNetworkAddon.java
@@ -103,6 +103,11 @@ protected void handleRegistration(Identifier channelName) {
protected void handleUnregistration(Identifier channelName) {
}
+ @Override
+ public @Nullable String getBrand() {
+ return null;
+ }
+
@Override
protected void invokeDisconnectEvent() {
ClientLoginConnectionEvents.DISCONNECT.invoker().onLoginDisconnect(this.handler, this.client);
diff --git a/fabric-networking-api-v1/src/main/java/net/fabricmc/fabric/api/networking/v1/ServerConfigurationNetworking.java b/fabric-networking-api-v1/src/main/java/net/fabricmc/fabric/api/networking/v1/ServerConfigurationNetworking.java
index 7f6c36cef8..79d8e7632c 100644
--- a/fabric-networking-api-v1/src/main/java/net/fabricmc/fabric/api/networking/v1/ServerConfigurationNetworking.java
+++ b/fabric-networking-api-v1/src/main/java/net/fabricmc/fabric/api/networking/v1/ServerConfigurationNetworking.java
@@ -224,6 +224,19 @@ public static void send(ServerConfigurationNetworkHandler handler, CustomPayload
// Helper methods
+ /**
+ * Gets the brand of the client the player connected with.
+ *
+ * @param handler the network handler, representing the connection to the player/client
+ * @return client's brand, or {@code null} if not sent
+ */
+ @Nullable
+ public static String getBrand(ServerConfigurationNetworkHandler handler) {
+ Objects.requireNonNull(handler, "Server configuration network handler cannot be null");
+
+ return ServerNetworkingImpl.getAddon(handler).getBrand();
+ }
+
/**
* Returns the Minecraft Server of a server configuration network handler.
*
diff --git a/fabric-networking-api-v1/src/main/java/net/fabricmc/fabric/api/networking/v1/ServerPlayNetworking.java b/fabric-networking-api-v1/src/main/java/net/fabricmc/fabric/api/networking/v1/ServerPlayNetworking.java
index 23d415919b..9b498c35fa 100644
--- a/fabric-networking-api-v1/src/main/java/net/fabricmc/fabric/api/networking/v1/ServerPlayNetworking.java
+++ b/fabric-networking-api-v1/src/main/java/net/fabricmc/fabric/api/networking/v1/ServerPlayNetworking.java
@@ -293,6 +293,32 @@ public static void send(ServerPlayerEntity player, CustomPayload payload) {
player.networkHandler.sendPacket(createS2CPacket(payload));
}
+ /**
+ * Gets the brand of the client the player connected with.
+ *
+ * @param player the player
+ * @return client's brand, or {@code null} if not sent
+ */
+ @Nullable
+ public static String getBrand(ServerPlayerEntity player) {
+ Objects.requireNonNull(player, "Server player entity cannot be null");
+
+ return getBrand(player.networkHandler);
+ }
+
+ /**
+ * Gets the brand of the client the player connected with.
+ *
+ * @param handler the network handler, representing the connection to the player/client
+ * @return client's brand, or {@code null} if not sent
+ */
+ @Nullable
+ public static String getBrand(ServerPlayNetworkHandler handler) {
+ Objects.requireNonNull(handler, "Server play network handler cannot be null");
+
+ return ServerNetworkingImpl.getAddon(handler).getBrand();
+ }
+
private ServerPlayNetworking() {
}
diff --git a/fabric-networking-api-v1/src/main/java/net/fabricmc/fabric/impl/networking/AbstractChanneledNetworkAddon.java b/fabric-networking-api-v1/src/main/java/net/fabricmc/fabric/impl/networking/AbstractChanneledNetworkAddon.java
index efae4a474b..de59ad37f3 100644
--- a/fabric-networking-api-v1/src/main/java/net/fabricmc/fabric/impl/networking/AbstractChanneledNetworkAddon.java
+++ b/fabric-networking-api-v1/src/main/java/net/fabricmc/fabric/impl/networking/AbstractChanneledNetworkAddon.java
@@ -29,6 +29,7 @@
import net.minecraft.network.ClientConnection;
import net.minecraft.network.NetworkPhase;
import net.minecraft.network.PacketCallbacks;
+import net.minecraft.network.packet.BrandCustomPayload;
import net.minecraft.network.packet.CustomPayload;
import net.minecraft.network.packet.Packet;
import net.minecraft.text.Text;
@@ -52,6 +53,8 @@ public abstract class AbstractChanneledNetworkAddon extends AbstractNetworkAd
protected final Set sendableChannels;
protected int commonVersion = -1;
+ @Nullable
+ protected String brand = null;
protected AbstractChanneledNetworkAddon(GlobalReceiverRegistry receiver, ClientConnection connection, String description) {
super(receiver, description);
@@ -87,6 +90,10 @@ public boolean handle(CustomPayload payload) {
}
}
+ if (payload instanceof BrandCustomPayload brand) {
+ this.brand = brand.brand();
+ }
+
@Nullable H handler = this.getHandler(channelName);
if (handler == null) {
@@ -218,6 +225,15 @@ public CommonRegisterPayload createRegisterPayload() {
return new CommonRegisterPayload(getNegotiatedVersion(), getPhase(), this.getReceivableChannels());
}
+ @Nullable
+ public String getBrand() {
+ return this.brand;
+ }
+
+ public void setBrand(String brand) {
+ this.brand = brand;
+ }
+
@Override
public int getNegotiatedVersion() {
if (commonVersion == -1) {
diff --git a/fabric-networking-api-v1/src/main/java/net/fabricmc/fabric/impl/networking/AbstractNetworkAddon.java b/fabric-networking-api-v1/src/main/java/net/fabricmc/fabric/impl/networking/AbstractNetworkAddon.java
index 3ce0a675c9..2266c8b8c9 100644
--- a/fabric-networking-api-v1/src/main/java/net/fabricmc/fabric/impl/networking/AbstractNetworkAddon.java
+++ b/fabric-networking-api-v1/src/main/java/net/fabricmc/fabric/impl/networking/AbstractNetworkAddon.java
@@ -155,6 +155,9 @@ public Set getReceivableChannels() {
protected abstract void handleUnregistration(Identifier channelName);
+ @Nullable
+ public abstract String getBrand();
+
public final void handleDisconnect() {
if (disconnected.compareAndSet(false, true)) {
invokeDisconnectEvent();
diff --git a/fabric-networking-api-v1/src/main/java/net/fabricmc/fabric/impl/networking/server/ServerLoginNetworkAddon.java b/fabric-networking-api-v1/src/main/java/net/fabricmc/fabric/impl/networking/server/ServerLoginNetworkAddon.java
index 8615467f45..0d214db343 100644
--- a/fabric-networking-api-v1/src/main/java/net/fabricmc/fabric/impl/networking/server/ServerLoginNetworkAddon.java
+++ b/fabric-networking-api-v1/src/main/java/net/fabricmc/fabric/impl/networking/server/ServerLoginNetworkAddon.java
@@ -195,6 +195,11 @@ protected void handleRegistration(Identifier channelName) {
protected void handleUnregistration(Identifier channelName) {
}
+ @Override
+ public @Nullable String getBrand() {
+ return null;
+ }
+
@Override
protected void invokeDisconnectEvent() {
ServerLoginConnectionEvents.DISCONNECT.invoker().onLoginDisconnect(this.handler, this.server);
diff --git a/fabric-networking-api-v1/src/main/java/net/fabricmc/fabric/mixin/networking/ServerPlayNetworkHandlerMixin.java b/fabric-networking-api-v1/src/main/java/net/fabricmc/fabric/mixin/networking/ServerPlayNetworkHandlerMixin.java
index 7fae773f1d..d2e0ae5967 100644
--- a/fabric-networking-api-v1/src/main/java/net/fabricmc/fabric/mixin/networking/ServerPlayNetworkHandlerMixin.java
+++ b/fabric-networking-api-v1/src/main/java/net/fabricmc/fabric/mixin/networking/ServerPlayNetworkHandlerMixin.java
@@ -16,6 +16,7 @@
package net.fabricmc.fabric.mixin.networking;
+import com.llamalad7.mixinextras.sugar.Local;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
@@ -44,9 +45,13 @@ abstract class ServerPlayNetworkHandlerMixin extends ServerCommonNetworkHandler
}
@Inject(method = "", at = @At("RETURN"))
- private void initAddon(CallbackInfo ci) {
+ private void initAddon(CallbackInfo ci, @Local(argsOnly = true) ClientConnection connection) {
this.addon = new ServerPlayNetworkAddon((ServerPlayNetworkHandler) (Object) this, connection, server);
+ if (connection.getPacketListener() instanceof NetworkHandlerExtensions extension) {
+ this.addon.setBrand(extension.getAddon().getBrand());
+ }
+
if (!(this instanceof UntrackedNetworkHandler)) {
// A bit of a hack but it allows the field above to be set in case someone registers handlers during INIT event which refers to said field
this.addon.lateInit();
diff --git a/fabric-networking-api-v1/src/test/java/net/fabricmc/fabric/test/networking/unit/CommonPacketTests.java b/fabric-networking-api-v1/src/test/java/net/fabricmc/fabric/test/networking/unit/CommonPacketTests.java
index 1674ef82cc..16ff78a53b 100644
--- a/fabric-networking-api-v1/src/test/java/net/fabricmc/fabric/test/networking/unit/CommonPacketTests.java
+++ b/fabric-networking-api-v1/src/test/java/net/fabricmc/fabric/test/networking/unit/CommonPacketTests.java
@@ -138,6 +138,11 @@ public MinecraftClient client() {
return null;
}
+ @Override
+ public ClientConfigurationNetworkHandler networkHandler() {
+ return null;
+ }
+
@Override
public PacketSender responseSender() {
return packetSender;
diff --git a/fabric-registry-sync-v0/src/main/java/net/fabricmc/fabric/impl/registry/sync/FabricRegistryInit.java b/fabric-registry-sync-v0/src/main/java/net/fabricmc/fabric/impl/registry/sync/FabricRegistryInit.java
index ef20ab4160..3bc0a7b039 100644
--- a/fabric-registry-sync-v0/src/main/java/net/fabricmc/fabric/impl/registry/sync/FabricRegistryInit.java
+++ b/fabric-registry-sync-v0/src/main/java/net/fabricmc/fabric/impl/registry/sync/FabricRegistryInit.java
@@ -17,8 +17,10 @@
package net.fabricmc.fabric.impl.registry.sync;
import net.minecraft.registry.Registries;
+import net.minecraft.util.Identifier;
import net.fabricmc.api.ModInitializer;
+import net.fabricmc.fabric.api.event.Event;
import net.fabricmc.fabric.api.event.registry.RegistryAttribute;
import net.fabricmc.fabric.api.event.registry.RegistryAttributeHolder;
import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry;
@@ -32,7 +34,10 @@ public void onInitialize() {
PayloadTypeRegistry.configurationC2S().register(SyncCompletePayload.ID, SyncCompletePayload.CODEC);
PayloadTypeRegistry.configurationS2C().register(DirectRegistryPacketHandler.Payload.ID, DirectRegistryPacketHandler.Payload.CODEC);
- ServerConfigurationConnectionEvents.BEFORE_CONFIGURE.register(RegistrySyncManager::configureClient);
+ Identifier registrySync = Identifier.of("fabric", "registry_sync");
+
+ ServerConfigurationConnectionEvents.BEFORE_CONFIGURE.addPhaseOrdering(registrySync, Event.DEFAULT_PHASE);
+ ServerConfigurationConnectionEvents.BEFORE_CONFIGURE.register(registrySync, RegistrySyncManager::configureClient);
ServerConfigurationNetworking.registerGlobalReceiver(SyncCompletePayload.ID, (payload, context) -> {
context.networkHandler().completeTask(RegistrySyncManager.SyncConfigurationTask.KEY);
});
diff --git a/gradle.properties b/gradle.properties
index a98e51cb8e..e1545cd567 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -38,6 +38,7 @@ fabric-lifecycle-events-v1-version=2.3.12
fabric-loot-api-v2-version=3.0.14
fabric-loot-api-v3-version=1.0.2
fabric-message-api-v1-version=6.0.13
+fabric-mod-protocol-api-v1-version=1.0.0
fabric-model-loading-api-v1-version=2.0.0
fabric-networking-api-v1-version=4.2.2
fabric-object-builder-api-v1-version=15.2.0
diff --git a/settings.gradle b/settings.gradle
index 30f7ff72cb..a033c2d74c 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -40,6 +40,7 @@ include 'fabric-lifecycle-events-v1'
include 'fabric-loot-api-v3'
include 'fabric-message-api-v1'
include 'fabric-model-loading-api-v1'
+include 'fabric-mod-protocol-api-v1'
include 'fabric-networking-api-v1'
include 'fabric-object-builder-api-v1'
include 'fabric-particles-v1'