12 KiB
Server Application Layer Conventions
Purpose
This document describes the contracts that the server application layer must follow when built on top of the shared networking layer under Assets/Scripts/Network/.
The goal is to keep the server-side gameplay/application code aligned with the existing dual-lane transport, session lifecycle, tick filtering, and metrics model.
Scope Boundary
- Shared networking concerns belong in
Assets/Scripts/Network/. - Server application concerns should sit above the shared network layer and consume it through
ServerNetworkHost,ServerRuntimeEntryPoint,ServerRuntimeHandle,MessageManager, and the authoritative coordinators. - Do not move Unity-only logic into shared network code.
- Do not make the shared network layer depend on scene objects, MonoBehaviours, or Unity presentation state.
Startup Contract
- Start the dedicated server through
ServerRuntimeEntryPoint.StartAsync(...)orNetworkIntegrationFactory.CreateServerHost(...). - Configure distinct reliable and sync ports when dual-lane behavior is required.
- Do not bind reliable and sync traffic to the same port when the intention is to validate mixed sync behavior.
NetworkIntegrationFactorytreats identical ports as invalid. - Validate server-side tuning through
ServerRuntimeConfiguration,ServerAuthoritativeMovementConfiguration, andServerAuthoritativeCombatConfigurationinstead of ad hoc constants spread across gameplay code.
Session Lifecycle Contract
The server application layer is responsible for driving session state transitions at the correct time.
- Call
NotifyLoginStarted(remoteEndPoint)when login processing begins. - Call
NotifyLoginSucceeded(remoteEndPoint, playerId)after the login has been accepted and the peer identity is known. - Call
NotifyLoginFailed(remoteEndPoint, reason)when login is rejected. - Call
NotifyHeartbeatReceived(remoteEndPoint, serverTick)when a heartbeat request is accepted and answered. - Call
NotifyInboundActivity(remoteEndPoint)only for accepted peer traffic that should refresh liveness. - Call
RemoveSession(remoteEndPoint, reason)when the player disconnects, logs out, times out permanently, or is forcefully removed.
Important implications:
NotifyLoginSucceeded(remoteEndPoint, playerId)is not just bookkeeping. It also bootstraps the peer's authoritative movement state throughServerNetworkHost.- If the application layer forgets to send login success into the host, a freshly logged-in idle player may never receive initial authoritative state and may fail later gameplay assumptions.
- Removing a session through the host also clears authoritative movement state, combat state, and remembered player identity for that peer.
Message Routing Contract
The application layer must preserve the shared lane mapping.
MoveInputusesDeliveryPolicy.HighFrequencySync.PlayerStateusesDeliveryPolicy.HighFrequencySync.ShootInputusesDeliveryPolicy.ReliableOrdered.CombatEventusesDeliveryPolicy.ReliableOrdered.- Any new high-frequency "latest wins" snapshot-like message must be explicitly mapped to the sync lane.
- Any gameplay event that must not be dropped must remain on the reliable ordered lane.
Do not collapse movement and combat intent back into one broad input message if they require different delivery behavior.
Envelope And Handler Contract
- All inbound and outbound gameplay messages must go through
MessageManager. - Do not bypass
MessageManagerby writing transport-specific send logic in application code. - Register handlers on
MessageManagerusingMessageType. - Assume the shared layer always wraps payloads in
Envelope.
Implications for server application code:
- If you add a new gameplay message, you need both a protobuf definition and a handler registration path.
- Broadcasts should use
MessageManager.BroadcastMessage(...). - Directed replies should use
MessageManager.SendMessage(..., target).
Tick And Sequence Contract
The shared sync filter currently applies stale-packet rejection to:
MoveInput, keyed by sender + playerId + tick.PlayerState, keyed by playerId + tick.
Server application rules:
- Treat
tickas required for all gameplay-relevant messages. - Keep
MoveInput.Tickmonotonic per player. - Keep
PlayerState.Tickmonotonic per player. - Do not expect stale
MoveInputpackets to reach gameplay handlers. - Do not add stale filtering assumptions for reliable gameplay events unless the shared layer is explicitly extended for that purpose.
Authoritative Ownership Contract
The current shared/server runtime assumes the server application layer owns gameplay truth.
Server authoritative data includes:
- final position
- final rotation used for authoritative state
- HP
- accepted or rejected shooting
- hit resolution
- death state
Application-layer rules:
- Clients may submit intent only; they must not finalize gameplay outcomes.
- The server should apply
MoveInputandShootInputas requests, not as trusted outcomes. - Authoritative snapshots sent back to clients must be derived from server state, not echoed from client state.
Movement Contract
The movement-side server application layer should follow these rules:
- Ensure every logged-in player has authoritative movement state, even before the first non-zero
MoveInput. - Accept zero-vector
MoveInputas valid current intent. - Keep authoritative movement progression on the server, not on the client.
- Broadcast authoritative
PlayerStateon a fixed interval using server-owned timing. - Include
position,rotation,hp, andvelocitywhen buildingPlayerState.
Do not assume a player who has not moved yet can be omitted from authoritative broadcast state.
Combat Contract
The combat-side server application layer should follow these rules:
- Treat
ShootInputas a reliable gameplay request. - Validate shooter identity against the sending peer.
- Reject shots from dead players or from stale shoot ticks.
- Support both explicit-target and aim-based resolution if
targetIdis optional in the gameplay contract. - Apply damage and death server-side before broadcasting combat results.
- Emit
CombatEventas the only authoritative combat result stream consumed by clients.
Current expected CombatEvent usage:
Hitfor resolved contact.DamageAppliedfor HP loss.Deathfor kill resolution.ShootRejectedfor invalid fire requests.
Broadcast Contract
- Use
BroadcastMessage(PlayerState, MessageType.PlayerState)for authoritative state snapshots. - Use
BroadcastMessage(CombatEvent, MessageType.CombatEvent)for authoritative combat outcomes. - Do not use per-client divergent truth for shared gameplay state unless the design explicitly requires private information.
If a message represents common world truth, broadcast one authoritative result instead of recomputing or customizing it client by client.
Metrics Contract
The transport layer already supports metrics and diagnostics capture. The server application layer should preserve that signal instead of bypassing it.
- Use transports that implement the existing metrics sink path when running diagnostics or bad-network tests.
- Keep session transitions routed through
ServerNetworkHostso transport/application snapshots remain coherent. - Record login success, heartbeat activity, disconnects, and authoritative tick progress through the host/runtime APIs rather than out-of-band state machines.
- When testing with tools like Clumsy, compare sync-lane degradation and reliable-lane survivability using the generated transport reports instead of subjective observation alone.
Practical expectation:
- sync lane may degrade in freshness under packet loss or jitter
- reliable lane may pay retransmission and latency cost
- gameplay semantics on the reliable lane should still remain correct and ordered
Dispatcher Contract
- Client code may use deferred dispatchers such as
MainThreadNetworkDispatcher. - Server code defaults to immediate dispatch through
ServerNetworkHost. - If the server application layer injects a custom dispatcher, it must preserve deterministic handling expectations and must not accidentally require Unity main-thread semantics.
Do not introduce a Unity main-thread dependency into the dedicated server path.
Identity Contract
- The authoritative identity for a peer is the combination of endpoint and accepted player id.
- Login handling must establish that mapping before gameplay is allowed to proceed.
- Any gameplay message whose
playerIddoes not match the accepted identity for the sender should be treated as invalid.
This is especially important for:
MoveInputShootInput- future player-owned gameplay messages
Extending The Message Set
When adding a new gameplay message, the server application layer should answer all of the following before implementation:
- Is this a sync message or a reliable gameplay event?
- Does it require monotonic tick semantics?
- Does stale filtering apply?
- Is the message peer-owned input, server-owned state, or server-owned event?
- Is the message broadcast world truth or a directed reply?
- What metrics should confirm correct behavior under packet loss, latency, and jitter?
If these answers are unclear, the message design is not ready.
Test Contract
Any server application-layer change that touches gameplay networking should add or update edit-mode regression coverage.
Minimum expectations:
- one test for lane routing if delivery behavior changes
- one test for authoritative server acceptance/rejection behavior
- one test for client-visible end-to-end outcome when the gameplay flow changes
- one test for a realistic edge case if the change fixes a regression
Examples of edge cases worth preserving:
- idle logged-in player can still receive
PlayerState - idle logged-in player can still shoot
- stale
MoveInputis ignored ShootInputwithouttargetIdstill resolves correctly when aim-based targeting is intended
Anti-Patterns To Avoid
- Bypassing
MessageManagerand writing directly toITransport. - Letting the client authoritatively decide damage, death, or final position.
- Treating login/session state as a parallel system disconnected from
ServerNetworkHost. - Emitting authoritative gameplay state before the server has established peer identity.
- Depending on Unity scene state inside shared network code.
- Adding new gameplay messages without deciding their lane and tick behavior.
- Passing bad or missing player identity into
NotifyLoginSucceeded. - Forgetting to remove authoritative state when the session is removed.
Checklist For New Server Application Features
- define protobuf message fields and ownership clearly
- choose reliable lane or sync lane explicitly
- decide tick semantics explicitly
- register handlers through
MessageManager - validate sender identity against accepted session identity
- update authoritative server state only on the server
- emit
PlayerStateorCombatEventstyle outputs as needed - route login/heartbeat/disconnect transitions through
ServerNetworkHost - verify metrics remain readable under Clumsy or equivalent bad-network simulation
- add regression tests
Current Reference Types
Assets/Scripts/Network/NetworkApplication/MessageManager.csAssets/Scripts/Network/NetworkApplication/DefaultMessageDeliveryPolicyResolver.csAssets/Scripts/Network/NetworkApplication/SyncSequenceTracker.csAssets/Scripts/Network/NetworkApplication/NetworkIntegrationFactory.csAssets/Scripts/Network/NetworkHost/ServerNetworkHost.csAssets/Scripts/Network/NetworkHost/ServerRuntimeEntryPoint.csAssets/Scripts/Network/NetworkHost/ServerRuntimeConfiguration.csAssets/Scripts/Network/NetworkHost/ServerAuthoritativeMovementCoordinator.csAssets/Scripts/Network/NetworkHost/ServerAuthoritativeCombatCoordinator.cs