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.
292 lines
9.2 KiB
C#
292 lines
9.2 KiB
C#
using Microsoft.Xna.Framework;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
|
|
namespace Barotrauma.Networking
|
|
{
|
|
[NetworkSerialize]
|
|
struct TempClient : INetSerializableStruct
|
|
{
|
|
public string Name;
|
|
public Identifier PreferredJob;
|
|
public CharacterTeamType TeamID;
|
|
public CharacterTeamType PreferredTeam;
|
|
public UInt16 NameId;
|
|
public AccountInfo AccountInfo;
|
|
public byte SessionId;
|
|
public UInt16 CharacterId;
|
|
public float Karma;
|
|
public bool Muted;
|
|
public bool InGame;
|
|
public bool HasPermissions;
|
|
public bool IsOwner;
|
|
public bool IsDownloading;
|
|
}
|
|
|
|
partial class Client : IDisposable
|
|
{
|
|
public const int MaxNameLength = 32;
|
|
|
|
public string Name; public UInt16 NameId;
|
|
|
|
/// <summary>
|
|
/// An ID for this client for the current session.
|
|
/// THIS IS NOT A PERSISTENT VALUE. DO NOT STORE THIS LONG-TERM.
|
|
/// IT CANNOT BE USED TO IDENTIFY PLAYERS ACROSS SESSIONS.
|
|
/// </summary>
|
|
public readonly byte SessionId;
|
|
|
|
public AccountInfo AccountInfo;
|
|
|
|
/// <summary>
|
|
/// The ID of the account used to authenticate this session.
|
|
/// This value can be used as a persistent value to identify
|
|
/// players in the banlist and campaign saves.
|
|
/// </summary>
|
|
public Option<AccountId> AccountId => AccountInfo.AccountId;
|
|
|
|
public LanguageIdentifier Language;
|
|
|
|
public UInt16 Ping;
|
|
|
|
public Identifier PreferredJob;
|
|
|
|
private CharacterTeamType teamID;
|
|
public CharacterTeamType TeamID
|
|
{
|
|
get { return teamID; }
|
|
set
|
|
{
|
|
if (value != teamID)
|
|
{
|
|
DebugConsole.Log($"Changed client {Name}'s team to {teamID}.");
|
|
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
|
|
{
|
|
GameMain.NetworkMember.IncrementLastClientListUpdateID();
|
|
}
|
|
teamID = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
public CharacterTeamType PreferredTeam;
|
|
|
|
private Character character;
|
|
public Character Character
|
|
{
|
|
get
|
|
{
|
|
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && (character?.ID ?? 0) != CharacterID)
|
|
{
|
|
Character = Entity.FindEntityByID(CharacterID) as Character;
|
|
}
|
|
return character;
|
|
}
|
|
set
|
|
{
|
|
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
|
|
{
|
|
GameMain.NetworkMember.IncrementLastClientListUpdateID();
|
|
if (value != null)
|
|
{
|
|
CharacterID = value.ID;
|
|
}
|
|
}
|
|
character = value;
|
|
if (character != null)
|
|
{
|
|
HasSpawned = true;
|
|
UsingFreeCam = false;
|
|
#if CLIENT
|
|
GameMain.GameSession?.CrewManager?.SetPlayerVoiceIconState(this, muted, mutedLocally);
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Is the client using the 'freecam' console command?
|
|
/// </summary>
|
|
public bool UsingFreeCam;
|
|
|
|
public UInt16 CharacterID;
|
|
|
|
private Vector2 spectatePos;
|
|
public Vector2? SpectatePos
|
|
{
|
|
get
|
|
{
|
|
if (character == null || character.IsDead)
|
|
{
|
|
return spectatePos;
|
|
}
|
|
else
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
set
|
|
{
|
|
spectatePos = value.Value;
|
|
}
|
|
}
|
|
|
|
public bool Spectating
|
|
{
|
|
get
|
|
{
|
|
return inGame && character == null;
|
|
}
|
|
}
|
|
|
|
private bool muted;
|
|
public bool Muted
|
|
{
|
|
get { return muted; }
|
|
set
|
|
{
|
|
if (muted == value) { return; }
|
|
muted = value;
|
|
#if CLIENT
|
|
GameMain.NetLobbyScreen.SetPlayerVoiceIconState(this, muted, mutedLocally);
|
|
GameMain.GameSession?.CrewManager?.SetPlayerVoiceIconState(this, muted, mutedLocally);
|
|
#endif
|
|
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
|
|
{
|
|
GameMain.NetworkMember.IncrementLastClientListUpdateID();
|
|
}
|
|
}
|
|
}
|
|
|
|
public bool HasPermissions => Permissions != ClientPermissions.None;
|
|
|
|
public VoipQueue VoipQueue
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
private bool inGame;
|
|
public bool InGame
|
|
{
|
|
get
|
|
{
|
|
return inGame;
|
|
}
|
|
set
|
|
{
|
|
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
|
|
{
|
|
GameMain.NetworkMember.IncrementLastClientListUpdateID();
|
|
}
|
|
inGame = value;
|
|
}
|
|
}
|
|
public bool HasSpawned; //has the client spawned as a character during the current round
|
|
|
|
public HashSet<Identifier> GivenAchievements = new HashSet<Identifier>();
|
|
|
|
public ClientPermissions Permissions = ClientPermissions.None;
|
|
public readonly HashSet<DebugConsole.Command> PermittedConsoleCommands = new HashSet<DebugConsole.Command>();
|
|
|
|
private readonly object[] votes;
|
|
|
|
partial void InitProjSpecific();
|
|
partial void DisposeProjSpecific();
|
|
public Client(string name, byte sessionId)
|
|
{
|
|
this.Name = name;
|
|
this.SessionId = sessionId;
|
|
|
|
votes = new object[Enum.GetNames(typeof(VoteType)).Length];
|
|
|
|
InitProjSpecific();
|
|
}
|
|
|
|
public T GetVote<T>(VoteType voteType)
|
|
{
|
|
return (votes[(int)voteType] is T t) ? t : default;
|
|
}
|
|
|
|
public void SetVote(VoteType voteType, object value)
|
|
{
|
|
votes[(int)voteType] = value;
|
|
}
|
|
|
|
public bool SessionOrAccountIdMatches(string userId)
|
|
=> (AccountId.IsSome() && Networking.AccountId.Parse(userId) == AccountId)
|
|
|| (byte.TryParse(userId, out byte sessionId) && SessionId == sessionId);
|
|
|
|
public void WritePermissions(IWriteMessage msg)
|
|
{
|
|
msg.WriteByte(SessionId);
|
|
msg.WriteRangedInteger((int)Permissions, 0, (int)ClientPermissions.All);
|
|
if (HasPermission(ClientPermissions.ConsoleCommands))
|
|
{
|
|
msg.WriteUInt16((UInt16)PermittedConsoleCommands.Count);
|
|
foreach (DebugConsole.Command command in PermittedConsoleCommands)
|
|
{
|
|
msg.WriteIdentifier(command.Names[0]);
|
|
}
|
|
}
|
|
}
|
|
public static void ReadPermissions(IReadMessage inc, out ClientPermissions permissions, out List<DebugConsole.Command> permittedCommands)
|
|
{
|
|
int permissionsInt = inc.ReadRangedInteger(0, (int)ClientPermissions.All);
|
|
permissions = ClientPermissions.None;
|
|
permittedCommands = new List<DebugConsole.Command>();
|
|
try
|
|
{
|
|
permissions = (ClientPermissions)permissionsInt;
|
|
}
|
|
catch (InvalidCastException)
|
|
{
|
|
return;
|
|
}
|
|
if (permissions.HasFlag(ClientPermissions.ConsoleCommands))
|
|
{
|
|
UInt16 commandCount = inc.ReadUInt16();
|
|
for (int i = 0; i < commandCount; i++)
|
|
{
|
|
Identifier commandName = inc.ReadIdentifier();
|
|
var consoleCommand = DebugConsole.Commands.Find(c => c.Names.Contains(commandName));
|
|
if (consoleCommand != null)
|
|
{
|
|
permittedCommands.Add(consoleCommand);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public void ReadPermissions(IReadMessage inc)
|
|
{
|
|
ReadPermissions(inc, out ClientPermissions permissions, out List<DebugConsole.Command> permittedCommands);
|
|
SetPermissions(permissions, permittedCommands);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Strips out newlines and some common non-renderable symbols (ASCII codes below 32) out of the name, and optionally limits the maximum size.
|
|
/// </summary>
|
|
public static string SanitizeName(string name, int maxLength = MaxNameLength)
|
|
{
|
|
name = name.Trim().Replace("\r\n", " ").Replace('\n', ' ').Replace('\r', ' ');
|
|
if (name.Length > maxLength)
|
|
{
|
|
name = name.Substring(0, maxLength);
|
|
}
|
|
string rName = "";
|
|
for (int i = 0; i < name.Length; i++)
|
|
{
|
|
rName += name[i] < 32 ? '?' : name[i];
|
|
}
|
|
return rName;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
DisposeProjSpecific();
|
|
}
|
|
}
|
|
}
|