收口 Client 端,添加服务端约束
This commit is contained in:
@@ -29,6 +29,24 @@ public static class ClientGameplayInputFlow
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool TryCreateShootInput(
|
||||
string playerId,
|
||||
long tick,
|
||||
bool fireTriggered,
|
||||
Vector3 aimDirection,
|
||||
out ShootInput message,
|
||||
string targetId = "")
|
||||
{
|
||||
if (!fireTriggered)
|
||||
{
|
||||
message = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
message = CreateShootInput(playerId, tick, aimDirection, targetId);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static ShootInput CreateShootInput(string playerId, long tick, Vector3 aimDirection, string targetId = "")
|
||||
{
|
||||
var planarDirection = new Vector3(aimDirection.x, 0f, aimDirection.z);
|
||||
@@ -210,12 +228,14 @@ public class MovementComponent : MonoBehaviour
|
||||
|
||||
private ShootInput CaptureShootInput()
|
||||
{
|
||||
if (!Input.GetMouseButtonDown(0))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return ClientGameplayInputFlow.CreateShootInput(_master.PlayerId, Tick, ResolveAimDirection());
|
||||
return ClientGameplayInputFlow.TryCreateShootInput(
|
||||
_master.PlayerId,
|
||||
Tick,
|
||||
Input.GetMouseButtonDown(0),
|
||||
ResolveAimDirection(),
|
||||
out var shootInput)
|
||||
? shootInput
|
||||
: null;
|
||||
}
|
||||
|
||||
private Vector3 ResolveAimDirection()
|
||||
|
||||
@@ -165,8 +165,6 @@ namespace Network.NetworkHost
|
||||
|
||||
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))
|
||||
{
|
||||
@@ -179,8 +177,13 @@ namespace Network.NetworkHost
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!movementCoordinator.TryGetState(sender, out attackerState) ||
|
||||
!string.Equals(attackerState.PlayerId, input.PlayerId, StringComparison.Ordinal) ||
|
||||
if (!movementCoordinator.TryGetState(sender, out attackerState) &&
|
||||
!movementCoordinator.EnsureState(sender, input.PlayerId, out attackerState))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!string.Equals(attackerState.PlayerId, input.PlayerId, StringComparison.Ordinal) ||
|
||||
attackerState.IsDead ||
|
||||
attackerState.Hp <= 0 ||
|
||||
input.Tick <= attackerState.LastAcceptedShootTick)
|
||||
@@ -188,14 +191,93 @@ namespace Network.NetworkHost
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!movementCoordinator.TryGetStateByPlayerId(input.TargetId, out targetState) ||
|
||||
targetState.IsDead ||
|
||||
targetState.Hp <= 0)
|
||||
return TryResolveTargetState(input, attackerState, out targetState);
|
||||
}
|
||||
|
||||
private bool TryResolveTargetState(
|
||||
ShootInput input,
|
||||
ServerAuthoritativeMovementState attackerState,
|
||||
out ServerAuthoritativeMovementState targetState)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(input.TargetId))
|
||||
{
|
||||
if (string.Equals(attackerState.PlayerId, input.TargetId, StringComparison.Ordinal))
|
||||
{
|
||||
targetState = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!movementCoordinator.TryGetStateByPlayerId(input.TargetId, out targetState) ||
|
||||
targetState.IsDead ||
|
||||
targetState.Hp <= 0)
|
||||
{
|
||||
targetState = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return TryResolveTargetStateFromAim(input, attackerState, out targetState);
|
||||
}
|
||||
|
||||
private bool TryResolveTargetStateFromAim(
|
||||
ShootInput input,
|
||||
ServerAuthoritativeMovementState attackerState,
|
||||
out ServerAuthoritativeMovementState targetState)
|
||||
{
|
||||
targetState = null;
|
||||
|
||||
var aimLength = MathF.Sqrt((input.DirX * input.DirX) + (input.DirY * input.DirY));
|
||||
if (aimLength <= 0f)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
var aimX = input.DirX / aimLength;
|
||||
var aimY = input.DirY / aimLength;
|
||||
var bestAlignment = float.NegativeInfinity;
|
||||
var bestDistanceSquared = float.PositiveInfinity;
|
||||
|
||||
foreach (var candidate in movementCoordinator.States)
|
||||
{
|
||||
if (candidate == null ||
|
||||
string.Equals(candidate.PlayerId, attackerState.PlayerId, StringComparison.Ordinal) ||
|
||||
candidate.IsDead ||
|
||||
candidate.Hp <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var offsetX = candidate.PositionX - attackerState.PositionX;
|
||||
var offsetY = candidate.PositionZ - attackerState.PositionZ;
|
||||
var distanceSquared = (offsetX * offsetX) + (offsetY * offsetY);
|
||||
if (distanceSquared <= 0f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var inverseDistance = 1f / MathF.Sqrt(distanceSquared);
|
||||
var alignment = (offsetX * inverseDistance * aimX) + (offsetY * inverseDistance * aimY);
|
||||
if (alignment <= 0f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (targetState == null ||
|
||||
alignment > bestAlignment + 0.0001f ||
|
||||
(MathF.Abs(alignment - bestAlignment) <= 0.0001f &&
|
||||
(distanceSquared < bestDistanceSquared - 0.0001f ||
|
||||
(MathF.Abs(distanceSquared - bestDistanceSquared) <= 0.0001f &&
|
||||
string.CompareOrdinal(candidate.PlayerId, targetState.PlayerId) < 0))))
|
||||
{
|
||||
targetState = candidate;
|
||||
bestAlignment = alignment;
|
||||
bestDistanceSquared = distanceSquared;
|
||||
}
|
||||
}
|
||||
|
||||
return targetState != null;
|
||||
}
|
||||
|
||||
private void BroadcastRejectedShot(ShootInput input)
|
||||
|
||||
@@ -41,6 +41,46 @@ namespace Network.NetworkHost
|
||||
}
|
||||
}
|
||||
|
||||
public bool EnsureState(IPEndPoint remoteEndPoint, string playerId, out ServerAuthoritativeMovementState state)
|
||||
{
|
||||
if (remoteEndPoint == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(remoteEndPoint));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(playerId))
|
||||
{
|
||||
state = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
var normalizedSender = Normalize(remoteEndPoint);
|
||||
var key = normalizedSender.ToString();
|
||||
|
||||
lock (gate)
|
||||
{
|
||||
if (statesByPeer.TryGetValue(key, out var existingState))
|
||||
{
|
||||
if (!string.Equals(existingState.PlayerId, playerId, StringComparison.Ordinal))
|
||||
{
|
||||
state = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
state = CloneState(existingState);
|
||||
return true;
|
||||
}
|
||||
|
||||
var createdState = new ServerAuthoritativeMovementState(
|
||||
normalizedSender,
|
||||
playerId,
|
||||
configuration.DefaultHp);
|
||||
statesByPeer.Add(key, createdState);
|
||||
state = CloneState(createdState);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public Task HandleMoveInputAsync(byte[] payload, IPEndPoint sender)
|
||||
{
|
||||
if (payload == null || sender == null)
|
||||
|
||||
@@ -15,6 +15,8 @@ namespace Network.NetworkHost
|
||||
private readonly MessageManager messageManager;
|
||||
private readonly ServerAuthoritativeMovementCoordinator authoritativeMovementCoordinator;
|
||||
private readonly ServerAuthoritativeCombatCoordinator authoritativeCombatCoordinator;
|
||||
private readonly object playerIdentityGate = new();
|
||||
private readonly Dictionary<string, string> playerIdsByPeer = new();
|
||||
|
||||
public ServerNetworkHost(
|
||||
ITransport transport,
|
||||
@@ -97,6 +99,10 @@ namespace Network.NetworkHost
|
||||
SessionCoordinator.RemoveAllSessions("Transport stopped");
|
||||
authoritativeMovementCoordinator.Clear();
|
||||
authoritativeCombatCoordinator.Clear();
|
||||
lock (playerIdentityGate)
|
||||
{
|
||||
playerIdsByPeer.Clear();
|
||||
}
|
||||
PublishMetricsSessionSnapshots();
|
||||
}
|
||||
|
||||
@@ -140,12 +146,20 @@ namespace Network.NetworkHost
|
||||
public void NotifyLoginSucceeded(IPEndPoint remoteEndPoint)
|
||||
{
|
||||
SessionCoordinator.NotifyLoginSucceeded(remoteEndPoint);
|
||||
BootstrapAuthoritativeMovementState(remoteEndPoint);
|
||||
PublishMetricsSessionSnapshot(remoteEndPoint);
|
||||
}
|
||||
|
||||
public void NotifyLoginSucceeded(IPEndPoint remoteEndPoint, string playerId)
|
||||
{
|
||||
RememberPlayerId(remoteEndPoint, playerId);
|
||||
NotifyLoginSucceeded(remoteEndPoint);
|
||||
}
|
||||
|
||||
public void NotifyLoginFailed(IPEndPoint remoteEndPoint, string reason = null)
|
||||
{
|
||||
SessionCoordinator.NotifyLoginFailed(remoteEndPoint, reason);
|
||||
ForgetPlayerId(remoteEndPoint);
|
||||
PublishMetricsSessionSnapshot(remoteEndPoint);
|
||||
}
|
||||
|
||||
@@ -188,6 +202,7 @@ namespace Network.NetworkHost
|
||||
|
||||
authoritativeMovementCoordinator.RemoveState(remoteEndPoint);
|
||||
authoritativeCombatCoordinator.RemoveState(remoteEndPoint);
|
||||
ForgetPlayerId(remoteEndPoint);
|
||||
|
||||
RecordMetricsSessionSnapshot(transport, "server-host", session, ConnectionState.Disconnected);
|
||||
if (syncTransport != null && !ReferenceEquals(syncTransport, transport))
|
||||
@@ -198,12 +213,111 @@ namespace Network.NetworkHost
|
||||
return true;
|
||||
}
|
||||
|
||||
private void HandleTransportReceive(byte[] _, IPEndPoint sender)
|
||||
private void HandleTransportReceive(byte[] data, IPEndPoint sender)
|
||||
{
|
||||
SessionCoordinator.ObserveTransportActivity(sender);
|
||||
ObservePlayerIdentity(data, sender);
|
||||
PublishMetricsSessionSnapshot(sender);
|
||||
}
|
||||
|
||||
private void BootstrapAuthoritativeMovementState(IPEndPoint remoteEndPoint)
|
||||
{
|
||||
if (!TryGetKnownPlayerId(remoteEndPoint, out var playerId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
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))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var key = Normalize(remoteEndPoint).ToString();
|
||||
lock (playerIdentityGate)
|
||||
{
|
||||
playerIdsByPeer[key] = playerId;
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryGetKnownPlayerId(IPEndPoint remoteEndPoint, out string playerId)
|
||||
{
|
||||
playerId = null;
|
||||
if (remoteEndPoint == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var key = Normalize(remoteEndPoint).ToString();
|
||||
lock (playerIdentityGate)
|
||||
{
|
||||
return playerIdsByPeer.TryGetValue(key, out playerId);
|
||||
}
|
||||
}
|
||||
|
||||
private void ForgetPlayerId(IPEndPoint remoteEndPoint)
|
||||
{
|
||||
if (remoteEndPoint == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var key = Normalize(remoteEndPoint).ToString();
|
||||
lock (playerIdentityGate)
|
||||
{
|
||||
playerIdsByPeer.Remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
private static IPEndPoint Normalize(IPEndPoint remoteEndPoint)
|
||||
{
|
||||
if (remoteEndPoint == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(remoteEndPoint));
|
||||
}
|
||||
|
||||
return new IPEndPoint(remoteEndPoint.Address, remoteEndPoint.Port);
|
||||
}
|
||||
|
||||
private void PublishMetricsSessionSnapshots()
|
||||
{
|
||||
foreach (var session in ManagedSessions)
|
||||
|
||||
Reference in New Issue
Block a user