multiplayer-state-machine-coverage
Build a coverage matrix for a networked-game state machine that exercises connect / authority-handoff / disconnect / reconnect / host-migration paths across Unity Netcode for GameObjects, Unreal Engine replication, and Mirror Networking. Workflow: enumerate the engine's connection states + ownership states + replicated-property update rules, cross them against latency / loss / out-of-order packet injection, encode each combination as a test fixture, and emit a go / no-go gate. Use before submitting a multiplayer title to platform cert - Microsoft's cert guide lists 'Multiplayer does not work as expected' as one of the most common Hold reasons, and Xbox XR-067 (MPSD session state) is failed by uncovered state-machine paths.
Install with skills.sh (any agent)
npx skills add testland/qa --skill multiplayer-state-machine-coveragemultiplayer-state-machine-coverage
Overview
Networked games are state machines: each player connection, each replicated entity, and each authority handoff transitions through a documented set of states. Cert failures and on-the-wire bugs overwhelmingly happen at transition edges - connect, host migration, disconnect-mid-action, reconnect-after-network-loss - not in the steady-state gameplay loop.
This skill is a build-an-X workflow: produce a state-machine coverage matrix that enumerates every connection + ownership + replication state in your engine, crosses it with the network fault matrix (latency / loss / out-of-order / disconnect), and emits a fixture list + a go / no-go gate.
Composes with:
When to use
Microsoft's Certification step-by-step guide (opens in new window) explicitly cites "Multiplayer does not work as expected" as one of the three most common reasons titles are placed on Hold. Holds are calendar-week delays - coverage authored ahead of cert is the cheapest mitigation.
Inputs
Before walking the workflow, gather:
| Input | Where | Why |
|---|---|---|
| Engine + netcode stack | Project - Unity NGO / Unreal replication / Mirror | Determines the state vocabulary you enumerate |
| Topology - host / dedicated / listen | Game design doc | Listen-server has different states than dedicated server (see Unreal section below) |
Max concurrent players (MaxPlayers) | Backend config / multiplayer service | Caps the fixture matrix |
| Persistence model - does the session resume after host-migration / suspend? | Game design + platform cert requirements | XR-067 requires the title maintain MPSD session state |
| Platform target - Xbox / PSN / Switch / Steam | Cert plan | Drives the XR / TRC / Lotcheck clauses to cover |
Workflow
Step 1 - Enumerate connection states
Per engine docs, list the states the engine exposes. The matrix is fixed by the framework - you cannot add or remove states, only choose which to cover. The full per-engine connection-state tables (Unity NGO, Unreal replication, Mirror) - each state with its trigger and observability hook - are in references/engine-states.md.
Step 2 - Enumerate ownership states
Authority handoff is where most "ghost item" / "ability use after death" bugs live. The per-engine authority-state enumeration (NGO OwnerClientId / IsOwner; Unreal ROLE_*; Mirror isServer / isOwned) is in references/engine-states.md. Authority transitions to cover:
Step 3 - Enumerate replicated-property transitions
For every replicated property (NGO NetworkVariable<T>, Unreal UPROPERTY(Replicated) / RepNotify, Mirror [SyncVar]), identify:
Each OnRep_ / hook handler is a transition edge that needs at least one fixture exercising it.
Step 4 - Cross with the network fault matrix
The engine state machines are deterministic on a perfect network. Real networks are not perfect. Cross-product each state from Step 1 with:
| Fault | How to inject |
|---|---|
| Latency 50 / 200 / 500 ms | OS-level traffic shaper (tc qdisc add dev eth0 root netem delay 200ms); Unity Multiplayer Tools' Network Simulator; Mirror's LatencySimulation component |
| Packet loss 1 / 5 / 20 % | tc qdisc … loss 5 %; engine-specific simulator |
| Reordering | tc qdisc … delay 50ms reorder 25 % |
| Connection drop | Detach NIC / kill UDP socket / engine-specific Disconnect() |
| Host suspend (console only) | Platform-specific suspend → resume |
| Host migration (where supported) | Force-quit host; verify new host election |
The full Cartesian product is too big - sample by risk-weighted buckets:
Step 5 - Encode each combination as a fixture
For Unity NGO, the fixture is a UTF [UnityTest] PlayMode test (see unity-test-framework):
[UnityTest]
public IEnumerator HostMigration_TransfersAuthority_OnHostDisconnect()
{
// Arrange - host + 2 clients
var host = StartHost();
yield return new WaitForSeconds(1f);
var c1 = StartClient(); yield return new WaitForSeconds(0.5f);
var c2 = StartClient(); yield return new WaitForSeconds(0.5f);
// Spawn an authority-bearing object owned by host
var npc = host.SpawnNpc();
yield return new WaitForSeconds(0.5f);
Assert.AreEqual(host.LocalClientId, npc.OwnerClientId);
// Act - kill the host
host.Shutdown();
// Assert - surviving client becomes new host within
// <= 5 s and reassigns NPC authority.
yield return new WaitForSeconds(5f);
Assert.IsTrue(c1.IsHost || c2.IsHost);
Assert.IsNotNull(NetworkManager.Singleton.SpawnManager.SpawnedObjects[npc.NetworkObjectId]);
}For Unreal, a unreal-automation-system spec wrapping IAutomationDriverModule doesn't drive netcode directly - instead, drive a multi-process test harness (UE 5.x's "Multi-User Editor" / Multi-Process PIE) and use specs to observe the resulting OnRep_ invocations.
For Mirror, the fixture is a Unity NGO-style UTF test plus Mirror's built-in network simulation transports.
Step 6 - Wire to platform-cert clauses
Map every fixture to a specific cert clause it covers - e.g. graceful disconnect on network loss → XR-074, MPSD session retained across host migration → XR-067, joinable via the Xbox shell → XR-064, privilege check before joining → XR-045. The full fixture→XR mapping table (and the Sony TRC / Nintendo Lotcheck NDA stable-ID convention) is in references/cert-clauses.md.
Step 7 - Emit the go / no-go gate
Aggregate the matrix into a coverage report:
Multiplayer state-machine coverage - MyGame v1.4.2
====================================================
Connection states enumerated: 7 / 7 ✓
Ownership transitions enumerated: 5 / 5 ✓
Replicated-property edges enumerated: 23 / 23 ✓
Fault-matrix coverage:
High-risk bucket: 18 / 18 fixtures ✓
Medium bucket: 9 / 12 fixtures (75 %) ⚠
Low bucket: 3 / 4 fixtures (75 %)
Cert-clause coverage:
XR-067 MPSD session state ✓
XR-074 Service connectivity loss ✓
XR-064 Joinable via shell ✓
XR-045 Privilege checks ✓
XR-015 Comm-privacy ⚠ (CommunicateUsingVoice path uncovered)
XR-115 Controller add/remove mid-MP ✓
XR-130 Save roams across SKUs ✓
VERDICT: NO-GO (XR-015 voice-privacy path uncovered;
medium-risk bucket below 80 % threshold)The gate refuses to advance to platform-cert submission until every cert-mapped clause is covered and the high-risk bucket is at 100 %.
Worked example - Unity NGO host migration
Inputs:
Step 1 - connection states from the NGO v2.11 manual (opens in new window): Disconnected, Connecting, Connected (Approved), Connected (Pending Spawn), Connected (Spawned), Disconnecting, Host.
Step 2 - ownership transitions: spawn → owner assigned; host quits → ownership re-elected; client picks up host-owned item.
Step 3 - replicated properties under coverage: currentHealth (NetworkVariable<float>), inventoryHash (NetworkVariable<int>), questFlags (NetworkVariable<NetworkSerializableQuestState>).
Step 4 - fault matrix selection: high-risk = Host → Connected (other client takes over) under each of {200 ms latency, 5 % loss, host-kill, host-suspend (Xbox)}.
Step 5 - encode each combination as a UTF PlayMode [UnityTest] (see code sample in Step 5 above).
Step 6 - map fixtures to cert clauses (per the XR list (opens in new window)):
Step 7 - emit gate. Failing fixture: voice mute is reapplied after host migration only 4 / 5 runs (flake). Verdict: NO-GO, flake on the XR-015 voice path; needs a deterministic re-application path before cert.
Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
| Testing only the happy path | Cert findings concentrate on transition edges | Cover every state × fault combination in the high-risk bucket |
| LAN-only multiplayer testing | Submission fails under WAN latency / loss | Inject latency + loss with tc qdisc or engine simulator |
| No host-migration coverage on titles that claim to support it | XR-067 fails mid-session | At least one fixture per supported migration path |
Ignoring IsOwned / authority flags in tests | False positives (test passes because client mirrors authority anyway) | Per Mirror docs (opens in new window), assert isOwned / IsOwner explicitly |
| Replication-property hook coverage by inspection only | OnRep_ doesn't fire if value unchanged - silent contracts | Tests that explicitly mutate the property and assert the hook ran |
| Coverage matrix only on engine states, not cert clauses | Passes internal QA, fails cert | Step 6 mapping is mandatory |
| Trusting "host migration works" without a deterministic election test | Election timing is racy | Bound the election window (e.g., new host elected within 5 s) and assert on it |
Using [ClientRpc] for all communication | Bandwidth hog; non-reliable RPCs preferred for frequent calls per Networking Overview (opens in new window) | Replicated properties for state; RPCs only for events |
| Voice chat covered only with text chat | Per XR-015 permission table (opens in new window), CommunicateUsingText and CommunicateUsingVoice are separate privileges | Test both paths independently |
Limitations
Fixture-to-cert-clause mapping - reference
View source (opens in new window)Fixture-to-cert-clause mapping - reference
Map every fixture to the specific cert clause it covers, for multiplayer-state-machine-coverage Step 6. Examples from the Xbox Requirements page (opens in new window):
| Test fixture | Xbox XR covered |
|---|---|
| Client gracefully disconnects on Xbox network loss | XR-074: "Titles must gracefully handle errors with Xbox and partner services connectivity." |
| MPSD session state retains member list across host migration | XR-067: "titles with online multiplayer functionality must maintain session-state information on the Xbox network … through the Xbox Multiplayer Session Directory (MPSD)" |
| Joining via Xbox shell launches into multiplayer session | XR-064: "titles that offer joinable game sessions must enable joinability through the Xbox shell interface" |
| Privilege check before joining MP session | XR-045: XPRIVILEGE_MULTIPLAYER_SESSIONS (ID 254) per the XR-045 privilege table (opens in new window) |
| Player communication respects privacy settings | XR-015: CommunicateUsingText / CommunicateUsingVoice privilege checks per the XR-015 permissions table (opens in new window) |
| Save roams across console types within a generation | XR-130: "Ensure that saved games work across console types within the generation" |
| Cross-network play visual identification | XR-007: "Titles must visually identify Xbox network users when they're playing with players from non-Xbox gaming networks" |
| Controller disconnect mid-multiplayer | XR-115: re-establish active controller; see XR-115 (opens in new window) |
For Sony TRC and Nintendo Lotcheck, the analogous clauses are NDA - cite by stable ID per platform-cert-overview-reference and tag the fixture with the partner-portal clause number.
Networked engine state and ownership tables - reference
View source (opens in new window)Networked engine state and ownership tables - reference
Per-engine connection-state and authority-state enumerations for multiplayer-state-machine-coverage Steps 1-2. The matrix is fixed by each framework - you cannot add or remove states, only choose which to cover.
Connection states
Unity Netcode for GameObjects (per the v2.11 manual (opens in new window)):
| State | Trigger | Observability |
|---|---|---|
Disconnected | Initial / after disconnect | NetworkManager.IsConnectedClient == false |
Connecting | NetworkManager.StartClient() invoked | Between request and approval |
Connected (Approved) | Server accepts client | OnClientConnectedCallback |
Connected (Pending Spawn) | Approved but player object not yet spawned | Wait for OnNetworkSpawn |
Connected (Spawned) | NetworkObject.IsSpawned == true | Gameplay-ready |
Disconnecting | Shutdown() / link loss | OnClientDisconnectCallback fires next |
Host | Same process is both server + client | NetworkManager.IsHost |
Unreal Engine replication (per the Networking Overview (opens in new window)):
| State | Trigger | Observability |
|---|---|---|
NM_Standalone | Single-player | World->GetNetMode() |
NM_DedicatedServer | "Separate machine with no local players" | IsRunningDedicatedServer() |
NM_ListenServer | "Host machine where the server operator also plays locally" | IsRunningListenServer() |
NM_Client | Connected as remote client | World->IsClient() |
Login | AGameModeBase::PreLogin → Login → PostLogin | Override PostLogin |
Travel (seamless / hard) | ServerTravel to new map | PlayerController->bIsClientReplicationPausedForFrame |
Logout | Logout() callback | Override on GameModeBase |
Mirror Networking (per the Mirror docs on NetworkBehaviour (opens in new window)):
| State | Trigger | Observability |
|---|---|---|
OnStartServer | "called on server when a game object spawns on the server" | NetworkBehaviour override |
OnStartClient | "called on clients when the game object spawns on the client" | NetworkBehaviour override |
OnStartLocalPlayer | Local player only, after OnStartClient | NetworkBehaviour override |
OnStartAuthority / OnStopAuthority | "Called when ownership changes" | NetworkBehaviour override |
OnStopServer / OnStopClient | "Cleanup when objects are destroyed" | NetworkBehaviour override |
Ownership / authority states
| Engine | Authority states |
|---|---|
| Unity NGO | OwnerClientId (per NetworkObject); IsOwner, IsServer, IsHost flags |
| Unreal | ROLE_Authority (server), ROLE_AutonomousProxy (owning client), ROLE_SimulatedProxy (other clients), ROLE_None |
| Mirror | isServer, isClient, isLocalPlayer, isOwned per Mirror NetworkBehaviour docs (opens in new window) - isOwned "Returns true on the client if this client has authority over this game object" |
Related skills
game-perf-profiling
Profiles game builds against frame-time, memory, GPU draw-call, and GC-spike budgets using Unity Profiler + Profile Analyzer + Performance Testing package and Unreal Insights + stat commands. Establishes pass/fail thresholds (16.6 ms at 60 fps, 33.3 ms at 30 fps), writes automated performance regression tests that run in CI, and emits a structured budget report per SKU. Use when a title must hit a declared frame-time or memory budget before a milestone gate or platform-cert submission, or when a recent change needs a performance regression check.
gameplay-recording-replay
Build a deterministic gameplay record/replay test artefact for Unity, Unreal, or Godot - record a player session, save it to disk, replay it bit-for-bit, and assert that the resulting game state matches the original. Covers Unity Input System's InputEventTrace API (Enable / Disable / WriteTo / ReadFrom / Replay) for input-level capture, Unreal's Replay System (DemoRec / DemoPlay / DemoStop console commands plus DemoNetDriver + NetworkReplayStreamer, default storage at %LOCALAPPDATA%/{Project}/Saved/Demos) for replication-stream capture, and Godot's community-pattern deterministic-RNG + input-script replay since Godot ships no first-party replay system. Use when authoring a regression-test artefact for player-recorded sessions, building a netcode replay for spectator / esports, or producing reproducible bug repros for cert teams.
godot-gut-tests
Author and run GUT (Godot Unit Test) - the community-canonical GDScript test framework at github.com/bitwes/Gut and gut.readthedocs.io. Covers install (Godot Asset Library or manual `addons/gut/` copy + plugin enable), GUT panel inside the editor, writing tests that extend GutTest with `test_` prefix methods, the assertion family (assert_eq / assert_almost_eq / assert_true / assert_signal_emitted), lifecycle hooks (before_each / after_each / before_all / after_all), inner classes for grouping, parameterized tests via `params=[...]`, doubles / stubs / spies, async / coroutine tests, the command-line runner (`-d -s addons/gut/gut_cmdln.gd -gdir=res://test -gjunit_xml_file=... -gexit`), JUnit XML export, and CI integration. Godot 4.x uses GUT 9.x (current main branch supports 4.6.x; godot_4_7 branch for 4.7.x); Godot 3.x uses GUT 7.x. Use when the unit under test is GDScript code in a Godot project.
platform-cert-overview-reference
Reference catalog of game-QA test categories and the four platform-holder certification regimes a multi-platform title submits to before release. Defines the six canonical test categories (functional / compliance / compatibility / performance / localization / accessibility) plus the multiplayer and content-rating cross-axes; documents the submission workflow, severity vocabulary, test-bench configurations, and known SLAs for Microsoft Xbox Requirements (XR), Sony TRC (gated PlayStation DevNet), Nintendo Lotcheck (gated Nintendo Developer Portal), and Steam Direct review; and includes the building-a-cert-checklist workflow that maps a target platform's requirement items to the six categories. Cites public sources inline; cites gated NDA portals by stable ID per PLUGIN_AUTHORING.md Step 4 fallback. Use when planning a game test pass or cert calendar, mapping internal QA findings to the platform's vocabulary, sequencing submissions across platforms, or emitting a pre-submission checklist.
unity-test-framework
Author and run the Unity game-engine Test Framework (`com.unity.test-framework`, currently v1.8). Distinct from the ThrowTheSwitch Unity C testing library at throwtheswitch.org/unity - the two tools share only a name. Covers package install via Package Manager, the EditMode vs PlayMode split, the [Test] / [UnityTest] / [SetUp] / [TearDown] / [UnityPlatform] attributes, assembly-definition setup (Editor folder vs asmdef with `includePlatforms` / `optionalUnityReferences: [TestAssemblies]`), Test Runner window, command-line batch invocation with `-runTests` / `-testPlatform` / `-testResults` / `-testFilter` / `-testCategory`, NUnit 3.5 assertion API, and CI integration. Use when the unit under test is C# Unity code that needs to exercise the Unity runtime or editor.
unreal-automation-system
Author and run Unreal Engine's Automation Test Framework - Epic's C++ test framework for UE 4.x / 5.x, documented at dev.epicgames.com/documentation/en-us/unreal-engine. Covers the five test categories Epic defines (Unit / Feature / Smoke / Content Stress / Screenshot Comparison), the IMPLEMENT_SIMPLE_AUTOMATION_TEST and IMPLEMENT_COMPLEX_AUTOMATION_TEST macros, the BDD-style Automation Spec API (DEFINE_SPEC / BEGIN_DEFINE_SPEC / Describe / It / BeforeEach / LatentIt / xIt), latent commands (ADD_LATENT_AUTOMATION_COMMAND), the Automation Driver for UI input simulation (IAutomationDriverModule::Get().CreateDriver(), By::Id / By::Path locators), running via Session Frontend (Window > Test Automation) and command line (-ExecCmds="Automation RunTests …"), and CI integration. Use when the unit under test is C++ Unreal code that needs the UE runtime, editor, or UMG UI surface.