process TODO.md step7

This commit is contained in:
SepComet
2026-03-29 10:28:10 +08:00
parent 4826bf15a5
commit ef01760924
22 changed files with 941 additions and 31 deletions
@@ -58,7 +58,8 @@ namespace Network.NetworkApplication
Func<DateTimeOffset> utcNowProvider = null,
IMessageDeliveryPolicyResolver deliveryPolicyResolver = null,
SyncSequenceTracker syncSequenceTracker = null,
Func<int, ITransport> transportFactory = null)
Func<int, ITransport> transportFactory = null,
ServerAuthoritativeMovementConfiguration authoritativeMovement = null)
{
ValidateDualPortConfiguration(reliablePort, syncPort);
@@ -82,7 +83,8 @@ namespace Network.NetworkApplication
utcNowProvider,
syncTransport,
deliveryPolicyResolver,
syncSequenceTracker);
syncSequenceTracker,
authoritativeMovement);
}
public static Task<ServerRuntimeHandle> StartServerRuntimeAsync(ServerRuntimeConfiguration configuration)
@@ -0,0 +1,26 @@
using System;
namespace Network.NetworkHost
{
public sealed class ServerAuthoritativeMovementConfiguration
{
public float MoveSpeed { get; set; } = 5f;
public TimeSpan BroadcastInterval { get; set; } = TimeSpan.FromMilliseconds(50);
public int DefaultHp { get; set; } = 100;
internal void Validate()
{
if (float.IsNaN(MoveSpeed) || float.IsInfinity(MoveSpeed) || MoveSpeed < 0f)
{
throw new ArgumentOutOfRangeException(nameof(MoveSpeed), "Move speed must be finite and non-negative.");
}
if (BroadcastInterval <= TimeSpan.Zero)
{
throw new ArgumentOutOfRangeException(nameof(BroadcastInterval), "Broadcast interval must be positive.");
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8b8b5d0a5ff84ab696e3b302f92314e1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,301 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using Network.Defines;
using Network.NetworkApplication;
namespace Network.NetworkHost
{
internal sealed class ServerAuthoritativeMovementCoordinator
{
private readonly object gate = new();
private readonly MessageManager messageManager;
private readonly ServerNetworkHost host;
private readonly ServerAuthoritativeMovementConfiguration configuration;
private readonly Dictionary<string, ServerAuthoritativeMovementState> statesByPeer = new();
private long nextBroadcastTick = 1;
private TimeSpan accumulatedBroadcastTime;
public ServerAuthoritativeMovementCoordinator(
ServerNetworkHost host,
MessageManager messageManager,
ServerAuthoritativeMovementConfiguration configuration)
{
this.host = host ?? throw new ArgumentNullException(nameof(host));
this.messageManager = messageManager ?? throw new ArgumentNullException(nameof(messageManager));
this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
}
public IReadOnlyList<ServerAuthoritativeMovementState> States
{
get
{
lock (gate)
{
return statesByPeer.Values
.Select(CloneState)
.ToArray();
}
}
}
public Task HandleMoveInputAsync(byte[] payload, IPEndPoint sender)
{
if (payload == null || sender == null)
{
return Task.CompletedTask;
}
MoveInput input;
try
{
input = MoveInput.Parser.ParseFrom(payload);
}
catch
{
return Task.CompletedTask;
}
if (string.IsNullOrWhiteSpace(input.PlayerId) ||
!IsFinite(input.MoveX) ||
!IsFinite(input.MoveY))
{
return Task.CompletedTask;
}
var normalizedSender = Normalize(sender);
var key = normalizedSender.ToString();
lock (gate)
{
if (statesByPeer.TryGetValue(key, out var existingState))
{
if (!string.Equals(existingState.PlayerId, input.PlayerId, StringComparison.Ordinal) ||
input.Tick <= existingState.LastAcceptedMoveTick)
{
return Task.CompletedTask;
}
ApplyInput(existingState, input);
return Task.CompletedTask;
}
var state = new ServerAuthoritativeMovementState(
normalizedSender,
input.PlayerId,
configuration.DefaultHp);
ApplyInput(state, input);
statesByPeer.Add(key, state);
return Task.CompletedTask;
}
}
public void Update(TimeSpan elapsed)
{
if (elapsed < TimeSpan.Zero)
{
throw new ArgumentOutOfRangeException(nameof(elapsed), "Elapsed time must be non-negative.");
}
List<PendingBroadcast> pendingBroadcasts = null;
lock (gate)
{
foreach (var state in statesByPeer.Values)
{
IntegrateState(state, elapsed);
}
accumulatedBroadcastTime += elapsed;
while (accumulatedBroadcastTime >= configuration.BroadcastInterval)
{
accumulatedBroadcastTime -= configuration.BroadcastInterval;
pendingBroadcasts ??= new List<PendingBroadcast>();
foreach (var state in statesByPeer.Values)
{
state.LastBroadcastTick = nextBroadcastTick;
pendingBroadcasts.Add(new PendingBroadcast(
state.RemoteEndPoint,
BuildPlayerState(state, nextBroadcastTick)));
}
nextBroadcastTick++;
}
}
if (pendingBroadcasts == null)
{
return;
}
foreach (var pendingBroadcast in pendingBroadcasts)
{
messageManager.BroadcastMessage(pendingBroadcast.PlayerState, MessageType.PlayerState);
host.ObserveAuthoritativeState(pendingBroadcast.RemoteEndPoint, pendingBroadcast.PlayerState.Tick);
}
}
public bool TryGetState(IPEndPoint remoteEndPoint, out ServerAuthoritativeMovementState state)
{
var key = Normalize(remoteEndPoint).ToString();
lock (gate)
{
if (statesByPeer.TryGetValue(key, out state))
{
state = CloneState(state);
return true;
}
}
state = null;
return false;
}
public void RemoveState(IPEndPoint remoteEndPoint)
{
var key = Normalize(remoteEndPoint).ToString();
lock (gate)
{
statesByPeer.Remove(key);
}
}
public void Clear()
{
lock (gate)
{
statesByPeer.Clear();
accumulatedBroadcastTime = TimeSpan.Zero;
}
}
private static bool IsFinite(float value)
{
return !float.IsNaN(value) && !float.IsInfinity(value);
}
private static IPEndPoint Normalize(IPEndPoint remoteEndPoint)
{
if (remoteEndPoint == null)
{
throw new ArgumentNullException(nameof(remoteEndPoint));
}
return new IPEndPoint(remoteEndPoint.Address, remoteEndPoint.Port);
}
private static ServerAuthoritativeMovementState CloneState(ServerAuthoritativeMovementState state)
{
return new ServerAuthoritativeMovementState(state.RemoteEndPoint, state.PlayerId, state.Hp)
{
LastAcceptedMoveTick = state.LastAcceptedMoveTick,
LastBroadcastTick = state.LastBroadcastTick,
PositionX = state.PositionX,
PositionY = state.PositionY,
PositionZ = state.PositionZ,
VelocityX = state.VelocityX,
VelocityY = state.VelocityY,
VelocityZ = state.VelocityZ,
Rotation = state.Rotation,
InputX = state.InputX,
InputY = state.InputY
};
}
private static void ApplyInput(ServerAuthoritativeMovementState state, MoveInput input)
{
state.LastAcceptedMoveTick = input.Tick;
state.InputX = input.MoveX;
state.InputY = input.MoveY;
if (input.MoveX == 0f && input.MoveY == 0f)
{
state.VelocityX = 0f;
state.VelocityY = 0f;
state.VelocityZ = 0f;
return;
}
var length = MathF.Sqrt((input.MoveX * input.MoveX) + (input.MoveY * input.MoveY));
if (length <= 0f)
{
state.VelocityX = 0f;
state.VelocityY = 0f;
state.VelocityZ = 0f;
return;
}
state.Rotation = MathF.Atan2(input.MoveY, input.MoveX) * (180f / MathF.PI);
}
private void IntegrateState(ServerAuthoritativeMovementState state, TimeSpan elapsed)
{
if (state.InputX == 0f && state.InputY == 0f)
{
state.VelocityX = 0f;
state.VelocityY = 0f;
state.VelocityZ = 0f;
return;
}
var length = MathF.Sqrt((state.InputX * state.InputX) + (state.InputY * state.InputY));
if (length <= 0f)
{
state.VelocityX = 0f;
state.VelocityY = 0f;
state.VelocityZ = 0f;
return;
}
var normalizedX = state.InputX / length;
var normalizedY = state.InputY / length;
state.VelocityX = normalizedX * configuration.MoveSpeed;
state.VelocityY = 0f;
state.VelocityZ = normalizedY * configuration.MoveSpeed;
var deltaSeconds = (float)elapsed.TotalSeconds;
state.PositionX += state.VelocityX * deltaSeconds;
state.PositionY += state.VelocityY * deltaSeconds;
state.PositionZ += state.VelocityZ * deltaSeconds;
}
private static PlayerState BuildPlayerState(ServerAuthoritativeMovementState state, long tick)
{
return new PlayerState
{
PlayerId = state.PlayerId,
Tick = tick,
Position = new Vector3
{
X = state.PositionX,
Y = state.PositionY,
Z = state.PositionZ
},
Velocity = new Vector3
{
X = state.VelocityX,
Y = state.VelocityY,
Z = state.VelocityZ
},
Rotation = state.Rotation,
Hp = state.Hp
};
}
private sealed class PendingBroadcast
{
public PendingBroadcast(IPEndPoint remoteEndPoint, PlayerState playerState)
{
RemoteEndPoint = remoteEndPoint;
PlayerState = playerState;
}
public IPEndPoint RemoteEndPoint { get; }
public PlayerState PlayerState { get; }
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 91c6c9377bb94c4f88cc4f37dbed16ab
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,43 @@
using System;
using System.Net;
namespace Network.NetworkHost
{
public sealed class ServerAuthoritativeMovementState
{
public ServerAuthoritativeMovementState(IPEndPoint remoteEndPoint, string playerId, int hp)
{
RemoteEndPoint = remoteEndPoint ?? throw new ArgumentNullException(nameof(remoteEndPoint));
PlayerId = playerId ?? throw new ArgumentNullException(nameof(playerId));
Hp = hp;
}
public IPEndPoint RemoteEndPoint { get; }
public string PlayerId { get; }
public long LastAcceptedMoveTick { get; internal set; }
public long LastBroadcastTick { get; internal set; }
public float PositionX { get; internal set; }
public float PositionY { get; internal set; }
public float PositionZ { get; internal set; }
public float VelocityX { get; internal set; }
public float VelocityY { get; internal set; }
public float VelocityZ { get; internal set; }
public float Rotation { get; internal set; }
public int Hp { get; internal set; }
public float InputX { get; internal set; }
public float InputY { get; internal set; }
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0f2f1baf3a314f51a8d4b1caea7ee905
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,7 +1,8 @@
using System;
using System;
using System.Collections.Generic;
using System.Net;
using System.Threading.Tasks;
using Network.Defines;
using Network.NetworkApplication;
using Network.NetworkTransport;
@@ -12,6 +13,7 @@ namespace Network.NetworkHost
private readonly ITransport transport;
private readonly ITransport syncTransport;
private readonly MessageManager messageManager;
private readonly ServerAuthoritativeMovementCoordinator authoritativeMovementCoordinator;
public ServerNetworkHost(
ITransport transport,
@@ -20,7 +22,8 @@ namespace Network.NetworkHost
Func<DateTimeOffset> utcNowProvider = null,
ITransport syncTransport = null,
IMessageDeliveryPolicyResolver deliveryPolicyResolver = null,
SyncSequenceTracker syncSequenceTracker = null)
SyncSequenceTracker syncSequenceTracker = null,
ServerAuthoritativeMovementConfiguration authoritativeMovement = null)
{
this.transport = transport ?? throw new ArgumentNullException(nameof(transport));
this.syncTransport = syncTransport;
@@ -37,6 +40,11 @@ namespace Network.NetworkHost
deliveryPolicyResolver ?? new DefaultMessageDeliveryPolicyResolver(),
this.syncTransport,
syncSequenceTracker ?? new SyncSequenceTracker());
authoritativeMovementCoordinator = new ServerAuthoritativeMovementCoordinator(
this,
messageManager,
authoritativeMovement ?? new ServerAuthoritativeMovementConfiguration());
messageManager.RegisterHandler(MessageType.MoveInput, authoritativeMovementCoordinator.HandleMoveInputAsync);
}
public MessageManager MessageManager => messageManager;
@@ -45,11 +53,12 @@ namespace Network.NetworkHost
public ITransport SyncTransport => syncTransport;
// Server-side lifecycle entry point: inspect and control per-peer session state here.
public MultiSessionManager SessionCoordinator { get; }
public IReadOnlyList<ManagedNetworkSession> ManagedSessions => SessionCoordinator.Sessions;
public IReadOnlyList<ServerAuthoritativeMovementState> AuthoritativeMovementStates => authoritativeMovementCoordinator.States;
public event Action<MultiSessionLifecycleEvent> LifecycleChanged
{
add => SessionCoordinator.LifecycleChanged += value;
@@ -77,6 +86,7 @@ namespace Network.NetworkHost
}
SessionCoordinator.RemoveAllSessions("Transport stopped");
authoritativeMovementCoordinator.Clear();
PublishMetricsSessionSnapshots();
}
@@ -91,11 +101,21 @@ namespace Network.NetworkHost
PublishMetricsSessionSnapshots();
}
public void UpdateAuthoritativeMovement(TimeSpan elapsed)
{
authoritativeMovementCoordinator.Update(elapsed);
}
public bool TryGetSession(IPEndPoint remoteEndPoint, out ManagedNetworkSession session)
{
return SessionCoordinator.TryGetSession(remoteEndPoint, out session);
}
public bool TryGetAuthoritativeMovementState(IPEndPoint remoteEndPoint, out ServerAuthoritativeMovementState state)
{
return authoritativeMovementCoordinator.TryGetState(remoteEndPoint, out state);
}
public void NotifyLoginStarted(IPEndPoint remoteEndPoint)
{
SessionCoordinator.NotifyLoginStarted(remoteEndPoint);
@@ -151,6 +171,8 @@ namespace Network.NetworkHost
return false;
}
authoritativeMovementCoordinator.RemoveState(remoteEndPoint);
RecordMetricsSessionSnapshot(transport, "server-host", session, ConnectionState.Disconnected);
if (syncTransport != null && !ReferenceEquals(syncTransport, transport))
{
@@ -32,6 +32,8 @@ namespace Network.NetworkHost
public Func<int, ITransport> TransportFactory { get; set; }
public ServerAuthoritativeMovementConfiguration AuthoritativeMovement { get; set; }
internal void Validate()
{
if (ReliablePort <= 0)
@@ -39,20 +41,20 @@ namespace Network.NetworkHost
throw new ArgumentOutOfRangeException(nameof(ReliablePort), "Reliable port must be positive.");
}
if (!SyncPort.HasValue)
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));
}
}
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));
}
AuthoritativeMovement?.Validate();
}
}
}
@@ -23,7 +23,8 @@ namespace Network.NetworkHost
configuration.UtcNowProvider,
configuration.DeliveryPolicyResolver,
configuration.SyncSequenceTracker,
configuration.TransportFactory);
configuration.TransportFactory,
configuration.AuthoritativeMovement);
try
{
@@ -23,6 +23,8 @@ namespace Network.NetworkHost
public IReadOnlyList<ManagedNetworkSession> ManagedSessions => host.ManagedSessions;
public IReadOnlyList<ServerAuthoritativeMovementState> AuthoritativeMovementStates => host.AuthoritativeMovementStates;
public event Action<MultiSessionLifecycleEvent> LifecycleChanged
{
add => host.LifecycleChanged += value;
@@ -39,11 +41,21 @@ namespace Network.NetworkHost
host.UpdateLifecycle();
}
public void UpdateAuthoritativeMovement(TimeSpan elapsed)
{
host.UpdateAuthoritativeMovement(elapsed);
}
public bool TryGetSession(IPEndPoint remoteEndPoint, out ManagedNetworkSession session)
{
return host.TryGetSession(remoteEndPoint, out session);
}
public bool TryGetAuthoritativeMovementState(IPEndPoint remoteEndPoint, out ServerAuthoritativeMovementState state)
{
return host.TryGetAuthoritativeMovementState(remoteEndPoint, out state);
}
public void Stop()
{
if (isStopped)