process TODO.md step8

This commit is contained in:
SepComet
2026-03-29 10:49:32 +08:00
parent ef01760924
commit c5fbd8e36d
26 changed files with 1005 additions and 20 deletions
@@ -0,0 +1,17 @@
using System;
namespace Network.NetworkHost
{
public sealed class ServerAuthoritativeCombatConfiguration
{
public int DamagePerShot { get; set; } = 25;
internal void Validate()
{
if (DamagePerShot <= 0)
{
throw new ArgumentOutOfRangeException(nameof(DamagePerShot), "Damage per shot must be positive.");
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 255d10cba50a58345800f75f1bc402d1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,270 @@
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 ServerAuthoritativeCombatCoordinator
{
private readonly object gate = new();
private readonly MessageManager messageManager;
private readonly ServerAuthoritativeMovementCoordinator movementCoordinator;
private readonly Dictionary<string, ServerAuthoritativeCombatState> statesByPeer = new();
private readonly ServerAuthoritativeCombatConfiguration configuration;
public ServerAuthoritativeCombatCoordinator(
MessageManager messageManager,
ServerAuthoritativeMovementCoordinator movementCoordinator,
ServerAuthoritativeCombatConfiguration configuration)
{
this.messageManager = messageManager ?? throw new ArgumentNullException(nameof(messageManager));
this.movementCoordinator = movementCoordinator ?? throw new ArgumentNullException(nameof(movementCoordinator));
this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
}
public IReadOnlyList<ServerAuthoritativeCombatState> States
{
get
{
lock (gate)
{
return statesByPeer.Values
.Select(CloneState)
.ToArray();
}
}
}
public Task HandleShootInputAsync(byte[] payload, IPEndPoint sender)
{
if (payload == null || sender == null)
{
return Task.CompletedTask;
}
ShootInput input;
try
{
input = ShootInput.Parser.ParseFrom(payload);
}
catch
{
return Task.CompletedTask;
}
if (!TryValidateAcceptedShot(input, sender, out var attackerState, out var targetState))
{
BroadcastRejectedShot(input);
return Task.CompletedTask;
}
movementCoordinator.TryUpdateState(sender, state =>
{
state.LastAcceptedShootTick = input.Tick;
}, out attackerState);
var wasTargetDead = targetState.IsDead;
movementCoordinator.TryUpdateStateByPlayerId(targetState.PlayerId, state =>
{
state.Hp = Math.Max(0, state.Hp - configuration.DamagePerShot);
state.IsDead = state.Hp <= 0;
}, out var updatedTargetState);
targetState = updatedTargetState;
UpdateCombatState(attackerState, input.Tick, input.Tick);
UpdateCombatState(targetState, targetState.LastAcceptedShootTick, input.Tick);
var hitPosition = new Vector3
{
X = targetState.PositionX,
Y = targetState.PositionY,
Z = targetState.PositionZ
};
messageManager.BroadcastMessage(new CombatEvent
{
Tick = input.Tick,
EventType = CombatEventType.Hit,
AttackerId = attackerState.PlayerId,
TargetId = targetState.PlayerId,
Damage = 0,
HitPosition = hitPosition
}, MessageType.CombatEvent);
messageManager.BroadcastMessage(new CombatEvent
{
Tick = input.Tick,
EventType = CombatEventType.DamageApplied,
AttackerId = attackerState.PlayerId,
TargetId = targetState.PlayerId,
Damage = configuration.DamagePerShot,
HitPosition = hitPosition
}, MessageType.CombatEvent);
if (!wasTargetDead && targetState.IsDead)
{
messageManager.BroadcastMessage(new CombatEvent
{
Tick = input.Tick,
EventType = CombatEventType.Death,
AttackerId = attackerState.PlayerId,
TargetId = targetState.PlayerId,
Damage = 0,
HitPosition = hitPosition
}, MessageType.CombatEvent);
}
return Task.CompletedTask;
}
public bool TryGetState(IPEndPoint remoteEndPoint, out ServerAuthoritativeCombatState state)
{
var key = Normalize(remoteEndPoint).ToString();
lock (gate)
{
if (statesByPeer.TryGetValue(key, out var existingState))
{
state = CloneState(existingState);
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();
}
}
private bool TryValidateAcceptedShot(
ShootInput input,
IPEndPoint sender,
out ServerAuthoritativeMovementState attackerState,
out ServerAuthoritativeMovementState targetState)
{
attackerState = null;
targetState = null;
if (input == null ||
string.IsNullOrWhiteSpace(input.PlayerId) ||
string.IsNullOrWhiteSpace(input.TargetId) ||
string.Equals(input.PlayerId, input.TargetId, StringComparison.Ordinal) ||
!IsFinite(input.DirX) ||
!IsFinite(input.DirY))
{
return false;
}
var lengthSquared = (input.DirX * input.DirX) + (input.DirY * input.DirY);
if (lengthSquared <= 0f)
{
return false;
}
if (!movementCoordinator.TryGetState(sender, out attackerState) ||
!string.Equals(attackerState.PlayerId, input.PlayerId, StringComparison.Ordinal) ||
attackerState.IsDead ||
attackerState.Hp <= 0 ||
input.Tick <= attackerState.LastAcceptedShootTick)
{
return false;
}
if (!movementCoordinator.TryGetStateByPlayerId(input.TargetId, out targetState) ||
targetState.IsDead ||
targetState.Hp <= 0)
{
return false;
}
return true;
}
private void BroadcastRejectedShot(ShootInput input)
{
if (input == null)
{
return;
}
messageManager.BroadcastMessage(new CombatEvent
{
Tick = input.Tick,
EventType = CombatEventType.ShootRejected,
AttackerId = input.PlayerId ?? string.Empty,
TargetId = input.TargetId ?? string.Empty,
Damage = 0,
HitPosition = new Vector3()
}, MessageType.CombatEvent);
}
private void UpdateCombatState(ServerAuthoritativeMovementState movementState, long acceptedShootTick, long resolvedCombatTick)
{
if (movementState == null)
{
return;
}
var key = Normalize(movementState.RemoteEndPoint).ToString();
lock (gate)
{
if (!statesByPeer.TryGetValue(key, out var combatState))
{
combatState = new ServerAuthoritativeCombatState(
Normalize(movementState.RemoteEndPoint),
movementState.PlayerId,
movementState.Hp,
movementState.IsDead);
statesByPeer.Add(key, combatState);
}
combatState.LastAcceptedShootTick = Math.Max(combatState.LastAcceptedShootTick, acceptedShootTick);
combatState.LastResolvedCombatTick = Math.Max(combatState.LastResolvedCombatTick, resolvedCombatTick);
combatState.Hp = movementState.Hp;
combatState.IsDead = movementState.IsDead;
}
}
private static ServerAuthoritativeCombatState CloneState(ServerAuthoritativeCombatState state)
{
return new ServerAuthoritativeCombatState(state.RemoteEndPoint, state.PlayerId, state.Hp, state.IsDead)
{
LastAcceptedShootTick = state.LastAcceptedShootTick,
LastResolvedCombatTick = state.LastResolvedCombatTick
};
}
private static IPEndPoint Normalize(IPEndPoint remoteEndPoint)
{
if (remoteEndPoint == null)
{
throw new ArgumentNullException(nameof(remoteEndPoint));
}
return new IPEndPoint(remoteEndPoint.Address, remoteEndPoint.Port);
}
private static bool IsFinite(float value)
{
return !float.IsNaN(value) && !float.IsInfinity(value);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 443cfc0d6b4759241a79409adc1c4519
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,28 @@
using System;
using System.Net;
namespace Network.NetworkHost
{
public sealed class ServerAuthoritativeCombatState
{
public ServerAuthoritativeCombatState(IPEndPoint remoteEndPoint, string playerId, int hp, bool isDead)
{
RemoteEndPoint = remoteEndPoint ?? throw new ArgumentNullException(nameof(remoteEndPoint));
PlayerId = playerId ?? throw new ArgumentNullException(nameof(playerId));
Hp = hp;
IsDead = isDead;
}
public IPEndPoint RemoteEndPoint { get; }
public string PlayerId { get; }
public long LastAcceptedShootTick { get; internal set; }
public long LastResolvedCombatTick { get; internal set; }
public int Hp { get; internal set; }
public bool IsDead { get; internal set; }
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 753945348e8486a46a51b455ce01b95c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -154,6 +154,86 @@ namespace Network.NetworkHost
return false;
}
public bool TryGetStateByPlayerId(string playerId, out ServerAuthoritativeMovementState state)
{
if (string.IsNullOrWhiteSpace(playerId))
{
state = null;
return false;
}
lock (gate)
{
foreach (var candidate in statesByPeer.Values)
{
if (!string.Equals(candidate.PlayerId, playerId, StringComparison.Ordinal))
{
continue;
}
state = CloneState(candidate);
return true;
}
}
state = null;
return false;
}
public bool TryUpdateState(IPEndPoint remoteEndPoint, Action<ServerAuthoritativeMovementState> updater, out ServerAuthoritativeMovementState state)
{
if (updater == null)
{
throw new ArgumentNullException(nameof(updater));
}
var key = Normalize(remoteEndPoint).ToString();
lock (gate)
{
if (!statesByPeer.TryGetValue(key, out var currentState))
{
state = null;
return false;
}
updater(currentState);
state = CloneState(currentState);
return true;
}
}
public bool TryUpdateStateByPlayerId(string playerId, Action<ServerAuthoritativeMovementState> updater, out ServerAuthoritativeMovementState state)
{
if (updater == null)
{
throw new ArgumentNullException(nameof(updater));
}
if (string.IsNullOrWhiteSpace(playerId))
{
state = null;
return false;
}
lock (gate)
{
foreach (var currentState in statesByPeer.Values)
{
if (!string.Equals(currentState.PlayerId, playerId, StringComparison.Ordinal))
{
continue;
}
updater(currentState);
state = CloneState(currentState);
return true;
}
}
state = null;
return false;
}
public void RemoveState(IPEndPoint remoteEndPoint)
{
var key = Normalize(remoteEndPoint).ToString();
@@ -192,6 +272,7 @@ namespace Network.NetworkHost
return new ServerAuthoritativeMovementState(state.RemoteEndPoint, state.PlayerId, state.Hp)
{
LastAcceptedMoveTick = state.LastAcceptedMoveTick,
LastAcceptedShootTick = state.LastAcceptedShootTick,
LastBroadcastTick = state.LastBroadcastTick,
PositionX = state.PositionX,
PositionY = state.PositionY,
@@ -200,6 +281,7 @@ namespace Network.NetworkHost
VelocityY = state.VelocityY,
VelocityZ = state.VelocityZ,
Rotation = state.Rotation,
IsDead = state.IsDead,
InputX = state.InputX,
InputY = state.InputY
};
@@ -233,7 +315,7 @@ namespace Network.NetworkHost
private void IntegrateState(ServerAuthoritativeMovementState state, TimeSpan elapsed)
{
if (state.InputX == 0f && state.InputY == 0f)
if (state.IsDead || (state.InputX == 0f && state.InputY == 0f))
{
state.VelocityX = 0f;
state.VelocityY = 0f;
@@ -10,6 +10,7 @@ namespace Network.NetworkHost
RemoteEndPoint = remoteEndPoint ?? throw new ArgumentNullException(nameof(remoteEndPoint));
PlayerId = playerId ?? throw new ArgumentNullException(nameof(playerId));
Hp = hp;
IsDead = hp <= 0;
}
public IPEndPoint RemoteEndPoint { get; }
@@ -18,6 +19,8 @@ namespace Network.NetworkHost
public long LastAcceptedMoveTick { get; internal set; }
public long LastAcceptedShootTick { get; internal set; }
public long LastBroadcastTick { get; internal set; }
public float PositionX { get; internal set; }
@@ -36,6 +39,8 @@ namespace Network.NetworkHost
public int Hp { get; internal set; }
public bool IsDead { get; internal set; }
public float InputX { get; internal set; }
public float InputY { get; internal set; }
@@ -14,6 +14,7 @@ namespace Network.NetworkHost
private readonly ITransport syncTransport;
private readonly MessageManager messageManager;
private readonly ServerAuthoritativeMovementCoordinator authoritativeMovementCoordinator;
private readonly ServerAuthoritativeCombatCoordinator authoritativeCombatCoordinator;
public ServerNetworkHost(
ITransport transport,
@@ -23,7 +24,8 @@ namespace Network.NetworkHost
ITransport syncTransport = null,
IMessageDeliveryPolicyResolver deliveryPolicyResolver = null,
SyncSequenceTracker syncSequenceTracker = null,
ServerAuthoritativeMovementConfiguration authoritativeMovement = null)
ServerAuthoritativeMovementConfiguration authoritativeMovement = null,
ServerAuthoritativeCombatConfiguration authoritativeCombat = null)
{
this.transport = transport ?? throw new ArgumentNullException(nameof(transport));
this.syncTransport = syncTransport;
@@ -44,7 +46,12 @@ namespace Network.NetworkHost
this,
messageManager,
authoritativeMovement ?? new ServerAuthoritativeMovementConfiguration());
authoritativeCombatCoordinator = new ServerAuthoritativeCombatCoordinator(
messageManager,
authoritativeMovementCoordinator,
authoritativeCombat ?? new ServerAuthoritativeCombatConfiguration());
messageManager.RegisterHandler(MessageType.MoveInput, authoritativeMovementCoordinator.HandleMoveInputAsync);
messageManager.RegisterHandler(MessageType.ShootInput, authoritativeCombatCoordinator.HandleShootInputAsync);
}
public MessageManager MessageManager => messageManager;
@@ -59,6 +66,8 @@ namespace Network.NetworkHost
public IReadOnlyList<ServerAuthoritativeMovementState> AuthoritativeMovementStates => authoritativeMovementCoordinator.States;
public IReadOnlyList<ServerAuthoritativeCombatState> AuthoritativeCombatStates => authoritativeCombatCoordinator.States;
public event Action<MultiSessionLifecycleEvent> LifecycleChanged
{
add => SessionCoordinator.LifecycleChanged += value;
@@ -87,6 +96,7 @@ namespace Network.NetworkHost
SessionCoordinator.RemoveAllSessions("Transport stopped");
authoritativeMovementCoordinator.Clear();
authoritativeCombatCoordinator.Clear();
PublishMetricsSessionSnapshots();
}
@@ -116,6 +126,11 @@ namespace Network.NetworkHost
return authoritativeMovementCoordinator.TryGetState(remoteEndPoint, out state);
}
public bool TryGetAuthoritativeCombatState(IPEndPoint remoteEndPoint, out ServerAuthoritativeCombatState state)
{
return authoritativeCombatCoordinator.TryGetState(remoteEndPoint, out state);
}
public void NotifyLoginStarted(IPEndPoint remoteEndPoint)
{
SessionCoordinator.NotifyLoginStarted(remoteEndPoint);
@@ -172,6 +187,7 @@ namespace Network.NetworkHost
}
authoritativeMovementCoordinator.RemoveState(remoteEndPoint);
authoritativeCombatCoordinator.RemoveState(remoteEndPoint);
RecordMetricsSessionSnapshot(transport, "server-host", session, ConnectionState.Disconnected);
if (syncTransport != null && !ReferenceEquals(syncTransport, transport))
@@ -34,6 +34,8 @@ namespace Network.NetworkHost
public ServerAuthoritativeMovementConfiguration AuthoritativeMovement { get; set; }
public ServerAuthoritativeCombatConfiguration AuthoritativeCombat { get; set; }
internal void Validate()
{
if (ReliablePort <= 0)
@@ -55,6 +57,7 @@ namespace Network.NetworkHost
}
AuthoritativeMovement?.Validate();
AuthoritativeCombat?.Validate();
}
}
}
@@ -24,7 +24,8 @@ namespace Network.NetworkHost
configuration.DeliveryPolicyResolver,
configuration.SyncSequenceTracker,
configuration.TransportFactory,
configuration.AuthoritativeMovement);
configuration.AuthoritativeMovement,
configuration.AuthoritativeCombat);
try
{
@@ -25,6 +25,8 @@ namespace Network.NetworkHost
public IReadOnlyList<ServerAuthoritativeMovementState> AuthoritativeMovementStates => host.AuthoritativeMovementStates;
public IReadOnlyList<ServerAuthoritativeCombatState> AuthoritativeCombatStates => host.AuthoritativeCombatStates;
public event Action<MultiSessionLifecycleEvent> LifecycleChanged
{
add => host.LifecycleChanged += value;
@@ -56,6 +58,11 @@ namespace Network.NetworkHost
return host.TryGetAuthoritativeMovementState(remoteEndPoint, out state);
}
public bool TryGetAuthoritativeCombatState(IPEndPoint remoteEndPoint, out ServerAuthoritativeCombatState state)
{
return host.TryGetAuthoritativeCombatState(remoteEndPoint, out state);
}
public void Stop()
{
if (isStopped)