# Common networking Protocol V2 This protocol defines a common way of communicating between server and client to setup an agreed upon environment for mods to register and use their own networking protocol components. ## Initial work ### Minecraft Minecraft comes with a specification for custom payloads that can be sent within its protocols. This custom payload specification, as of version 1.20.4 of the game, looks as follows: ```plantuml @startuml interface "CustomPacketPayload" as spec { void write(FriendlyByteBuf buf) ResourceLocation id() } hide fields @enduml ``` Where `FriendlyByteBuf` is Mojangs wrapper around the native Netty `ByteBuf` and `ResourceLocation` is Mojangs namespaced object identifier. ### Dinnerbones design of minecraft:register and minecraft:unregister Originaly Dinnerbone worked on _Bukkit_ and with that he designed the initial protocol we still use to this day to determine what channel exist within a connection between a client and a server: https://web.archive.org/web/20220711204310/https://dinnerbone.com/blog/2012/01/13/minecraft-plugin-channels-messaging/ This original version has been overhauled a little, most noteably that the 16 character long strings are now `ResourceLocations` bit but the diagram for the `minecraft:register` and `minecraft:unregister` channels looks as follows: ```plantuml @startuml protocol "minecraft:register" as register protocol "minecraft:unregister" as unregister register : String channels unregister : String channels note as ChannelsFormattingNote All channel ids that possibly could be send on a connection by the sender of this payload are concatenated and split with a "\0" end note ChannelsFormattingNote .. register::channels ChannelsFormattingNote .. unregister::channels hide methods @enduml ``` A set of class models that could represent the entities for the payloads above would then be: ```plantuml @startuml class "MinecraftRegisterPayload" as register <<record>> extends CustomPacketPayload class "MinecraftUnregisterPayload" as unregister <<record>> extends CustomPacketPayload register : List<ResourceLocations> channels unregister : List<ResourceLocation> channels register : List<ResourceLocations> channels() unregister : List<ResourceLocation> channels() note as ChannelsFormattingNote The write method implementation within these payloads and the reader for these payloads would implement the specification mentioned above. As such the representation of these payloads across a connection would be a list of strings, one for each of the channel ids in their fields respectively, concatenated with a "\0" end note ChannelsFormattingNote .. register ChannelsFormattingNote .. unregister hide CustomPacketPayload Members @enduml ``` Of note in this protocol is that the sending side communicates to the receiving side, which channels it can receive, not on which channels it will send payloads. ### Legacy Forge Networking implementation Minecraft Forge and FML originally received an implementation based on the Dinnerbone protocol, but extended it by sending an additional set of payloads across the connection to determine eligibiltiy of the individual "channels". :::info Of note here is the concept of "channels" within this implementation. The platforms that use this system offered two kinds of implementations of these channels: Event driven where each payload type had its own channel. And Simple where per "Channel" a single name was used and the individual payload types where seperated by a single integer based descrimininator. ::: The difference between Dinnerbones protocol and the legacy MCF implementation is the use of an unversioned side-channel negotiation using a set of payloads over the `fml:handshake` connection. This side-channel followed the following protocol information: ```plantuml @startuml protocol "fml:handshake:99" as C2SAcknowledge <<C2SAcknowledge>> protocol "fml:handshake:5" as S2CModData <<S2CModData>> protocol "fml:handshake:1" as S2CModList <<S2CModList>> protocol "fml:handshake:2" as C2SModListReply <<C2SModListReply>> protocol "fml:handshake:3" as S2CRegistry <<S2CRegistry>> protocol "fml:handshake:4" as S2CConfigData <<S2CConfigData>> protocol "fml:handshake:6" as S2CChannelMismatchData <<S2CChannelMismatchData>> S2CModData : Map<String, Pair<String, String>> mods S2CModList : List<String> mods S2CModList : Map<ResourceLocation, String> channels S2CModList : List<ResourceLocation> registries S2CModList : List<ResourceLocation> dataPackRegistries C2SModListReply : List<String> mods C2SModListReply : Map<ResourceLocation, String> channels C2SModListReply : Map<ResourceLocation, String> registries S2CRegistry : ResourceLocation registryName S2CRegistry : bool hasSnapshot S2CRegistry : byte[] snapshot S2CConfigData : String fileName S2CConfigData : byte[] fileData S2CChannelMismatchData : Map<ResourceLocation, String> mismatchedChannelData S2CModList <|--- C2SModListReply : Reply to C2SModListReply <|--- S2CChannelMismatchData : Replied in case of failure S2CRegistry <|--- C2SAcknowledge : Reply to S2CConfigData <|--- C2SAcknowledge : Reply to note as TriggeredByLogin These payloads are send by entering the `NEGOTIATION` phase of the minecraft networking protocol. end note TriggeredByLogin .. S2CModData TriggeredByLogin .. S2CModList TriggeredByLogin .. S2CRegistry TriggeredByLogin .. S2CConfigData hide methods hide C2SAcknowledge members @enduml ``` _Note: Allthough not displayed in the protocol above, the system on forge used SimpleChannel, meaning that the payload are always prefixed with their descriminator. The descriminator is shown above as the third section of the protocol element name_ Some details of the protocol are: ```plantuml @startuml protocol "fml:handshake:5" as S2CModData <<S2CModData>> S2CModData : Map<String, Pair<String, String>> mods note as ModData The map for this part of the protocol has as keys the individual mod ids, then for each mod id a pair, with as key the displayname, and as value the version of the mod on the server. end note ModData .. S2CModData::mods hide methods @enduml ``` ```plantuml @startuml protocol "fml:handshake:4" as S2CConfigData <<S2CConfigData>> S2CConfigData : String fileName S2CConfigData : byte[] fileData note as ConfigFileName The `fileName` part of the protocol is the exact path under the "./config" directory end note note as ConfigFileData The `fileData` part of the protocol is the exact binary contents of the servers counter part. end note ConfigFileName .. S2CConfigData::fileName ConfigFileData .. S2CConfigData::fileData hide methods @enduml ``` ```plantuml @startuml protocol "fml:handshake:6" as S2CChannelMismatchData <<S2CChannelMismatchData>> S2CChannelMismatchData : Map<ResourceLocation, String> mismatchedChannelData note as NegotiationFailure This payload is send when the server fails to find a common set of network channels, mods, registries or datapack registries. end note NegotiationFailure .. S2CChannelMismatchData hide methods @enduml ``` Code wise the constructs for such an implementation of the protocol would look like: ```plantuml @startuml class C2SAcknowledge class S2CModData class S2CModList class C2SModListReply class S2CRegistry class S2CConfigData class S2CChannelMismatchData S2CModData : Map<String, Pair<String, String>> mods S2CModList : List<String> mods S2CModList : Map<ResourceLocation, String> channels S2CModList : List<ResourceLocation> registries S2CModList : List<ResourceKey<Registry>> dataPackRegistries C2SModListReply : List<String> mods C2SModListReply : Map<ResourceLocation, String> channels C2SModListReply : Map<ResourceLocation, String> registries S2CRegistry : ResourceLocation registryName S2CRegistry : bool hasSnapshot S2CRegistry : byte[] snapshot S2CConfigData : String fileName S2CConfigData : byte[] fileData S2CChannelMismatchData : Map<ResourceLocation, String> mismatchedChannelData hide methods hide C2SAcknowledge members @enduml ``` This protocol is executed during the **login** phase of establishing of the connection. Internally this is then executed using the following plan: ```plantuml Server ->(20) Client : S2CModData Server ->(20) Client : S2CModList loop #LightBlue foreach : Registry Server ->(20) Client : S2CRegistry end loop #LightGreen foreach : ServerConfig Server ->(20) Client : S2CConfigFile end Client ->(20) Server : C2SModListReply loop #LightBlue foreach : ReceivedRegistry Client ->(20) Server : C2SAcknowledge end loop #LightGreen foreach : ReceivedServerConfig Client ->(20) Server : C2SAcknowledge end ``` ### Fabric configuration phase protocol With the introduction of the configuration phase, so did also come the need to refactor the way mods used and negotiated the available channels of custom packet payloads. Modmuss50 from the Fabric team designed the first protocol in this PR: https://github.com/FabricMC/fabric/pull/3244 After attempts to implement this in NeoForge some drawbacks and missing features where discovered. For completeness the protocol is described below: ```plantuml @startuml protocol "minecraft:register" as MinecraftRegister protocol "minecraft:unregister" as MinecraftUnregister protocol "c:register" as CommonRegister protocol "c:version" as CommonVersion protocol "ping" as Ping protocol "pong" as Pong MinecraftRegister : String channels MinecraftUnregister : String channels CommonVersion : Array<int> versions CommonRegister : int version CommonRegister : String phase CommonRegister : Array<ResourceLocation> channels note as ChannelsFormattingNote All channel ids that possibly could be send on a connection by the sender of this payload are concatenated and split with a "\0" end note note as InitialTrigger These payloads are send to the client by the server. Starting with the "minecraft:register" payload, followed by the "ping" packet end note ChannelsFormattingNote .. MinecraftRegister::channels ChannelsFormattingNote .. MinecraftUnregister::channels InitialTrigger .. MinecraftRegister InitialTrigger .. Ping Ping <|--- Pong: Reply to MinecraftRegister <|--- MinecraftRegister : Reply with own channels CommonVersion <|--- CommonVersion : Reply with accepted version CommonRegister <|--- CommonRegister : Reply with own channel information CommonRegister -[hidden]--> CommonVersion hide methods hide Ping members hide Pong members @enduml ``` The execution plan for this protocol is structured as follows: ```plantuml @startuml participant Server as Server participant Client as Client [-[#red]-> Client : Phase switch to "Configuration" [-[#red]-> Server : Phase switch to "Configuration" == Initialization == Server ->(20) Client : minecraft:register note left Channels are always optional and unversioned. So all channels, known to the mod loader, are listed in this initial payload. end note Server -[#LightBlue]>(20) Client : ping note left Additionally a Ping packet is sent. The vanilla client will not respond to "minecraft:register", but it will to Ping packet. This allows for detection of the type of client from the server side. end note Client ->(20) Server : minecraft:register note right The client now responds with the known channels. As with the server, the mod loader lists all known channels, unversioned and optional. end note Client -[#LightBlue]>(20) Server : Pong note right Because a ping packet was received we now also respond with a pong. The response order is retained because we use a serial sending logic and TCP connection types. end note == Client type determination == note across Once the, up to, 4 packets are sent and their responses received the server can determine what kind of client it is talking to. This can then be used to selectively configure configuration tasks for the next section. Additionally the channel state is stored on the connection. end note == Configuration phases == loop foreach : ConfigurationTask note across Allthough vanilla implements a small amount of configuration tasks it-self, most tasks come from mods. As such this section will describe the tasks generically. end note Server ->(20) Client : DataPacket note left Most configuration tasks will send some form of data gram packet to the client. Examples are: Payloads to sync the registry or server configuration entries, but also vanilla uses this phase to sync the server resource pack. end note Client -->(20) Server : AcknowledgementPacket note right The client can optionally acknowledge the received packet. If the server is expecting an acknowledgement packet to be sent by the client it will wait for that packet to be received by it, before starting the next task. end note end == Negotiation for Play == note across We need to know what kind of protocol the server speaks for the play phase. end note Server ->(20) Client : c:version note left Tell the client all versions of the protocol that the server supports end note Client ->(20) Server : c:version note right The client answers with a single selected version that it supports. The logic for this is opaque to the server, but it is suggested that the client should pick the highest version that they have in common, or disconnect end note Server ->(20) Client : c:register note left The server will send the payloads it supports for the playphase of the protocol end note Client ->(20) Server : c:register note right The client will respond with the payloads it supports for the playphase of the procotol end note Server --[#red]>] : "Phase switch to Play" Client --[#red]>] : "Phase switch to Play" @enduml ``` ### Neoforge 20.4.70-beta implementation During the initial networking migration within neoforged a new protocol was implemented. It provided support for all features needed, but did not follow the "minecraft:register" protocol as designed by dinnerbone: ```plantuml protocol "minecraft:register" as MinecraftRegister <<ModdedNetworkQueryPayload>> protocol "minecraft:network" as MinecraftNetwork <<ModdedNetworkPayload>> protocol "neoforge:modded_network_setup_failed" as ModdedNetworkSetupFailedPayload <<ModdedNetworkSetupFailedPayload>> protocol ModdedNetworkQueryComponent protocol ModdedNetworkComponent MinecraftRegister : Set<ModdedNetworkQueryComponent> configuration MinecraftRegister : Set<ModdedNetworkQueryComponent> play MinecraftNetwork : Set<ModdedNetworkComponent> configuration MinecraftNetwork : Set<ModdedNetworkComponent> play ModdedNetworkQueryComponent : ResourceLocation id ModdedNetworkQueryComponent : Optional<String> version ModdedNetworkQueryComponent : Optional<PacketFlow> flow ModdedNetworkQueryComponent : boolean optional ModdedNetworkComponent : ResourceLocation id ModdedNetworkComponent : Optional<String> version ModdedNetworkSetupFailedPayload : Map<ResourceLocation, Component> failureReasons note as InitialTrigger This payloads is send to the client by the server. Starting with an empty "minecraft:register" payload, followed by the "ping" packet end note note as ResultResponse The server sends this payload to the client to transmit the result of the payload version negotiation end note note as FailureResponse The server transmits this payload when the negotiation fails. This contains all the reasons in a display- read form for each of the payload channels end note InitialTrigger .. MinecraftRegister ResultResponse .. MinecraftNetwork FailureResponse .. ModdedNetworkSetupFailedPayload MinecraftRegister "0..*" -- "1..*" ModdedNetworkQueryComponent MinecraftNetwork "0..*" -- "1..*" ModdedNetworkComponent hide methods ``` If we look at the flow for this then the following execution plan can be defined: ```plantuml @startuml participant Server as Server participant Client as Client [-[#red]-> Client : Phase switch to "Configuration" [-[#red]-> Server : Phase switch to "Configuration" == Initialization == Server ->(20) Client : minecraft:register note left First an empty "minecraft:register" is transmitted. This triggers the client to respond with its payload channel information end note Server -[#LightBlue]>(20) Client : ping note left Additionally a Ping packet is sent. The vanilla client will not respond to "minecraft:register", but it will to Ping packet. This allows for detection of the type of client from the server side. end note Client ->(20) Server : minecraft:register note right The client now responds with the known channels. end note Client -[#LightBlue]>(20) Server : Pong note right Because a ping packet was received we now also respond with a pong. The response order is retained because we use a serial sending logic and TCP connection types. end note Server ->(20) Client : minecraft:network note left Once the negotiation completes the resulting network structure is send over to the client to it can do its own bookkeeping of which channels to use. end note == Client type determination == note across Once the, up to, 5 packets are sent and their responses received the server can determine what kind of client it is talking to. This can then be used to selectively configure configuration tasks for the next section. Additionally the channel state is stored on the connection. end note == Configuration phases == loop foreach : ConfigurationTask note across Allthough vanilla implements a small amount of configuration tasks it-self, most tasks come from mods. As such this section will describe the tasks generically. end note Server ->(20) Client : DataPacket note left Most configuration tasks will send some form of data gram packet to the client. Examples are: Payloads to sync the registry or server configuration entries, but also vanilla uses this phase to sync the server resource pack. end note Client -->(20) Server : AcknowledgementPacket note right The client can optionally acknowledge the received packet. If the server is expecting an acknowledgement packet to be sent by the client it will wait for that packet to be received by it, before starting the next task. end note end Server --[#red]>] : Phase switch to "Play" Client --[#red]>] : Phase switch to "Play" @enduml ``` ## Analysis With all of these network protocols described above, we can now extract an opinionated list of advantages and disadvantages for each protocol: | Protocol | :thumbsup: Advantages | :thumbsdown: Disadvantages | | -------- | -------- | -------- | | Dinnerbone | <ul><li>Simple</li><li>Small</li><li>Understood by a large amount of platforms</li></ul> | <ul><li>Old</li><li>Unversioned</li><li>Not a lot of information</li><li>Normal payload flow driven by sender</li></ul> | | Legacy Forge | <ul><li>Based on Dinnerbone's protocol</li><li>Side-Channel</li><li>Many features</li><li>Easy to implement</li><li>Fully registry synced</li></ul> | <ul><li>Propriatary protocol</li><li>All payload types required</li><li>No fallbacks</li><li>No versioning</li></ul> | | Fabric | <ul><li>Build on the new configuration phase</li><li>Based on Dinnerbone's protocol</li><li>Partially versioned</li><li>Community driven</li><li>Side-Channel</li><li>Fully registry synced</li></ul> | <ul><li>Partially versioned</li><li>Not a lot of information</li><li>Not many features</li></ul> | | NeoForge 20.4.70-Beta | <ul><li>Negotiation at the start</li><li>Simple</li><li>Server driven</li><li>Feature rich</li><li>Generic Packet Splitter</li><li>Partially registry synced</li></ul> | <ul><li>Breaks Dinnerbone's protocol</li><li>Unversioned</li><li>No fallbacks</li></ul> ## Requirements From the analysis above we can collect a list of properties that we want the new protocol to have: - Compatible with Dinnerbone's protocol - Versioned - With fallbacks, different protocols will always exist, we should do our best to be backwards, forwards and side-ways compatible - Feature rich, but with optional sections - Side-Channel driven - Server driven, client interogation - Supports generic packet splitting, at least on the receiver end. Sending is not needed. This is as such optional. - Fully registry synced - Payloads should be considered legitemate, so not limited to the 1Mb S2C and 32Kb C2S limits of unknown mojang payloads - Optionally the mods list is synchronized between client and server, but it is not required. ## New protocol ### Concept Based on the above requirements we can now start to design a new protocol. The protocol will derive from the concept of channels: ```plantuml @startuml class Channel Channel : ResourceLocation id() Channel : Optional<String> version() Channel : Optional<PacketFlow> flow() Channel : boolean isRequired() Channel : boolean isAdHoc() Channel : boolean isConnected() Channel : void connect() Channel : void disconnect() note left of Channel::id This represents the id of the channel. It has to be unique. end note note left of Channel::version This represents the current version of the channel. The version is optional If the version is supplied then the other end also needs the exact same version set. end note note left of Channel::flow Defines the direction in which the payloads send in this channel can be send. Payloads received on an invalid side represent undefined behavbiour. It is recommended that in such cases the connection is aborted and an error is shown. end note note left of Channel::isRequired Indicates whether this channel is required for a proper connection to be established. If at least one channel is found with this property set, then no vanilla client can connect to a modded server and vice versa. end note note right of Channel::isAdHoc This flag is set when the channel is created due to a `minecraft:register` payload, however it was not negotiated during the protocol initialization. This can happen when connecting to legacy systems, or plugin based servers. end note note right of Channel::isConnected Indicates whether the channel is connected. For a required channel this is always true. For an optional channel this depends on whether the opposite end of the connection also has a channel with the correct version and flow set. end note note right of Channel::connect This can be used to re-connect a channel that previously had been connected. Invoking this on an already connected channel has no effect. end note note right of Channel::disconnect This can be used to disconnect a channel that is currently connected. Invoking this on an already disconnected channel has no effect. Invoking this on a required channel, causes an exception. end note hide fields @enduml ``` <br/> <br/> Within this concept of a channel, is the connection state important. Because Dinnerbone's protocol only indicates whether the sender of the `minecraft:register` payload is able to receive payloads, and thus process them, on a given channel, we need to have the ability to disable the sending of specific payloads which are not supported by the receiving side. Additionally an adhoc channel is something we need to keep in mind. Because `minecraft:register` and its companion `minecraft:unregister` can be send at any time during the play phase, it is conceivable that a channel with an id previously unknown to us, would be send from the server. In this case an ad-hoc optional channel without version, or flow direction is immediatly created and can be used in the future for further sending **and** receiving of messages. Which will then directly be indicated by sending a `minecraft:register` response to the server with the new channel id as a listening channel included. ### Compatibility with legacy systems To remain compatible, we will send all channels on which we know we can receive payloads (so with a flow bound to the current side of the protocol) in the initial `minecraft:register` (see below) payloads. #### Protocol packets in legacy mode As can be seen below, the protocol will remain in a "legacy"/initialization mode untill the protocol version and channels have been negotiated. This means that for all intends and purposes the channels up untill that point are unversioned from the perspective of the protocol. However they do have an inherit flow attached to them. Sending a protocol initialization payload on a channel in the wrong direction causes a disconnect for breach of protocol. The protocol initialization channels are included in the initial `minecraft:register` payloads. #### Fallback to legacy mode. If the other side of the connection does not support the new protocol, then the system will abort the protocol flow after the `legacy packet exchange`, or during the `protocol version negotation` phases, depending on what is supported by the oposing side. If the opposing side is a vanilla connection, then the system will determine channel compatibility and optionally, if required channels are found, disconnect the user with a message to install the relevant platform. If the opposing side is a modded connection, but supports other protocols, like MCF, or older Fabric versions, then the system will switch into a `legacy protocol mode`. It will check for required channels, with versions or flow directions. If any such channel is present in the configuration of the network setup then the connection is disconnected with a relevant message to update the platform. ### Definition #### Channel negotiation ```plantuml @startuml protocol ChannelSpecification protocol ChannelConfiguration enum PacketFlow { CLIENT_TO_SERVER SERVER_TO_CLIENT BIDIRECTIONAL NONE } protocol "c:supported_channels_negotiatie" as SupportedChannelsNegotiate protocol "c:supported_channels_suggested" as SupportedChannelsSuggested protocol "c:supported_channels_selected" as SupportedChannelsSelected SupportedChannelsSuggested : ChannelSpecification[] configuration SupportedChannelsSuggested : ChannelSpecification[] play SupportedChannelsSelected : ChannelConfiguration[] configuration SupportedChannelsSelected : ChannelConfiguration[] play ChannelSpecification : ResourceLocation id ChannelSpecification : Optional<String> version ChannelSpecification : Optional<PacketFlow> flow ChannelSpecification : boolean optional ChannelConfiguration : ResourceLocation id ChannelConfiguration : Optional<String> version <> SuggestedDiamond <> SelectedDiamond SupportedChannelsSuggested -- SuggestedDiamond SupportedChannelsSelected -- SelectedDiamond SuggestedDiamond "1" *-- " * (play)" ChannelSpecification SuggestedDiamond "1" *-- " * (configuration)" ChannelSpecification SelectedDiamond "1" *-- " * (play)" ChannelConfiguration SelectedDiamond "1" *-- " * (configuration)" ChannelConfiguration ChannelSpecification "1" *-- "1?" PacketFlow SupportedChannelsNegotiate <|--- SupportedChannelsSuggested : Client replies with SupportedChannelsSuggested <|--- SupportedChannelsSelected : Server replies with hide SupportedChannelsNegotiate fields hide methods note left of PacketFlow::NONE If the direction NONE is chosen then no payloads can be send on this channel. It is then purely used to ensure compatibility between two parties end note @enduml ``` #### Protocol negotiation ```plantuml @startuml protocol "c:protocol_version_negotiate" as ProtocolVersionNegotiate protocol "c:protocol_version_suggested" as ProtocolVersionSuggested protocol "c:protocol_version_selected" as ProtocolVersionSelected ProtocolVersionNegotiate <|--- ProtocolVersionSuggested : Client replies with ProtocolVersionSuggested <|--- ProtocolVersionSelected : Server replies with ProtocolVersionSuggested : int[] supportedVersions ProtocolVersionSelected : int selectedVersion hide ProtocolVersionNegotiate fields hide methods @enduml ``` #### Registry sync ```plantuml @startuml protocol "c:registry_id_sync_start" as RegistryIdSyncStart protocol "c:registry_id_sync" as RegistryIdSync protocol "c:registry_state_sync" as RegistryStateSync protocol "c:registry_id_sync_end" as RegistryIdSyncEnd protocol "c:registry_id_sync_acknowledged" as RegistryIdSyncAcknowledged class RegistryNamespaceGroup << (M, orchid) >> class RegistryNamespaceGroupBulk << (M, orchid) >> class RegistryStateNamespaceGroup << (M, orchid) >> class RegistryStateNamespaceGroupBulk << (M, orchid) >> RegistryNamespaceGroup : String namespace RegistryNamespaceGroup : RegistryNamespaceGroupBulk[] bulks RegistryNamespaceGroupBulk : int bulkRawIdStartDiff RegistryNamespaceGroupBulk : String[] names RegistryStateNamespaceGroup : string namespace RegistryStateNamespaceGroup : RegistryStateNamespaceGroupBulk[] bulks RegistryStateNamespaceGroupBulk : int bulkRawIdStartDiff RegistryStateNamespaceGroupBulk : byte flags RegistryStateNamespaceGroupBulk : string|byte[] namesOrStates note as RegistryBulkOffset This field holds the offset of the id since the end of the last bulk, regardless of namespace! The first bulk has an offset compared to 0. end note note as FlagsNamesOrStates The flags byte indicates with its least significant bit if the array namesOrStates contains pure names, and as such for those entries the default state of that named entry should be used, or if it contains full states, serialized with the particular registries state codec. If names are serialized then the array contains pure strings. If full states are serialized using the codec then it should be read as a set of objects encoded to bytes using the codec end note RegistryBulkOffset .. RegistryNamespaceGroupBulk::bulkRawIdStartDiff RegistryBulkOffset .. RegistryStateNamespaceGroupBulk::bulkRawIdStartDiff FlagsNamesOrStates .. RegistryStateNamespaceGroupBulk::flags FlagsNamesOrStates .. RegistryStateNamespaceGroupBulk::namesOrStates RegistryIdSync "1" -- "*" RegistryNamespaceGroup RegistryNamespaceGroup "1" -- "1+" RegistryNamespaceGroupBulk RegistryStateSync "1" -- "*" RegistryStateNamespaceGroup RegistryStateNamespaceGroup "1" -- "1+" RegistryStateNamespaceGroupBulk RegistryIdSyncStart : Set<String> registries RegistryIdSyncStart : Set<String> statefullRegistries RegistryIdSync : String registryId RegistryIdSync : RegistryNamespaceGroup[] namespaces RegistryStateSync : String registryId RegistryStateSync : RegistryStateNamespaceGroup[] namespaces RegistryIdSyncEnd <|--- RegistryIdSyncAcknowledged : Client replies with RegistryIdSyncStart -[hidden]--> RegistryIdSync RegistryIdSyncStart -[hidden]--> RegistryIdSync RegistryNamespaceGroupBulk -[hidden]--> RegistryStateSync hide RegistryIdSyncEnd fields hide RegistryIdSyncAcknowledged fields hide methods @enduml ``` #### Packet splitting ```plantuml @startuml protocol "o:split_packet" as SplitPacket SplitPacket : byte stateFlags SplitPacket : VarInt innerPacketId SplitPacket : byte[] payloadSlice note as PacketId The inner packet id is always the same for the set of "o:split_packet" payloads which make up the split packet. end note PacketId .. SplitPacket::innerPacketId hide methods @enduml ``` #### Legacy ```plantuml @startuml protocol "minecraft:register" as MinecraftRegister protocol "minecraft:unregister" as MinecraftUnregister MinecraftRegister : String knownPayloadIds MinecraftUnregister : String forgottenPayloadIds MinecraftRegister <|--- MinecraftRegister : When new channels, then receiver replies with the new ad-hoc channels MinecraftUnregister <|--- MinecraftUnregister: When old channels: then receiver replies with old ad-hoc channels MinecraftUnregister -[hidden]--> MinecraftRegister note as ChannelsFormattingNote All channel ids that possibly could be send on a connection by the sender of this payload are concatenated and split with a "\0" end note ChannelsFormattingNote .. MinecraftRegister ChannelsFormattingNote .. MinecraftUnregister hide methods @enduml ``` #### Modlist negotiation ```plantuml @startuml protocol ModEntry << (M, orchid) >> protocol "o:mod_list_client" as ModListClient protocol "o:mod_list_server" as ModListServer ModListClient : ModEntry[] modEntries ModListServer : ModEntry[] modEntries ModListClient : bool enabled ModListServer : bool enabled note as EnabledState The user or server can disable this synchronization. If it is disabled this will indicate as such. end note EnabledState .. ModListClient::enabled EnabledState .. ModListServer::enabled ModEntry : String modId ModEntry : String version ModListServer <|--- ModListClient : Client replies with ModListServer "1" -- "1+" ModEntry ModListClient "1" -- "1+" ModEntry hide methods @enduml ``` ### Flow #### Full protocol support _Note by Tech: we can send c:protocol_version_negotiate, mc:register, Ping in that order and directly see from the client's first reply which kind of connection it is. Faster than 2 RTs._ ```plantuml @startuml participant Server as Server participant Client as Client [-[#red]-> Client : Phase switch to "Configuration" [-[#red]-> Server : Phase switch to "Configuration" == Initialization == group Support legacy systems Server ->(20) Client : minecraft:register Server -[#LightBlue]>(20) Client : Ping Client ->(20) Server : minecraft:register Client -[#LightBlue]>(20) Server : Pong end note across Once the, up to, 4 packets are sent and their responses received the server can determine what kind of client it is talking to. This can then be used to selectively configure configuration tasks for the next section. Additionally the channel state is stored on the connection. end note group Negotiate protocol version Server ->(20) Client : c:protocol_version_negotiate Server -[#LightBlue]>(20) Client : Ping Client ->(20) Server : c:protocol_version_suggested Client -[#LightBlue]>(20) Server : Pong Server ->(20) Client : c:protocol_version_selected end note across Once the, up to, 5 packets are sent and their responses received the server can determine what the compatibility level of the client it is talking to is. This can then be used to selectively configure configuration tasks for the next section. If the compatibility with this protocol can not be guaranteed, because the Pong packet was received before the version suggested payload, then the system will switch into legacy mode. end note group Negotiate supported channels Server ->(20) Client : c:supported_channels_negotiate Client ->(20) Server : c:supported_channels_suggested Server ->(20) Client : c:supported_channels_selected end == Before vanilla configuration payloads == note across This needs to run before vanilla sends any packet, especially the packet with tags, as it contains int based ids for registry contents. end note group #Pink Perform registry sync note across This is an optional part of the protocol end note note across This specific phase in the configuration performs a synchronization of the int based ids of registry entries. This is always the first task to execute, which allows other task to use registry ids for their payloads end note note over Server This process is triggered on the server side by the game triggering the relevant configuration phase end note Server ->(20) Client : c:registry_id_sync_start loop foreach : Registry Server ->(20) Client : c:registry_id_sync end loop foreach : Statefull registry Server ->(20) Client : c:registry_state_sync end Server ->(20) Client : c:registry_id_sync_end note across The server waits for an acknowledge ment, because the client can and probably should process the registry ids on the main thread, and the server should not continue with further tasks untill it is guaranteed that the registry ids are processed end note Client ->(20) Server : c:registry_id_sync_acknowledged end group #lightgreen Optionally perform configuration sync note across This is an optional part of the protocol, it is examplary shown here for neoforge. Allthough the protocol does not specify how to synchronize mod configuration, it hereby does specify when it should happen. end note note across This is always the second task to execute, as it makes sure that client has the same configuration as the server. If this phase is skipped then defaults configurations are loaded and assumed end note Server ->(20) Client : neoforge:server_config_start loop foreach : Config Server ->(20) Client : neoforge:server_config_file end Server ->(20) Client : neoforge:server_config_end note across The server waits for an acknowledge ment, because the client can and probably should process the configs on the main thread, and the server should not continue with further tasks untill it is guaranteed that the configs are processed end note Client ->(20) Server : neoforge:server_config_sync_acknowledged end == Configuration tasks == loop foreach : ConfigurationTask note across Allthough vanilla implements a small amount of configuration tasks it-self, most tasks come from mods. As such this section will describe the tasks generically. end note Server ->(20) Client : DataPacket note left Most configuration tasks will send some form of data gram packet to the client. Examples are: Payloads to sync the registry or server configuration entries, but also vanilla uses this phase to sync the server resource pack. end note Client -->(20) Server : AcknowledgementPacket note right The client can optionally acknowledge the received packet. If the server is expecting an acknowledgement packet to be sent by the client it will wait for that packet to be received by it, before starting the next task. end note end group #lightblue Optionally perform mod list sync as configuration task note across This is an optional part of the protocol. The moment were this synchronization happens does not matter to the protocol. We suggest it to happen as a configuration phase, additionally this can be disabled by the user. end note Server ->(20) Client : o:mod_list_server Client ->(20) Server : o:mod_list_client end == Hand-off == Server --[#red]>] : Phase switch to "Play" Client --[#red]>] : Phase switch to "Play" @enduml ``` #### Legacy protocol support The flow diagram below assumes that for all `c:*` and obviously for the `o:*` and `neoforge:*` payloads it is first checked if the opposing side listens for that payload before excuting for example those configuration phases. ```plantuml @startuml participant Server as Server participant Client as Client [-[#red]-> Client : Phase switch to "Configuration" [-[#red]-> Server : Phase switch to "Configuration" == Initialization == group Support legacy systems Server ->(20) Client : minecraft:register Server -[#LightBlue]>(20) Client : Ping Client ->(20) Server : minecraft:register Client -[#LightBlue]>(20) Server : Pong end group Negotiate protocol version Server ->(20) Client : c:protocol_version_negotiate Server -[#LightBlue]>(20) Client : Ping Client -[#LightBlue]>(20) Server : Pong end note across The client did not reply with the suggested channels, but the pong was returned. In such case we can assume that the opposing side has no support for this protocol. We enable legacy mode. end note == Before vanilla configuration payloads == note across This needs to run before vanilla sends any packet, especially the packet with tags, as it contains int based ids for registry contents. end note group #Pink Perform registry sync note across This specific phase in the configuration performs a synchronization of the int based ids of registry entries. This is always the first task to execute, which allows other task to use registry ids for their payloads end note note over Server This process is triggered on the server side by the game triggering the relevant configuration phase end note Server ->(20) Client : c:registry_id_sync_start loop foreach : Registry Server ->(20) Client : c:registry_id_sync end loop foreach : Statefull registry Server ->(20) Client : c:registry_state_sync end Server ->(20) Client : c:registry_id_sync_end note across The server waits for an acknowledge ment, because the client can and probably should process the registry ids on the main thread, and the server should not continue with further tasks untill it is guaranteed that the registry ids are processed end note Client ->(20) Server : c:registry_id_sync_acknowledged end group #lightgreen Optionally perform configuration sync note across This is an optional part of the protocol, it is examplary shown here for neoforge. Allthough the protocol does not specify how to synchronize mod configuration, it hereby does specify when it should happen. end note note across This is always the second task to execute, as it makes sure that client has the same configuration as the server. If this phase is skipped then defaults configurations are loaded and assumed end note Server ->(20) Client : neoforge:server_config_start loop foreach : Config Server ->(20) Client : neoforge:server_config_file end Server ->(20) Client : neoforge:server_config_end note across The server waits for an acknowledge ment, because the client can and probably should process the configs on the main thread, and the server should not continue with further tasks untill it is guaranteed that the configs are processed end note Client ->(20) Server : neoforge:server_config_sync_acknowledged end == Configuration tasks == loop foreach : ConfigurationTask note across Allthough vanilla implements a small amount of configuration tasks it-self, most tasks come from mods. As such this section will describe the tasks generically. end note Server ->(20) Client : DataPacket note left Most configuration tasks will send some form of data gram packet to the client. Examples are: Payloads to sync the registry or server configuration entries, but also vanilla uses this phase to sync the server resource pack. end note Client -->(20) Server : AcknowledgementPacket note right The client can optionally acknowledge the received packet. If the server is expecting an acknowledgement packet to be sent by the client it will wait for that packet to be received by it, before starting the next task. end note end group #lightblue Optionally perform mod list sync as configuration task note across This is an optional part of the protocol. The moment were this synchronization happens does not matter to the protocol. We suggest it to happen as a configuration phase, additionally this can be disabled by the user. end note Server ->(20) Client : o:mod_list_server Client ->(20) Server : o:mod_list_client end == Hand-off == Server --[#red]>] : Phase switch to "Play" Client --[#red]>] : Phase switch to "Play" @enduml ``` #### Vanilla support ```plantuml @startuml participant Server as Server participant Client as Client [-[#red]-> Client : Phase switch to "Configuration" [-[#red]-> Server : Phase switch to "Configuration" == Initialization == group Support legacy systems Server ->(20) Client : minecraft:register Server -[#LightBlue]>(20) Client : Ping Client -[#LightBlue]>(20) Server : Pong end note across The client did not reply with the correct `minecraft:register` payload, but the pong was returned. In such case we can assume that the opposing side has no support for this protocol. We enable vanilla mode. end note == Configuration tasks == loop foreach : ConfigurationTask note across Allthough vanilla implements a small amount of configuration tasks it-self, most tasks come from mods. As such this section will describe the tasks generically. end note Server ->(20) Client : DataPacket note left Most configuration tasks will send some form of data gram packet to the client. Examples are: Payloads to sync the registry or server configuration entries, but also vanilla uses this phase to sync the server resource pack. end note Client -->(20) Server : AcknowledgementPacket note right The client can optionally acknowledge the received packet. If the server is expecting an acknowledgement packet to be sent by the client it will wait for that packet to be received by it, before starting the next task. end note end == Hand-off == Server --[#red]>] : Phase switch to "Play" Client --[#red]>] : Phase switch to "Play" @enduml ``` ### Packet splitting flow #### Encode ##### Small enough ```plantuml @startuml participant Sender as Sender participant PacketSplitter as Splitter participant Encoder as Encoder Sender -> Splitter : Attempts Send activate Sender activate Splitter Splitter -> Splitter : Write packet Splitter -> Splitter : Check size note across This causes an additional call to the underlying packet end note Splitter -> Encoder : Continue processing original packet Encoder --[#red]>] : Send packet out over network deactivate Splitter deactivate Sender @enduml ``` ##### Too big ```plantuml @startuml participant Sender as Sender participant PacketSplitter as Splitter participant Encoder as Encoder Sender -> Splitter : Attempts Send activate Sender activate Splitter Splitter -> Splitter : Write packet Splitter -> Splitter : Check size note across Packet is determined to be larger then 8Mb (as of writing) end note Splitter -> Splitter : Split data loop foreach : Payload slice Splitter -> Sender : Send wrapped slice activate Sender Sender --> Splitter : Passthrough of split packets activate Splitter Splitter --> Encoder : Immediatly encode split packets deactivate Splitter deactivate Sender end Encoder --[#red]>] : Send packet out over network deactivate Splitter deactivate Sender @enduml ``` #### Decode ##### Small enough ```plantuml @startuml participant Decoder as Decoder participant PacketSplitter as Splitter participant Receiver as Receiver [--[#red]> Decoder : Receive packet from the network activate Decoder Decoder -> Receiver : Decode Packet and handoff for processing note across The PacketSplitter is completely skipped here. It is only triggered if a split packet payload is received. end note Receiver -> Receiver : Check thread and/or process deactivate Decoder @enduml ``` ##### Too big ```plantuml @startuml participant Decoder as Decoder participant PacketSplitter as Splitter participant Receiver as Receiver loop foreach : Split packet slices [--[#red]> Decoder : Receive split packet payload from the network activate Decoder Decoder -> Splitter : Hand-off to packet splitter as its receiver Splitter -> Splitter : Store payload slice and inner payload id deactivate Decoder end note over Splitter Once the final split packet slice is received the processing continues. end note Splitter -> Splitter : Combine all payload slices activate Splitter Splitter -> Decoder : Decode payload activate Decoder note across It should be impossible for this decoded packet to be a packet with a split payload as its contents. end note Decoder --> Receiver : Trigger receiver for decoded packet deactivate Splitter deactivate Decoder @enduml ```