WIP Make networking code thread-safe and refactor update ID

Replaces direct increments of LastClientListUpdateID with a thread-safe IncrementLastClientListUpdateID method and uses Interlocked for atomic operations. Refactors EntitySpawner to lock access to the spawn/remove queue for thread safety. Updates INetSerializableStruct to use concurrent collections for cached variables and type behaviors, improving thread safety in networking code.
This commit is contained in:
Eero
2025-12-28 13:10:17 +08:00
parent 31812d524d
commit c5fa49405f
8 changed files with 111 additions and 51 deletions
@@ -126,7 +126,7 @@ namespace Barotrauma.Networking
if (!MathUtils.NearlyEqual(karma, syncedKarma, 10.0f)) if (!MathUtils.NearlyEqual(karma, syncedKarma, 10.0f))
{ {
syncedKarma = karma; syncedKarma = karma;
GameMain.NetworkMember.LastClientListUpdateID++; GameMain.NetworkMember.IncrementLastClientListUpdateID();
} }
} }
} }
@@ -174,7 +174,7 @@ namespace Barotrauma.Networking
StartTime = DateTime.Now; StartTime = DateTime.Now;
OnStarted(transfer); OnStarted(transfer);
GameMain.Server.LastClientListUpdateID++; GameMain.Server.IncrementLastClientListUpdateID();
return transfer; return transfer;
} }
@@ -204,7 +204,7 @@ namespace Barotrauma.Networking
if (numRemoved > 0 || endedTransfers.Count > 0) if (numRemoved > 0 || endedTransfers.Count > 0)
{ {
GameMain.Server.LastClientListUpdateID++; GameMain.Server.IncrementLastClientListUpdateID();
} }
} }
@@ -327,7 +327,7 @@ namespace Barotrauma.Networking
} }
} }
LastClientListUpdateID++; IncrementLastClientListUpdateID();
if (newClient.Connection == OwnerConnection && OwnerConnection != null) if (newClient.Connection == OwnerConnection && OwnerConnection != null)
{ {
@@ -3222,7 +3222,7 @@ namespace Barotrauma.Networking
initiatedStartGame = false; initiatedStartGame = false;
GameMain.ResetFrameTime(); GameMain.ResetFrameTime();
LastClientListUpdateID++; IncrementLastClientListUpdateID();
roundStartTime = DateTime.Now; roundStartTime = DateTime.Now;
@@ -3532,7 +3532,7 @@ namespace Barotrauma.Networking
{ {
var coolDownRemaining = Client.NameChangeCoolDown - timeSinceNameChange; var coolDownRemaining = Client.NameChangeCoolDown - timeSinceNameChange;
SendDirectChatMessage($"ServerMessage.NameChangeFailedCooldownActive~[seconds]={(int)coolDownRemaining.TotalSeconds}", c); SendDirectChatMessage($"ServerMessage.NameChangeFailedCooldownActive~[seconds]={(int)coolDownRemaining.TotalSeconds}", c);
LastClientListUpdateID++; IncrementLastClientListUpdateID();
//increment the ID to make sure the current server-side name is treated as the "latest", //increment the ID to make sure the current server-side name is treated as the "latest",
//and the client correctly reverts back to the old name //and the client correctly reverts back to the old name
c.NameId++; c.NameId++;
@@ -3545,7 +3545,7 @@ namespace Barotrauma.Networking
if (result != null) if (result != null)
{ {
LastClientListUpdateID++; IncrementLastClientListUpdateID();
return result.Value; return result.Value;
} }
@@ -3562,14 +3562,14 @@ namespace Barotrauma.Networking
c.Name = newName; c.Name = newName;
c.RejectedName = string.Empty; c.RejectedName = string.Empty;
SendChatMessage($"ServerMessage.NameChangeSuccessful~[oldname]={oldName}~[newname]={newName}", ChatMessageType.Server); SendChatMessage($"ServerMessage.NameChangeSuccessful~[oldname]={oldName}~[newname]={newName}", ChatMessageType.Server);
LastClientListUpdateID++; IncrementLastClientListUpdateID();
return true; return true;
} }
else else
{ {
//update client list even if the name cannot be changed to the one sent by the client, //update client list even if the name cannot be changed to the one sent by the client,
//so the client will be informed what their actual name is //so the client will be informed what their actual name is
LastClientListUpdateID++; IncrementLastClientListUpdateID();
return false; return false;
} }
} }
@@ -4857,7 +4857,7 @@ namespace Barotrauma.Networking
private void UpdateClientLobbies() private void UpdateClientLobbies()
{ {
// Triggers a call to WriteClientList(), which causes clients to call GameClient.ReadClientList() // Triggers a call to WriteClientList(), which causes clients to call GameClient.ReadClientList()
LastClientListUpdateID++; IncrementLastClientListUpdateID();
} }
private List<Client> GetPlayingClients() private List<Client> GetPlayingClients()
@@ -163,7 +163,7 @@ namespace Barotrauma
{ {
client.Character.CharacterHealth.ApplyAffliction(null, new Affliction(herpesAffliction, herpesStrength)); client.Character.CharacterHealth.ApplyAffliction(null, new Affliction(herpesAffliction, herpesStrength));
GameServer.Log($"{GameServer.ClientLogName(client)} has contracted space herpes due to low karma.", ServerLog.MessageType.Karma); GameServer.Log($"{GameServer.ClientLogName(client)} has contracted space herpes due to low karma.", ServerLog.MessageType.Karma);
GameMain.NetworkMember.LastClientListUpdateID++; GameMain.NetworkMember.IncrementLastClientListUpdateID();
} }
else if (existingAffliction != null) else if (existingAffliction != null)
{ {
@@ -62,7 +62,7 @@ namespace Barotrauma.Networking
DebugConsole.Log($"Changed client {Name}'s team to {teamID}."); DebugConsole.Log($"Changed client {Name}'s team to {teamID}.");
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{ {
GameMain.NetworkMember.LastClientListUpdateID++; GameMain.NetworkMember.IncrementLastClientListUpdateID();
} }
teamID = value; teamID = value;
} }
@@ -86,7 +86,7 @@ namespace Barotrauma.Networking
{ {
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{ {
GameMain.NetworkMember.LastClientListUpdateID++; GameMain.NetworkMember.IncrementLastClientListUpdateID();
if (value != null) if (value != null)
{ {
CharacterID = value.ID; CharacterID = value.ID;
@@ -154,7 +154,7 @@ namespace Barotrauma.Networking
#endif #endif
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{ {
GameMain.NetworkMember.LastClientListUpdateID++; GameMain.NetworkMember.IncrementLastClientListUpdateID();
} }
} }
} }
@@ -178,7 +178,7 @@ namespace Barotrauma.Networking
{ {
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{ {
GameMain.NetworkMember.LastClientListUpdateID++; GameMain.NetworkMember.IncrementLastClientListUpdateID();
} }
inGame = value; inGame = value;
} }
@@ -5,6 +5,7 @@ using Microsoft.Xna.Framework;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading;
namespace Barotrauma namespace Barotrauma
{ {
@@ -205,6 +206,7 @@ namespace Barotrauma
} }
private readonly Queue<Either<IEntitySpawnInfo, Entity>> spawnOrRemoveQueue; private readonly Queue<Either<IEntitySpawnInfo, Entity>> spawnOrRemoveQueue;
private readonly object spawnOrRemoveQueueLock = new object();
public abstract class SpawnOrRemove : NetEntityEvent.IData public abstract class SpawnOrRemove : NetEntityEvent.IData
{ {
@@ -282,8 +284,11 @@ namespace Barotrauma
GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue1:ItemPrefabNull", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue1:ItemPrefabNull", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
return; return;
} }
lock (spawnOrRemoveQueueLock)
{
spawnOrRemoveQueue.Enqueue(new ItemSpawnInfo(itemPrefab, worldPosition, onSpawned, condition, quality)); spawnOrRemoveQueue.Enqueue(new ItemSpawnInfo(itemPrefab, worldPosition, onSpawned, condition, quality));
} }
}
public void AddItemToSpawnQueue(ItemPrefab itemPrefab, Vector2 position, Submarine sub, float? condition = null, int? quality = null, Action<Item> onSpawned = null) public void AddItemToSpawnQueue(ItemPrefab itemPrefab, Vector2 position, Submarine sub, float? condition = null, int? quality = null, Action<Item> onSpawned = null)
{ {
@@ -295,8 +300,11 @@ namespace Barotrauma
GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue2:ItemPrefabNull", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue2:ItemPrefabNull", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
return; return;
} }
lock (spawnOrRemoveQueueLock)
{
spawnOrRemoveQueue.Enqueue(new ItemSpawnInfo(itemPrefab, position, sub, onSpawned, condition, quality)); spawnOrRemoveQueue.Enqueue(new ItemSpawnInfo(itemPrefab, position, sub, onSpawned, condition, quality));
} }
}
public void AddItemToSpawnQueue(ItemPrefab itemPrefab, Inventory inventory, float? condition = null, int? quality = null, Action<Item> onSpawned = null, bool spawnIfInventoryFull = true, bool ignoreLimbSlots = false, InvSlotType slot = InvSlotType.None) public void AddItemToSpawnQueue(ItemPrefab itemPrefab, Inventory inventory, float? condition = null, int? quality = null, Action<Item> onSpawned = null, bool spawnIfInventoryFull = true, bool ignoreLimbSlots = false, InvSlotType slot = InvSlotType.None)
{ {
@@ -308,6 +316,8 @@ namespace Barotrauma
GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue3:ItemPrefabNull", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue3:ItemPrefabNull", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
return; return;
} }
lock (spawnOrRemoveQueueLock)
{
spawnOrRemoveQueue.Enqueue(new ItemSpawnInfo(itemPrefab, inventory, onSpawned, condition, quality) spawnOrRemoveQueue.Enqueue(new ItemSpawnInfo(itemPrefab, inventory, onSpawned, condition, quality)
{ {
SpawnIfInventoryFull = spawnIfInventoryFull, SpawnIfInventoryFull = spawnIfInventoryFull,
@@ -315,6 +325,7 @@ namespace Barotrauma
Slot = slot Slot = slot
}); });
} }
}
public void AddCharacterToSpawnQueue(Identifier speciesName, Vector2 worldPosition, Action<Character> onSpawn = null) public void AddCharacterToSpawnQueue(Identifier speciesName, Vector2 worldPosition, Action<Character> onSpawn = null)
{ {
@@ -326,8 +337,11 @@ namespace Barotrauma
GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue4:SpeciesNameNullOrEmpty", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue4:SpeciesNameNullOrEmpty", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
return; return;
} }
lock (spawnOrRemoveQueueLock)
{
spawnOrRemoveQueue.Enqueue(new CharacterSpawnInfo(speciesName, worldPosition, onSpawn)); spawnOrRemoveQueue.Enqueue(new CharacterSpawnInfo(speciesName, worldPosition, onSpawn));
} }
}
public void AddCharacterToSpawnQueue(Identifier speciesName, Vector2 position, Submarine sub, Action<Character> onSpawn = null) public void AddCharacterToSpawnQueue(Identifier speciesName, Vector2 position, Submarine sub, Action<Character> onSpawn = null)
{ {
@@ -339,8 +353,11 @@ namespace Barotrauma
GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue5:SpeciesNameNullOrEmpty", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue5:SpeciesNameNullOrEmpty", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
return; return;
} }
lock (spawnOrRemoveQueueLock)
{
spawnOrRemoveQueue.Enqueue(new CharacterSpawnInfo(speciesName, position, sub, onSpawn)); spawnOrRemoveQueue.Enqueue(new CharacterSpawnInfo(speciesName, position, sub, onSpawn));
} }
}
public void AddCharacterToSpawnQueue(Identifier speciesName, Vector2 worldPosition, CharacterInfo characterInfo, Action<Character> onSpawn = null) public void AddCharacterToSpawnQueue(Identifier speciesName, Vector2 worldPosition, CharacterInfo characterInfo, Action<Character> onSpawn = null)
{ {
@@ -352,8 +369,11 @@ namespace Barotrauma
GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue4:SpeciesNameNullOrEmpty", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue4:SpeciesNameNullOrEmpty", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
return; return;
} }
lock (spawnOrRemoveQueueLock)
{
spawnOrRemoveQueue.Enqueue(new CharacterSpawnInfo(speciesName, worldPosition, characterInfo, onSpawn)); spawnOrRemoveQueue.Enqueue(new CharacterSpawnInfo(speciesName, worldPosition, characterInfo, onSpawn));
} }
}
public void AddEntityToRemoveQueue(Entity entity) public void AddEntityToRemoveQueue(Entity entity)
{ {
@@ -375,15 +395,21 @@ namespace Barotrauma
#endif #endif
} }
lock (spawnOrRemoveQueueLock)
{
spawnOrRemoveQueue.Enqueue(entity); spawnOrRemoveQueue.Enqueue(entity);
} }
}
public void AddItemToRemoveQueue(Item item) public void AddItemToRemoveQueue(Item item)
{ {
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
if (IsInRemoveQueue(item) || item.Removed) { return; } if (IsInRemoveQueue(item) || item.Removed) { return; }
lock (spawnOrRemoveQueueLock)
{
spawnOrRemoveQueue.Enqueue(item); spawnOrRemoveQueue.Enqueue(item);
}
item.IsInRemoveQueue = true; item.IsInRemoveQueue = true;
foreach (var containedItem in item.ContainedItems) foreach (var containedItem in item.ContainedItems)
@@ -399,6 +425,8 @@ namespace Barotrauma
/// Are there any entities in the spawn queue that match the given predicate /// Are there any entities in the spawn queue that match the given predicate
/// </summary> /// </summary>
public bool IsInSpawnQueue(Predicate<IEntitySpawnInfo> predicate) public bool IsInSpawnQueue(Predicate<IEntitySpawnInfo> predicate)
{
lock (spawnOrRemoveQueueLock)
{ {
foreach (var spawnOrRemove in spawnOrRemoveQueue) foreach (var spawnOrRemove in spawnOrRemoveQueue)
{ {
@@ -406,11 +434,14 @@ namespace Barotrauma
} }
return false; return false;
} }
}
/// <summary> /// <summary>
/// How many entities in the spawn queue match the given predicate /// How many entities in the spawn queue match the given predicate
/// </summary> /// </summary>
public int CountSpawnQueue(Predicate<IEntitySpawnInfo> predicate) public int CountSpawnQueue(Predicate<IEntitySpawnInfo> predicate)
{
lock (spawnOrRemoveQueueLock)
{ {
int count = 0; int count = 0;
foreach (var spawnOrRemove in spawnOrRemoveQueue) foreach (var spawnOrRemove in spawnOrRemoveQueue)
@@ -419,8 +450,11 @@ namespace Barotrauma
} }
return count; return count;
} }
}
public bool IsInRemoveQueue(Entity entity) public bool IsInRemoveQueue(Entity entity)
{
lock (spawnOrRemoveQueueLock)
{ {
foreach (var spawnOrRemove in spawnOrRemoveQueue) foreach (var spawnOrRemove in spawnOrRemoveQueue)
{ {
@@ -428,13 +462,19 @@ namespace Barotrauma
} }
return false; return false;
} }
}
public void Update(bool createNetworkEvents = true) public void Update(bool createNetworkEvents = true)
{ {
if (GameMain.NetworkMember is { IsClient: true }) { return; } if (GameMain.NetworkMember is { IsClient: true }) { return; }
while (spawnOrRemoveQueue.Count > 0) while (true)
{ {
var spawnOrRemove = spawnOrRemoveQueue.Dequeue(); Either<IEntitySpawnInfo, Entity> spawnOrRemove;
lock (spawnOrRemoveQueueLock)
{
if (spawnOrRemoveQueue.Count == 0) { break; }
spawnOrRemove = spawnOrRemoveQueue.Dequeue();
}
if (spawnOrRemove.TryGet(out Entity entityToRemove)) if (spawnOrRemove.TryGet(out Entity entityToRemove))
{ {
if (entityToRemove is Item item) if (entityToRemove is Item item)
@@ -464,8 +504,11 @@ namespace Barotrauma
partial void CreateNetworkEventProjSpecific(SpawnOrRemove spawnOrRemove); partial void CreateNetworkEventProjSpecific(SpawnOrRemove spawnOrRemove);
public void Reset() public void Reset()
{
lock (spawnOrRemoveQueueLock)
{ {
spawnOrRemoveQueue.Clear(); spawnOrRemoveQueue.Clear();
}
#if CLIENT #if CLIENT
receivedEvents.Clear(); receivedEvents.Clear();
#endif #endif
@@ -1,5 +1,6 @@
#nullable enable #nullable enable
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
@@ -146,10 +147,10 @@ namespace Barotrauma
} }
} }
private static readonly Dictionary<Type, ImmutableArray<CachedReflectedVariable>> CachedVariables = new Dictionary<Type, ImmutableArray<CachedReflectedVariable>>(); private static readonly ConcurrentDictionary<Type, ImmutableArray<CachedReflectedVariable>> CachedVariables = new ConcurrentDictionary<Type, ImmutableArray<CachedReflectedVariable>>();
private static readonly Dictionary<Type, IReadWriteBehavior> TypeBehaviors private static readonly ConcurrentDictionary<Type, IReadWriteBehavior> TypeBehaviors
= new Dictionary<Type, IReadWriteBehavior> = new ConcurrentDictionary<Type, IReadWriteBehavior>(new Dictionary<Type, IReadWriteBehavior>
{ {
{ typeof(Boolean), new ReadWriteBehavior<Boolean>(ReadBoolean, WriteBoolean) }, { typeof(Boolean), new ReadWriteBehavior<Boolean>(ReadBoolean, WriteBoolean) },
{ typeof(Byte), new ReadWriteBehavior<Byte>(ReadByte, WriteByte) }, { typeof(Byte), new ReadWriteBehavior<Byte>(ReadByte, WriteByte) },
@@ -168,7 +169,7 @@ namespace Barotrauma
{ typeof(Vector2), new ReadWriteBehavior<Vector2>(ReadVector2, WriteVector2) }, { typeof(Vector2), new ReadWriteBehavior<Vector2>(ReadVector2, WriteVector2) },
{ typeof(SerializableDateTime), new ReadWriteBehavior<SerializableDateTime>(ReadSerializableDateTime, WriteSerializableDateTime) }, { typeof(SerializableDateTime), new ReadWriteBehavior<SerializableDateTime>(ReadSerializableDateTime, WriteSerializableDateTime) },
{ typeof(NetLimitedString), new ReadWriteBehavior<NetLimitedString>(ReadNetLString, WriteNetLString) } { typeof(NetLimitedString), new ReadWriteBehavior<NetLimitedString>(ReadNetLString, WriteNetLString) }
}; });
private static readonly ImmutableDictionary<Predicate<Type>, Func<Type, IReadWriteBehavior>> BehaviorFactories = new Dictionary<Predicate<Type>, Func<Type, IReadWriteBehavior>> private static readonly ImmutableDictionary<Predicate<Type>, Func<Type, IReadWriteBehavior>> BehaviorFactories = new Dictionary<Predicate<Type>, Func<Type, IReadWriteBehavior>>
{ {
@@ -584,7 +585,11 @@ namespace Barotrauma
if (!predicate(type)) { continue; } if (!predicate(type)) { continue; }
behavior = factory(type); behavior = factory(type);
TypeBehaviors.Add(type, behavior); // Use TryAdd for thread-safety; if another thread already added, use that value
if (!TypeBehaviors.TryAdd(type, behavior))
{
behavior = TypeBehaviors[type];
}
return true; return true;
} }
@@ -594,8 +599,11 @@ namespace Barotrauma
public static ImmutableArray<CachedReflectedVariable> GetPropertiesAndFields(Type type) public static ImmutableArray<CachedReflectedVariable> GetPropertiesAndFields(Type type)
{ {
if (CachedVariables.TryGetValue(type, out var cached)) { return cached; } return CachedVariables.GetOrAdd(type, static t => CreateCachedVariables(t));
}
private static ImmutableArray<CachedReflectedVariable> CreateCachedVariables(Type type)
{
List<CachedReflectedVariable> variables = new List<CachedReflectedVariable>(); List<CachedReflectedVariable> variables = new List<CachedReflectedVariable>();
IEnumerable<PropertyInfo> propertyInfos = type.GetProperties().Where(HasAttribute).Where(NotStatic); IEnumerable<PropertyInfo> propertyInfos = type.GetProperties().Where(HasAttribute).Where(NotStatic);
@@ -633,7 +641,6 @@ namespace Barotrauma
} }
ImmutableArray<CachedReflectedVariable> array = variables.All(v => v.HasOwnAttribute) ? variables.OrderBy(v => v.Attribute.OrderKey).ToImmutableArray() : variables.ToImmutableArray(); ImmutableArray<CachedReflectedVariable> array = variables.All(v => v.HasOwnAttribute) ? variables.OrderBy(v => v.Attribute.OrderKey).ToImmutableArray() : variables.ToImmutableArray();
CachedVariables.Add(type, array);
return array; return array;
bool HasAttribute(MemberInfo info) => (info.GetCustomAttribute<NetworkSerialize>() ?? type.GetCustomAttribute<NetworkSerialize>()) != null; bool HasAttribute(MemberInfo info) => (info.GetCustomAttribute<NetworkSerialize>() ?? type.GetCustomAttribute<NetworkSerialize>()) != null;
@@ -1,6 +1,7 @@
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading;
namespace Barotrauma.Networking namespace Barotrauma.Networking
{ {
@@ -186,10 +187,19 @@ namespace Barotrauma.Networking
{ {
protected const int MaxSubNameLengthInErrorMessages = 16; protected const int MaxSubNameLengthInErrorMessages = 16;
private int lastClientListUpdateID;
public UInt16 LastClientListUpdateID public UInt16 LastClientListUpdateID
{ {
get; get => (UInt16)Interlocked.CompareExchange(ref lastClientListUpdateID, 0, 0);
set; set => Interlocked.Exchange(ref lastClientListUpdateID, value);
}
/// <summary>
/// Thread-safe increment of LastClientListUpdateID
/// </summary>
public void IncrementLastClientListUpdateID()
{
Interlocked.Increment(ref lastClientListUpdateID);
} }
public abstract bool IsServer { get; } public abstract bool IsServer { get; }