Proposed RFC Feature: Multiplayer Versioning #81
Labels
final-comment-period
RFCs in final comment period
rfc-feature
Request for Comments for a Feature
triage/accepted
Indicates an issue or PR has been accepted by the SIG and is ready to be actively worked on.
Summary
This feature will provide a way to detect when a multiplayer server and client are using different versions of multiplayer components.
This RFC was created as a proposed fix for github.com/o3de/o3de/issues/9669
What is the relevance of this feature?
As a developer I want to know if my server and client have a version mismatch.
It's important for servers and clients to be running the same version of all the multiplayer components (components which communicate to each other over the network). Any mismatch will lead to unexpected behavior. This feature will warn users immediately upon connecting to the server if there is a mismatch.
Example:
A developer wants to push a game update to players in the wild. One of the changes was made to a multiplayer auto-component, but the change was minimal; a developer reordered the auto-components RPCs inside the XML so they were in alphabetical order (just because felt easier to read that way). Before pushing the update live, the developer tries testing the new build and starts a match by connecting to one of their production branch servers on the cloud. The game appears to be going smoothly, except the RPCs associated with this auto-component they reordered seemed have stopped working. Unbeknownst to the developer, this is because the server’s RPC index is different; although it felt minor at the time, reordering the RPCs inside the XML has broken the logic. The developer now needs to go and debug why the RPCs aren’t working as expected, which can take a long time if they don't realize the server and client are actually running different multiplayer auto-component versions.
After this feature is implemented, the developer would know right away that their client and server builds are different. Now upon connecting to the server, the client and server would share a hash-value representing the current snapshot of all their multiplayer components. If the hashes don’t match, an error message will be logged, and an error popup will be prominently displayed in the game's viewport. The developer will see the message, and know that he forgot to update the server.
Note: developers may want to add their own extensions to client/server "versioning" (ie region checks), this would instead be a feature for a custom game-specific handshake which is already supported.
Feature design description:
The multiplayer system will be updated so that it can detect when a server and client are using different multiplayer auto-components. The server and client machines will then be warned of a mismatch, at which point the user can go and update their server or client builds to the latest build version. The hash generation, error detection, and reporting will all happen automatically without adding any extra steps to the developer's normal build process.
Common example workflows:
Technical design description:
In order for this feature not to impact normal developer workflows, we'll aim to have the normal build process automatically generate all of the code required for comparing multiplayer components. For this, we'll rely on AzCodeGen.
High-level overview:
Auto-component XML To Multiplayer Component Registry
[Image: image.png]## Hash Method Available to AzCodeGen/Jinja:
Ultimately we want to compare the server and client auto-component XMLs. Instead of having to send and receive 100's of kilobytes of XML files over the network, we'll instead parse the XML and create a unique hash. Parsing the XML and creating a hash can be achieved by leveraging the same AzCodeGen that's used to parse and convert XML into game components and packets. For converting XML into a 64-bit hash, a new method will be added to \cmake\AzAutoGen.py called CreateHashValue64(string). It will return a AZ::HashValue64. Then in Jinja one can call the code as follows:
{{ SomeStringRepresentingTheAutoComponent | createHashValue64string}}
Hash Generation (Per-Auto-Component):
Auto-components contain a name, namespace, RPCs, RPC parameters, network properties, network inputs, and archetypes. Each of these properties would need to be considered when generating a hash. The auto-component classes would have a new member variable “AZ::HashValue64 m_versionHash" and a public accessor method "GetVersionHash()".
Creating the auto-component hash value will be done during AzCodeGen. The multiplayer auto-component Jinja will need to be updated to parse all the auto-component attributes (name, namespace, RPCs, RPC parameters, network properties, archetypes, and network inputs), store it in a string, pass it to the createHashValue64string method, and store it in m_versionHash.
Relevant Files:
\Gems\Multiplayer\Code\Include\Multiplayer\AutoGen\AutoComponent_Header.jinja
\Gems\Multiplayer\Code\Include\Multiplayer\AutoGen\AutoComponent_Source.jinja
Hash Generation (Per-Application):
Instead of checking every auto-component on the server against every auto-component on the client, we will first compare a single holistic hash that’s generated per-application.
This hash, unlike the individual auto-component hash, will not be generated during AzCodeGen and instead will be produced at runtime.
Currently, at the start of the app, all of the auto-components register themselves with the global MultiplayerComponentRegistry. MultiplayerComponentRegistry will be updated so that as each component registers itself, the MultiplayerComponentRegistry will update its own AZ::HashValue64, by combining the component hashes together. (Note: A lazy init may be more appropriate here whereby the hash is generated once it’s asked for and then stored for future use)
\Gems\Multiplayer\Code\Include\Multiplayer\Components\MultiplayerComponentRegistry.h
class MultiplayerComponentRegistry
{
public:
void RegisterMultiplayerComponent( ComponentData componentData );
...
private:
AZ::HashValue64 m_versionHash;
}
\Gems\Multiplayer\Code\Include\Multiplayer\Components\MultiplayerComponentRegistry.cpp
void MultiplayerComponentRegistry::RegisterMultiplayerComponent( ComponentData componentData )
{
...
// update m_versionHash by adding in componentData.m_versionHash;
}
Sending and Receiving Hashes Between Client and Server
Relevant Files:
\Gems\Multiplayer\Code\Source\MultiplayerSystemComponent.cpp\h
\Gems\Multiplayer\Code\Source\AutoGen\Multiplayer.AutoPackets.xml
What are the advantages of the feature?
This feature will alert developers and players when their game doesn't match the server version. It's an important safe-guard. By knowing which components don't match, developers can then check the code on either machine to see which is out-of-date.
What are the disadvantages of the feature?
This feature will add to build times, although probably not much considering the new Jinja logic will only be a fraction of what’s being generated per-autocomponent. To be safe, the GitHub pull-request should include a comment about before and after build times using Multiplayer Sample. If parsing each auto-component by hand is too slow, maybe it's faster to turn the entire XML into a string in order to hash; but be careful not to consider non-essential characters (such as newlines) which can be different depending on the operating system.
How will this be implemented or integrated into the O3DE environment?
This will be added as default behavior in the MultiplayerGem connect logic. Users of the gem will get the behavior for free, when they update the gem. Users who require more control may utilize the IsHandshakeComplete method which already exists in the IMultiplayer interface; users can specify what constitutes a complete handshake specific to their game’s requirements.
Are there any alternatives to this feature?
Alternative Hash Generation (Per-Auto-Component and Per-Auto-Packet):
There's a possibility to hash the entire auto-component XML file instead of having to parse its individual auto-component properties to create a hash. We'd want to be careful that the internal XML data-structure passed into Jinja ignores newlines as these could produce different results based on a machine's operating system. For instance, if a Windows machine using CR/LF line endings connects to a Linux server using CR line endings, the hash function could incorrectly produce a different hash. The upside is that using an XML "ToString()" method would be computationally faster, and also quicker to implement (less lines of code and easier to read).
Leverage Existing Engine-Version API:
Proposed RFC Feature Engine, Project and Gem Versions sig-core#44
Instead of automatically generating versions for multiplayer components specifically, we could simply leverage the upcoming O3DE engine versioning API to send the current engine/project/gem versions. The problem with this is it likely won't catch any multiplayer component mismatches unless developers specifically remember to update versions after every change.
Instead of automatically generating versions, we could add a "version" field to the auto-component XML. The problem with this is it won't catch any multiplayer component mismatches unless developers specifically remembers to update versions after every change.
How will users learn this feature?
Users will not need to learn of this feature in order to enable it as it will be enabled automatically.
Should a version mismatch occur, debug logs, and an on-screen warning message will appear on the client machine tipping off users as to the version mismatch, however, a detailed networking troubleshooting doc should cover what to do in order to fix a version mismatch.
Out of Scope
Are there any open questions?
The text was updated successfully, but these errors were encountered: