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,103 @@
/*
* 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 identifier's can be any valid identifier, through by default mods defining it will use "mod" namespace and path equal to its id.
Patbox marked this conversation as resolved.
Show resolved Hide resolved
* See {@link ModProtocolIds} for more information.</p>
Patbox marked this conversation as resolved.
Show resolved Hide resolved
*/
public final class ClientModProtocolLookup {
public static final int UNSUPPORTED = -1;
Patbox marked this conversation as resolved.
Show resolved Hide resolved
private ClientModProtocolLookup() { }

/**
* Gets protocol version supported by the server.
Patbox marked this conversation as resolved.
Show resolved Hide resolved
*
* @param handler the network handler connected to the server
* @param protocolId protocol's id
* @return Protocol version supported by the server
Patbox marked this conversation as resolved.
Show resolved Hide resolved
*/
public static int getSupportedProtocol(ClientCommonNetworkHandler handler, Identifier protocolId) {
return RemoteProtocolStorage.getProtocol(handler, protocolId);
}

/**
* Gets protocol version supported by the server.
Patbox marked this conversation as resolved.
Show resolved Hide resolved
*
* @param connection the ClientConnection connected to the server
* @param protocolId protocol's id
* @return Protocol version supported by the server
Patbox marked this conversation as resolved.
Show resolved Hide resolved
*/
public static int getSupportedProtocol(ClientConnection connection, Identifier protocolId) {
return RemoteProtocolStorage.getProtocol(connection, protocolId);
}

/**
* Gets protocol version supported by the server.
Patbox marked this conversation as resolved.
Show resolved Hide resolved
*
* @param handler the network handler connected to the server
* @param protocol protocol to check against
* @return Protocol version supported by the server
Patbox marked this conversation as resolved.
Show resolved Hide resolved
*/
public static int getSupportedProtocol(ClientCommonNetworkHandler handler, ModProtocol protocol) {
return RemoteProtocolStorage.getProtocol(handler, protocol.id());
}

/**
* Gets protocol version supported by the server.
Patbox marked this conversation as resolved.
Show resolved Hide resolved
*
* @param connection the ClientConnection connected to the server
* @param protocol protocol to check against
* @return Protocol version supported by the server
Patbox marked this conversation as resolved.
Show resolved Hide resolved
*/
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 Map of protocols supported by the server
Patbox marked this conversation as resolved.
Show resolved Hide resolved
*/
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 Map of protocols supported by the server
Patbox marked this conversation as resolved.
Show resolved Hide resolved
*/
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,57 @@
/*
* 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.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 {
public static void clientInit() {
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();
}
Original file line number Diff line number Diff line change
@@ -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";
Comment on lines +25 to +27
Copy link
Member

Choose a reason for hiding this comment

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

Explain what these do and are used for in the javadoc. I have no clue just looking at the API.

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 mostly to indicate the source and display priority, special being always displayed first, then mod and ending up with feature. But these can be added upon in event-ordering styled fashion

Copy link
Member

Choose a reason for hiding this comment

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

I'd just keep it simple and sort alphabetically for now. Maybe add it later as a follow up, or even have a custom UI drawn by the client making it easier to consume the data.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorting is already done and yeah it still sorts alphabetically for paths. It's mostly useful as another mod could register a modpack version/name in the special namespace, making it always display as the first thing

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);
}
}
Loading
Loading