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