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

Conversation

Patbox
Copy link
Contributor

@Patbox Patbox commented Aug 7, 2024

This api implements Mod Protocols, a simple way for a mods to quickly check if they are present on client/server, while also allowing for prevention of joining if it's missing on either side (through not being forced to). It can be also used for syncing compatible protocol versions for general networking api usage or feature toggling, without making the server/client incompatible with vanilla clients/servers.

Example screenshots of mismatch screen:

Vanilla client joining modded server:

image

Modded client with some mismatched mods joining modded server:

image

Modded client (with server required entries) joining vanilla server:

image

Vanillish clients get disconnected by the server, while rest is validated client side.

(Also see https://github.com/Patbox/fabric/blob/mod-protocol/fabric-mod-protocol-api-v1/src/main/java/net/fabricmc/fabric/api/modprotocol/v1/package-info.java for info)

Worth noting for people who might get confused, this is not an "anticheat"! This api allows any mod to register any protocol names and even then cheat clients could easily work around it. This api is purely to improve experience of regular users / limit amount of reports for mod devs or pack makers.

One thing to do with this pr would be bit more testing + maybe writing some unit tests, through from what I've checked everything should work. Otherwise I think it should be all done.

@modmuss50 modmuss50 added the enhancement New feature or request label Aug 7, 2024
@modmuss50 modmuss50 requested review from modmuss50 and a team August 7, 2024 19:09
@@ -0,0 +1,42 @@
{
Copy link
Contributor

Choose a reason for hiding this comment

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

Indent appears to be messed up.

@@ -0,0 +1,8 @@
{
"text.fabric.mod_protocol.mismatched.title": "Required protocols are mismatched!",
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this title is unclear, but I haven't come up with a better one 🤷🏼

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Tbf it's best one I could think of too

PacketCodecs.map(ModProtocolResponseC2SPayload::createMap, Identifier.PACKET_CODEC, PacketCodecs.INTEGER)
.xmap(ModProtocolResponseC2SPayload::new, ModProtocolResponseC2SPayload::supported).cast();

private static Object2IntMap<Identifier> createMap(int i) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe there should be a cap on the number of protocols clients can send.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Would require writing a custom lazy codec since the class is initialized on init, but should be doable. Client should never sent more than amount server did.

Copy link
Contributor

@apple502j apple502j Aug 9, 2024

Choose a reason for hiding this comment

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

Can be a hardcoded cap, just so that bad clients won't request a map of size 1M.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There is no hardcap on api side, so ideally it would be dynamic

public final class ClientModProtocolInit {
public static void clientInit() {
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.

@christopherplaysminecraft

Just wondering, wouldn't this allow any server to continuously annoy users by requesting them to install a mod?

Also wondering about the wording in the original message

through not being forced to

Does that mean that one can simply tell it (click a button) to ignore any missing mods?

@modmuss50
Copy link
Member

It has always been possible (and quite easy) for a server mod to write code that disconnects the player if the corresponding client side mod isn't present. This PR also won't allow server owners to request any client mod to be installed.

@christopherplaysminecraft

Okay
Tbh I understood this whole PR to basically just do that... whoops.

Copy link
Member

@modmuss50 modmuss50 left a comment

Choose a reason for hiding this comment

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

This looks like a great start, just got a few points/questions regarding the API and networking changes before looking at the impl in detail.

I think the protocols should be attached to the ModContainer/modid in a stronger fashion, I dont think a general ID has much benefit.

I am still a little bit skeptical of using int ids as apposed to a semver version, I worry people wont remeber to change their protocol id. My inital thoughts about this problem were that we would re-use the same versioning system as used by loading when determining mod compatibility. I would expect most mod developers will just do a strict match of the specific versions, so its a setup and forget deal. I think we should discuss this and agree on the spec before spending ages perfecting the impl. See #463 for some prior thoughts on this topic.

I like having an API to query the client/server branding, I think this would be better off as a seperate PR so its not lost within this one, and could likely go in quite quickly. I would strongly suggest splitting that into another PR.

Comment on lines +36 to +43
* <dt>"fabric:mod_protocol": {
* "protocol": [1, 2],
* "id: "custom:id",
* "name": "Mod Name",
* "version": "v1.2.3",
* "require_client": false,
* "require_server": true
* }</dt>
Copy link
Member

Choose a reason for hiding this comment

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

This should be versioned, e.g fabric:mod_protocol_v1

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah fair, through I imagine ideally it would never need incompatible format change, but can do it.

* <p>"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.
* </p>
* <p>"name" is a name displayed if protocol doesn't match. It's optional and by default it uses one from mod's metadata.</p>
Copy link
Member

Choose a reason for hiding this comment

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

Im not sure this is that useful tbh, why would a mod have 2 names?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The protocol array support (and naming/etc) is mostly done to mirror "provides", allowing people to check against old ids when they change/include other mods protocol easily.

Copy link
Member

Choose a reason for hiding this comment

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

Being able to change other mods protocols seems like a massive recipe for confusion. Is this not what the dependency system already handles?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is not really for changing other mods protocol. It's mostly to mirror the "provides" functionality (your own mod changing it's id, but still being network compatible). Through to be fair it could be moved to allow this in code only

* It is optional and defaults to an ID with "mod" namespace and path equal to mod's id.
* </p>
* <p>"name" is a name displayed if protocol doesn't match. It's optional and by default it uses one from mod's metadata.</p>
* <p>"version" is a version displayed if protocol doesn't match. It's optional and by default it uses one from mod's metadata.</p>
Copy link
Member

Choose a reason for hiding this comment

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

Again, why duplicate 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.

Same as above

* <p>"name" is a name displayed if protocol doesn't match. It's optional and by default it uses one from mod's metadata.</p>
* <p>"version" is a version displayed if protocol doesn't match. It's optional and by default it uses one from mod's metadata.</p>
* <p>"require_client" controls if clients without this protocol can join the server, defaults to true, preventing joining</p>
* <p>"require_server" controls if clients can join servers without this mod, defaults to true, preventing joining</p>
Copy link
Member

Choose a reason for hiding this comment

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

Is true the correct default here? I wonder if most mods would be happy to join a server without their mod 🤔 Maybe there should be no default for these values?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Wasn't sure myself, but though it makes sense to default to something since the just number inline does. But can be changed

* @param protocolId protocol's id
* @return Protocol version supported by the player
*/
public static int getSupportedProtocol(ServerPlayerEntity player, Identifier protocolId) {
Copy link
Member

Choose a reason for hiding this comment

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

Whats the usecase for getting this later on? I do agree this doesnt seem that helpful.

Im also not too keen of APIs that return -1, I think it should throw (name the method to suggest it throws) and also provide one that returns an OptionalInt

@@ -103,6 +103,11 @@ protected void handleRegistration(Identifier channelName) {
protected void handleUnregistration(Identifier channelName) {
}

@Override
public @Nullable String getBrand() {
Copy link
Member

Choose a reason for hiding this comment

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

Ideally this wouldnt exist on the login addon, could the root method go in ClientCommonNetworkAddon? Im not too worried about this as its impl, and can easily be refactored later. Or even sooner before this PR goes in as its own thing.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Originally I wanted to use it purely for changing the message (through ended up using the same one for everyone), so yeah I can drop this for now

Comment on lines +227 to +234
/**
* 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) {
Copy link
Member

Choose a reason for hiding this comment

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

What are your toughts on moving this out into its own PR? Will make things a bit easier to review, this change can likely go in quite quickly.

Copy link
Member

Choose a reason for hiding this comment

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

It would also be nice to have a way to get the servers brand.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure thing

@@ -87,6 +90,10 @@ public boolean handle(CustomPayload payload) {
}
}

if (payload instanceof BrandCustomPayload brand) {
this.brand = brand.brand();
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 return false after this point. False becuase we only need to "peek" at this packet and not consume it, allowing vanilla or any exisitng mod to also read it.

Comment on lines +51 to +53
if (connection.getPacketListener() instanceof NetworkHandlerExtensions extension) {
this.addon.setBrand(extension.getAddon().getBrand());
}
Copy link
Member

Choose a reason for hiding this comment

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

A comment explaing this may help.

@modmuss50
Copy link
Member

modmuss50 commented Aug 9, 2024

In its current form I dont believe this supports the use case where you have an optional mod but want to have a requiment when it does exist.

E.g you have a mod that is optional on the server, but the protocol must match if it does exist on the client. Care should be taken to allow a vanilla client to join in this example.

@Patbox
Copy link
Contributor Author

Patbox commented Aug 10, 2024

This looks like a great start, just got a few points/questions regarding the API and networking changes before looking at the impl in detail.

I think the protocols should be attached to the ModContainer/modid in a stronger fashion, I dont think a general ID has much benefit.

I am still a little bit skeptical of using int ids as apposed to a semver version, I worry people wont remeber to change their protocol id. My inital thoughts about this problem were that we would re-use the same versioning system as used by loading when determining mod compatibility. I would expect most mod developers will just do a strict match of the specific versions, so its a setup and forget deal. I think we should discuss this and agree on the spec before spending ages perfecting the impl. See #463 for some prior thoughts on this topic.

I like having an API to query the client/server branding, I think this would be better off as a seperate PR so its not lost within this one, and could likely go in quite quickly. I would strongly suggest splitting that into another PR.

Protocols are dettached from containers due to in api usage, which can be useful for other tools. This is why it's namespaced.

Using strings would be bit more annoying when trying to use the getSupportedProtocol api's for any checks, since you might need to do a bit more matching. Aside of not all mods using valid semver in the first place. Ints are simplest way to check against version compat (which most software, including Minecraft does for protocol). My idea for improving visibility, is to put it alongside other values in gradle.properties, which makes it as simple of a change as version one. Having dependency style matching won't work for more complex cases or will make people more likely to make it way stricter than it needs to be, which could end up being annoying for modpack server makers which might want to get a hotfix for their server quickly without needing to wait for full modpack update.

@modmuss50
Copy link
Member

Protocols are dettached from containers due to in api usage, which can be useful for other tools. This is why it's namespaced.

Can you expand on this? I dont see what issues attaching them to containers/modids would cause.

Using strings would be bit more annoying when trying to use the getSupportedProtocol api's for any checks, since you might need to do a bit more matching.

I am not a massive fan of getSupportedProtocol I dont think this whole PR should be used for versioning packets, the main goal is to check that the client is compatible. Versioning packets is better done by using canSend imo.

Aside of not all mods using valid semver in the first place.

We could easily have a fallback to support basic comparsions, such as the version being identical. Mods are highly encouraged to use semver, you already miss out on a lot of stuff and get a warning from loader if you dont.

Ints are simplest way to check against version compat

The code for comparing versions already exists in loader, we need to make this easy for most people to use in their mods. I really dont see people updating an int every time they break protocol, it gets even more complex when you have 2 or more branches for differing minecraft versions. These problems have all been solved with semver.

it alongside other values in gradle.properties

I dont think we can do this, processResources is not suited to this. One solution may be to move everything to the fmj but this is another discussion.

Having dependency style matching won't work for more complex cases

Such as? There are complex cases that this PR in its current state doesnt handle.

will make people more likely to make it way stricter than it needs to be

I agree with this, I would expect the default to allow patch version changes. We need to find a good middle ground as like I said I dont see people every changing this int value, I know I will certainly forget.

@apple502j
Copy link
Contributor

Can you expand on this? I dont see what issues attaching them to containers/modids would cause.

String ID would allow mods to define multiple protocols, though I am unsure why you need them.

re: canSend - I agree this module should only be used for the automatic disconnection, fail-soft protocol checks should continue to use canSend.

@Patbox
Copy link
Contributor Author

Patbox commented Aug 12, 2024

Can you expand on this? I dont see what issues attaching them to containers/modids would cause.
A single mod can for example change vanilla protocol depending on it's configuration, which then could just use mod protocol to inform about it / enforce it without requiring it's own syncing (or ending up with bad errors). Other thing this could be used for is a mod to register configurable protocol (for example for the modpack), to inform player that they run on outdated version. In both cases you want custom name and version.

I am not a massive fan of getSupportedProtocol I dont think this whole PR should be used for versioning packets, the main goal is to check that the client is compatible. Versioning packets is better done by using canSend imo.

Can send is a just a boolean, which doesn't indicate packet version, just whatever that identifier is handled (which by itself is bit different concept, through it would require some extensions to be more useful with fabric's net api). It's pretty much similar to how network api has c:version packet, just more generic. I have similar system in polymer and I can say it's pretty useful.

The code for comparing versions already exists in loader, we need to make this easy for most people to use in their mods. I really dont see people updating an int every time they break protocol, it gets even more complex when you have 2 or more branches for differing minecraft versions. These problems have all been solved with semver.

"Semver" doesn't really tell much about network compatibility, as Mojang themselves break it in minor versions, but also keeps it the same if they can. I can imagine modders doing things similarly to that. To be fair I myself don't use semver correctly, just have mods that use semver-like in format.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants