process TODO.md step 9

This commit is contained in:
SepComet 2026-03-28 14:26:40 +08:00
parent 101694a3b0
commit 1b1598dbb7
15 changed files with 396 additions and 13 deletions

View File

@ -0,0 +1,110 @@
using System;
using Network.NetworkHost;
using Network.NetworkTransport;
namespace Network.NetworkApplication
{
public static class NetworkIntegrationFactory
{
public static SharedNetworkRuntime CreateClientRuntime(
string serverIp,
int reliablePort,
INetworkMessageDispatcher dispatcher,
int? syncPort = null,
SessionReconnectPolicy reconnectPolicy = null,
Func<DateTimeOffset> utcNowProvider = null,
IMessageDeliveryPolicyResolver deliveryPolicyResolver = null,
SyncSequenceTracker syncSequenceTracker = null,
ClockSyncState clockSync = null,
Func<string, int, ITransport> transportFactory = null)
{
if (dispatcher == null)
{
throw new ArgumentNullException(nameof(dispatcher));
}
ValidateDualPortConfiguration(reliablePort, syncPort);
transportFactory ??= static (ip, port) => new KcpTransport(ip, port);
var reliableTransport = transportFactory(serverIp, reliablePort)
?? throw new InvalidOperationException("Reliable transport factory returned null.");
var syncTransport = syncPort.HasValue
? transportFactory(serverIp, syncPort.Value)
: null;
if (syncPort.HasValue && syncTransport == null)
{
throw new InvalidOperationException("Sync transport factory returned null.");
}
return new SharedNetworkRuntime(
reliableTransport,
dispatcher,
reconnectPolicy,
utcNowProvider,
syncTransport,
deliveryPolicyResolver,
syncSequenceTracker,
clockSync);
}
public static ServerNetworkHost CreateServerHost(
int reliablePort,
int? syncPort = null,
INetworkMessageDispatcher dispatcher = null,
SessionReconnectPolicy reconnectPolicy = null,
Func<DateTimeOffset> utcNowProvider = null,
IMessageDeliveryPolicyResolver deliveryPolicyResolver = null,
SyncSequenceTracker syncSequenceTracker = null,
Func<int, ITransport> transportFactory = null)
{
ValidateDualPortConfiguration(reliablePort, syncPort);
transportFactory ??= static port => new KcpTransport(port);
var reliableTransport = transportFactory(reliablePort)
?? throw new InvalidOperationException("Reliable transport factory returned null.");
var syncTransport = syncPort.HasValue
? transportFactory(syncPort.Value)
: null;
if (syncPort.HasValue && syncTransport == null)
{
throw new InvalidOperationException("Sync transport factory returned null.");
}
return new ServerNetworkHost(
reliableTransport,
dispatcher,
reconnectPolicy,
utcNowProvider,
syncTransport,
deliveryPolicyResolver,
syncSequenceTracker);
}
private static void ValidateDualPortConfiguration(int reliablePort, int? syncPort)
{
if (reliablePort <= 0)
{
throw new ArgumentOutOfRangeException(nameof(reliablePort), "Reliable port must be positive.");
}
if (!syncPort.HasValue)
{
return;
}
if (syncPort.Value <= 0)
{
throw new ArgumentOutOfRangeException(nameof(syncPort), "Sync port must be positive.");
}
if (syncPort.Value == reliablePort)
{
throw new ArgumentException("Sync port must differ from reliable port.", nameof(syncPort));
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: bfefd18b5face1d4496541e42b3a011b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -3,13 +3,15 @@ using System.Net;
using System.Threading.Tasks; using System.Threading.Tasks;
using Network.Defines; using Network.Defines;
using Network.NetworkApplication; using Network.NetworkApplication;
using Network.NetworkTransport;
using UnityEngine; using UnityEngine;
using Vector3 = UnityEngine.Vector3; using Vector3 = UnityEngine.Vector3;
public class NetworkManager : MonoBehaviour public class NetworkManager : MonoBehaviour
{ {
private const int MaxNetworkMessagesPerFrame = 32; private const int MaxNetworkMessagesPerFrame = 32;
private const string DefaultServerIp = "127.0.0.1";
private const int DefaultReliablePort = 8080;
private const int DefaultSyncPort = 8081;
public static NetworkManager Instance; public static NetworkManager Instance;
private SharedNetworkRuntime _networkRuntime; private SharedNetworkRuntime _networkRuntime;
@ -18,6 +20,9 @@ public class NetworkManager : MonoBehaviour
private Task _networkDrainTask = Task.CompletedTask; private Task _networkDrainTask = Task.CompletedTask;
[SerializeField] private GameObject _wrongWindow; [SerializeField] private GameObject _wrongWindow;
[SerializeField] private bool _enableNetworkDiagnosticsOverlay = true; [SerializeField] private bool _enableNetworkDiagnosticsOverlay = true;
[SerializeField] private string _serverIp = DefaultServerIp;
[SerializeField] private int _reliablePort = DefaultReliablePort;
[SerializeField] private int _syncPort = DefaultSyncPort;
private void Awake() private void Awake()
{ {
@ -28,9 +33,13 @@ public class NetworkManager : MonoBehaviour
private IEnumerator InitNetwork() private IEnumerator InitNetwork()
{ {
var transport = new KcpTransport("127.0.0.1", 8080);
var dispatcher = new MainThreadNetworkDispatcher(); var dispatcher = new MainThreadNetworkDispatcher();
_networkRuntime = new SharedNetworkRuntime(transport, dispatcher); int? syncPort = _syncPort > 0 ? _syncPort : null;
_networkRuntime = NetworkIntegrationFactory.CreateClientRuntime(
_serverIp,
_reliablePort,
dispatcher,
syncPort: syncPort);
_networkRuntime.LifecycleChanged += HandleLifecycleChanged; _networkRuntime.LifecycleChanged += HandleLifecycleChanged;
var startTask = _networkRuntime.StartAsync(); var startTask = _networkRuntime.StartAsync();

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Net; using System.Net;
using System.Threading.Tasks; using System.Threading.Tasks;
using Google.Protobuf; using Google.Protobuf;
@ -141,6 +142,89 @@ namespace Tests.EditMode.Network
Assert.That(session.SessionManager.State, Is.EqualTo(ConnectionState.TransportConnected)); Assert.That(session.SessionManager.State, Is.EqualTo(ConnectionState.TransportConnected));
} }
[Test]
public void NetworkIntegrationFactory_CreateClientRuntime_WithSyncPort_UsesDistinctTransportsPerLane()
{
var createdTransports = new Dictionary<int, FakeTransport>();
var runtime = NetworkIntegrationFactory.CreateClientRuntime(
"127.0.0.1",
8080,
new ImmediateNetworkMessageDispatcher(),
syncPort: 8081,
transportFactory: (serverIp, port) =>
{
var transport = new FakeTransport();
createdTransports.Add(port, transport);
return transport;
});
var moveInput = new MoveInput
{
PlayerId = "shared-player",
Tick = 77,
MoveX = 1f
};
runtime.MessageManager.SendMessage(moveInput, MessageType.MoveInput);
runtime.MessageManager.SendMessage(new Heartbeat(), MessageType.Heartbeat);
Assert.That(createdTransports.Keys, Is.EquivalentTo(new[] { 8080, 8081 }));
Assert.That(runtime.Transport, Is.SameAs(createdTransports[8080]));
Assert.That(runtime.SyncTransport, Is.SameAs(createdTransports[8081]));
Assert.That(createdTransports[8080].SendCallCount, Is.EqualTo(1));
Assert.That(createdTransports[8081].SendCallCount, Is.EqualTo(1));
}
[Test]
public void NetworkIntegrationFactory_CreateServerHost_WithSyncPort_UsesDistinctTransportsPerLane()
{
var createdTransports = new Dictionary<int, FakeTransport>();
var host = NetworkIntegrationFactory.CreateServerHost(
9000,
syncPort: 9001,
transportFactory: port =>
{
var transport = new FakeTransport();
createdTransports.Add(port, transport);
return transport;
});
var moveInput = new MoveInput
{
PlayerId = "server-player",
Tick = 88,
MoveY = 1f
};
host.MessageManager.SendMessage(moveInput, MessageType.MoveInput);
host.MessageManager.SendMessage(new Heartbeat(), MessageType.Heartbeat);
Assert.That(createdTransports.Keys, Is.EquivalentTo(new[] { 9000, 9001 }));
Assert.That(host.Transport, Is.SameAs(createdTransports[9000]));
Assert.That(host.SyncTransport, Is.SameAs(createdTransports[9001]));
Assert.That(createdTransports[9000].SendCallCount, Is.EqualTo(1));
Assert.That(createdTransports[9001].SendCallCount, Is.EqualTo(1));
}
[Test]
public void NetworkIntegrationFactory_CreateServerHost_WithoutSyncPort_PreservesSingleTransportFallback()
{
var reliableTransport = new FakeTransport();
var host = NetworkIntegrationFactory.CreateServerHost(
9000,
transportFactory: _ => reliableTransport);
var moveInput = new MoveInput
{
PlayerId = "fallback-player",
Tick = 99,
MoveX = -1f
};
host.MessageManager.SendMessage(moveInput, MessageType.MoveInput);
Assert.That(host.Transport, Is.SameAs(reliableTransport));
Assert.That(host.SyncTransport, Is.Null);
Assert.That(reliableTransport.SendCallCount, Is.EqualTo(1));
}
private static byte[] BuildEnvelope(MessageType type, IMessage payload) private static byte[] BuildEnvelope(MessageType type, IMessage payload)
{ {
return new Envelope return new Envelope

12
TODO.md
View File

@ -117,15 +117,15 @@ Acceptance:
### 9. Wire Dual Transports In The Integration Layer ### 9. Wire Dual Transports In The Integration Layer
- [ ] Update the client integration entry point, likely [`Assets/Scripts/NetworkManager.cs`](D:/Learn/GameLearn/UnityProjects/NetworkFW/Assets/Scripts/NetworkManager.cs) - [x] Update the client integration entry point, likely [`Assets/Scripts/NetworkManager.cs`](D:/Learn/GameLearn/UnityProjects/NetworkFW/Assets/Scripts/NetworkManager.cs)
- [ ] Update the server startup integration point - [x] Update the server startup integration point
- [ ] Instantiate one reliable transport and one sync transport - [x] Instantiate one reliable transport and one sync transport
- [ ] Ensure runtime construction uses both transports instead of a single shared instance - [x] Ensure runtime construction uses both transports instead of a single shared instance
Acceptance: Acceptance:
- [ ] Runtime uses logical dual-lane routing backed by two transport instances - [x] Runtime uses logical dual-lane routing backed by two transport instances
- [ ] Logging or tests confirm movement/state traffic and reliable event traffic are separated - [x] Logging or tests confirm movement/state traffic and reliable event traffic are separated
### 10. Build And Test ### 10. Build And Test

View File

@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-03-28

View File

@ -0,0 +1,55 @@
## Context
The shared networking layer already separates reliable messaging from sync-heavy traffic conceptually, but the runtime integration path still tends to compose a single transport dependency and leaves lane selection to host-specific setup. This makes the dual-transport design incomplete: shared services can express sync intent, yet composition can still collapse both lanes into one implicit path. The repository also requires preserving the existing client single-session flow and avoiding Unity-specific dependencies in shared networking code.
## Goals / Non-Goals
**Goals:**
- Allow integration-layer composition to pass both reliable and sync transports into shared networking services.
- Keep the single-transport path valid by falling back to the reliable transport when no dedicated sync transport is configured.
- Centralize lane selection inside shared networking/session code so client and server hosts follow the same wiring rules.
- Add regression coverage for both client single-session and server multi-session composition paths.
**Non-Goals:**
- Redesign transport implementations or message schemas.
- Introduce Unity-only adapters into `Assets/Scripts/Network/`.
- Change message delivery policy beyond what is required to connect existing sync traffic to the proper transport.
## Decisions
### Decision: Treat sync transport as an optional secondary dependency
The integration layer will accept a primary reliable transport and an optional sync transport. Shared services will retain a deterministic fallback to the primary transport when the secondary dependency is absent.
This preserves existing callers and lets the change land without forcing all hosts to upgrade in one step.
Alternative considered: require dual transports everywhere. Rejected because it would break the current single-session client setup and create unnecessary migration pressure.
### Decision: Keep transport ownership at session/runtime composition boundaries
Session managers, message managers, or equivalent composition roots should receive both transport references during construction so downstream routing logic can stay host-agnostic.
This keeps transport selection in shared code and prevents Unity or host bootstrapping layers from re-implementing routing rules.
Alternative considered: inject a host-side selector callback. Rejected because it spreads transport policy across hosts and makes regression coverage weaker.
### Decision: Encode fallback behavior in requirements and tests
The change will specify that sync-routed traffic uses the sync transport when available and otherwise uses the primary reliable transport. Tests should cover both client single-session and server multi-session variants.
Alternative considered: rely on implementation comments only. Rejected because the fallback contract is easy to regress during future refactors.
## Risks / Trade-offs
- [Risk] Integration constructors may grow more complex with optional transport parameters. → Mitigation: keep the extra dependency scoped to composition roots and default the sync transport explicitly.
- [Risk] Hosts may accidentally provide mismatched transport instances across session types. → Mitigation: express wiring requirements in specs and add regression tests for composition paths.
- [Risk] Existing tests may only cover single-session client behavior. → Mitigation: add server multi-session coverage as part of the task list.
## Migration Plan
1. Extend composition APIs to accept the optional sync transport without breaking existing call sites.
2. Update shared routing/session initialization to store and use both dependencies.
3. Add or adjust edit-mode tests for fallback and dedicated sync-lane behavior.
4. Rollback, if needed, by removing the optional sync transport wiring while retaining the original single-transport constructor path.
## Open Questions
- Which concrete integration types currently own transport composition for client and server entry points?
- Are there any message categories besides sync traffic that should explicitly target the secondary transport at composition time?

View File

@ -0,0 +1,26 @@
## Why
The networking stack already distinguishes between reliable gameplay messaging and high-frequency sync traffic, but the integration layer still assumes a single transport path in its runtime wiring. Step 9 is needed now to expose the dual-transport design at composition time so hosts can route each lane consistently without breaking the existing single-session client path.
## What Changes
- Wire the integration layer so session/runtime composition can accept both a primary reliable transport and a sync transport.
- Preserve backward-compatible behavior when only the primary transport is provided by continuing to use the reliable path for all traffic that lacks a dedicated sync lane.
- Update message/session integration contracts so transport selection is resolved in shared networking code rather than by host-specific call sites.
- Add regression coverage for client single-session and server multi-session integration paths that depend on dual-transport wiring.
## Capabilities
### New Capabilities
- None.
### Modified Capabilities
- `shared-network-foundation`: Extend the shared composition contract so network managers and related services can be constructed with dual transports while preserving the existing single-transport fallback.
- `network-session-lifecycle`: Update runtime/session wiring requirements so sessions initialize and retain both reliable and sync transport dependencies where available.
- `network-sync-strategy`: Require integration-layer routing to connect sync traffic to the dedicated sync transport instead of relying on host-side manual wiring.
## Impact
- Affected code under `Assets/Scripts/Network/` for integration/composition, session initialization, and transport-aware message dispatch.
- Edit-mode regression tests under `Assets/Tests/EditMode/Network/`.
- No Unity-specific dependency changes; Unity adapters should remain outside shared networking code.

View File

@ -0,0 +1,12 @@
## ADDED Requirements
### Requirement: Sessions retain dual transport wiring
Session lifecycle components SHALL initialize session-scoped networking services with both the primary reliable transport and the optional sync transport supplied by the integration layer.
#### Scenario: Client single-session initialization with dual transports
- **WHEN** the client integration path creates a single session and a sync transport is configured
- **THEN** the session-scoped services SHALL retain both transport references for subsequent message routing
#### Scenario: Server multi-session initialization with fallback transport
- **WHEN** the server integration path creates session-scoped services without a dedicated sync transport
- **THEN** each session SHALL continue to initialize successfully and SHALL use the primary reliable transport as the fallback lane

View File

@ -0,0 +1,12 @@
## ADDED Requirements
### Requirement: Integration wiring enforces sync lane selection
The networking stack SHALL route sync-designated traffic through the sync transport when the integration layer provides one, and SHALL fall back to the primary reliable transport when it does not.
#### Scenario: Dedicated sync transport available
- **WHEN** sync-designated traffic is sent from a session whose integration wiring includes a sync transport
- **THEN** the traffic SHALL be dispatched on the sync transport instead of the primary reliable transport
#### Scenario: Dedicated sync transport unavailable
- **WHEN** sync-designated traffic is sent from a session whose integration wiring does not include a sync transport
- **THEN** the traffic SHALL be dispatched on the primary reliable transport without failing session operation

View File

@ -0,0 +1,12 @@
## ADDED Requirements
### Requirement: Shared network composition accepts dual transports
The shared networking composition layer SHALL allow construction of network managers and related shared services with a primary reliable transport and an optional sync transport.
#### Scenario: Integration receives both transports
- **WHEN** a host composes the shared networking stack with both a reliable transport and a sync transport
- **THEN** the shared composition path SHALL retain both dependencies for downstream routing and session services
#### Scenario: Integration receives only one transport
- **WHEN** a host composes the shared networking stack with only the reliable transport
- **THEN** the shared composition path SHALL remain valid and SHALL treat the reliable transport as the fallback lane for traffic without a dedicated secondary transport

View File

@ -0,0 +1,17 @@
## 1. Extend Integration Composition
- [x] 1.1 Identify the shared integration/composition entry points that currently construct networking services with a single transport.
- [x] 1.2 Update the relevant constructors or factory methods to accept an optional sync transport alongside the primary reliable transport.
- [x] 1.3 Preserve backward-compatible call paths so existing single-transport composition still builds and defaults correctly.
## 2. Wire Dual Transports Through Session Services
- [x] 2.1 Update session-scoped networking services to retain both transport references provided by the integration layer.
- [x] 2.2 Route sync-designated traffic to the sync transport when present and fall back to the reliable transport otherwise.
- [x] 2.3 Ensure the shared wiring keeps host-specific transport policy out of Unity-only or integration call sites.
## 3. Verify Regression Coverage
- [x] 3.1 Add or update edit-mode tests for client single-session composition with both reliable and sync transports.
- [x] 3.2 Add or update edit-mode tests for server multi-session composition when no dedicated sync transport is provided.
- [x] 3.3 Run `dotnet build Network.EditMode.Tests.csproj -v minimal` and `dotnet test Network.EditMode.Tests.csproj --no-build -v minimal` after implementation.

View File

@ -42,3 +42,14 @@ The shared networking core SHALL manage timeout detection, disconnect transition
- **WHEN** authentication or login fails while the transport session is still active - **WHEN** authentication or login fails while the transport session is still active
- **THEN** the shared lifecycle reports a login-failed state for that managed session - **THEN** the shared lifecycle reports a login-failed state for that managed session
- **THEN** hosts can handle that failure separately from a transport disconnect or heartbeat timeout - **THEN** hosts can handle that failure separately from a transport disconnect or heartbeat timeout
### Requirement: Sessions retain dual transport wiring
Session lifecycle components SHALL initialize session-scoped networking services with both the primary reliable transport and the optional sync transport supplied by the integration layer.
#### Scenario: Client single-session initialization with dual transports
- **WHEN** the client integration path creates a single session and a sync transport is configured
- **THEN** the session-scoped services SHALL retain both transport references for subsequent message routing
#### Scenario: Server multi-session initialization with fallback transport
- **WHEN** the server integration path creates session-scoped services without a dedicated sync transport
- **THEN** each session SHALL continue to initialize successfully and SHALL use the primary reliable transport as the fallback lane

View File

@ -60,3 +60,14 @@ The shared networking core SHALL process server-tick or clock-synchronization sa
- **WHEN** prediction or reconciliation code needs the current server-time estimate - **WHEN** prediction or reconciliation code needs the current server-time estimate
- **THEN** it reads that estimate from the clock-sync strategy or state object - **THEN** it reads that estimate from the clock-sync strategy or state object
- **THEN** it does not query `SessionManager` for authoritative clock ownership - **THEN** it does not query `SessionManager` for authoritative clock ownership
### Requirement: Integration wiring enforces sync lane selection
The networking stack SHALL route sync-designated traffic through the sync transport when the integration layer provides one, and SHALL fall back to the primary reliable transport when it does not.
#### Scenario: Dedicated sync transport available
- **WHEN** sync-designated traffic is sent from a session whose integration wiring includes a sync transport
- **THEN** the traffic SHALL be dispatched on the sync transport instead of the primary reliable transport
#### Scenario: Dedicated sync transport unavailable
- **WHEN** sync-designated traffic is sent from a session whose integration wiring does not include a sync transport
- **THEN** the traffic SHALL be dispatched on the primary reliable transport without failing session operation

View File

@ -70,3 +70,14 @@ The shared network foundation SHALL include host-agnostic session lifecycle orch
- **WHEN** a non-Unity server host constructs the runtime networking stack for multiple remote peers - **WHEN** a non-Unity server host constructs the runtime networking stack for multiple remote peers
- **THEN** it uses the shared transport and message-routing foundation together with shared multi-session lifecycle orchestration - **THEN** it uses the shared transport and message-routing foundation together with shared multi-session lifecycle orchestration
- **THEN** server-specific cleanup, admission, and gameplay reactions stay in the server host adapter rather than forking the shared lifecycle contract - **THEN** server-specific cleanup, admission, and gameplay reactions stay in the server host adapter rather than forking the shared lifecycle contract
### Requirement: Shared network composition accepts dual transports
The shared networking composition layer SHALL allow construction of network managers and related shared services with a primary reliable transport and an optional sync transport.
#### Scenario: Integration receives both transports
- **WHEN** a host composes the shared networking stack with both a reliable transport and a sync transport
- **THEN** the shared composition path SHALL retain both dependencies for downstream routing and session services
#### Scenario: Integration receives only one transport
- **WHEN** a host composes the shared networking stack with only the reliable transport
- **THEN** the shared composition path SHALL remain valid and SHALL treat the reliable transport as the fallback lane for traffic without a dedicated secondary transport