修改操作方式为 坦克式运动
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
|
||||
namespace Network.NetworkHost
|
||||
{
|
||||
public sealed class AuthoritativeMovementWorldValidationRequest
|
||||
{
|
||||
public AuthoritativeMovementWorldValidationRequest(
|
||||
IPEndPoint remoteEndPoint,
|
||||
string playerId,
|
||||
float currentPositionX,
|
||||
float currentPositionY,
|
||||
float currentPositionZ,
|
||||
float candidatePositionX,
|
||||
float candidatePositionY,
|
||||
float candidatePositionZ,
|
||||
float velocityX,
|
||||
float velocityY,
|
||||
float velocityZ)
|
||||
{
|
||||
RemoteEndPoint = remoteEndPoint ?? throw new ArgumentNullException(nameof(remoteEndPoint));
|
||||
PlayerId = playerId ?? throw new ArgumentNullException(nameof(playerId));
|
||||
CurrentPositionX = currentPositionX;
|
||||
CurrentPositionY = currentPositionY;
|
||||
CurrentPositionZ = currentPositionZ;
|
||||
CandidatePositionX = candidatePositionX;
|
||||
CandidatePositionY = candidatePositionY;
|
||||
CandidatePositionZ = candidatePositionZ;
|
||||
VelocityX = velocityX;
|
||||
VelocityY = velocityY;
|
||||
VelocityZ = velocityZ;
|
||||
}
|
||||
|
||||
public IPEndPoint RemoteEndPoint { get; }
|
||||
|
||||
public string PlayerId { get; }
|
||||
|
||||
public float CurrentPositionX { get; }
|
||||
|
||||
public float CurrentPositionY { get; }
|
||||
|
||||
public float CurrentPositionZ { get; }
|
||||
|
||||
public float CandidatePositionX { get; }
|
||||
|
||||
public float CandidatePositionY { get; }
|
||||
|
||||
public float CandidatePositionZ { get; }
|
||||
|
||||
public float VelocityX { get; }
|
||||
|
||||
public float VelocityY { get; }
|
||||
|
||||
public float VelocityZ { get; }
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 44fc9b6a7ea518c419516af9014d6a46
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace Network.NetworkHost
|
||||
{
|
||||
public readonly struct AuthoritativeMovementWorldValidationResult
|
||||
{
|
||||
private AuthoritativeMovementWorldValidationResult(bool isAllowed)
|
||||
{
|
||||
IsAllowed = isAllowed;
|
||||
}
|
||||
|
||||
public bool IsAllowed { get; }
|
||||
|
||||
public static AuthoritativeMovementWorldValidationResult Allow()
|
||||
{
|
||||
return new AuthoritativeMovementWorldValidationResult(true);
|
||||
}
|
||||
|
||||
public static AuthoritativeMovementWorldValidationResult Reject()
|
||||
{
|
||||
return new AuthoritativeMovementWorldValidationResult(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4838ecf8df980f8498d225a15ab84df3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Network.NetworkHost
|
||||
{
|
||||
public interface IAuthoritativeMovementWorldValidator
|
||||
{
|
||||
AuthoritativeMovementWorldValidationResult Validate(AuthoritativeMovementWorldValidationRequest request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b6ae924c07d515d44b00298c08c121b2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,23 @@
|
||||
using System;
|
||||
|
||||
namespace Network.NetworkHost
|
||||
{
|
||||
public sealed class PermissiveAuthoritativeMovementWorldValidator : IAuthoritativeMovementWorldValidator
|
||||
{
|
||||
private PermissiveAuthoritativeMovementWorldValidator()
|
||||
{
|
||||
}
|
||||
|
||||
public static PermissiveAuthoritativeMovementWorldValidator Instance { get; } = new PermissiveAuthoritativeMovementWorldValidator();
|
||||
|
||||
public AuthoritativeMovementWorldValidationResult Validate(AuthoritativeMovementWorldValidationRequest request)
|
||||
{
|
||||
if (request == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
}
|
||||
|
||||
return AuthoritativeMovementWorldValidationResult.Allow();
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 251269ca4373a134b955c8a2a5786f95
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -11,16 +11,19 @@ namespace Network.NetworkHost
|
||||
internal sealed class ServerAuthoritativeCombatCoordinator
|
||||
{
|
||||
private readonly object gate = new();
|
||||
private readonly ServerNetworkHost host;
|
||||
private readonly MessageManager messageManager;
|
||||
private readonly ServerAuthoritativeMovementCoordinator movementCoordinator;
|
||||
private readonly Dictionary<string, ServerAuthoritativeCombatState> statesByPeer = new();
|
||||
private readonly ServerAuthoritativeCombatConfiguration configuration;
|
||||
|
||||
public ServerAuthoritativeCombatCoordinator(
|
||||
ServerNetworkHost host,
|
||||
MessageManager messageManager,
|
||||
ServerAuthoritativeMovementCoordinator movementCoordinator,
|
||||
ServerAuthoritativeCombatConfiguration configuration)
|
||||
{
|
||||
this.host = host ?? throw new ArgumentNullException(nameof(host));
|
||||
this.messageManager = messageManager ?? throw new ArgumentNullException(nameof(messageManager));
|
||||
this.movementCoordinator = movementCoordinator ?? throw new ArgumentNullException(nameof(movementCoordinator));
|
||||
this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
|
||||
@@ -56,13 +59,13 @@ namespace Network.NetworkHost
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
if (!TryValidateAcceptedShot(input, sender, out var attackerState, out var targetState))
|
||||
if (!TryValidateAcceptedShot(input, sender, out var acceptedPeer, out var attackerState, out var targetState))
|
||||
{
|
||||
BroadcastRejectedShot(input);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
movementCoordinator.TryUpdateState(sender, state =>
|
||||
movementCoordinator.TryUpdateState(acceptedPeer, state =>
|
||||
{
|
||||
state.LastAcceptedShootTick = input.Tick;
|
||||
}, out attackerState);
|
||||
@@ -85,6 +88,7 @@ namespace Network.NetworkHost
|
||||
Z = targetState.PositionZ
|
||||
};
|
||||
|
||||
// Keep the coordinator as the only gameplay-relevant authoritative combat-result emitter.
|
||||
messageManager.BroadcastMessage(new CombatEvent
|
||||
{
|
||||
Tick = input.Tick,
|
||||
@@ -157,16 +161,19 @@ namespace Network.NetworkHost
|
||||
private bool TryValidateAcceptedShot(
|
||||
ShootInput input,
|
||||
IPEndPoint sender,
|
||||
out IPEndPoint acceptedPeer,
|
||||
out ServerAuthoritativeMovementState attackerState,
|
||||
out ServerAuthoritativeMovementState targetState)
|
||||
{
|
||||
acceptedPeer = null;
|
||||
attackerState = null;
|
||||
targetState = null;
|
||||
|
||||
if (input == null ||
|
||||
string.IsNullOrWhiteSpace(input.PlayerId) ||
|
||||
!IsFinite(input.DirX) ||
|
||||
!IsFinite(input.DirY))
|
||||
!IsFinite(input.DirY) ||
|
||||
!host.TryResolveAcceptedPeer(sender, input.PlayerId, out acceptedPeer))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -177,8 +184,8 @@ namespace Network.NetworkHost
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!movementCoordinator.TryGetState(sender, out attackerState) &&
|
||||
!movementCoordinator.EnsureState(sender, input.PlayerId, out attackerState))
|
||||
if (!movementCoordinator.TryGetState(acceptedPeer, out attackerState) &&
|
||||
!movementCoordinator.EnsureState(acceptedPeer, input.PlayerId, out attackerState))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -191,7 +198,12 @@ namespace Network.NetworkHost
|
||||
return false;
|
||||
}
|
||||
|
||||
return TryResolveTargetState(input, attackerState, out targetState);
|
||||
if (!TryResolveTargetState(input, attackerState, out targetState))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return host.TryRefreshAcceptedGameplayActivity(sender, input.PlayerId);
|
||||
}
|
||||
|
||||
private bool TryResolveTargetState(
|
||||
|
||||
@@ -6,6 +6,8 @@ namespace Network.NetworkHost
|
||||
{
|
||||
public float MoveSpeed { get; set; } = 5f;
|
||||
|
||||
public float TurnSpeedDegreesPerSecond { get; set; } = 180f;
|
||||
|
||||
public TimeSpan BroadcastInterval { get; set; } = TimeSpan.FromMilliseconds(50);
|
||||
|
||||
public int DefaultHp { get; set; } = 100;
|
||||
@@ -17,6 +19,11 @@ namespace Network.NetworkHost
|
||||
throw new ArgumentOutOfRangeException(nameof(MoveSpeed), "Move speed must be finite and non-negative.");
|
||||
}
|
||||
|
||||
if (float.IsNaN(TurnSpeedDegreesPerSecond) || float.IsInfinity(TurnSpeedDegreesPerSecond) || TurnSpeedDegreesPerSecond < 0f)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(TurnSpeedDegreesPerSecond), "Turn speed must be finite and non-negative.");
|
||||
}
|
||||
|
||||
if (BroadcastInterval <= TimeSpan.Zero)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(BroadcastInterval), "Broadcast interval must be positive.");
|
||||
|
||||
@@ -14,6 +14,7 @@ namespace Network.NetworkHost
|
||||
private readonly MessageManager messageManager;
|
||||
private readonly ServerNetworkHost host;
|
||||
private readonly ServerAuthoritativeMovementConfiguration configuration;
|
||||
private readonly IAuthoritativeMovementWorldValidator worldValidator;
|
||||
private readonly Dictionary<string, ServerAuthoritativeMovementState> statesByPeer = new();
|
||||
private long nextBroadcastTick = 1;
|
||||
private TimeSpan accumulatedBroadcastTime;
|
||||
@@ -21,11 +22,13 @@ namespace Network.NetworkHost
|
||||
public ServerAuthoritativeMovementCoordinator(
|
||||
ServerNetworkHost host,
|
||||
MessageManager messageManager,
|
||||
ServerAuthoritativeMovementConfiguration configuration)
|
||||
ServerAuthoritativeMovementConfiguration configuration,
|
||||
IAuthoritativeMovementWorldValidator worldValidator)
|
||||
{
|
||||
this.host = host ?? throw new ArgumentNullException(nameof(host));
|
||||
this.messageManager = messageManager ?? throw new ArgumentNullException(nameof(messageManager));
|
||||
this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
|
||||
this.worldValidator = worldValidator ?? throw new ArgumentNullException(nameof(worldValidator));
|
||||
}
|
||||
|
||||
public IReadOnlyList<ServerAuthoritativeMovementState> States
|
||||
@@ -48,7 +51,7 @@ namespace Network.NetworkHost
|
||||
throw new ArgumentNullException(nameof(remoteEndPoint));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(playerId))
|
||||
if (string.IsNullOrWhiteSpace(playerId) || !host.IsAcceptedPlayer(remoteEndPoint, playerId))
|
||||
{
|
||||
state = null;
|
||||
return false;
|
||||
@@ -99,13 +102,14 @@ namespace Network.NetworkHost
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(input.PlayerId) ||
|
||||
!IsFinite(input.MoveX) ||
|
||||
!IsFinite(input.MoveY))
|
||||
!IsFinite(input.TurnInput) ||
|
||||
!IsFinite(input.ThrottleInput) ||
|
||||
!host.TryResolveAcceptedPeer(sender, input.PlayerId, out var acceptedPeer))
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
var normalizedSender = Normalize(sender);
|
||||
var normalizedSender = Normalize(acceptedPeer);
|
||||
var key = normalizedSender.ToString();
|
||||
|
||||
lock (gate)
|
||||
@@ -113,7 +117,8 @@ namespace Network.NetworkHost
|
||||
if (statesByPeer.TryGetValue(key, out var existingState))
|
||||
{
|
||||
if (!string.Equals(existingState.PlayerId, input.PlayerId, StringComparison.Ordinal) ||
|
||||
input.Tick <= existingState.LastAcceptedMoveTick)
|
||||
input.Tick <= existingState.LastAcceptedMoveTick ||
|
||||
!host.TryRefreshAcceptedGameplayActivity(sender, input.PlayerId))
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
@@ -122,6 +127,11 @@ namespace Network.NetworkHost
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
if (!host.TryRefreshAcceptedGameplayActivity(sender, input.PlayerId))
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
var state = new ServerAuthoritativeMovementState(
|
||||
normalizedSender,
|
||||
input.PlayerId,
|
||||
@@ -172,6 +182,7 @@ namespace Network.NetworkHost
|
||||
|
||||
foreach (var pendingBroadcast in pendingBroadcasts)
|
||||
{
|
||||
// Keep the coordinator as the only gameplay-relevant PlayerState broadcast path.
|
||||
messageManager.BroadcastMessage(pendingBroadcast.PlayerState, MessageType.PlayerState);
|
||||
host.ObserveAuthoritativeState(pendingBroadcast.RemoteEndPoint, pendingBroadcast.PlayerState.Tick);
|
||||
}
|
||||
@@ -330,32 +341,20 @@ namespace Network.NetworkHost
|
||||
private static void ApplyInput(ServerAuthoritativeMovementState state, MoveInput input)
|
||||
{
|
||||
state.LastAcceptedMoveTick = input.Tick;
|
||||
state.InputX = input.MoveX;
|
||||
state.InputY = input.MoveY;
|
||||
state.InputX = ClampInput(input.TurnInput);
|
||||
state.InputY = ClampInput(input.ThrottleInput);
|
||||
|
||||
if (input.MoveX == 0f && input.MoveY == 0f)
|
||||
if (state.InputY == 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.IsDead || (state.InputX == 0f && state.InputY == 0f))
|
||||
if (state.IsDead)
|
||||
{
|
||||
state.VelocityX = 0f;
|
||||
state.VelocityY = 0f;
|
||||
@@ -363,25 +362,83 @@ namespace Network.NetworkHost
|
||||
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;
|
||||
if (deltaSeconds <= 0f)
|
||||
{
|
||||
state.VelocityX = 0f;
|
||||
state.VelocityY = 0f;
|
||||
state.VelocityZ = 0f;
|
||||
return;
|
||||
}
|
||||
|
||||
var turnInput = ClampInput(state.InputX);
|
||||
var throttleInput = ClampInput(state.InputY);
|
||||
if (turnInput != 0f)
|
||||
{
|
||||
state.Rotation = NormalizeDegrees(state.Rotation + (turnInput * configuration.TurnSpeedDegreesPerSecond * deltaSeconds));
|
||||
}
|
||||
|
||||
if (throttleInput == 0f)
|
||||
{
|
||||
state.VelocityX = 0f;
|
||||
state.VelocityY = 0f;
|
||||
state.VelocityZ = 0f;
|
||||
return;
|
||||
}
|
||||
|
||||
var rotationRadians = state.Rotation * (MathF.PI / 180f);
|
||||
var forwardX = MathF.Cos(rotationRadians);
|
||||
var forwardZ = MathF.Sin(rotationRadians);
|
||||
state.VelocityX = forwardX * (throttleInput * configuration.MoveSpeed);
|
||||
state.VelocityY = 0f;
|
||||
state.VelocityZ = forwardZ * (throttleInput * configuration.MoveSpeed);
|
||||
|
||||
var candidatePositionX = state.PositionX + (state.VelocityX * deltaSeconds);
|
||||
var candidatePositionY = state.PositionY + (state.VelocityY * deltaSeconds);
|
||||
var candidatePositionZ = state.PositionZ + (state.VelocityZ * deltaSeconds);
|
||||
var validationResult = worldValidator.Validate(new AuthoritativeMovementWorldValidationRequest(
|
||||
state.RemoteEndPoint,
|
||||
state.PlayerId,
|
||||
state.PositionX,
|
||||
state.PositionY,
|
||||
state.PositionZ,
|
||||
candidatePositionX,
|
||||
candidatePositionY,
|
||||
candidatePositionZ,
|
||||
state.VelocityX,
|
||||
state.VelocityY,
|
||||
state.VelocityZ));
|
||||
if (!validationResult.IsAllowed)
|
||||
{
|
||||
state.VelocityX = 0f;
|
||||
state.VelocityY = 0f;
|
||||
state.VelocityZ = 0f;
|
||||
return;
|
||||
}
|
||||
|
||||
state.PositionX = candidatePositionX;
|
||||
state.PositionY = candidatePositionY;
|
||||
state.PositionZ = candidatePositionZ;
|
||||
}
|
||||
|
||||
private static float ClampInput(float value)
|
||||
{
|
||||
return MathF.Max(-1f, MathF.Min(1f, value));
|
||||
}
|
||||
|
||||
private static float NormalizeDegrees(float degrees)
|
||||
{
|
||||
var normalized = degrees % 360f;
|
||||
if (normalized <= -180f)
|
||||
{
|
||||
normalized += 360f;
|
||||
}
|
||||
else if (normalized > 180f)
|
||||
{
|
||||
normalized -= 360f;
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private static PlayerState BuildPlayerState(ServerAuthoritativeMovementState state, long tick)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using Network.Defines;
|
||||
@@ -17,6 +18,8 @@ namespace Network.NetworkHost
|
||||
private readonly ServerAuthoritativeCombatCoordinator authoritativeCombatCoordinator;
|
||||
private readonly object playerIdentityGate = new();
|
||||
private readonly Dictionary<string, string> playerIdsByPeer = new();
|
||||
private readonly Dictionary<string, IPEndPoint> canonicalPeersByPlayerId = new(StringComparer.Ordinal);
|
||||
private readonly Dictionary<string, HashSet<string>> peerKeysByPlayerId = new(StringComparer.Ordinal);
|
||||
|
||||
public ServerNetworkHost(
|
||||
ITransport transport,
|
||||
@@ -27,7 +30,8 @@ namespace Network.NetworkHost
|
||||
IMessageDeliveryPolicyResolver deliveryPolicyResolver = null,
|
||||
SyncSequenceTracker syncSequenceTracker = null,
|
||||
ServerAuthoritativeMovementConfiguration authoritativeMovement = null,
|
||||
ServerAuthoritativeCombatConfiguration authoritativeCombat = null)
|
||||
ServerAuthoritativeCombatConfiguration authoritativeCombat = null,
|
||||
IAuthoritativeMovementWorldValidator authoritativeMovementWorldValidator = null)
|
||||
{
|
||||
this.transport = transport ?? throw new ArgumentNullException(nameof(transport));
|
||||
this.syncTransport = syncTransport;
|
||||
@@ -44,11 +48,14 @@ namespace Network.NetworkHost
|
||||
deliveryPolicyResolver ?? new DefaultMessageDeliveryPolicyResolver(),
|
||||
this.syncTransport,
|
||||
syncSequenceTracker ?? new SyncSequenceTracker());
|
||||
var resolvedWorldValidator = authoritativeMovementWorldValidator ?? PermissiveAuthoritativeMovementWorldValidator.Instance;
|
||||
authoritativeMovementCoordinator = new ServerAuthoritativeMovementCoordinator(
|
||||
this,
|
||||
messageManager,
|
||||
authoritativeMovement ?? new ServerAuthoritativeMovementConfiguration());
|
||||
authoritativeMovement ?? new ServerAuthoritativeMovementConfiguration(),
|
||||
resolvedWorldValidator);
|
||||
authoritativeCombatCoordinator = new ServerAuthoritativeCombatCoordinator(
|
||||
this,
|
||||
messageManager,
|
||||
authoritativeMovementCoordinator,
|
||||
authoritativeCombat ?? new ServerAuthoritativeCombatConfiguration());
|
||||
@@ -102,6 +109,8 @@ namespace Network.NetworkHost
|
||||
lock (playerIdentityGate)
|
||||
{
|
||||
playerIdsByPeer.Clear();
|
||||
canonicalPeersByPlayerId.Clear();
|
||||
peerKeysByPlayerId.Clear();
|
||||
}
|
||||
PublishMetricsSessionSnapshots();
|
||||
}
|
||||
@@ -137,6 +146,117 @@ namespace Network.NetworkHost
|
||||
return authoritativeCombatCoordinator.TryGetState(remoteEndPoint, out state);
|
||||
}
|
||||
|
||||
public bool TryGetAcceptedPlayerId(IPEndPoint remoteEndPoint, out string playerId)
|
||||
{
|
||||
return TryGetKnownPlayerId(remoteEndPoint, out playerId);
|
||||
}
|
||||
|
||||
public bool IsAcceptedPlayer(IPEndPoint remoteEndPoint, string playerId)
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(playerId) &&
|
||||
TryGetKnownPlayerId(remoteEndPoint, out var acceptedPlayerId) &&
|
||||
string.Equals(acceptedPlayerId, playerId, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
public bool TryResolveAcceptedPeer(IPEndPoint remoteEndPoint, string playerId, out IPEndPoint acceptedPeer)
|
||||
{
|
||||
acceptedPeer = null;
|
||||
if (remoteEndPoint == null || string.IsNullOrWhiteSpace(playerId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var normalizedRemoteEndPoint = Normalize(remoteEndPoint);
|
||||
var remoteKey = normalizedRemoteEndPoint.ToString();
|
||||
|
||||
lock (playerIdentityGate)
|
||||
{
|
||||
if (playerIdsByPeer.TryGetValue(remoteKey, out var mappedPlayerId))
|
||||
{
|
||||
if (!string.Equals(mappedPlayerId, playerId, StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!canonicalPeersByPlayerId.TryGetValue(playerId, out acceptedPeer))
|
||||
{
|
||||
acceptedPeer = normalizedRemoteEndPoint;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!canonicalPeersByPlayerId.TryGetValue(playerId, out acceptedPeer))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
playerIdsByPeer[remoteKey] = playerId;
|
||||
if (!peerKeysByPlayerId.TryGetValue(playerId, out var peerKeys))
|
||||
{
|
||||
peerKeys = new HashSet<string>(StringComparer.Ordinal);
|
||||
peerKeysByPlayerId[playerId] = peerKeys;
|
||||
}
|
||||
|
||||
peerKeys.Add(remoteKey);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsPlayerIdInUse(string playerId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(playerId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
lock (playerIdentityGate)
|
||||
{
|
||||
foreach (var acceptedPlayerId in playerIdsByPeer.Values)
|
||||
{
|
||||
if (string.Equals(acceptedPlayerId, playerId, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var state in authoritativeMovementCoordinator.States)
|
||||
{
|
||||
if (string.Equals(state.PlayerId, playerId, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var state in authoritativeCombatCoordinator.States)
|
||||
{
|
||||
if (string.Equals(state.PlayerId, playerId, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryRefreshAcceptedGameplayActivity(IPEndPoint remoteEndPoint, string playerId)
|
||||
{
|
||||
if (!TryResolveAcceptedPeer(remoteEndPoint, playerId, out var acceptedPeer))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryGetSession(acceptedPeer, out var session) ||
|
||||
session.SessionManager.State != ConnectionState.LoggedIn)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
NotifyInboundActivity(acceptedPeer);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void NotifyLoginStarted(IPEndPoint remoteEndPoint)
|
||||
{
|
||||
SessionCoordinator.NotifyLoginStarted(remoteEndPoint);
|
||||
@@ -159,7 +279,7 @@ namespace Network.NetworkHost
|
||||
public void NotifyLoginFailed(IPEndPoint remoteEndPoint, string reason = null)
|
||||
{
|
||||
SessionCoordinator.NotifyLoginFailed(remoteEndPoint, reason);
|
||||
ForgetPlayerId(remoteEndPoint);
|
||||
ForgetPeerIdentity(remoteEndPoint);
|
||||
PublishMetricsSessionSnapshot(remoteEndPoint);
|
||||
}
|
||||
|
||||
@@ -189,20 +309,34 @@ namespace Network.NetworkHost
|
||||
|
||||
public bool RemoveSession(IPEndPoint remoteEndPoint, string reason = null)
|
||||
{
|
||||
if (!SessionCoordinator.TryGetSession(remoteEndPoint, out var session))
|
||||
if (!TryGetKnownPlayerId(remoteEndPoint, out var playerId) ||
|
||||
!TryResolveAcceptedPeer(remoteEndPoint, playerId, out var acceptedPeer) ||
|
||||
!SessionCoordinator.TryGetSession(acceptedPeer, out var session))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var removed = SessionCoordinator.RemoveSession(remoteEndPoint, reason);
|
||||
var removed = SessionCoordinator.RemoveSession(acceptedPeer, reason);
|
||||
if (!removed)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
authoritativeMovementCoordinator.RemoveState(remoteEndPoint);
|
||||
authoritativeCombatCoordinator.RemoveState(remoteEndPoint);
|
||||
ForgetPlayerId(remoteEndPoint);
|
||||
authoritativeMovementCoordinator.RemoveState(acceptedPeer);
|
||||
authoritativeCombatCoordinator.RemoveState(acceptedPeer);
|
||||
|
||||
var knownPeerEndpoints = GetKnownPeerEndpointsForPlayerId(playerId);
|
||||
ForgetPlayerId(playerId);
|
||||
|
||||
foreach (var peerEndPoint in knownPeerEndpoints)
|
||||
{
|
||||
SessionCoordinator.RemoveSession(peerEndPoint, reason);
|
||||
RemoveTransportPeerSession(transport, peerEndPoint);
|
||||
if (syncTransport != null && !ReferenceEquals(syncTransport, transport))
|
||||
{
|
||||
RemoveTransportPeerSession(syncTransport, peerEndPoint);
|
||||
}
|
||||
}
|
||||
|
||||
RecordMetricsSessionSnapshot(transport, "server-host", session, ConnectionState.Disconnected);
|
||||
if (syncTransport != null && !ReferenceEquals(syncTransport, transport))
|
||||
@@ -216,10 +350,17 @@ namespace Network.NetworkHost
|
||||
private void HandleTransportReceive(byte[] data, IPEndPoint sender)
|
||||
{
|
||||
SessionCoordinator.ObserveTransportActivity(sender);
|
||||
ObservePlayerIdentity(data, sender);
|
||||
PublishMetricsSessionSnapshot(sender);
|
||||
}
|
||||
|
||||
private static void RemoveTransportPeerSession(ITransport transport, IPEndPoint remoteEndPoint)
|
||||
{
|
||||
if (transport is IPeerSessionTransport peerSessionTransport)
|
||||
{
|
||||
peerSessionTransport.RemovePeerSession(remoteEndPoint);
|
||||
}
|
||||
}
|
||||
|
||||
private void BootstrapAuthoritativeMovementState(IPEndPoint remoteEndPoint)
|
||||
{
|
||||
if (!TryGetKnownPlayerId(remoteEndPoint, out var playerId))
|
||||
@@ -230,41 +371,6 @@ namespace Network.NetworkHost
|
||||
authoritativeMovementCoordinator.EnsureState(remoteEndPoint, playerId, out _);
|
||||
}
|
||||
|
||||
private void ObservePlayerIdentity(byte[] data, IPEndPoint sender)
|
||||
{
|
||||
if (data == null || sender == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Envelope envelope;
|
||||
try
|
||||
{
|
||||
envelope = Envelope.Parser.ParseFrom(data);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if ((MessageType)envelope.Type != MessageType.LoginRequest)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
LoginRequest request;
|
||||
try
|
||||
{
|
||||
request = LoginRequest.Parser.ParseFrom(envelope.Payload);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
RememberPlayerId(sender, request.PlayerId);
|
||||
}
|
||||
|
||||
private void RememberPlayerId(IPEndPoint remoteEndPoint, string playerId)
|
||||
{
|
||||
if (remoteEndPoint == null || string.IsNullOrWhiteSpace(playerId))
|
||||
@@ -272,10 +378,19 @@ namespace Network.NetworkHost
|
||||
return;
|
||||
}
|
||||
|
||||
var key = Normalize(remoteEndPoint).ToString();
|
||||
var normalizedRemoteEndPoint = Normalize(remoteEndPoint);
|
||||
var key = normalizedRemoteEndPoint.ToString();
|
||||
lock (playerIdentityGate)
|
||||
{
|
||||
playerIdsByPeer[key] = playerId;
|
||||
canonicalPeersByPlayerId[playerId] = normalizedRemoteEndPoint;
|
||||
if (!peerKeysByPlayerId.TryGetValue(playerId, out var peerKeys))
|
||||
{
|
||||
peerKeys = new HashSet<string>(StringComparer.Ordinal);
|
||||
peerKeysByPlayerId[playerId] = peerKeys;
|
||||
}
|
||||
|
||||
peerKeys.Add(key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,7 +409,51 @@ namespace Network.NetworkHost
|
||||
}
|
||||
}
|
||||
|
||||
private void ForgetPlayerId(IPEndPoint remoteEndPoint)
|
||||
private IReadOnlyList<IPEndPoint> GetKnownPeerEndpointsForPlayerId(string playerId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(playerId))
|
||||
{
|
||||
return Array.Empty<IPEndPoint>();
|
||||
}
|
||||
|
||||
lock (playerIdentityGate)
|
||||
{
|
||||
if (!peerKeysByPlayerId.TryGetValue(playerId, out var peerKeys))
|
||||
{
|
||||
return Array.Empty<IPEndPoint>();
|
||||
}
|
||||
|
||||
return peerKeys
|
||||
.Select(ParseEndPoint)
|
||||
.Where(static endpoint => endpoint != null)
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
private void ForgetPlayerId(string playerId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(playerId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (playerIdentityGate)
|
||||
{
|
||||
if (peerKeysByPlayerId.TryGetValue(playerId, out var peerKeys))
|
||||
{
|
||||
foreach (var peerKey in peerKeys)
|
||||
{
|
||||
playerIdsByPeer.Remove(peerKey);
|
||||
}
|
||||
|
||||
peerKeysByPlayerId.Remove(playerId);
|
||||
}
|
||||
|
||||
canonicalPeersByPlayerId.Remove(playerId);
|
||||
}
|
||||
}
|
||||
|
||||
private void ForgetPeerIdentity(IPEndPoint remoteEndPoint)
|
||||
{
|
||||
if (remoteEndPoint == null)
|
||||
{
|
||||
@@ -304,10 +463,49 @@ namespace Network.NetworkHost
|
||||
var key = Normalize(remoteEndPoint).ToString();
|
||||
lock (playerIdentityGate)
|
||||
{
|
||||
if (!playerIdsByPeer.TryGetValue(key, out var playerId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
playerIdsByPeer.Remove(key);
|
||||
if (peerKeysByPlayerId.TryGetValue(playerId, out var peerKeys))
|
||||
{
|
||||
peerKeys.Remove(key);
|
||||
if (peerKeys.Count == 0)
|
||||
{
|
||||
peerKeysByPlayerId.Remove(playerId);
|
||||
canonicalPeersByPlayerId.Remove(playerId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static IPEndPoint ParseEndPoint(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var lastColonIndex = value.LastIndexOf(':');
|
||||
if (lastColonIndex <= 0 || lastColonIndex >= value.Length - 1)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var addressText = value.Substring(0, lastColonIndex);
|
||||
if (addressText.Length > 1 && addressText[0] == '[' && addressText[addressText.Length - 1] == ']')
|
||||
{
|
||||
addressText = addressText.Substring(1, addressText.Length - 2);
|
||||
}
|
||||
|
||||
return IPAddress.TryParse(addressText, out var address) &&
|
||||
int.TryParse(value.Substring(lastColonIndex + 1), out var port)
|
||||
? new IPEndPoint(address, port)
|
||||
: null;
|
||||
}
|
||||
|
||||
private static IPEndPoint Normalize(IPEndPoint remoteEndPoint)
|
||||
{
|
||||
if (remoteEndPoint == null)
|
||||
|
||||
@@ -14,6 +14,7 @@ namespace Network.NetworkHost
|
||||
}
|
||||
|
||||
ReliablePort = reliablePort;
|
||||
AuthoritativeMovementWorldValidator = PermissiveAuthoritativeMovementWorldValidator.Instance;
|
||||
}
|
||||
|
||||
public int ReliablePort { get; }
|
||||
@@ -36,6 +37,8 @@ namespace Network.NetworkHost
|
||||
|
||||
public ServerAuthoritativeCombatConfiguration AuthoritativeCombat { get; set; }
|
||||
|
||||
public IAuthoritativeMovementWorldValidator AuthoritativeMovementWorldValidator { get; set; }
|
||||
|
||||
internal void Validate()
|
||||
{
|
||||
if (ReliablePort <= 0)
|
||||
@@ -58,6 +61,10 @@ namespace Network.NetworkHost
|
||||
|
||||
AuthoritativeMovement?.Validate();
|
||||
AuthoritativeCombat?.Validate();
|
||||
if (AuthoritativeMovementWorldValidator == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(AuthoritativeMovementWorldValidator));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,8 @@ namespace Network.NetworkHost
|
||||
configuration.SyncSequenceTracker,
|
||||
configuration.TransportFactory,
|
||||
configuration.AuthoritativeMovement,
|
||||
configuration.AuthoritativeCombat);
|
||||
configuration.AuthoritativeCombat,
|
||||
configuration.AuthoritativeMovementWorldValidator);
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user