Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

The Mod Protocol API, aka better validation of mod presence of both-side mods with extra protocol version syncing #4011

Open
wants to merge 12 commits into
base: 1.21.1
Choose a base branch
from
Open
3 changes: 3 additions & 0 deletions fabric-mod-protocol-api-v1/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
version = getSubprojectVersion(project)

moduleDependencies(project, ['fabric-api-base', 'fabric-networking-api-v1'])
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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;
Patbox marked this conversation as resolved.
Show resolved Hide resolved
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<Identifier> 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<Identifier> getAllSupportedProtocols(ClientConnection connection) {
return RemoteProtocolStorage.getMap(connection);
}
}
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>See {@link net.fabricmc.fabric.api.modprotocol.v1}</p>
*/

@ApiStatus.Experimental
package net.fabricmc.fabric.api.client.modprotocol.v1;
modmuss50 marked this conversation as resolved.
Show resolved Hide resolved

import org.jetbrains.annotations.ApiStatus;
Original file line number Diff line number Diff line change
@@ -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) -> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use @Override.

var map = new HashMap<Identifier, ModProtocolImpl>(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());
});
}
}
Original file line number Diff line number Diff line change
@@ -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<Identifier> fabric$getRemoteProtocol() {
return ((RemoteProtocolStorage) this.connection).fabric$getRemoteProtocol();
}

@Override
public void fabric$setRemoteProtocol(Object2IntMap<Identifier> protocol) {
((RemoteProtocolStorage) this.connection).fabric$setRemoteProtocol(protocol);
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"required": true,
"package": "net.fabricmc.fabric.mixin.modprotocol.client",
"compatibilityLevel": "JAVA_17",
"mixins": [
"ClientCommonNetworkHandlerMixin"
],
"injectors": {
"defaultRequire": 1
},
"client": [
"ClientConfigurationNetworkHandlerMixin"
]
}
Original file line number Diff line number Diff line change
@@ -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();
Comment on lines +29 to +32
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question (ive not looked at the impl), where does this ID come from when a protocol is specified in the fabric.mod.json?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By default it's "mod:(modid)", but can be overriten with any other identifier

/**
* @return Display name of this protocol
*/
String name();
/**
* @return Display version of this protocol
*/
String version();
Comment on lines +33 to +40
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Somewhat realted to my above question, why are these needed, would it be better tieing this to the ModContainer/modid?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since (currently) mod can provide multiple protocols (or make them in code) it still makes sense for this to exist. As a bonus people might want to have it different/more specific with something like feature namespace.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do think tieing it to the mod solves a lot of problems mostly by removing a lot of duplicated data from the fmj. You also get support for "provides" for free.

/**
* @return Protocol versions supported by this protocol
*/
IntList protocol();
modmuss50 marked this conversation as resolved.
Show resolved Hide resolved
/**
* @return Client requirement of this protocol
*/
boolean requireClient();
/**
* @return Server requirement of this protocol
*/
boolean requireServer();
}
Loading
Loading