修改操作方式为 坦克式运动
This commit is contained in:
parent
f6ca6fa6e4
commit
0aab757091
|
|
@ -83,3 +83,6 @@ crashlytics-build.properties
|
||||||
/.dotnet
|
/.dotnet
|
||||||
/.dotnet-home
|
/.dotnet-home
|
||||||
|
|
||||||
|
|
||||||
|
EditMode-err.txt
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1595,90 +1595,6 @@ MeshFilter:
|
||||||
m_PrefabAsset: {fileID: 0}
|
m_PrefabAsset: {fileID: 0}
|
||||||
m_GameObject: {fileID: 490537900}
|
m_GameObject: {fileID: 490537900}
|
||||||
m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0}
|
m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0}
|
||||||
--- !u!1 &520045882
|
|
||||||
GameObject:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
serializedVersion: 6
|
|
||||||
m_Component:
|
|
||||||
- component: {fileID: 520045884}
|
|
||||||
- component: {fileID: 520045883}
|
|
||||||
m_Layer: 0
|
|
||||||
m_Name: GameObject
|
|
||||||
m_TagString: Untagged
|
|
||||||
m_Icon: {fileID: 0}
|
|
||||||
m_NavMeshLayer: 0
|
|
||||||
m_StaticEditorFlags: 0
|
|
||||||
m_IsActive: 1
|
|
||||||
--- !u!212 &520045883
|
|
||||||
SpriteRenderer:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 520045882}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_CastShadows: 0
|
|
||||||
m_ReceiveShadows: 0
|
|
||||||
m_DynamicOccludee: 1
|
|
||||||
m_StaticShadowCaster: 0
|
|
||||||
m_MotionVectors: 1
|
|
||||||
m_LightProbeUsage: 1
|
|
||||||
m_ReflectionProbeUsage: 1
|
|
||||||
m_RayTracingMode: 0
|
|
||||||
m_RayTraceProcedural: 0
|
|
||||||
m_RenderingLayerMask: 1
|
|
||||||
m_RendererPriority: 0
|
|
||||||
m_Materials:
|
|
||||||
- {fileID: 10754, guid: 0000000000000000f000000000000000, type: 0}
|
|
||||||
m_StaticBatchInfo:
|
|
||||||
firstSubMesh: 0
|
|
||||||
subMeshCount: 0
|
|
||||||
m_StaticBatchRoot: {fileID: 0}
|
|
||||||
m_ProbeAnchor: {fileID: 0}
|
|
||||||
m_LightProbeVolumeOverride: {fileID: 0}
|
|
||||||
m_ScaleInLightmap: 1
|
|
||||||
m_ReceiveGI: 1
|
|
||||||
m_PreserveUVs: 0
|
|
||||||
m_IgnoreNormalsForChartDetection: 0
|
|
||||||
m_ImportantGI: 0
|
|
||||||
m_StitchLightmapSeams: 1
|
|
||||||
m_SelectedEditorRenderState: 0
|
|
||||||
m_MinimumChartSize: 4
|
|
||||||
m_AutoUVMaxDistance: 0.5
|
|
||||||
m_AutoUVMaxAngle: 89
|
|
||||||
m_LightmapParameters: {fileID: 0}
|
|
||||||
m_SortingLayerID: 0
|
|
||||||
m_SortingLayer: 0
|
|
||||||
m_SortingOrder: 0
|
|
||||||
m_Sprite: {fileID: 0}
|
|
||||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
|
||||||
m_FlipX: 0
|
|
||||||
m_FlipY: 0
|
|
||||||
m_DrawMode: 0
|
|
||||||
m_Size: {x: 1, y: 1}
|
|
||||||
m_AdaptiveModeThreshold: 0.5
|
|
||||||
m_SpriteTileMode: 0
|
|
||||||
m_WasSpriteAssigned: 0
|
|
||||||
m_MaskInteraction: 0
|
|
||||||
m_SpriteSortPoint: 0
|
|
||||||
--- !u!4 &520045884
|
|
||||||
Transform:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 520045882}
|
|
||||||
serializedVersion: 2
|
|
||||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
|
||||||
m_LocalPosition: {x: -23.49001, y: 0.5700337, z: 11.206667}
|
|
||||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
|
||||||
m_ConstrainProportionsScale: 0
|
|
||||||
m_Children: []
|
|
||||||
m_Father: {fileID: 0}
|
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
|
||||||
--- !u!1 &632206104
|
--- !u!1 &632206104
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|
@ -3733,7 +3649,7 @@ Transform:
|
||||||
m_GameObject: {fileID: 1186082399}
|
m_GameObject: {fileID: 1186082399}
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
m_LocalPosition: {x: 0, y: 0, z: -30}
|
||||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
m_ConstrainProportionsScale: 0
|
m_ConstrainProportionsScale: 0
|
||||||
m_Children:
|
m_Children:
|
||||||
|
|
@ -6194,4 +6110,3 @@ SceneRoots:
|
||||||
- {fileID: 1298430417}
|
- {fileID: 1298430417}
|
||||||
- {fileID: 789249236}
|
- {fileID: 789249236}
|
||||||
- {fileID: 679132025}
|
- {fileID: 679132025}
|
||||||
- {fileID: 520045884}
|
|
||||||
|
|
|
||||||
|
|
@ -118,7 +118,18 @@ public sealed class ClientAuthoritativePlayerStateSnapshot
|
||||||
|
|
||||||
public int Hp { get; }
|
public int Hp { get; }
|
||||||
|
|
||||||
public Quaternion RotationQuaternion => Quaternion.Euler(0f, Rotation, 0f);
|
public Quaternion RotationQuaternion => Quaternion.Euler(0f, NormalizeDegrees(90f - Rotation), 0f);
|
||||||
|
|
||||||
|
private static float NormalizeDegrees(float degrees)
|
||||||
|
{
|
||||||
|
var normalized = degrees % 360f;
|
||||||
|
if (normalized < 0f)
|
||||||
|
{
|
||||||
|
normalized += 360f;
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class ClientCombatPresentationSnapshot
|
public sealed class ClientCombatPresentationSnapshot
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,8 @@ public static class ClientGameplayInputFlow
|
||||||
{
|
{
|
||||||
PlayerId = playerId,
|
PlayerId = playerId,
|
||||||
Tick = tick,
|
Tick = tick,
|
||||||
MoveX = input.x,
|
TurnInput = -input.x,
|
||||||
MoveY = input.z
|
ThrottleInput = input.z
|
||||||
};
|
};
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -104,6 +104,8 @@ public class MovementComponent : MonoBehaviour
|
||||||
{
|
{
|
||||||
[SerializeField] private float _sendInterval = 0.05f;
|
[SerializeField] private float _sendInterval = 0.05f;
|
||||||
private Player _master;
|
private Player _master;
|
||||||
|
private const float TurnSpeedDegreesPerSecond = 180f;
|
||||||
|
private const float UnityYawOffsetDegrees = 90f;
|
||||||
private int _speed = 2;
|
private int _speed = 2;
|
||||||
[SerializeField] private Rigidbody _rigid;
|
[SerializeField] private Rigidbody _rigid;
|
||||||
private float _lastSendTime = 0;
|
private float _lastSendTime = 0;
|
||||||
|
|
@ -131,9 +133,10 @@ public class MovementComponent : MonoBehaviour
|
||||||
_isControlled = isControlled;
|
_isControlled = isControlled;
|
||||||
_speed = speed;
|
_speed = speed;
|
||||||
_startTickOffset = serverTick;
|
_startTickOffset = serverTick;
|
||||||
_rigid.interpolation = RigidbodyInterpolation.Interpolate;
|
_rigid.interpolation = isControlled ? RigidbodyInterpolation.None : RigidbodyInterpolation.Interpolate;
|
||||||
_rigid.isKinematic = !isControlled;
|
_rigid.isKinematic = !isControlled;
|
||||||
_rigid.velocity = Vector3.zero;
|
_rigid.velocity = Vector3.zero;
|
||||||
|
_rigid.angularVelocity = Vector3.zero;
|
||||||
if (serverTick != 0 && _isControlled && MainUI.Instance != null) MainUI.Instance.OnStartTickOffsetChanged(serverTick);
|
if (serverTick != 0 && _isControlled && MainUI.Instance != null) MainUI.Instance.OnStartTickOffsetChanged(serverTick);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -145,7 +148,6 @@ public class MovementComponent : MonoBehaviour
|
||||||
var hasMovement = ClientGameplayInputFlow.HasPlanarInput(_cachedMoveInput);
|
var hasMovement = ClientGameplayInputFlow.HasPlanarInput(_cachedMoveInput);
|
||||||
if (hasMovement)
|
if (hasMovement)
|
||||||
{
|
{
|
||||||
_lastAimDirection = _cachedMoveInput;
|
|
||||||
_stopMessagePending = false;
|
_stopMessagePending = false;
|
||||||
}
|
}
|
||||||
else if (_wasMovingLastFrame)
|
else if (_wasMovingLastFrame)
|
||||||
|
|
@ -215,9 +217,10 @@ public class MovementComponent : MonoBehaviour
|
||||||
}
|
}
|
||||||
|
|
||||||
_serverPosition = snapshot.Position;
|
_serverPosition = snapshot.Position;
|
||||||
_rigid.position = Vector3.Lerp(_rigid.position, _serverPosition, _lerpRate);
|
_rigid.position = _serverPosition;
|
||||||
_rigid.rotation = Quaternion.Slerp(_rigid.rotation, snapshot.RotationQuaternion, _lerpRate);
|
_rigid.rotation = snapshot.RotationQuaternion;
|
||||||
_rigid.velocity = snapshot.Velocity;
|
_rigid.velocity = snapshot.Velocity;
|
||||||
|
_rigid.angularVelocity = Vector3.zero;
|
||||||
ReplayPendingInputs(replayInputs);
|
ReplayPendingInputs(replayInputs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -240,19 +243,19 @@ public class MovementComponent : MonoBehaviour
|
||||||
|
|
||||||
private Vector3 ResolveAimDirection()
|
private Vector3 ResolveAimDirection()
|
||||||
{
|
{
|
||||||
if (ClientGameplayInputFlow.HasPlanarInput(_lastAimDirection))
|
var planarForward = Vector3.ProjectOnPlane(_rigid.transform.forward, Vector3.up);
|
||||||
|
if (ClientGameplayInputFlow.HasPlanarInput(planarForward))
|
||||||
{
|
{
|
||||||
return _lastAimDirection;
|
_lastAimDirection = planarForward;
|
||||||
|
return planarForward;
|
||||||
}
|
}
|
||||||
|
|
||||||
var forward = _master != null ? _master.transform.forward : transform.forward;
|
return ClientGameplayInputFlow.HasPlanarInput(_lastAimDirection) ? _lastAimDirection : ResolveHeadingForward(UnityYawToHeading(_rigid.rotation.eulerAngles.y));
|
||||||
var planarForward = new Vector3(forward.x, 0f, forward.z);
|
|
||||||
return ClientGameplayInputFlow.HasPlanarInput(planarForward) ? planarForward : Vector3.forward;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Simulate(Vector3 input)
|
private void Simulate(Vector3 input)
|
||||||
{
|
{
|
||||||
_rigid.velocity = _speed * input;
|
ApplyTankMovement(-input.x, input.z, Time.fixedDeltaTime);
|
||||||
if (_isControlled)
|
if (_isControlled)
|
||||||
{
|
{
|
||||||
if (MainUI.Instance != null)
|
if (MainUI.Instance != null)
|
||||||
|
|
@ -301,7 +304,7 @@ public class MovementComponent : MonoBehaviour
|
||||||
{
|
{
|
||||||
foreach (var replayInput in replayInputs)
|
foreach (var replayInput in replayInputs)
|
||||||
{
|
{
|
||||||
_rigid.position += _speed * new Vector3(replayInput.MoveX, 0f, replayInput.MoveY) * _sendInterval;
|
ApplyTankMovement(replayInput.TurnInput, replayInput.ThrottleInput, _sendInterval);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_isControlled)
|
if (_isControlled)
|
||||||
|
|
@ -312,4 +315,50 @@ public class MovementComponent : MonoBehaviour
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ApplyTankMovement(float turnInput, float throttleInput, float deltaTime)
|
||||||
|
{
|
||||||
|
if (deltaTime <= 0f)
|
||||||
|
{
|
||||||
|
_rigid.velocity = Vector3.zero;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var clampedTurnInput = Mathf.Clamp(turnInput, -1f, 1f);
|
||||||
|
var clampedThrottleInput = Mathf.Clamp(throttleInput, -1f, 1f);
|
||||||
|
var heading = NormalizeDegrees(UnityYawToHeading(_rigid.rotation.eulerAngles.y) + (clampedTurnInput * TurnSpeedDegreesPerSecond * deltaTime));
|
||||||
|
_rigid.rotation = Quaternion.Euler(0f, HeadingToUnityYaw(heading), 0f);
|
||||||
|
|
||||||
|
var forward = ResolveHeadingForward(heading);
|
||||||
|
var velocity = forward * (clampedThrottleInput * _speed);
|
||||||
|
_rigid.velocity = velocity;
|
||||||
|
_rigid.position += velocity * deltaTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Vector3 ResolveHeadingForward(float headingDegrees)
|
||||||
|
{
|
||||||
|
var rotationRadians = headingDegrees * Mathf.Deg2Rad;
|
||||||
|
return new Vector3(Mathf.Cos(rotationRadians), 0f, Mathf.Sin(rotationRadians));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float HeadingToUnityYaw(float headingDegrees)
|
||||||
|
{
|
||||||
|
return NormalizeDegrees(UnityYawOffsetDegrees - headingDegrees);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float UnityYawToHeading(float unityYawDegrees)
|
||||||
|
{
|
||||||
|
return NormalizeDegrees(UnityYawOffsetDegrees - unityYawDegrees);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float NormalizeDegrees(float degrees)
|
||||||
|
{
|
||||||
|
var normalized = degrees % 360f;
|
||||||
|
if (normalized < 0f)
|
||||||
|
{
|
||||||
|
normalized += 360f;
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ namespace Network.Defines {
|
||||||
new pbr::GeneratedClrTypeInfo(typeof(global::Network.Defines.LoginResponse), global::Network.Defines.LoginResponse.Parser, new[]{ "PlayerId", "Positions", "Speed", "ServerTick", "Result" }, null, null, null, null),
|
new pbr::GeneratedClrTypeInfo(typeof(global::Network.Defines.LoginResponse), global::Network.Defines.LoginResponse.Parser, new[]{ "PlayerId", "Positions", "Speed", "ServerTick", "Result" }, null, null, null, null),
|
||||||
new pbr::GeneratedClrTypeInfo(typeof(global::Network.Defines.PlayerJoin), global::Network.Defines.PlayerJoin.Parser, new[]{ "PlayerId", "Position" }, null, null, null, null),
|
new pbr::GeneratedClrTypeInfo(typeof(global::Network.Defines.PlayerJoin), global::Network.Defines.PlayerJoin.Parser, new[]{ "PlayerId", "Position" }, null, null, null, null),
|
||||||
new pbr::GeneratedClrTypeInfo(typeof(global::Network.Defines.LogoutRequest), global::Network.Defines.LogoutRequest.Parser, new[]{ "PlayerId" }, null, null, null, null),
|
new pbr::GeneratedClrTypeInfo(typeof(global::Network.Defines.LogoutRequest), global::Network.Defines.LogoutRequest.Parser, new[]{ "PlayerId" }, null, null, null, null),
|
||||||
new pbr::GeneratedClrTypeInfo(typeof(global::Network.Defines.MoveInput), global::Network.Defines.MoveInput.Parser, new[]{ "PlayerId", "Tick", "MoveX", "MoveY" }, null, null, null, null),
|
new pbr::GeneratedClrTypeInfo(typeof(global::Network.Defines.MoveInput), global::Network.Defines.MoveInput.Parser, new[]{ "PlayerId", "Tick", "TurnInput", "ThrottleInput" }, null, null, null, null),
|
||||||
new pbr::GeneratedClrTypeInfo(typeof(global::Network.Defines.ShootInput), global::Network.Defines.ShootInput.Parser, new[]{ "PlayerId", "Tick", "DirX", "DirY", "TargetId" }, null, null, null, null),
|
new pbr::GeneratedClrTypeInfo(typeof(global::Network.Defines.ShootInput), global::Network.Defines.ShootInput.Parser, new[]{ "PlayerId", "Tick", "DirX", "DirY", "TargetId" }, null, null, null, null),
|
||||||
new pbr::GeneratedClrTypeInfo(typeof(global::Network.Defines.CombatEvent), global::Network.Defines.CombatEvent.Parser, new[]{ "Tick", "EventType", "AttackerId", "TargetId", "Damage", "HitPosition" }, null, null, null, null),
|
new pbr::GeneratedClrTypeInfo(typeof(global::Network.Defines.CombatEvent), global::Network.Defines.CombatEvent.Parser, new[]{ "Tick", "EventType", "AttackerId", "TargetId", "Damage", "HitPosition" }, null, null, null, null),
|
||||||
new pbr::GeneratedClrTypeInfo(typeof(global::Network.Defines.PlayerState), global::Network.Defines.PlayerState.Parser, new[]{ "PlayerId", "Position", "Velocity", "Rotation", "Tick", "Hp" }, null, null, null, null),
|
new pbr::GeneratedClrTypeInfo(typeof(global::Network.Defines.PlayerState), global::Network.Defines.PlayerState.Parser, new[]{ "PlayerId", "Position", "Velocity", "Rotation", "Tick", "Hp" }, null, null, null, null),
|
||||||
|
|
@ -1628,8 +1628,8 @@ namespace Network.Defines {
|
||||||
public MoveInput(MoveInput other) : this() {
|
public MoveInput(MoveInput other) : this() {
|
||||||
playerId_ = other.playerId_;
|
playerId_ = other.playerId_;
|
||||||
tick_ = other.tick_;
|
tick_ = other.tick_;
|
||||||
moveX_ = other.moveX_;
|
turnInput_ = other.turnInput_;
|
||||||
moveY_ = other.moveY_;
|
throttleInput_ = other.throttleInput_;
|
||||||
_unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields);
|
_unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1663,27 +1663,27 @@ namespace Network.Defines {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Field number for the "move_x" field.</summary>
|
/// <summary>Field number for the "turn_input" field.</summary>
|
||||||
public const int MoveXFieldNumber = 3;
|
public const int TurnInputFieldNumber = 3;
|
||||||
private float moveX_;
|
private float turnInput_;
|
||||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||||
public float MoveX {
|
public float TurnInput {
|
||||||
get { return moveX_; }
|
get { return turnInput_; }
|
||||||
set {
|
set {
|
||||||
moveX_ = value;
|
turnInput_ = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Field number for the "move_y" field.</summary>
|
/// <summary>Field number for the "throttle_input" field.</summary>
|
||||||
public const int MoveYFieldNumber = 4;
|
public const int ThrottleInputFieldNumber = 4;
|
||||||
private float moveY_;
|
private float throttleInput_;
|
||||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||||
public float MoveY {
|
public float ThrottleInput {
|
||||||
get { return moveY_; }
|
get { return throttleInput_; }
|
||||||
set {
|
set {
|
||||||
moveY_ = value;
|
throttleInput_ = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1704,8 +1704,8 @@ namespace Network.Defines {
|
||||||
}
|
}
|
||||||
if (PlayerId != other.PlayerId) return false;
|
if (PlayerId != other.PlayerId) return false;
|
||||||
if (Tick != other.Tick) return false;
|
if (Tick != other.Tick) return false;
|
||||||
if (!pbc::ProtobufEqualityComparers.BitwiseSingleEqualityComparer.Equals(MoveX, other.MoveX)) return false;
|
if (!pbc::ProtobufEqualityComparers.BitwiseSingleEqualityComparer.Equals(TurnInput, other.TurnInput)) return false;
|
||||||
if (!pbc::ProtobufEqualityComparers.BitwiseSingleEqualityComparer.Equals(MoveY, other.MoveY)) return false;
|
if (!pbc::ProtobufEqualityComparers.BitwiseSingleEqualityComparer.Equals(ThrottleInput, other.ThrottleInput)) return false;
|
||||||
return Equals(_unknownFields, other._unknownFields);
|
return Equals(_unknownFields, other._unknownFields);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1715,8 +1715,8 @@ namespace Network.Defines {
|
||||||
int hash = 1;
|
int hash = 1;
|
||||||
if (PlayerId.Length != 0) hash ^= PlayerId.GetHashCode();
|
if (PlayerId.Length != 0) hash ^= PlayerId.GetHashCode();
|
||||||
if (Tick != 0L) hash ^= Tick.GetHashCode();
|
if (Tick != 0L) hash ^= Tick.GetHashCode();
|
||||||
if (MoveX != 0F) hash ^= pbc::ProtobufEqualityComparers.BitwiseSingleEqualityComparer.GetHashCode(MoveX);
|
if (TurnInput != 0F) hash ^= pbc::ProtobufEqualityComparers.BitwiseSingleEqualityComparer.GetHashCode(TurnInput);
|
||||||
if (MoveY != 0F) hash ^= pbc::ProtobufEqualityComparers.BitwiseSingleEqualityComparer.GetHashCode(MoveY);
|
if (ThrottleInput != 0F) hash ^= pbc::ProtobufEqualityComparers.BitwiseSingleEqualityComparer.GetHashCode(ThrottleInput);
|
||||||
if (_unknownFields != null) {
|
if (_unknownFields != null) {
|
||||||
hash ^= _unknownFields.GetHashCode();
|
hash ^= _unknownFields.GetHashCode();
|
||||||
}
|
}
|
||||||
|
|
@ -1743,13 +1743,13 @@ namespace Network.Defines {
|
||||||
output.WriteRawTag(16);
|
output.WriteRawTag(16);
|
||||||
output.WriteInt64(Tick);
|
output.WriteInt64(Tick);
|
||||||
}
|
}
|
||||||
if (MoveX != 0F) {
|
if (TurnInput != 0F) {
|
||||||
output.WriteRawTag(29);
|
output.WriteRawTag(29);
|
||||||
output.WriteFloat(MoveX);
|
output.WriteFloat(TurnInput);
|
||||||
}
|
}
|
||||||
if (MoveY != 0F) {
|
if (ThrottleInput != 0F) {
|
||||||
output.WriteRawTag(37);
|
output.WriteRawTag(37);
|
||||||
output.WriteFloat(MoveY);
|
output.WriteFloat(ThrottleInput);
|
||||||
}
|
}
|
||||||
if (_unknownFields != null) {
|
if (_unknownFields != null) {
|
||||||
_unknownFields.WriteTo(output);
|
_unknownFields.WriteTo(output);
|
||||||
|
|
@ -1769,13 +1769,13 @@ namespace Network.Defines {
|
||||||
output.WriteRawTag(16);
|
output.WriteRawTag(16);
|
||||||
output.WriteInt64(Tick);
|
output.WriteInt64(Tick);
|
||||||
}
|
}
|
||||||
if (MoveX != 0F) {
|
if (TurnInput != 0F) {
|
||||||
output.WriteRawTag(29);
|
output.WriteRawTag(29);
|
||||||
output.WriteFloat(MoveX);
|
output.WriteFloat(TurnInput);
|
||||||
}
|
}
|
||||||
if (MoveY != 0F) {
|
if (ThrottleInput != 0F) {
|
||||||
output.WriteRawTag(37);
|
output.WriteRawTag(37);
|
||||||
output.WriteFloat(MoveY);
|
output.WriteFloat(ThrottleInput);
|
||||||
}
|
}
|
||||||
if (_unknownFields != null) {
|
if (_unknownFields != null) {
|
||||||
_unknownFields.WriteTo(ref output);
|
_unknownFields.WriteTo(ref output);
|
||||||
|
|
@ -1793,10 +1793,10 @@ namespace Network.Defines {
|
||||||
if (Tick != 0L) {
|
if (Tick != 0L) {
|
||||||
size += 1 + pb::CodedOutputStream.ComputeInt64Size(Tick);
|
size += 1 + pb::CodedOutputStream.ComputeInt64Size(Tick);
|
||||||
}
|
}
|
||||||
if (MoveX != 0F) {
|
if (TurnInput != 0F) {
|
||||||
size += 1 + 4;
|
size += 1 + 4;
|
||||||
}
|
}
|
||||||
if (MoveY != 0F) {
|
if (ThrottleInput != 0F) {
|
||||||
size += 1 + 4;
|
size += 1 + 4;
|
||||||
}
|
}
|
||||||
if (_unknownFields != null) {
|
if (_unknownFields != null) {
|
||||||
|
|
@ -1817,11 +1817,11 @@ namespace Network.Defines {
|
||||||
if (other.Tick != 0L) {
|
if (other.Tick != 0L) {
|
||||||
Tick = other.Tick;
|
Tick = other.Tick;
|
||||||
}
|
}
|
||||||
if (other.MoveX != 0F) {
|
if (other.TurnInput != 0F) {
|
||||||
MoveX = other.MoveX;
|
TurnInput = other.TurnInput;
|
||||||
}
|
}
|
||||||
if (other.MoveY != 0F) {
|
if (other.ThrottleInput != 0F) {
|
||||||
MoveY = other.MoveY;
|
ThrottleInput = other.ThrottleInput;
|
||||||
}
|
}
|
||||||
_unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields);
|
_unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields);
|
||||||
}
|
}
|
||||||
|
|
@ -1851,11 +1851,11 @@ namespace Network.Defines {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 29: {
|
case 29: {
|
||||||
MoveX = input.ReadFloat();
|
TurnInput = input.ReadFloat();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 37: {
|
case 37: {
|
||||||
MoveY = input.ReadFloat();
|
ThrottleInput = input.ReadFloat();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1886,11 +1886,11 @@ namespace Network.Defines {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 29: {
|
case 29: {
|
||||||
MoveX = input.ReadFloat();
|
TurnInput = input.ReadFloat();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 37: {
|
case 37: {
|
||||||
MoveY = input.ReadFloat();
|
ThrottleInput = input.ReadFloat();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,34 +4,18 @@ namespace Network.Defines
|
||||||
{
|
{
|
||||||
Unknow = 0,
|
Unknow = 0,
|
||||||
|
|
||||||
// Gameplay
|
// Canonical dedicated-server MVP runtime vocabulary.
|
||||||
MoveInput = 1,
|
MoveInput = 1,
|
||||||
PlayerState = 2,
|
PlayerState = 2,
|
||||||
ShootInput = 3,
|
ShootInput = 3,
|
||||||
CombatEvent = 4,
|
CombatEvent = 4,
|
||||||
PlayerJoin = 5,
|
PlayerJoin = 5,
|
||||||
PlayerLeave = 6,
|
|
||||||
PlayerAction = 7,
|
|
||||||
GameState = 8,
|
|
||||||
|
|
||||||
// Chat
|
|
||||||
ChatMessage = 10,
|
|
||||||
PrivateMessage = 11,
|
|
||||||
SystemMessage = 12,
|
|
||||||
|
|
||||||
// Session
|
|
||||||
HeartBeat = 20,
|
|
||||||
LoginRequest = 21,
|
LoginRequest = 21,
|
||||||
LoginResponse = 22,
|
LoginResponse = 22,
|
||||||
LogoutRequest = 23,
|
LogoutRequest = 23,
|
||||||
|
|
||||||
// Room management
|
|
||||||
CreateRoom = 30,
|
|
||||||
JoinRoom = 31,
|
|
||||||
LeaveRoom = 32,
|
|
||||||
RoomList = 33,
|
|
||||||
|
|
||||||
Heartbeat = 40,
|
Heartbeat = 40,
|
||||||
HeartbeatResponse = 41,
|
HeartbeatResponse = 41,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,8 +39,8 @@ message LogoutRequest {
|
||||||
message MoveInput {
|
message MoveInput {
|
||||||
string player_id = 1;
|
string player_id = 1;
|
||||||
int64 tick = 2;
|
int64 tick = 2;
|
||||||
float move_x = 3;
|
float turn_input = 3;
|
||||||
float move_y = 4;
|
float throttle_input = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ShootInput {
|
message ShootInput {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: 3dc7b1ecbad541ea86a9d700dd5148e0
|
guid: 3dc7b1ecbad541ea86a9d700dd5148e0
|
||||||
MonoImporter:
|
MonoImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
|
@ -87,9 +87,10 @@ namespace Network.NetworkApplication
|
||||||
if (sessionManager.State == ConnectionState.Disconnected)
|
if (sessionManager.State == ConnectionState.Disconnected)
|
||||||
{
|
{
|
||||||
sessionManager.NotifyTransportConnected();
|
sessionManager.NotifyTransportConnected();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
sessionManager.NotifyInboundActivity();
|
sessionManager.NotifyTransportActivity();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void NotifyTransportConnected(IPEndPoint remoteEndPoint)
|
public void NotifyTransportConnected(IPEndPoint remoteEndPoint)
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,8 @@ namespace Network.NetworkApplication
|
||||||
SyncSequenceTracker syncSequenceTracker = null,
|
SyncSequenceTracker syncSequenceTracker = null,
|
||||||
Func<int, ITransport> transportFactory = null,
|
Func<int, ITransport> transportFactory = null,
|
||||||
ServerAuthoritativeMovementConfiguration authoritativeMovement = null,
|
ServerAuthoritativeMovementConfiguration authoritativeMovement = null,
|
||||||
ServerAuthoritativeCombatConfiguration authoritativeCombat = null)
|
ServerAuthoritativeCombatConfiguration authoritativeCombat = null,
|
||||||
|
IAuthoritativeMovementWorldValidator authoritativeMovementWorldValidator = null)
|
||||||
{
|
{
|
||||||
ValidateDualPortConfiguration(reliablePort, syncPort);
|
ValidateDualPortConfiguration(reliablePort, syncPort);
|
||||||
|
|
||||||
|
|
@ -86,7 +87,8 @@ namespace Network.NetworkApplication
|
||||||
deliveryPolicyResolver,
|
deliveryPolicyResolver,
|
||||||
syncSequenceTracker,
|
syncSequenceTracker,
|
||||||
authoritativeMovement,
|
authoritativeMovement,
|
||||||
authoritativeCombat);
|
authoritativeCombat,
|
||||||
|
authoritativeMovementWorldValidator ?? PermissiveAuthoritativeMovementWorldValidator.Instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Task<ServerRuntimeHandle> StartServerRuntimeAsync(ServerRuntimeConfiguration configuration)
|
public static Task<ServerRuntimeHandle> StartServerRuntimeAsync(ServerRuntimeConfiguration configuration)
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace Network.NetworkApplication
|
namespace Network.NetworkApplication
|
||||||
{
|
{
|
||||||
public sealed class SessionManager
|
public sealed class SessionManager
|
||||||
{
|
{
|
||||||
private readonly Func<DateTimeOffset> utcNowProvider;
|
private readonly Func<DateTimeOffset> utcNowProvider;
|
||||||
private DateTimeOffset? lastLivenessUtc;
|
private DateTimeOffset? lastAcceptedLivenessUtc;
|
||||||
|
private DateTimeOffset? lastTransportActivityUtc;
|
||||||
private DateTimeOffset? lastHeartbeatSentUtc;
|
private DateTimeOffset? lastHeartbeatSentUtc;
|
||||||
private DateTimeOffset? nextReconnectAtUtc;
|
private DateTimeOffset? nextReconnectAtUtc;
|
||||||
|
|
||||||
|
|
@ -24,7 +25,9 @@ namespace Network.NetworkApplication
|
||||||
|
|
||||||
public SessionReconnectPolicy ReconnectPolicy { get; }
|
public SessionReconnectPolicy ReconnectPolicy { get; }
|
||||||
|
|
||||||
public DateTimeOffset? LastLivenessUtc => lastLivenessUtc;
|
public DateTimeOffset? LastLivenessUtc => lastAcceptedLivenessUtc;
|
||||||
|
|
||||||
|
public DateTimeOffset? LastTransportActivityUtc => lastTransportActivityUtc;
|
||||||
|
|
||||||
public DateTimeOffset? LastHeartbeatSentUtc => lastHeartbeatSentUtc;
|
public DateTimeOffset? LastHeartbeatSentUtc => lastHeartbeatSentUtc;
|
||||||
|
|
||||||
|
|
@ -70,13 +73,18 @@ namespace Network.NetworkApplication
|
||||||
public void NotifyTransportConnected()
|
public void NotifyTransportConnected()
|
||||||
{
|
{
|
||||||
var now = utcNowProvider();
|
var now = utcNowProvider();
|
||||||
lastLivenessUtc = now;
|
lastTransportActivityUtc = now;
|
||||||
lastHeartbeatSentUtc = null;
|
lastHeartbeatSentUtc = null;
|
||||||
nextReconnectAtUtc = null;
|
nextReconnectAtUtc = null;
|
||||||
LastFailureReason = null;
|
LastFailureReason = null;
|
||||||
TransitionTo(ConnectionState.TransportConnected, SessionEventKind.TransportConnected, now);
|
TransitionTo(ConnectionState.TransportConnected, SessionEventKind.TransportConnected, now);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void NotifyTransportActivity()
|
||||||
|
{
|
||||||
|
lastTransportActivityUtc = utcNowProvider();
|
||||||
|
}
|
||||||
|
|
||||||
public void NotifyLoginStarted()
|
public void NotifyLoginStarted()
|
||||||
{
|
{
|
||||||
TransitionTo(ConnectionState.LoginPending, SessionEventKind.LoginStarted, utcNowProvider());
|
TransitionTo(ConnectionState.LoginPending, SessionEventKind.LoginStarted, utcNowProvider());
|
||||||
|
|
@ -85,7 +93,8 @@ namespace Network.NetworkApplication
|
||||||
public void NotifyLoginSucceeded()
|
public void NotifyLoginSucceeded()
|
||||||
{
|
{
|
||||||
var now = utcNowProvider();
|
var now = utcNowProvider();
|
||||||
lastLivenessUtc = now;
|
lastTransportActivityUtc = now;
|
||||||
|
lastAcceptedLivenessUtc = now;
|
||||||
LastFailureReason = null;
|
LastFailureReason = null;
|
||||||
TransitionTo(ConnectionState.LoggedIn, SessionEventKind.LoginSucceeded, now);
|
TransitionTo(ConnectionState.LoggedIn, SessionEventKind.LoginSucceeded, now);
|
||||||
}
|
}
|
||||||
|
|
@ -105,7 +114,8 @@ namespace Network.NetworkApplication
|
||||||
public void NotifyHeartbeatReceived()
|
public void NotifyHeartbeatReceived()
|
||||||
{
|
{
|
||||||
var now = utcNowProvider();
|
var now = utcNowProvider();
|
||||||
lastLivenessUtc = now;
|
lastTransportActivityUtc = now;
|
||||||
|
lastAcceptedLivenessUtc = now;
|
||||||
if (lastHeartbeatSentUtc.HasValue)
|
if (lastHeartbeatSentUtc.HasValue)
|
||||||
{
|
{
|
||||||
LastRoundTripTime = now - lastHeartbeatSentUtc.Value;
|
LastRoundTripTime = now - lastHeartbeatSentUtc.Value;
|
||||||
|
|
@ -116,7 +126,9 @@ namespace Network.NetworkApplication
|
||||||
|
|
||||||
public void NotifyInboundActivity()
|
public void NotifyInboundActivity()
|
||||||
{
|
{
|
||||||
lastLivenessUtc = utcNowProvider();
|
var now = utcNowProvider();
|
||||||
|
lastTransportActivityUtc = now;
|
||||||
|
lastAcceptedLivenessUtc = now;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void NotifyTransportDisconnected(string reason = null)
|
public void NotifyTransportDisconnected(string reason = null)
|
||||||
|
|
@ -152,17 +164,18 @@ namespace Network.NetworkApplication
|
||||||
|
|
||||||
private bool ShouldTimeout(DateTimeOffset now)
|
private bool ShouldTimeout(DateTimeOffset now)
|
||||||
{
|
{
|
||||||
if (State != ConnectionState.TransportConnected && State != ConnectionState.LoginPending && State != ConnectionState.LoggedIn)
|
switch (State)
|
||||||
{
|
{
|
||||||
return false;
|
case ConnectionState.TransportConnected:
|
||||||
|
case ConnectionState.LoginPending:
|
||||||
|
return lastTransportActivityUtc.HasValue &&
|
||||||
|
now - lastTransportActivityUtc.Value >= ReconnectPolicy.HeartbeatTimeout;
|
||||||
|
case ConnectionState.LoggedIn:
|
||||||
|
return lastAcceptedLivenessUtc.HasValue &&
|
||||||
|
now - lastAcceptedLivenessUtc.Value >= ReconnectPolicy.HeartbeatTimeout;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!lastLivenessUtc.HasValue)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return now - lastLivenessUtc.Value >= ReconnectPolicy.HeartbeatTimeout;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void TransitionTo(
|
private void TransitionTo(
|
||||||
|
|
|
||||||
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
internal sealed class ServerAuthoritativeCombatCoordinator
|
||||||
{
|
{
|
||||||
private readonly object gate = new();
|
private readonly object gate = new();
|
||||||
|
private readonly ServerNetworkHost host;
|
||||||
private readonly MessageManager messageManager;
|
private readonly MessageManager messageManager;
|
||||||
private readonly ServerAuthoritativeMovementCoordinator movementCoordinator;
|
private readonly ServerAuthoritativeMovementCoordinator movementCoordinator;
|
||||||
private readonly Dictionary<string, ServerAuthoritativeCombatState> statesByPeer = new();
|
private readonly Dictionary<string, ServerAuthoritativeCombatState> statesByPeer = new();
|
||||||
private readonly ServerAuthoritativeCombatConfiguration configuration;
|
private readonly ServerAuthoritativeCombatConfiguration configuration;
|
||||||
|
|
||||||
public ServerAuthoritativeCombatCoordinator(
|
public ServerAuthoritativeCombatCoordinator(
|
||||||
|
ServerNetworkHost host,
|
||||||
MessageManager messageManager,
|
MessageManager messageManager,
|
||||||
ServerAuthoritativeMovementCoordinator movementCoordinator,
|
ServerAuthoritativeMovementCoordinator movementCoordinator,
|
||||||
ServerAuthoritativeCombatConfiguration configuration)
|
ServerAuthoritativeCombatConfiguration configuration)
|
||||||
{
|
{
|
||||||
|
this.host = host ?? throw new ArgumentNullException(nameof(host));
|
||||||
this.messageManager = messageManager ?? throw new ArgumentNullException(nameof(messageManager));
|
this.messageManager = messageManager ?? throw new ArgumentNullException(nameof(messageManager));
|
||||||
this.movementCoordinator = movementCoordinator ?? throw new ArgumentNullException(nameof(movementCoordinator));
|
this.movementCoordinator = movementCoordinator ?? throw new ArgumentNullException(nameof(movementCoordinator));
|
||||||
this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
|
this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
|
||||||
|
|
@ -56,13 +59,13 @@ namespace Network.NetworkHost
|
||||||
return Task.CompletedTask;
|
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);
|
BroadcastRejectedShot(input);
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
movementCoordinator.TryUpdateState(sender, state =>
|
movementCoordinator.TryUpdateState(acceptedPeer, state =>
|
||||||
{
|
{
|
||||||
state.LastAcceptedShootTick = input.Tick;
|
state.LastAcceptedShootTick = input.Tick;
|
||||||
}, out attackerState);
|
}, out attackerState);
|
||||||
|
|
@ -85,6 +88,7 @@ namespace Network.NetworkHost
|
||||||
Z = targetState.PositionZ
|
Z = targetState.PositionZ
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Keep the coordinator as the only gameplay-relevant authoritative combat-result emitter.
|
||||||
messageManager.BroadcastMessage(new CombatEvent
|
messageManager.BroadcastMessage(new CombatEvent
|
||||||
{
|
{
|
||||||
Tick = input.Tick,
|
Tick = input.Tick,
|
||||||
|
|
@ -157,16 +161,19 @@ namespace Network.NetworkHost
|
||||||
private bool TryValidateAcceptedShot(
|
private bool TryValidateAcceptedShot(
|
||||||
ShootInput input,
|
ShootInput input,
|
||||||
IPEndPoint sender,
|
IPEndPoint sender,
|
||||||
|
out IPEndPoint acceptedPeer,
|
||||||
out ServerAuthoritativeMovementState attackerState,
|
out ServerAuthoritativeMovementState attackerState,
|
||||||
out ServerAuthoritativeMovementState targetState)
|
out ServerAuthoritativeMovementState targetState)
|
||||||
{
|
{
|
||||||
|
acceptedPeer = null;
|
||||||
attackerState = null;
|
attackerState = null;
|
||||||
targetState = null;
|
targetState = null;
|
||||||
|
|
||||||
if (input == null ||
|
if (input == null ||
|
||||||
string.IsNullOrWhiteSpace(input.PlayerId) ||
|
string.IsNullOrWhiteSpace(input.PlayerId) ||
|
||||||
!IsFinite(input.DirX) ||
|
!IsFinite(input.DirX) ||
|
||||||
!IsFinite(input.DirY))
|
!IsFinite(input.DirY) ||
|
||||||
|
!host.TryResolveAcceptedPeer(sender, input.PlayerId, out acceptedPeer))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -177,8 +184,8 @@ namespace Network.NetworkHost
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!movementCoordinator.TryGetState(sender, out attackerState) &&
|
if (!movementCoordinator.TryGetState(acceptedPeer, out attackerState) &&
|
||||||
!movementCoordinator.EnsureState(sender, input.PlayerId, out attackerState))
|
!movementCoordinator.EnsureState(acceptedPeer, input.PlayerId, out attackerState))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -191,7 +198,12 @@ namespace Network.NetworkHost
|
||||||
return false;
|
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(
|
private bool TryResolveTargetState(
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ namespace Network.NetworkHost
|
||||||
{
|
{
|
||||||
public float MoveSpeed { get; set; } = 5f;
|
public float MoveSpeed { get; set; } = 5f;
|
||||||
|
|
||||||
|
public float TurnSpeedDegreesPerSecond { get; set; } = 180f;
|
||||||
|
|
||||||
public TimeSpan BroadcastInterval { get; set; } = TimeSpan.FromMilliseconds(50);
|
public TimeSpan BroadcastInterval { get; set; } = TimeSpan.FromMilliseconds(50);
|
||||||
|
|
||||||
public int DefaultHp { get; set; } = 100;
|
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.");
|
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)
|
if (BroadcastInterval <= TimeSpan.Zero)
|
||||||
{
|
{
|
||||||
throw new ArgumentOutOfRangeException(nameof(BroadcastInterval), "Broadcast interval must be positive.");
|
throw new ArgumentOutOfRangeException(nameof(BroadcastInterval), "Broadcast interval must be positive.");
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ namespace Network.NetworkHost
|
||||||
private readonly MessageManager messageManager;
|
private readonly MessageManager messageManager;
|
||||||
private readonly ServerNetworkHost host;
|
private readonly ServerNetworkHost host;
|
||||||
private readonly ServerAuthoritativeMovementConfiguration configuration;
|
private readonly ServerAuthoritativeMovementConfiguration configuration;
|
||||||
|
private readonly IAuthoritativeMovementWorldValidator worldValidator;
|
||||||
private readonly Dictionary<string, ServerAuthoritativeMovementState> statesByPeer = new();
|
private readonly Dictionary<string, ServerAuthoritativeMovementState> statesByPeer = new();
|
||||||
private long nextBroadcastTick = 1;
|
private long nextBroadcastTick = 1;
|
||||||
private TimeSpan accumulatedBroadcastTime;
|
private TimeSpan accumulatedBroadcastTime;
|
||||||
|
|
@ -21,11 +22,13 @@ namespace Network.NetworkHost
|
||||||
public ServerAuthoritativeMovementCoordinator(
|
public ServerAuthoritativeMovementCoordinator(
|
||||||
ServerNetworkHost host,
|
ServerNetworkHost host,
|
||||||
MessageManager messageManager,
|
MessageManager messageManager,
|
||||||
ServerAuthoritativeMovementConfiguration configuration)
|
ServerAuthoritativeMovementConfiguration configuration,
|
||||||
|
IAuthoritativeMovementWorldValidator worldValidator)
|
||||||
{
|
{
|
||||||
this.host = host ?? throw new ArgumentNullException(nameof(host));
|
this.host = host ?? throw new ArgumentNullException(nameof(host));
|
||||||
this.messageManager = messageManager ?? throw new ArgumentNullException(nameof(messageManager));
|
this.messageManager = messageManager ?? throw new ArgumentNullException(nameof(messageManager));
|
||||||
this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
|
this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
|
||||||
|
this.worldValidator = worldValidator ?? throw new ArgumentNullException(nameof(worldValidator));
|
||||||
}
|
}
|
||||||
|
|
||||||
public IReadOnlyList<ServerAuthoritativeMovementState> States
|
public IReadOnlyList<ServerAuthoritativeMovementState> States
|
||||||
|
|
@ -48,7 +51,7 @@ namespace Network.NetworkHost
|
||||||
throw new ArgumentNullException(nameof(remoteEndPoint));
|
throw new ArgumentNullException(nameof(remoteEndPoint));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(playerId))
|
if (string.IsNullOrWhiteSpace(playerId) || !host.IsAcceptedPlayer(remoteEndPoint, playerId))
|
||||||
{
|
{
|
||||||
state = null;
|
state = null;
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -99,13 +102,14 @@ namespace Network.NetworkHost
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(input.PlayerId) ||
|
if (string.IsNullOrWhiteSpace(input.PlayerId) ||
|
||||||
!IsFinite(input.MoveX) ||
|
!IsFinite(input.TurnInput) ||
|
||||||
!IsFinite(input.MoveY))
|
!IsFinite(input.ThrottleInput) ||
|
||||||
|
!host.TryResolveAcceptedPeer(sender, input.PlayerId, out var acceptedPeer))
|
||||||
{
|
{
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
var normalizedSender = Normalize(sender);
|
var normalizedSender = Normalize(acceptedPeer);
|
||||||
var key = normalizedSender.ToString();
|
var key = normalizedSender.ToString();
|
||||||
|
|
||||||
lock (gate)
|
lock (gate)
|
||||||
|
|
@ -113,7 +117,8 @@ namespace Network.NetworkHost
|
||||||
if (statesByPeer.TryGetValue(key, out var existingState))
|
if (statesByPeer.TryGetValue(key, out var existingState))
|
||||||
{
|
{
|
||||||
if (!string.Equals(existingState.PlayerId, input.PlayerId, StringComparison.Ordinal) ||
|
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;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
@ -122,6 +127,11 @@ namespace Network.NetworkHost
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!host.TryRefreshAcceptedGameplayActivity(sender, input.PlayerId))
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
var state = new ServerAuthoritativeMovementState(
|
var state = new ServerAuthoritativeMovementState(
|
||||||
normalizedSender,
|
normalizedSender,
|
||||||
input.PlayerId,
|
input.PlayerId,
|
||||||
|
|
@ -172,6 +182,7 @@ namespace Network.NetworkHost
|
||||||
|
|
||||||
foreach (var pendingBroadcast in pendingBroadcasts)
|
foreach (var pendingBroadcast in pendingBroadcasts)
|
||||||
{
|
{
|
||||||
|
// Keep the coordinator as the only gameplay-relevant PlayerState broadcast path.
|
||||||
messageManager.BroadcastMessage(pendingBroadcast.PlayerState, MessageType.PlayerState);
|
messageManager.BroadcastMessage(pendingBroadcast.PlayerState, MessageType.PlayerState);
|
||||||
host.ObserveAuthoritativeState(pendingBroadcast.RemoteEndPoint, pendingBroadcast.PlayerState.Tick);
|
host.ObserveAuthoritativeState(pendingBroadcast.RemoteEndPoint, pendingBroadcast.PlayerState.Tick);
|
||||||
}
|
}
|
||||||
|
|
@ -330,32 +341,20 @@ namespace Network.NetworkHost
|
||||||
private static void ApplyInput(ServerAuthoritativeMovementState state, MoveInput input)
|
private static void ApplyInput(ServerAuthoritativeMovementState state, MoveInput input)
|
||||||
{
|
{
|
||||||
state.LastAcceptedMoveTick = input.Tick;
|
state.LastAcceptedMoveTick = input.Tick;
|
||||||
state.InputX = input.MoveX;
|
state.InputX = ClampInput(input.TurnInput);
|
||||||
state.InputY = input.MoveY;
|
state.InputY = ClampInput(input.ThrottleInput);
|
||||||
|
|
||||||
if (input.MoveX == 0f && input.MoveY == 0f)
|
if (state.InputY == 0f)
|
||||||
{
|
{
|
||||||
state.VelocityX = 0f;
|
state.VelocityX = 0f;
|
||||||
state.VelocityY = 0f;
|
state.VelocityY = 0f;
|
||||||
state.VelocityZ = 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)
|
private void IntegrateState(ServerAuthoritativeMovementState state, TimeSpan elapsed)
|
||||||
{
|
{
|
||||||
if (state.IsDead || (state.InputX == 0f && state.InputY == 0f))
|
if (state.IsDead)
|
||||||
{
|
{
|
||||||
state.VelocityX = 0f;
|
state.VelocityX = 0f;
|
||||||
state.VelocityY = 0f;
|
state.VelocityY = 0f;
|
||||||
|
|
@ -363,25 +362,83 @@ namespace Network.NetworkHost
|
||||||
return;
|
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;
|
var deltaSeconds = (float)elapsed.TotalSeconds;
|
||||||
state.PositionX += state.VelocityX * deltaSeconds;
|
if (deltaSeconds <= 0f)
|
||||||
state.PositionY += state.VelocityY * deltaSeconds;
|
{
|
||||||
state.PositionZ += state.VelocityZ * deltaSeconds;
|
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)
|
private static PlayerState BuildPlayerState(ServerAuthoritativeMovementState state, long tick)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Network.Defines;
|
using Network.Defines;
|
||||||
|
|
@ -17,6 +18,8 @@ namespace Network.NetworkHost
|
||||||
private readonly ServerAuthoritativeCombatCoordinator authoritativeCombatCoordinator;
|
private readonly ServerAuthoritativeCombatCoordinator authoritativeCombatCoordinator;
|
||||||
private readonly object playerIdentityGate = new();
|
private readonly object playerIdentityGate = new();
|
||||||
private readonly Dictionary<string, string> playerIdsByPeer = 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(
|
public ServerNetworkHost(
|
||||||
ITransport transport,
|
ITransport transport,
|
||||||
|
|
@ -27,7 +30,8 @@ namespace Network.NetworkHost
|
||||||
IMessageDeliveryPolicyResolver deliveryPolicyResolver = null,
|
IMessageDeliveryPolicyResolver deliveryPolicyResolver = null,
|
||||||
SyncSequenceTracker syncSequenceTracker = null,
|
SyncSequenceTracker syncSequenceTracker = null,
|
||||||
ServerAuthoritativeMovementConfiguration authoritativeMovement = null,
|
ServerAuthoritativeMovementConfiguration authoritativeMovement = null,
|
||||||
ServerAuthoritativeCombatConfiguration authoritativeCombat = null)
|
ServerAuthoritativeCombatConfiguration authoritativeCombat = null,
|
||||||
|
IAuthoritativeMovementWorldValidator authoritativeMovementWorldValidator = null)
|
||||||
{
|
{
|
||||||
this.transport = transport ?? throw new ArgumentNullException(nameof(transport));
|
this.transport = transport ?? throw new ArgumentNullException(nameof(transport));
|
||||||
this.syncTransport = syncTransport;
|
this.syncTransport = syncTransport;
|
||||||
|
|
@ -44,11 +48,14 @@ namespace Network.NetworkHost
|
||||||
deliveryPolicyResolver ?? new DefaultMessageDeliveryPolicyResolver(),
|
deliveryPolicyResolver ?? new DefaultMessageDeliveryPolicyResolver(),
|
||||||
this.syncTransport,
|
this.syncTransport,
|
||||||
syncSequenceTracker ?? new SyncSequenceTracker());
|
syncSequenceTracker ?? new SyncSequenceTracker());
|
||||||
|
var resolvedWorldValidator = authoritativeMovementWorldValidator ?? PermissiveAuthoritativeMovementWorldValidator.Instance;
|
||||||
authoritativeMovementCoordinator = new ServerAuthoritativeMovementCoordinator(
|
authoritativeMovementCoordinator = new ServerAuthoritativeMovementCoordinator(
|
||||||
this,
|
this,
|
||||||
messageManager,
|
messageManager,
|
||||||
authoritativeMovement ?? new ServerAuthoritativeMovementConfiguration());
|
authoritativeMovement ?? new ServerAuthoritativeMovementConfiguration(),
|
||||||
|
resolvedWorldValidator);
|
||||||
authoritativeCombatCoordinator = new ServerAuthoritativeCombatCoordinator(
|
authoritativeCombatCoordinator = new ServerAuthoritativeCombatCoordinator(
|
||||||
|
this,
|
||||||
messageManager,
|
messageManager,
|
||||||
authoritativeMovementCoordinator,
|
authoritativeMovementCoordinator,
|
||||||
authoritativeCombat ?? new ServerAuthoritativeCombatConfiguration());
|
authoritativeCombat ?? new ServerAuthoritativeCombatConfiguration());
|
||||||
|
|
@ -102,6 +109,8 @@ namespace Network.NetworkHost
|
||||||
lock (playerIdentityGate)
|
lock (playerIdentityGate)
|
||||||
{
|
{
|
||||||
playerIdsByPeer.Clear();
|
playerIdsByPeer.Clear();
|
||||||
|
canonicalPeersByPlayerId.Clear();
|
||||||
|
peerKeysByPlayerId.Clear();
|
||||||
}
|
}
|
||||||
PublishMetricsSessionSnapshots();
|
PublishMetricsSessionSnapshots();
|
||||||
}
|
}
|
||||||
|
|
@ -137,6 +146,117 @@ namespace Network.NetworkHost
|
||||||
return authoritativeCombatCoordinator.TryGetState(remoteEndPoint, out state);
|
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)
|
public void NotifyLoginStarted(IPEndPoint remoteEndPoint)
|
||||||
{
|
{
|
||||||
SessionCoordinator.NotifyLoginStarted(remoteEndPoint);
|
SessionCoordinator.NotifyLoginStarted(remoteEndPoint);
|
||||||
|
|
@ -159,7 +279,7 @@ namespace Network.NetworkHost
|
||||||
public void NotifyLoginFailed(IPEndPoint remoteEndPoint, string reason = null)
|
public void NotifyLoginFailed(IPEndPoint remoteEndPoint, string reason = null)
|
||||||
{
|
{
|
||||||
SessionCoordinator.NotifyLoginFailed(remoteEndPoint, reason);
|
SessionCoordinator.NotifyLoginFailed(remoteEndPoint, reason);
|
||||||
ForgetPlayerId(remoteEndPoint);
|
ForgetPeerIdentity(remoteEndPoint);
|
||||||
PublishMetricsSessionSnapshot(remoteEndPoint);
|
PublishMetricsSessionSnapshot(remoteEndPoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -189,20 +309,34 @@ namespace Network.NetworkHost
|
||||||
|
|
||||||
public bool RemoveSession(IPEndPoint remoteEndPoint, string reason = null)
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var removed = SessionCoordinator.RemoveSession(remoteEndPoint, reason);
|
var removed = SessionCoordinator.RemoveSession(acceptedPeer, reason);
|
||||||
if (!removed)
|
if (!removed)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
authoritativeMovementCoordinator.RemoveState(remoteEndPoint);
|
authoritativeMovementCoordinator.RemoveState(acceptedPeer);
|
||||||
authoritativeCombatCoordinator.RemoveState(remoteEndPoint);
|
authoritativeCombatCoordinator.RemoveState(acceptedPeer);
|
||||||
ForgetPlayerId(remoteEndPoint);
|
|
||||||
|
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);
|
RecordMetricsSessionSnapshot(transport, "server-host", session, ConnectionState.Disconnected);
|
||||||
if (syncTransport != null && !ReferenceEquals(syncTransport, transport))
|
if (syncTransport != null && !ReferenceEquals(syncTransport, transport))
|
||||||
|
|
@ -216,10 +350,17 @@ namespace Network.NetworkHost
|
||||||
private void HandleTransportReceive(byte[] data, IPEndPoint sender)
|
private void HandleTransportReceive(byte[] data, IPEndPoint sender)
|
||||||
{
|
{
|
||||||
SessionCoordinator.ObserveTransportActivity(sender);
|
SessionCoordinator.ObserveTransportActivity(sender);
|
||||||
ObservePlayerIdentity(data, sender);
|
|
||||||
PublishMetricsSessionSnapshot(sender);
|
PublishMetricsSessionSnapshot(sender);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void RemoveTransportPeerSession(ITransport transport, IPEndPoint remoteEndPoint)
|
||||||
|
{
|
||||||
|
if (transport is IPeerSessionTransport peerSessionTransport)
|
||||||
|
{
|
||||||
|
peerSessionTransport.RemovePeerSession(remoteEndPoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void BootstrapAuthoritativeMovementState(IPEndPoint remoteEndPoint)
|
private void BootstrapAuthoritativeMovementState(IPEndPoint remoteEndPoint)
|
||||||
{
|
{
|
||||||
if (!TryGetKnownPlayerId(remoteEndPoint, out var playerId))
|
if (!TryGetKnownPlayerId(remoteEndPoint, out var playerId))
|
||||||
|
|
@ -230,41 +371,6 @@ namespace Network.NetworkHost
|
||||||
authoritativeMovementCoordinator.EnsureState(remoteEndPoint, playerId, out _);
|
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)
|
private void RememberPlayerId(IPEndPoint remoteEndPoint, string playerId)
|
||||||
{
|
{
|
||||||
if (remoteEndPoint == null || string.IsNullOrWhiteSpace(playerId))
|
if (remoteEndPoint == null || string.IsNullOrWhiteSpace(playerId))
|
||||||
|
|
@ -272,10 +378,19 @@ namespace Network.NetworkHost
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var key = Normalize(remoteEndPoint).ToString();
|
var normalizedRemoteEndPoint = Normalize(remoteEndPoint);
|
||||||
|
var key = normalizedRemoteEndPoint.ToString();
|
||||||
lock (playerIdentityGate)
|
lock (playerIdentityGate)
|
||||||
{
|
{
|
||||||
playerIdsByPeer[key] = playerId;
|
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)
|
if (remoteEndPoint == null)
|
||||||
{
|
{
|
||||||
|
|
@ -304,10 +463,49 @@ namespace Network.NetworkHost
|
||||||
var key = Normalize(remoteEndPoint).ToString();
|
var key = Normalize(remoteEndPoint).ToString();
|
||||||
lock (playerIdentityGate)
|
lock (playerIdentityGate)
|
||||||
{
|
{
|
||||||
|
if (!playerIdsByPeer.TryGetValue(key, out var playerId))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
playerIdsByPeer.Remove(key);
|
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)
|
private static IPEndPoint Normalize(IPEndPoint remoteEndPoint)
|
||||||
{
|
{
|
||||||
if (remoteEndPoint == null)
|
if (remoteEndPoint == null)
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ namespace Network.NetworkHost
|
||||||
}
|
}
|
||||||
|
|
||||||
ReliablePort = reliablePort;
|
ReliablePort = reliablePort;
|
||||||
|
AuthoritativeMovementWorldValidator = PermissiveAuthoritativeMovementWorldValidator.Instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int ReliablePort { get; }
|
public int ReliablePort { get; }
|
||||||
|
|
@ -36,6 +37,8 @@ namespace Network.NetworkHost
|
||||||
|
|
||||||
public ServerAuthoritativeCombatConfiguration AuthoritativeCombat { get; set; }
|
public ServerAuthoritativeCombatConfiguration AuthoritativeCombat { get; set; }
|
||||||
|
|
||||||
|
public IAuthoritativeMovementWorldValidator AuthoritativeMovementWorldValidator { get; set; }
|
||||||
|
|
||||||
internal void Validate()
|
internal void Validate()
|
||||||
{
|
{
|
||||||
if (ReliablePort <= 0)
|
if (ReliablePort <= 0)
|
||||||
|
|
@ -58,6 +61,10 @@ namespace Network.NetworkHost
|
||||||
|
|
||||||
AuthoritativeMovement?.Validate();
|
AuthoritativeMovement?.Validate();
|
||||||
AuthoritativeCombat?.Validate();
|
AuthoritativeCombat?.Validate();
|
||||||
|
if (AuthoritativeMovementWorldValidator == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(AuthoritativeMovementWorldValidator));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,8 @@ namespace Network.NetworkHost
|
||||||
configuration.SyncSequenceTracker,
|
configuration.SyncSequenceTracker,
|
||||||
configuration.TransportFactory,
|
configuration.TransportFactory,
|
||||||
configuration.AuthoritativeMovement,
|
configuration.AuthoritativeMovement,
|
||||||
configuration.AuthoritativeCombat);
|
configuration.AuthoritativeCombat,
|
||||||
|
configuration.AuthoritativeMovementWorldValidator);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Net;
|
||||||
|
|
||||||
|
namespace Network.NetworkTransport
|
||||||
|
{
|
||||||
|
public class ClientSession
|
||||||
|
{
|
||||||
|
public IPEndPoint EndPoint { get; }
|
||||||
|
public DateTime LastActivity { get; private set; }
|
||||||
|
|
||||||
|
public uint SendSequenceNumber { get; private set; } = 0;
|
||||||
|
|
||||||
|
//TODO: 数据结构——ConcurrentDictionary
|
||||||
|
public ConcurrentDictionary<uint, (Packet packet, DateTime sendTime)> PendingAcks { get; } =
|
||||||
|
new ConcurrentDictionary<uint, (Packet packet, DateTime sendTime)>();
|
||||||
|
|
||||||
|
public uint ExpectedReceiveSequence { get; private set; } = 0;
|
||||||
|
private HashSet<uint> _receivedSequences { get; } = new HashSet<uint>();
|
||||||
|
|
||||||
|
private readonly object _lockObj = new object();
|
||||||
|
|
||||||
|
public ClientSession(IPEndPoint endPoint)
|
||||||
|
{
|
||||||
|
EndPoint = endPoint;
|
||||||
|
LastActivity = DateTime.Now;
|
||||||
|
}
|
||||||
|
|
||||||
|
public uint GetNextSendSequence()
|
||||||
|
{
|
||||||
|
lock (_lockObj)
|
||||||
|
{
|
||||||
|
return SendSequenceNumber++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryProcessReceiveSequence(uint sequenceNumber, out bool shouldDeliver)
|
||||||
|
{
|
||||||
|
lock (_lockObj)
|
||||||
|
{
|
||||||
|
LastActivity = DateTime.Now;
|
||||||
|
|
||||||
|
if (sequenceNumber == ExpectedReceiveSequence)
|
||||||
|
{
|
||||||
|
ExpectedReceiveSequence++;
|
||||||
|
_receivedSequences.Add(sequenceNumber);
|
||||||
|
shouldDeliver = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else if (sequenceNumber < ExpectedReceiveSequence)
|
||||||
|
{
|
||||||
|
shouldDeliver = false;
|
||||||
|
return _receivedSequences.Contains(sequenceNumber);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
shouldDeliver = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c007e0986e972b14cb4b4ab2151459c3
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
using System.Net;
|
||||||
|
|
||||||
|
namespace Network.NetworkTransport
|
||||||
|
{
|
||||||
|
public interface IPeerSessionTransport
|
||||||
|
{
|
||||||
|
bool RemovePeerSession(IPEndPoint remoteEndPoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b2637543abd875349aaa27b3e31f9dbe
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -1,3 +1,11 @@
|
||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: 63fb533d620e4ac0bf15ba0a9a71331e
|
guid: 63fb533d620e4ac0bf15ba0a9a71331e
|
||||||
timeCreated: 1774593005
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
|
@ -10,7 +10,7 @@ using kcp;
|
||||||
|
|
||||||
namespace Network.NetworkTransport
|
namespace Network.NetworkTransport
|
||||||
{
|
{
|
||||||
public partial class KcpTransport : ITransport, ITransportMetricsSink
|
public partial class KcpTransport : ITransport, ITransportMetricsSink, IPeerSessionTransport
|
||||||
{
|
{
|
||||||
private const uint DefaultConv = 1;
|
private const uint DefaultConv = 1;
|
||||||
private const int DefaultNoDelay = 1;
|
private const int DefaultNoDelay = 1;
|
||||||
|
|
@ -233,6 +233,26 @@ namespace Network.NetworkTransport
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool RemovePeerSession(IPEndPoint remoteEndPoint)
|
||||||
|
{
|
||||||
|
if (remoteEndPoint == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(remoteEndPoint));
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalizedEndPoint = NormalizeEndPoint(remoteEndPoint);
|
||||||
|
var key = normalizedEndPoint.ToString();
|
||||||
|
if (!_sessions.TryRemove(key, out var session))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
RecordSessionDiagnostics(session, "removed");
|
||||||
|
session.Dispose();
|
||||||
|
_metricsModule.RecordSessionClosed(session.RemoteEndPoint);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private KcpSession GetOrCreateSession(IPEndPoint remoteEndPoint, uint conv)
|
private KcpSession GetOrCreateSession(IPEndPoint remoteEndPoint, uint conv)
|
||||||
{
|
{
|
||||||
var normalizedEndPoint = NormalizeEndPoint(remoteEndPoint);
|
var normalizedEndPoint = NormalizeEndPoint(remoteEndPoint);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Network.NetworkTransport
|
||||||
|
{
|
||||||
|
public enum PacketType : byte
|
||||||
|
{
|
||||||
|
Data = 1,
|
||||||
|
Ack = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct Packet
|
||||||
|
{
|
||||||
|
public PacketType Type;
|
||||||
|
public uint SequenceNumber;
|
||||||
|
public byte[] Data;
|
||||||
|
|
||||||
|
public byte[] ToBytes()
|
||||||
|
{
|
||||||
|
var result = new byte[1 + 4 + Data.Length];
|
||||||
|
result[0] = (byte)Type;
|
||||||
|
BitConverter.GetBytes(SequenceNumber).CopyTo(result, 1);
|
||||||
|
Data.CopyTo(result, 5);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Packet FromBytes(byte[] data)
|
||||||
|
{
|
||||||
|
return new Packet
|
||||||
|
{
|
||||||
|
Type = (PacketType)data[0],
|
||||||
|
SequenceNumber = BitConverter.ToUInt32(data, 1),
|
||||||
|
//TODO: 结构体——ArraySegment
|
||||||
|
Data = new ArraySegment<byte>(data, 5, data.Length - 5).ToArray()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Packet CreateDataPacket(uint seqNum, byte[] data)
|
||||||
|
{
|
||||||
|
return new Packet
|
||||||
|
{
|
||||||
|
Type = PacketType.Data,
|
||||||
|
SequenceNumber = seqNum,
|
||||||
|
Data = data
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Packet CreateAckPacket(uint seqNum)
|
||||||
|
{
|
||||||
|
return new Packet
|
||||||
|
{
|
||||||
|
Type = PacketType.Ack,
|
||||||
|
SequenceNumber = seqNum,
|
||||||
|
Data = Array.Empty<byte>()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: a16c0628d84e6274bbe164e0e57533e4
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,312 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Network.NetworkTransport
|
||||||
|
{
|
||||||
|
internal sealed class LegacyUdpTransportAdapter : ITransport, IPeerSessionTransport
|
||||||
|
{
|
||||||
|
private readonly UdpClient _client;
|
||||||
|
private readonly IPEndPoint? _defaultRemoteEndPoint;
|
||||||
|
private readonly bool _isServer;
|
||||||
|
|
||||||
|
private readonly ConcurrentDictionary<string, ClientSession> _sessions = new ConcurrentDictionary<string, ClientSession>();
|
||||||
|
private readonly ConcurrentDictionary<Packet, int> _resentPacketTimes = new ConcurrentDictionary<Packet, int>();
|
||||||
|
private readonly Timer _retransmitTimer;
|
||||||
|
private readonly Timer _cleanupTimer;
|
||||||
|
|
||||||
|
//TODO: volatile 关键字
|
||||||
|
private volatile bool _isRunning;
|
||||||
|
|
||||||
|
// 配置参数
|
||||||
|
private const int RetransmitTimeoutMs = 1000;
|
||||||
|
private const int SessionTimeoutMs = 30000;
|
||||||
|
private const int MaxRetransmitAttempts = 5;
|
||||||
|
|
||||||
|
public event Action<byte[], IPEndPoint>? OnReceive;
|
||||||
|
|
||||||
|
// 构造函数——服务端模式
|
||||||
|
public LegacyUdpTransportAdapter(int listenPort)
|
||||||
|
{
|
||||||
|
_client = new UdpClient(listenPort);
|
||||||
|
_isServer = true;
|
||||||
|
_retransmitTimer = new Timer(CheckRetransmit, null, 100, 100);
|
||||||
|
_cleanupTimer = new Timer(CleanupSessions, null, 5000, 5000);
|
||||||
|
Console.WriteLine($"[Transport] 服务端模式,监听端口: {listenPort}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构造函数——客户端模式
|
||||||
|
public LegacyUdpTransportAdapter(string serverIP, int serverPort)
|
||||||
|
{
|
||||||
|
_client = new UdpClient(0);
|
||||||
|
_defaultRemoteEndPoint = new IPEndPoint(IPAddress.Parse(serverIP), serverPort);
|
||||||
|
|
||||||
|
_isServer = false;
|
||||||
|
_retransmitTimer = new Timer(CheckRetransmit, null, 100, 100);
|
||||||
|
_cleanupTimer = new Timer(CleanupSessions, null, 5000, 5000);
|
||||||
|
Console.WriteLine($"[Transport] 客户端模式,目标: {_defaultRemoteEndPoint}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task StartAsync()
|
||||||
|
{
|
||||||
|
_sessions.Clear();
|
||||||
|
|
||||||
|
_isRunning = true;
|
||||||
|
Console.WriteLine("[Transport] 传输层启动");
|
||||||
|
|
||||||
|
// 开始接收数据
|
||||||
|
_ = Task.Run(ReceiveLoop);
|
||||||
|
await Task.Delay(100); // 给接收循环一点启动时间
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Stop()
|
||||||
|
{
|
||||||
|
_isRunning = false;
|
||||||
|
_retransmitTimer.Dispose();
|
||||||
|
_cleanupTimer.Dispose();
|
||||||
|
_client.Close();
|
||||||
|
_sessions.Clear();
|
||||||
|
Console.WriteLine("[Transport] 传输层停止");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Send(byte[] data)
|
||||||
|
{
|
||||||
|
if (!_isServer && _defaultRemoteEndPoint != null)
|
||||||
|
{
|
||||||
|
SendTo(data, _defaultRemoteEndPoint);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("服务端模式必须使用 SendTo 指定目标");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SendTo(byte[] data, IPEndPoint target)
|
||||||
|
{
|
||||||
|
if (!_isRunning)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var session = GetOrCreateSession(target);
|
||||||
|
uint seqNum = session.GetNextSendSequence();
|
||||||
|
var packet = Packet.CreateDataPacket(seqNum, data);
|
||||||
|
|
||||||
|
session.PendingAcks[seqNum] = (packet, DateTime.Now);
|
||||||
|
|
||||||
|
SendPacketTo(packet, target);
|
||||||
|
Console.WriteLine($"[Transport] 发送数据包到 {target} SeqNum={seqNum}, DataLen={data.Length}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SendToAll(byte[] data)
|
||||||
|
{
|
||||||
|
foreach (var session in _sessions.Values)
|
||||||
|
{
|
||||||
|
SendTo(data, session.EndPoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool RemovePeerSession(IPEndPoint remoteEndPoint)
|
||||||
|
{
|
||||||
|
if (remoteEndPoint == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(remoteEndPoint));
|
||||||
|
}
|
||||||
|
|
||||||
|
return _sessions.TryRemove(remoteEndPoint.ToString(), out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void ReceiveLoop()
|
||||||
|
{
|
||||||
|
while (_isRunning)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await _client.ReceiveAsync();
|
||||||
|
var packet = Packet.FromBytes(result.Buffer);
|
||||||
|
|
||||||
|
if (packet.Type == PacketType.Data)
|
||||||
|
{
|
||||||
|
HandleDataPacket(packet, result.RemoteEndPoint);
|
||||||
|
}
|
||||||
|
else if (packet.Type == PacketType.Ack)
|
||||||
|
{
|
||||||
|
HandleAckPacket(packet, result.RemoteEndPoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
return; // 正常关闭
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[Transport] 接收错误:{e.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleDataPacket(Packet packet, IPEndPoint senderEndPoint)
|
||||||
|
{
|
||||||
|
var session = GetOrCreateSession(senderEndPoint);
|
||||||
|
|
||||||
|
Console.WriteLine(
|
||||||
|
$"[Transport] 收到数据包从{senderEndPoint} SeqNum={packet.SequenceNumber}, DataLen={packet.Data.Length}");
|
||||||
|
|
||||||
|
// 发送ACK
|
||||||
|
var ackPacket = Packet.CreateAckPacket(packet.SequenceNumber);
|
||||||
|
SendPacketTo(ackPacket, senderEndPoint);
|
||||||
|
Console.WriteLine($"[Transport] 发送ACK 到 {senderEndPoint} SeqNum={packet.SequenceNumber}");
|
||||||
|
|
||||||
|
// 检查是否应该交付
|
||||||
|
if (session.TryProcessReceiveSequence(packet.SequenceNumber, out bool shouldDeliver))
|
||||||
|
{
|
||||||
|
if (shouldDeliver)
|
||||||
|
{
|
||||||
|
OnReceive?.Invoke(packet.Data, senderEndPoint);
|
||||||
|
Console.WriteLine($"[Transport] 交付数据包从 {senderEndPoint} SeqNum={packet.SequenceNumber}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[Transport] 重复包从 {senderEndPoint} SeqNum={packet.SequenceNumber},忽略");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 乱序到达,暂存(简化处理:直接丢弃,依赖重传)
|
||||||
|
Console.WriteLine($"[Transport] 乱序包从 {senderEndPoint} SeqNum={packet.SequenceNumber},丢弃");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleAckPacket(Packet packet, IPEndPoint senderEndPoint)
|
||||||
|
{
|
||||||
|
var session = GetOrCreateSession(senderEndPoint);
|
||||||
|
Console.WriteLine($"[Transport] 收到ACK从 {senderEndPoint} SeqNum={packet.SequenceNumber}");
|
||||||
|
|
||||||
|
if (session.PendingAcks.TryRemove(packet.SequenceNumber, out _))
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[Transport] 确认包到 {senderEndPoint} SeqNum={packet.SequenceNumber}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ClientSession GetOrCreateSession(IPEndPoint endPoint)
|
||||||
|
{
|
||||||
|
string key = endPoint.ToString();
|
||||||
|
return _sessions.GetOrAdd(key, _ =>
|
||||||
|
{
|
||||||
|
var session = new ClientSession(endPoint);
|
||||||
|
Console.WriteLine($"创建新会话:{endPoint}");
|
||||||
|
return session;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CheckRetransmit(object? state)
|
||||||
|
{
|
||||||
|
if (!_isRunning)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var now = DateTime.Now;
|
||||||
|
var toRetransmit = new List<(IPEndPoint target, uint seqNum, Packet packet)>();
|
||||||
|
|
||||||
|
foreach (var sessionKvp in _sessions)
|
||||||
|
{
|
||||||
|
var session = sessionKvp.Value;
|
||||||
|
foreach (var ackKvp in session.PendingAcks)
|
||||||
|
{
|
||||||
|
var timeSinceLastSend = now - ackKvp.Value.sendTime;
|
||||||
|
if (timeSinceLastSend.TotalMilliseconds > RetransmitTimeoutMs)
|
||||||
|
{
|
||||||
|
toRetransmit.Add((session.EndPoint, ackKvp.Key, ackKvp.Value.packet));
|
||||||
|
_resentPacketTimes.TryAdd(ackKvp.Value.packet, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
foreach (var (target, seqNum, packet) in toRetransmit)
|
||||||
|
{
|
||||||
|
var session = GetOrCreateSession(target);
|
||||||
|
if (session.PendingAcks.ContainsKey(seqNum))
|
||||||
|
{
|
||||||
|
// 更新发送时间
|
||||||
|
session.PendingAcks[seqNum] = (packet, now);
|
||||||
|
SendPacketTo(packet, target);
|
||||||
|
Console.WriteLine($"[Transport] 重传包到 {target} SeqNum={seqNum}");
|
||||||
|
|
||||||
|
_resentPacketTimes[packet]++;
|
||||||
|
if (_resentPacketTimes[packet] >= MaxRetransmitAttempts)
|
||||||
|
{
|
||||||
|
// 达到最大重传次数,放弃该会话
|
||||||
|
Console.WriteLine($"[Transport] 达到最大重传次数,放弃会话 {target}");
|
||||||
|
_sessions.TryRemove(target.ToString(), out _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CleanupSessions(object? state)
|
||||||
|
{
|
||||||
|
if (!_isRunning)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var now = DateTime.Now;
|
||||||
|
var toRemove = new List<string>();
|
||||||
|
|
||||||
|
foreach (var sessionKvp in _sessions)
|
||||||
|
{
|
||||||
|
var session = sessionKvp.Value;
|
||||||
|
var timeSinceLastActivity = now - session.LastActivity;
|
||||||
|
|
||||||
|
if (timeSinceLastActivity.TotalMilliseconds > SessionTimeoutMs)
|
||||||
|
{
|
||||||
|
toRemove.Add(sessionKvp.Key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (string key in toRemove)
|
||||||
|
{
|
||||||
|
//TODO: 清理会话的同时清理PlayerManager中的玩家数据
|
||||||
|
if (_sessions.TryRemove(key, out var session))
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[Transport] 清理超时会话:{session.EndPoint}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_isServer)
|
||||||
|
{
|
||||||
|
PrintSessionInfo();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void SendPacketTo(Packet packet, IPEndPoint? endPoint)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var data = packet.ToBytes();
|
||||||
|
await _client.SendAsync(data, data.Length, endPoint);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[Transport] 发送错误:{e.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PrintSessionInfo()
|
||||||
|
{
|
||||||
|
Console.WriteLine($"当前活跃会话数:{_sessions.Count}");
|
||||||
|
foreach (var sessionKvp in _sessions)
|
||||||
|
{
|
||||||
|
var session = sessionKvp.Value;
|
||||||
|
Console.WriteLine(
|
||||||
|
$" 会话:{session.EndPoint},发送SeqNum:{session.SendSequenceNumber},期望接收:{session.ExpectedReceiveSequence},待确认: {session.PendingAcks.Count}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: e740e71ab20f62d40bf5f56c2142bf02
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -8,4 +8,4 @@ MonoImporter:
|
||||||
icon: {instanceID: 0}
|
icon: {instanceID: 0}
|
||||||
userData:
|
userData:
|
||||||
assetBundleName:
|
assetBundleName:
|
||||||
assetBundleVariant:
|
assetBundleVariant:
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,11 @@ namespace Tests.EditMode.Network
|
||||||
clientRuntime.StartAsync().GetAwaiter().GetResult();
|
clientRuntime.StartAsync().GetAwaiter().GetResult();
|
||||||
using var serverRuntime = ServerRuntimeEntryPoint.StartAsync(configuration).GetAwaiter().GetResult();
|
using var serverRuntime = ServerRuntimeEntryPoint.StartAsync(configuration).GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
serverRuntime.Host.NotifyLoginStarted(ClientPeer);
|
||||||
|
serverRuntime.Host.NotifyLoginSucceeded(ClientPeer, "player-a");
|
||||||
|
serverRuntime.Host.NotifyLoginStarted(RemotePeer);
|
||||||
|
serverRuntime.Host.NotifyLoginSucceeded(RemotePeer, "player-b");
|
||||||
|
|
||||||
serverTransports[9001].EmitReceive(
|
serverTransports[9001].EmitReceive(
|
||||||
GameplayFlowTestSupport.BuildEnvelope(
|
GameplayFlowTestSupport.BuildEnvelope(
|
||||||
MessageType.MoveInput,
|
MessageType.MoveInput,
|
||||||
|
|
@ -56,8 +61,8 @@ namespace Tests.EditMode.Network
|
||||||
{
|
{
|
||||||
PlayerId = "player-b",
|
PlayerId = "player-b",
|
||||||
Tick = 1,
|
Tick = 1,
|
||||||
MoveX = 0f,
|
TurnInput = 0f,
|
||||||
MoveY = 0f
|
ThrottleInput = 0f
|
||||||
}),
|
}),
|
||||||
RemotePeer);
|
RemotePeer);
|
||||||
serverRuntime.DrainPendingMessagesAsync().GetAwaiter().GetResult();
|
serverRuntime.DrainPendingMessagesAsync().GetAwaiter().GetResult();
|
||||||
|
|
@ -69,8 +74,8 @@ namespace Tests.EditMode.Network
|
||||||
{
|
{
|
||||||
PlayerId = "player-a",
|
PlayerId = "player-a",
|
||||||
Tick = 1,
|
Tick = 1,
|
||||||
MoveX = 1f,
|
TurnInput = 0f,
|
||||||
MoveY = 0f
|
ThrottleInput = 1f
|
||||||
},
|
},
|
||||||
MessageType.MoveInput);
|
MessageType.MoveInput);
|
||||||
ClientGameplayInputFlow.SendShootInput(
|
ClientGameplayInputFlow.SendShootInput(
|
||||||
|
|
@ -162,9 +167,9 @@ namespace Tests.EditMode.Network
|
||||||
RemotePeer);
|
RemotePeer);
|
||||||
|
|
||||||
serverRuntime.Host.NotifyLoginStarted(ClientPeer);
|
serverRuntime.Host.NotifyLoginStarted(ClientPeer);
|
||||||
serverRuntime.Host.NotifyLoginSucceeded(ClientPeer);
|
serverRuntime.Host.NotifyLoginSucceeded(ClientPeer, "player-a");
|
||||||
serverRuntime.Host.NotifyLoginStarted(RemotePeer);
|
serverRuntime.Host.NotifyLoginStarted(RemotePeer);
|
||||||
serverRuntime.Host.NotifyLoginSucceeded(RemotePeer);
|
serverRuntime.Host.NotifyLoginSucceeded(RemotePeer, "player-b");
|
||||||
serverRuntime.UpdateAuthoritativeMovement(TimeSpan.FromMilliseconds(50));
|
serverRuntime.UpdateAuthoritativeMovement(TimeSpan.FromMilliseconds(50));
|
||||||
|
|
||||||
TransferBroadcastMessages(serverTransports[9001], clientSyncTransport, ServerSender);
|
TransferBroadcastMessages(serverTransports[9001], clientSyncTransport, ServerSender);
|
||||||
|
|
@ -186,8 +191,8 @@ namespace Tests.EditMode.Network
|
||||||
{
|
{
|
||||||
PlayerId = "player-b",
|
PlayerId = "player-b",
|
||||||
Tick = 1,
|
Tick = 1,
|
||||||
MoveX = 1f,
|
TurnInput = 0f,
|
||||||
MoveY = 0f
|
ThrottleInput = 1f
|
||||||
}),
|
}),
|
||||||
RemotePeer);
|
RemotePeer);
|
||||||
serverRuntime.DrainPendingMessagesAsync().GetAwaiter().GetResult();
|
serverRuntime.DrainPendingMessagesAsync().GetAwaiter().GetResult();
|
||||||
|
|
|
||||||
|
|
@ -68,8 +68,8 @@ namespace Tests.EditMode.Network
|
||||||
{
|
{
|
||||||
PlayerId = "player-1",
|
PlayerId = "player-1",
|
||||||
Tick = 12,
|
Tick = 12,
|
||||||
MoveX = 1,
|
TurnInput = 1,
|
||||||
MoveY = -1
|
ThrottleInput = -1
|
||||||
};
|
};
|
||||||
|
|
||||||
manager.SendMessage(message, MessageType.MoveInput);
|
manager.SendMessage(message, MessageType.MoveInput);
|
||||||
|
|
@ -82,8 +82,8 @@ namespace Tests.EditMode.Network
|
||||||
Assert.That(envelope.Type, Is.EqualTo((int)MessageType.MoveInput));
|
Assert.That(envelope.Type, Is.EqualTo((int)MessageType.MoveInput));
|
||||||
Assert.That(parsed.PlayerId, Is.EqualTo("player-1"));
|
Assert.That(parsed.PlayerId, Is.EqualTo("player-1"));
|
||||||
Assert.That(parsed.Tick, Is.EqualTo(12));
|
Assert.That(parsed.Tick, Is.EqualTo(12));
|
||||||
Assert.That(parsed.MoveX, Is.EqualTo(1));
|
Assert.That(parsed.TurnInput, Is.EqualTo(1));
|
||||||
Assert.That(parsed.MoveY, Is.EqualTo(-1));
|
Assert.That(parsed.ThrottleInput, Is.EqualTo(-1));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|
@ -100,8 +100,8 @@ namespace Tests.EditMode.Network
|
||||||
{
|
{
|
||||||
PlayerId = "player-1",
|
PlayerId = "player-1",
|
||||||
Tick = 13,
|
Tick = 13,
|
||||||
MoveX = 0f,
|
TurnInput = 0f,
|
||||||
MoveY = 0f
|
ThrottleInput = 0f
|
||||||
};
|
};
|
||||||
|
|
||||||
manager.SendMessage(message, MessageType.MoveInput);
|
manager.SendMessage(message, MessageType.MoveInput);
|
||||||
|
|
@ -114,8 +114,8 @@ namespace Tests.EditMode.Network
|
||||||
Assert.That(envelope.Type, Is.EqualTo((int)MessageType.MoveInput));
|
Assert.That(envelope.Type, Is.EqualTo((int)MessageType.MoveInput));
|
||||||
Assert.That(parsed.PlayerId, Is.EqualTo("player-1"));
|
Assert.That(parsed.PlayerId, Is.EqualTo("player-1"));
|
||||||
Assert.That(parsed.Tick, Is.EqualTo(13));
|
Assert.That(parsed.Tick, Is.EqualTo(13));
|
||||||
Assert.That(parsed.MoveX, Is.EqualTo(0f));
|
Assert.That(parsed.TurnInput, Is.EqualTo(0f));
|
||||||
Assert.That(parsed.MoveY, Is.EqualTo(0f));
|
Assert.That(parsed.ThrottleInput, Is.EqualTo(0f));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|
@ -350,12 +350,12 @@ namespace Tests.EditMode.Network
|
||||||
});
|
});
|
||||||
|
|
||||||
transport.EmitReceive(
|
transport.EmitReceive(
|
||||||
BuildEnvelope(MessageType.MoveInput, new MoveInput { PlayerId = "player-1", Tick = 8, MoveX = 1 }),
|
BuildEnvelope(MessageType.MoveInput, new MoveInput { PlayerId = "player-1", Tick = 8, ThrottleInput = 1 }),
|
||||||
Sender);
|
Sender);
|
||||||
manager.DrainPendingMessagesAsync().GetAwaiter().GetResult();
|
manager.DrainPendingMessagesAsync().GetAwaiter().GetResult();
|
||||||
|
|
||||||
transport.EmitReceive(
|
transport.EmitReceive(
|
||||||
BuildEnvelope(MessageType.MoveInput, new MoveInput { PlayerId = "player-1", Tick = 6, MoveX = -1 }),
|
BuildEnvelope(MessageType.MoveInput, new MoveInput { PlayerId = "player-1", Tick = 6, ThrottleInput = -1 }),
|
||||||
Sender);
|
Sender);
|
||||||
manager.DrainPendingMessagesAsync().GetAwaiter().GetResult();
|
manager.DrainPendingMessagesAsync().GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,8 +39,8 @@ namespace Tests.EditMode.Network
|
||||||
|
|
||||||
using var runtime = ServerRuntimeEntryPoint.StartAsync(configuration).GetAwaiter().GetResult();
|
using var runtime = ServerRuntimeEntryPoint.StartAsync(configuration).GetAwaiter().GetResult();
|
||||||
|
|
||||||
PrimePlayer(createdTransports[9000], PeerA, "player-a", 1);
|
PrimePlayer(runtime, PeerA, "player-a");
|
||||||
PrimePlayer(createdTransports[9000], PeerB, "player-b", 1);
|
PrimePlayer(runtime, PeerB, "player-b");
|
||||||
runtime.DrainPendingMessagesAsync().GetAwaiter().GetResult();
|
runtime.DrainPendingMessagesAsync().GetAwaiter().GetResult();
|
||||||
|
|
||||||
createdTransports[9000].EmitReceive(BuildEnvelope(MessageType.ShootInput, new ShootInput
|
createdTransports[9000].EmitReceive(BuildEnvelope(MessageType.ShootInput, new ShootInput
|
||||||
|
|
@ -111,8 +111,8 @@ namespace Tests.EditMode.Network
|
||||||
|
|
||||||
using var runtime = ServerRuntimeEntryPoint.StartAsync(configuration).GetAwaiter().GetResult();
|
using var runtime = ServerRuntimeEntryPoint.StartAsync(configuration).GetAwaiter().GetResult();
|
||||||
|
|
||||||
PrimePlayer(createdTransports[9001], PeerA, "player-a", 1);
|
PrimePlayer(runtime, PeerA, "player-a");
|
||||||
PrimePlayer(createdTransports[9001], PeerB, "player-b", 1);
|
PrimePlayer(runtime, PeerB, "player-b");
|
||||||
runtime.DrainPendingMessagesAsync().GetAwaiter().GetResult();
|
runtime.DrainPendingMessagesAsync().GetAwaiter().GetResult();
|
||||||
|
|
||||||
createdTransports[9000].EmitReceive(BuildEnvelope(MessageType.ShootInput, new ShootInput
|
createdTransports[9000].EmitReceive(BuildEnvelope(MessageType.ShootInput, new ShootInput
|
||||||
|
|
@ -166,8 +166,8 @@ namespace Tests.EditMode.Network
|
||||||
|
|
||||||
using var runtime = ServerRuntimeEntryPoint.StartAsync(configuration).GetAwaiter().GetResult();
|
using var runtime = ServerRuntimeEntryPoint.StartAsync(configuration).GetAwaiter().GetResult();
|
||||||
|
|
||||||
PrimePlayer(createdTransports[9000], PeerA, "player-a", 1);
|
PrimePlayer(runtime, PeerA, "player-a");
|
||||||
PrimePlayer(createdTransports[9000], PeerB, "player-b", 1);
|
PrimePlayer(runtime, PeerB, "player-b");
|
||||||
runtime.DrainPendingMessagesAsync().GetAwaiter().GetResult();
|
runtime.DrainPendingMessagesAsync().GetAwaiter().GetResult();
|
||||||
|
|
||||||
createdTransports[9000].EmitReceive(BuildEnvelope(MessageType.ShootInput, new ShootInput
|
createdTransports[9000].EmitReceive(BuildEnvelope(MessageType.ShootInput, new ShootInput
|
||||||
|
|
@ -192,15 +192,10 @@ namespace Tests.EditMode.Network
|
||||||
Assert.That(runtime.AuthoritativeCombatStates.Count, Is.EqualTo(0));
|
Assert.That(runtime.AuthoritativeCombatStates.Count, Is.EqualTo(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void PrimePlayer(FakeTransport transport, IPEndPoint peer, string playerId, long tick)
|
private static void PrimePlayer(ServerRuntimeHandle runtime, IPEndPoint peer, string playerId)
|
||||||
{
|
{
|
||||||
transport.EmitReceive(BuildEnvelope(MessageType.MoveInput, new MoveInput
|
runtime.Host.NotifyLoginStarted(peer);
|
||||||
{
|
runtime.Host.NotifyLoginSucceeded(peer, playerId);
|
||||||
PlayerId = playerId,
|
|
||||||
Tick = tick,
|
|
||||||
MoveX = 0f,
|
|
||||||
MoveY = 0f
|
|
||||||
}), peer);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static FakeTransport CreateTransport(IDictionary<int, FakeTransport> createdTransports, int port)
|
private static FakeTransport CreateTransport(IDictionary<int, FakeTransport> createdTransports, int port)
|
||||||
|
|
|
||||||
|
|
@ -33,26 +33,31 @@ namespace Tests.EditMode.Network
|
||||||
|
|
||||||
using var runtime = ServerRuntimeEntryPoint.StartAsync(configuration).GetAwaiter().GetResult();
|
using var runtime = ServerRuntimeEntryPoint.StartAsync(configuration).GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
runtime.Host.NotifyLoginStarted(PeerA);
|
||||||
|
runtime.Host.NotifyLoginSucceeded(PeerA, "player-a");
|
||||||
|
runtime.Host.NotifyLoginStarted(PeerB);
|
||||||
|
runtime.Host.NotifyLoginSucceeded(PeerB, "player-b");
|
||||||
|
|
||||||
createdTransports[9000].EmitReceive(BuildEnvelope(MessageType.MoveInput, new MoveInput
|
createdTransports[9000].EmitReceive(BuildEnvelope(MessageType.MoveInput, new MoveInput
|
||||||
{
|
{
|
||||||
PlayerId = "player-a",
|
PlayerId = "player-a",
|
||||||
Tick = 10,
|
Tick = 10,
|
||||||
MoveX = 1f,
|
TurnInput = 0f,
|
||||||
MoveY = 0f
|
ThrottleInput = 1f
|
||||||
}), PeerA);
|
}), PeerA);
|
||||||
createdTransports[9000].EmitReceive(BuildEnvelope(MessageType.MoveInput, new MoveInput
|
createdTransports[9000].EmitReceive(BuildEnvelope(MessageType.MoveInput, new MoveInput
|
||||||
{
|
{
|
||||||
PlayerId = "player-a",
|
PlayerId = "player-a",
|
||||||
Tick = 8,
|
Tick = 8,
|
||||||
MoveX = 0f,
|
TurnInput = 1f,
|
||||||
MoveY = 1f
|
ThrottleInput = 0f
|
||||||
}), PeerA);
|
}), PeerA);
|
||||||
createdTransports[9000].EmitReceive(BuildEnvelope(MessageType.MoveInput, new MoveInput
|
createdTransports[9000].EmitReceive(BuildEnvelope(MessageType.MoveInput, new MoveInput
|
||||||
{
|
{
|
||||||
PlayerId = "player-b",
|
PlayerId = "player-b",
|
||||||
Tick = 3,
|
Tick = 3,
|
||||||
MoveX = 0f,
|
TurnInput = 0f,
|
||||||
MoveY = 1f
|
ThrottleInput = -1f
|
||||||
}), PeerB);
|
}), PeerB);
|
||||||
|
|
||||||
runtime.DrainPendingMessagesAsync().GetAwaiter().GetResult();
|
runtime.DrainPendingMessagesAsync().GetAwaiter().GetResult();
|
||||||
|
|
@ -66,8 +71,8 @@ namespace Tests.EditMode.Network
|
||||||
Assert.That(stateA.PositionZ, Is.EqualTo(0f).Within(0.0001f));
|
Assert.That(stateA.PositionZ, Is.EqualTo(0f).Within(0.0001f));
|
||||||
Assert.That(stateB.PlayerId, Is.EqualTo("player-b"));
|
Assert.That(stateB.PlayerId, Is.EqualTo("player-b"));
|
||||||
Assert.That(stateB.LastAcceptedMoveTick, Is.EqualTo(3));
|
Assert.That(stateB.LastAcceptedMoveTick, Is.EqualTo(3));
|
||||||
Assert.That(stateB.PositionX, Is.EqualTo(0f).Within(0.0001f));
|
Assert.That(stateB.PositionX, Is.EqualTo(-0.2f).Within(0.0001f));
|
||||||
Assert.That(stateB.PositionZ, Is.EqualTo(0.2f).Within(0.0001f));
|
Assert.That(stateB.PositionZ, Is.EqualTo(0f).Within(0.0001f));
|
||||||
Assert.That(createdTransports[9000].BroadcastMessages.Count, Is.EqualTo(2));
|
Assert.That(createdTransports[9000].BroadcastMessages.Count, Is.EqualTo(2));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -89,12 +94,15 @@ namespace Tests.EditMode.Network
|
||||||
|
|
||||||
using var runtime = ServerRuntimeEntryPoint.StartAsync(configuration).GetAwaiter().GetResult();
|
using var runtime = ServerRuntimeEntryPoint.StartAsync(configuration).GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
runtime.Host.NotifyLoginStarted(PeerA);
|
||||||
|
runtime.Host.NotifyLoginSucceeded(PeerA, "player-a");
|
||||||
|
|
||||||
createdTransports[9001].EmitReceive(BuildEnvelope(MessageType.MoveInput, new MoveInput
|
createdTransports[9001].EmitReceive(BuildEnvelope(MessageType.MoveInput, new MoveInput
|
||||||
{
|
{
|
||||||
PlayerId = "player-a",
|
PlayerId = "player-a",
|
||||||
Tick = 1,
|
Tick = 1,
|
||||||
MoveX = 1f,
|
TurnInput = 0f,
|
||||||
MoveY = 0f
|
ThrottleInput = 1f
|
||||||
}), PeerA);
|
}), PeerA);
|
||||||
|
|
||||||
runtime.DrainPendingMessagesAsync().GetAwaiter().GetResult();
|
runtime.DrainPendingMessagesAsync().GetAwaiter().GetResult();
|
||||||
|
|
@ -118,8 +126,8 @@ namespace Tests.EditMode.Network
|
||||||
{
|
{
|
||||||
PlayerId = "player-a",
|
PlayerId = "player-a",
|
||||||
Tick = 2,
|
Tick = 2,
|
||||||
MoveX = 0f,
|
TurnInput = 0f,
|
||||||
MoveY = 0f
|
ThrottleInput = 0f
|
||||||
}), PeerA);
|
}), PeerA);
|
||||||
|
|
||||||
runtime.DrainPendingMessagesAsync().GetAwaiter().GetResult();
|
runtime.DrainPendingMessagesAsync().GetAwaiter().GetResult();
|
||||||
|
|
@ -164,7 +172,7 @@ namespace Tests.EditMode.Network
|
||||||
}), PeerA);
|
}), PeerA);
|
||||||
|
|
||||||
runtime.Host.NotifyLoginStarted(PeerA);
|
runtime.Host.NotifyLoginStarted(PeerA);
|
||||||
runtime.Host.NotifyLoginSucceeded(PeerA);
|
runtime.Host.NotifyLoginSucceeded(PeerA, "player-a");
|
||||||
runtime.UpdateAuthoritativeMovement(TimeSpan.FromMilliseconds(50));
|
runtime.UpdateAuthoritativeMovement(TimeSpan.FromMilliseconds(50));
|
||||||
|
|
||||||
Assert.That(runtime.TryGetAuthoritativeMovementState(PeerA, out var state), Is.True);
|
Assert.That(runtime.TryGetAuthoritativeMovementState(PeerA, out var state), Is.True);
|
||||||
|
|
@ -202,12 +210,15 @@ namespace Tests.EditMode.Network
|
||||||
|
|
||||||
using var runtime = ServerRuntimeEntryPoint.StartAsync(configuration).GetAwaiter().GetResult();
|
using var runtime = ServerRuntimeEntryPoint.StartAsync(configuration).GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
runtime.Host.NotifyLoginStarted(PeerA);
|
||||||
|
runtime.Host.NotifyLoginSucceeded(PeerA, "player-a");
|
||||||
|
|
||||||
createdTransports[9000].EmitReceive(BuildEnvelope(MessageType.MoveInput, new MoveInput
|
createdTransports[9000].EmitReceive(BuildEnvelope(MessageType.MoveInput, new MoveInput
|
||||||
{
|
{
|
||||||
PlayerId = "player-a",
|
PlayerId = "player-a",
|
||||||
Tick = 5,
|
Tick = 5,
|
||||||
MoveX = 0f,
|
TurnInput = 0f,
|
||||||
MoveY = -1f
|
ThrottleInput = -1f
|
||||||
}), PeerA);
|
}), PeerA);
|
||||||
|
|
||||||
runtime.DrainPendingMessagesAsync().GetAwaiter().GetResult();
|
runtime.DrainPendingMessagesAsync().GetAwaiter().GetResult();
|
||||||
|
|
@ -217,9 +228,9 @@ namespace Tests.EditMode.Network
|
||||||
|
|
||||||
var broadcast = ParsePlayerState(createdTransports[9000].BroadcastMessages[0]);
|
var broadcast = ParsePlayerState(createdTransports[9000].BroadcastMessages[0]);
|
||||||
Assert.That(broadcast.Tick, Is.EqualTo(1));
|
Assert.That(broadcast.Tick, Is.EqualTo(1));
|
||||||
Assert.That(broadcast.Position.X, Is.EqualTo(0f).Within(0.0001f));
|
Assert.That(broadcast.Position.X, Is.EqualTo(-0.3f).Within(0.0001f));
|
||||||
Assert.That(broadcast.Position.Z, Is.EqualTo(-0.3f).Within(0.0001f));
|
Assert.That(broadcast.Position.Z, Is.EqualTo(0f).Within(0.0001f));
|
||||||
Assert.That(broadcast.Velocity.Z, Is.EqualTo(-6f).Within(0.0001f));
|
Assert.That(broadcast.Velocity.X, Is.EqualTo(-6f).Within(0.0001f));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static FakeTransport CreateTransport(IDictionary<int, FakeTransport> createdTransports, int port)
|
private static FakeTransport CreateTransport(IDictionary<int, FakeTransport> createdTransports, int port)
|
||||||
|
|
|
||||||
|
|
@ -196,6 +196,10 @@ namespace Tests.EditMode.Network
|
||||||
host.StartAsync().GetAwaiter().GetResult();
|
host.StartAsync().GetAwaiter().GetResult();
|
||||||
transport.EmitReceive(CreateEnvelope(MessageType.Heartbeat), peerA);
|
transport.EmitReceive(CreateEnvelope(MessageType.Heartbeat), peerA);
|
||||||
transport.EmitReceive(CreateEnvelope(MessageType.Heartbeat), peerB);
|
transport.EmitReceive(CreateEnvelope(MessageType.Heartbeat), peerB);
|
||||||
|
host.NotifyLoginStarted(peerA);
|
||||||
|
host.NotifyLoginSucceeded(peerA, "player-a");
|
||||||
|
host.NotifyLoginStarted(peerB);
|
||||||
|
host.NotifyLoginSucceeded(peerB, "player-b");
|
||||||
|
|
||||||
var removed = host.RemoveSession(peerA, "peer closed");
|
var removed = host.RemoveSession(peerA, "peer closed");
|
||||||
|
|
||||||
|
|
@ -203,7 +207,7 @@ namespace Tests.EditMode.Network
|
||||||
Assert.That(host.ManagedSessions.Count, Is.EqualTo(1));
|
Assert.That(host.ManagedSessions.Count, Is.EqualTo(1));
|
||||||
Assert.That(host.TryGetSession(peerA, out _), Is.False);
|
Assert.That(host.TryGetSession(peerA, out _), Is.False);
|
||||||
Assert.That(host.TryGetSession(peerB, out var sessionB), Is.True);
|
Assert.That(host.TryGetSession(peerB, out var sessionB), Is.True);
|
||||||
Assert.That(sessionB.SessionManager.State, Is.EqualTo(ConnectionState.TransportConnected));
|
Assert.That(sessionB.SessionManager.State, Is.EqualTo(ConnectionState.LoggedIn));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|
|
||||||
|
|
@ -110,8 +110,8 @@ namespace Tests.EditMode.Network
|
||||||
{
|
{
|
||||||
PlayerId = "shared-player",
|
PlayerId = "shared-player",
|
||||||
Tick = 33,
|
Tick = 33,
|
||||||
MoveX = 1f,
|
TurnInput = 1f,
|
||||||
MoveY = -1f
|
ThrottleInput = -1f
|
||||||
};
|
};
|
||||||
|
|
||||||
runtime.MessageManager.SendMessage(message, MessageType.MoveInput);
|
runtime.MessageManager.SendMessage(message, MessageType.MoveInput);
|
||||||
|
|
@ -161,7 +161,7 @@ namespace Tests.EditMode.Network
|
||||||
{
|
{
|
||||||
PlayerId = "shared-player",
|
PlayerId = "shared-player",
|
||||||
Tick = 77,
|
Tick = 77,
|
||||||
MoveX = 1f
|
ThrottleInput = 1f
|
||||||
};
|
};
|
||||||
|
|
||||||
runtime.MessageManager.SendMessage(moveInput, MessageType.MoveInput);
|
runtime.MessageManager.SendMessage(moveInput, MessageType.MoveInput);
|
||||||
|
|
@ -191,7 +191,7 @@ namespace Tests.EditMode.Network
|
||||||
{
|
{
|
||||||
PlayerId = "server-player",
|
PlayerId = "server-player",
|
||||||
Tick = 88,
|
Tick = 88,
|
||||||
MoveY = 1f
|
ThrottleInput = 1f
|
||||||
};
|
};
|
||||||
|
|
||||||
host.MessageManager.SendMessage(moveInput, MessageType.MoveInput);
|
host.MessageManager.SendMessage(moveInput, MessageType.MoveInput);
|
||||||
|
|
@ -215,7 +215,7 @@ namespace Tests.EditMode.Network
|
||||||
{
|
{
|
||||||
PlayerId = "fallback-player",
|
PlayerId = "fallback-player",
|
||||||
Tick = 99,
|
Tick = 99,
|
||||||
MoveX = -1f
|
TurnInput = -1f
|
||||||
};
|
};
|
||||||
|
|
||||||
host.MessageManager.SendMessage(moveInput, MessageType.MoveInput);
|
host.MessageManager.SendMessage(moveInput, MessageType.MoveInput);
|
||||||
|
|
|
||||||
|
|
@ -33,8 +33,8 @@ namespace Tests.EditMode.Network
|
||||||
Assert.That(stopInput, Is.Not.Null);
|
Assert.That(stopInput, Is.Not.Null);
|
||||||
Assert.That(stopInput.PlayerId, Is.EqualTo("player-1"));
|
Assert.That(stopInput.PlayerId, Is.EqualTo("player-1"));
|
||||||
Assert.That(stopInput.Tick, Is.EqualTo(8));
|
Assert.That(stopInput.Tick, Is.EqualTo(8));
|
||||||
Assert.That(stopInput.MoveX, Is.EqualTo(0f));
|
Assert.That(stopInput.TurnInput, Is.EqualTo(0f));
|
||||||
Assert.That(stopInput.MoveY, Is.EqualTo(0f));
|
Assert.That(stopInput.ThrottleInput, Is.EqualTo(0f));
|
||||||
Assert.That(continuedIdle, Is.False);
|
Assert.That(continuedIdle, Is.False);
|
||||||
Assert.That(idleInput, Is.Null);
|
Assert.That(idleInput, Is.Null);
|
||||||
}
|
}
|
||||||
|
|
@ -85,9 +85,9 @@ namespace Tests.EditMode.Network
|
||||||
public void ClientPredictionBuffer_AuthoritativeState_PrunesAcknowledgedMoveInputs()
|
public void ClientPredictionBuffer_AuthoritativeState_PrunesAcknowledgedMoveInputs()
|
||||||
{
|
{
|
||||||
var buffer = new ClientPredictionBuffer();
|
var buffer = new ClientPredictionBuffer();
|
||||||
buffer.Record(new MoveInput { PlayerId = "player-1", Tick = 10, MoveX = 1f });
|
buffer.Record(new MoveInput { PlayerId = "player-1", Tick = 10, ThrottleInput = 1f });
|
||||||
buffer.Record(new MoveInput { PlayerId = "player-1", Tick = 11, MoveX = 1f });
|
buffer.Record(new MoveInput { PlayerId = "player-1", Tick = 11, ThrottleInput = 1f });
|
||||||
buffer.Record(new MoveInput { PlayerId = "player-1", Tick = 12, MoveX = 1f });
|
buffer.Record(new MoveInput { PlayerId = "player-1", Tick = 12, ThrottleInput = 1f });
|
||||||
|
|
||||||
var accepted = buffer.TryApplyAuthoritativeState(
|
var accepted = buffer.TryApplyAuthoritativeState(
|
||||||
new PlayerState { PlayerId = "player-1", Tick = 11 },
|
new PlayerState { PlayerId = "player-1", Tick = 11 },
|
||||||
|
|
@ -104,7 +104,7 @@ namespace Tests.EditMode.Network
|
||||||
public void ClientPredictionBuffer_StaleAuthoritativeState_IsIgnored()
|
public void ClientPredictionBuffer_StaleAuthoritativeState_IsIgnored()
|
||||||
{
|
{
|
||||||
var buffer = new ClientPredictionBuffer();
|
var buffer = new ClientPredictionBuffer();
|
||||||
buffer.Record(new MoveInput { PlayerId = "player-1", Tick = 10, MoveX = 1f });
|
buffer.Record(new MoveInput { PlayerId = "player-1", Tick = 10, ThrottleInput = 1f });
|
||||||
buffer.TryApplyAuthoritativeState(new PlayerState { PlayerId = "player-1", Tick = 10 }, out _);
|
buffer.TryApplyAuthoritativeState(new PlayerState { PlayerId = "player-1", Tick = 10 }, out _);
|
||||||
|
|
||||||
var accepted = buffer.TryApplyAuthoritativeState(
|
var accepted = buffer.TryApplyAuthoritativeState(
|
||||||
|
|
@ -139,7 +139,7 @@ namespace Tests.EditMode.Network
|
||||||
Assert.That(snapshot.Position, Is.EqualTo(new Vector3(5f, 0f, -3f)));
|
Assert.That(snapshot.Position, Is.EqualTo(new Vector3(5f, 0f, -3f)));
|
||||||
Assert.That(snapshot.Velocity, Is.EqualTo(new Vector3(1.5f, 0f, 0.25f)));
|
Assert.That(snapshot.Velocity, Is.EqualTo(new Vector3(1.5f, 0f, 0.25f)));
|
||||||
Assert.That(snapshot.Rotation, Is.EqualTo(90f));
|
Assert.That(snapshot.Rotation, Is.EqualTo(90f));
|
||||||
Assert.That(snapshot.RotationQuaternion.eulerAngles.y, Is.EqualTo(90f).Within(0.01f));
|
Assert.That(snapshot.RotationQuaternion.eulerAngles.y, Is.EqualTo(0f).Within(0.01f));
|
||||||
Assert.That(snapshot.Hp, Is.EqualTo(73));
|
Assert.That(snapshot.Hp, Is.EqualTo(73));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -413,13 +413,13 @@ namespace Tests.EditMode.Network
|
||||||
});
|
});
|
||||||
|
|
||||||
transport.EmitReceive(
|
transport.EmitReceive(
|
||||||
BuildEnvelope(MessageType.MoveInput, new MoveInput { PlayerId = "player-a", Tick = 5, MoveX = 1f }),
|
BuildEnvelope(MessageType.MoveInput, new MoveInput { PlayerId = "player-a", Tick = 5, ThrottleInput = 1f }),
|
||||||
peerA);
|
peerA);
|
||||||
transport.EmitReceive(
|
transport.EmitReceive(
|
||||||
BuildEnvelope(MessageType.MoveInput, new MoveInput { PlayerId = "player-a", Tick = 4, MoveX = -1f }),
|
BuildEnvelope(MessageType.MoveInput, new MoveInput { PlayerId = "player-a", Tick = 4, ThrottleInput = -1f }),
|
||||||
peerA);
|
peerA);
|
||||||
transport.EmitReceive(
|
transport.EmitReceive(
|
||||||
BuildEnvelope(MessageType.MoveInput, new MoveInput { PlayerId = "player-b", Tick = 4, MoveY = 1f }),
|
BuildEnvelope(MessageType.MoveInput, new MoveInput { PlayerId = "player-b", Tick = 4, TurnInput = 1f }),
|
||||||
peerB);
|
peerB);
|
||||||
|
|
||||||
Assert.That(handledTicksByPeer[peerA.ToString()], Is.EqualTo(new long[] { 5 }));
|
Assert.That(handledTicksByPeer[peerA.ToString()], Is.EqualTo(new long[] { 5 }));
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue