2f107db...5202af9

This commit is contained in:
Joonas Rikkonen
2019-03-18 21:42:26 +02:00
parent 58c92888b7
commit 044fd3344b
395 changed files with 27417 additions and 19754 deletions
@@ -1,47 +1,17 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace Barotrauma.Networking
{
class BannedPlayer
partial class BannedPlayer
{
public string Name;
public string IP;
public ulong SteamID;
public string IP; public bool IsRangeBan;
public UInt64 SteamID;
public string Reason;
public DateTime? ExpirationTime;
public bool CompareTo(string ipCompare)
{
int rangeBanIndex = IP.IndexOf(".x");
if (rangeBanIndex <= -1)
{
return ipCompare == IP;
}
else
{
if (ipCompare.Length < rangeBanIndex) return false;
return ipCompare.Substring(0, rangeBanIndex) == IP.Substring(0, rangeBanIndex);
}
}
public BannedPlayer(string name, string ip, string reason, DateTime? expirationTime)
{
this.Name = name;
this.IP = ip;
this.Reason = reason;
this.ExpirationTime = expirationTime;
}
public BannedPlayer(string name, ulong steamID, string reason, DateTime? expirationTime)
{
this.Name = name;
this.SteamID = steamID;
this.Reason = reason;
this.ExpirationTime = expirationTime;
}
public UInt16 UniqueIdentifier;
}
partial class BanList
@@ -60,149 +30,13 @@ namespace Barotrauma.Networking
get { return bannedPlayers.Select(bp => bp.IP); }
}
partial void InitProjectSpecific();
public BanList()
{
bannedPlayers = new List<BannedPlayer>();
if (File.Exists(SavePath))
{
string[] lines;
try
{
lines = File.ReadAllLines(SavePath);
}
catch (Exception e)
{
DebugConsole.ThrowError("Failed to open the list of banned players in " + SavePath, e);
return;
}
foreach (string line in lines)
{
string[] separatedLine = line.Split(',');
if (separatedLine.Length < 2) continue;
string name = separatedLine[0];
string identifier = separatedLine[1];
DateTime? expirationTime = null;
if (separatedLine.Length > 2 && !string.IsNullOrEmpty(separatedLine[2]))
{
if (DateTime.TryParse(separatedLine[2], out DateTime parsedTime))
{
expirationTime = parsedTime;
}
}
string reason = separatedLine.Length > 3 ? string.Join(",", separatedLine.Skip(3)) : "";
if (expirationTime.HasValue && DateTime.Now > expirationTime.Value) continue;
if (identifier.Contains("."))
{
//identifier is an ip
bannedPlayers.Add(new BannedPlayer(name, identifier, reason, expirationTime));
}
else
{
//identifier should be a steam id
if (ulong.TryParse(identifier, out ulong steamID))
{
bannedPlayers.Add(new BannedPlayer(name, steamID, reason, expirationTime));
}
else
{
DebugConsole.ThrowError("Error in banlist: \"" + identifier + "\" is not a valid IP or a Steam ID");
}
}
}
}
}
public void BanPlayer(string name, string ip, string reason, TimeSpan? duration)
{
BanPlayer(name, ip, 0, reason, duration);
}
public void BanPlayer(string name, ulong steamID, string reason, TimeSpan? duration)
{
BanPlayer(name, "", steamID, reason, duration);
}
private void BanPlayer(string name, string ip, ulong steamID, string reason, TimeSpan? duration)
{
var existingBan = bannedPlayers.Find(bp => bp.IP == ip && bp.SteamID == steamID);
if (existingBan != null)
{
if (!duration.HasValue) return;
DebugConsole.Log("Set \"" + name + "\"'s ban duration to " + duration.Value);
existingBan.ExpirationTime = DateTime.Now + duration.Value;
Save();
return;
}
System.Diagnostics.Debug.Assert(!name.Contains(','));
string logMsg = "Banned " + name;
if (!string.IsNullOrEmpty(reason)) logMsg += ", reason: " + reason;
if (duration.HasValue) logMsg += ", duration: " + duration.Value.ToString();
DebugConsole.Log(logMsg);
DateTime? expirationTime = null;
if (duration.HasValue)
{
expirationTime = DateTime.Now + duration.Value;
}
bannedPlayers.Add(new BannedPlayer(name, ip, reason, expirationTime));
Save();
}
public void UnbanPlayer(string name)
{
var player = bannedPlayers.Find(bp => bp.Name == name);
if (player == null)
{
DebugConsole.Log("Could not unban player \"" + name + "\". Matching player not found.");
}
else
{
DebugConsole.Log("Unbanned \"" + name + ".");
bannedPlayers.Remove(player);
Save();
}
}
public void UnbanIP(string ip)
{
var player = bannedPlayers.Find(bp => bp.IP == ip);
if (player == null)
{
DebugConsole.Log("Could not unban IP \"" + ip + "\". Matching player not found.");
}
else
{
DebugConsole.Log("Unbanned \"" + ip + ".");
bannedPlayers.Remove(player);
Save();
}
}
public bool IsBanned(string IP, ulong steamID)
{
bannedPlayers.RemoveAll(bp => bp.ExpirationTime.HasValue && DateTime.Now > bp.ExpirationTime.Value);
return bannedPlayers.Any(bp => bp.CompareTo(IP) || (steamID != 0 && bp.SteamID == steamID));
}
private void RemoveBan(BannedPlayer banned)
{
DebugConsole.Log("Removing ban from " + banned.Name);
GameServer.Log("Removing ban from " + banned.Name, ServerLog.MessageType.ServerMessage);
bannedPlayers.Remove(banned);
Save();
InitProjectSpecific();
}
public string ToRange(string ip)
@@ -217,48 +51,5 @@ namespace Barotrauma.Networking
}
return ip;
}
private void RangeBan(BannedPlayer banned)
{
banned.IP = ToRange(banned.IP);
BannedPlayer bp;
while ((bp = bannedPlayers.Find(x => banned.CompareTo(x.IP))) != null)
{
//remove all specific bans that are now covered by the rangeban
bannedPlayers.Remove(bp);
}
bannedPlayers.Add(banned);
Save();
}
public void Save()
{
GameServer.Log("Saving banlist", ServerLog.MessageType.ServerMessage);
bannedPlayers.RemoveAll(bp => bp.ExpirationTime.HasValue && DateTime.Now > bp.ExpirationTime.Value);
List<string> lines = new List<string>();
foreach (BannedPlayer banned in bannedPlayers)
{
string line = banned.Name;
line += "," + ((banned.SteamID > 0) ? banned.SteamID.ToString() : banned.IP);
line += "," + (banned.ExpirationTime.HasValue ? banned.ExpirationTime.Value.ToString() : "");
if (!string.IsNullOrWhiteSpace(banned.Reason)) line += "," + banned.Reason;
lines.Add(line);
}
try
{
File.WriteAllLines(SavePath, lines);
}
catch (Exception e)
{
DebugConsole.ThrowError("Saving the list of banned players to " + SavePath + " failed", e);
}
}
}
}
@@ -32,15 +32,33 @@ namespace Barotrauma.Networking
new Color(255, 255, 255), //messagebox
new Color(255, 128, 0) //order
};
public readonly string Text;
private string translatedText;
public string TranslatedText
{
get
{
if (!Type.HasFlag(ChatMessageType.Server | ChatMessageType.Error))
{
return Text;
}
if (translatedText == null || translatedText.Length == 0)
{
translatedText = TextManager.GetServerMessage(Text);
}
return translatedText;
}
}
public ChatMessageType Type;
public readonly Character Sender;
public readonly string SenderName;
public Color Color
{
get { return MessageColor[(int)Type]; }
@@ -48,8 +66,10 @@ namespace Barotrauma.Networking
public string TextWithSender
{
get;
private set;
get
{
return string.IsNullOrWhiteSpace(SenderName) ? TranslatedText : SenderName + ": " + TranslatedText;
}
}
public static UInt16 LastID = 0;
@@ -68,8 +88,6 @@ namespace Barotrauma.Networking
Sender = sender;
SenderName = senderName;
TextWithSender = string.IsNullOrWhiteSpace(senderName) ? text : senderName + ": " + text;
}
public static ChatMessage Create(string senderName, string text, ChatMessageType type, Character sender)
@@ -103,6 +121,19 @@ namespace Barotrauma.Networking
return command;
}
public static float GetGarbleAmount(Entity listener, Entity sender, float range, float obstructionmult = 2.0f)
{
if (listener.WorldPosition == sender.WorldPosition) { return 0.0f; }
float dist = Vector2.Distance(listener.WorldPosition, sender.WorldPosition);
if (dist > range) { return 0.0f; }
if (Submarine.CheckVisibility(listener.SimPosition, sender.SimPosition) != null) dist = (dist + 100f) * obstructionmult;
if (dist > range) { return 0.0f; }
return dist / range;
}
public string ApplyDistanceEffect(Character listener)
{
if (Sender == null) return Text;
@@ -110,17 +141,9 @@ namespace Barotrauma.Networking
return ApplyDistanceEffect(listener, Sender, Text, SpeakRange);
}
public static string ApplyDistanceEffect(Entity listener, Entity Sender, string text, float range, float obstructionmult = 2.0f)
public static string ApplyDistanceEffect(Entity listener, Entity sender, string text, float range, float obstructionmult = 2.0f)
{
if (listener.WorldPosition == Sender.WorldPosition) return text;
float dist = Vector2.Distance(listener.WorldPosition, Sender.WorldPosition);
if (dist > range) return "";
if (Submarine.CheckVisibility(listener.SimPosition, Sender.SimPosition) != null) dist = (dist + 100f) * obstructionmult;
if (dist > range) return "";
return ApplyDistanceEffect(text, dist / range);
return ApplyDistanceEffect(text, GetGarbleAmount(listener, sender, range, obstructionmult));
}
public static string ApplyDistanceEffect(string text, float garbleAmount)
@@ -182,138 +205,6 @@ namespace Barotrauma.Networking
return message;
}
public static void ServerRead(NetIncomingMessage msg, Client c)
{
c.KickAFKTimer = 0.0f;
UInt16 ID = msg.ReadUInt16();
ChatMessageType type = (ChatMessageType)msg.ReadByte();
string txt = "";
int orderIndex = -1;
Character orderTargetCharacter = null;
Entity orderTargetEntity = null;
int orderOptionIndex = -1;
OrderChatMessage orderMsg = null;
if (type == ChatMessageType.Order)
{
orderIndex = msg.ReadByte();
orderTargetCharacter = Entity.FindEntityByID(msg.ReadUInt16()) as Character;
orderTargetEntity = Entity.FindEntityByID(msg.ReadUInt16()) as Entity;
orderOptionIndex = msg.ReadByte();
if (orderIndex < 0 || orderIndex >= Order.PrefabList.Count)
{
DebugConsole.ThrowError("Invalid order message from client \"" + c.Name + "\" - order index out of bounds.");
return;
}
Order order = Order.PrefabList[orderIndex];
string orderOption = orderOptionIndex < 0 || orderOptionIndex >= order.Options.Length ? "" : order.Options[orderOptionIndex];
orderMsg = new OrderChatMessage(order, orderOption, orderTargetEntity, orderTargetCharacter, c.Character);
txt = orderMsg.Text;
}
else
{
txt = msg.ReadString() ?? "";
}
if (!NetIdUtils.IdMoreRecent(ID, c.LastSentChatMsgID)) return;
c.LastSentChatMsgID = ID;
if (txt.Length > MaxLength)
{
txt = txt.Substring(0, MaxLength);
}
c.LastSentChatMessages.Add(txt);
if (c.LastSentChatMessages.Count > 10)
{
c.LastSentChatMessages.RemoveRange(0, c.LastSentChatMessages.Count - 10);
}
float similarity = 0.0f;
//don't do message similarity checks on order messages
if (orderMsg == null)
{
for (int i = 0; i < c.LastSentChatMessages.Count; i++)
{
float closeFactor = 1.0f / (c.LastSentChatMessages.Count - i);
if (string.IsNullOrEmpty(txt))
{
similarity += closeFactor;
}
else
{
int levenshteinDist = ToolBox.LevenshteinDistance(txt, c.LastSentChatMessages[i]);
similarity += Math.Max((txt.Length - levenshteinDist) / (float)txt.Length * closeFactor, 0.0f);
}
}
}
if (similarity + c.ChatSpamSpeed > 5.0f)
{
c.ChatSpamCount++;
if (c.ChatSpamCount > 3)
{
//kick for spamming too much
GameMain.Server.KickClient(c, TextManager.Get("SpamFilterKicked"));
}
else
{
ChatMessage denyMsg = Create("", TextManager.Get("SpamFilterBlocked"), ChatMessageType.Server, null);
c.ChatSpamTimer = 10.0f;
GameMain.Server.SendDirectChatMessage(denyMsg, c);
}
return;
}
c.ChatSpamSpeed += similarity + 0.5f;
if (c.ChatSpamTimer > 0.0f)
{
ChatMessage denyMsg = Create("", TextManager.Get("SpamFilterBlocked"), ChatMessageType.Server, null);
c.ChatSpamTimer = 10.0f;
GameMain.Server.SendDirectChatMessage(denyMsg, c);
return;
}
if (type == ChatMessageType.Order)
{
if (c.Character.SpeechImpediment >= 100.0f || c.Character.IsDead) return;
ChatMessageType messageType = CanUseRadio(orderMsg.Sender) ? ChatMessageType.Radio : ChatMessageType.Default;
if (orderMsg.Order.TargetAllCharacters)
{
#if CLIENT
//add the order to the crewmanager only if the host is not controlling a character
//OR the character is close enough to hear it
if (Character.Controlled == null ||
!string.IsNullOrEmpty(ApplyDistanceEffect(orderMsg.Text, messageType, orderMsg.Sender, Character.Controlled)))
{
GameMain.GameSession?.CrewManager?.AddOrder(
new Order(orderMsg.Order.Prefab, orderTargetEntity, (orderTargetEntity as Item)?.GetComponent<ItemComponent>()),
orderMsg.Order.Prefab.FadeOutTime);
}
#endif
}
else if (orderTargetCharacter != null)
{
orderTargetCharacter.SetOrder(
new Order(orderMsg.Order.Prefab, orderTargetEntity, (orderTargetEntity as Item)?.GetComponent<ItemComponent>()),
orderMsg.OrderOption, orderMsg.Sender);
}
GameMain.Server.SendOrderChatMessage(orderMsg);
}
else
{
GameMain.Server.SendChatMessage(txt, null, c);
}
}
public int EstimateLengthBytesClient()
{
int length = 1 + //(byte)ServerNetObject.CHAT_MESSAGE
@@ -323,46 +214,19 @@ namespace Barotrauma.Networking
return length;
}
public int EstimateLengthBytesServer(Client c)
{
int length = 1 + //(byte)ServerNetObject.CHAT_MESSAGE
2 + //(UInt16)NetStateID
1 + //(byte)Type
Encoding.UTF8.GetBytes(Text).Length + 2;
if (Sender != null && c.InGame)
{
length += 2; //sender ID (UInt16)
}
else if (SenderName != null)
{
length += Encoding.UTF8.GetBytes(SenderName).Length + 2;
}
return length;
}
public static bool CanUseRadio(Character sender)
{
if (sender == null) return false;
var senderItem = sender.Inventory.Items.FirstOrDefault(i => i?.GetComponent<WifiComponent>() != null);
return senderItem != null && sender.HasEquippedItem(senderItem) && senderItem.GetComponent<WifiComponent>().CanTransmit();
return CanUseRadio(sender, out _);
}
public virtual void ServerWrite(NetOutgoingMessage msg, Client c)
public static bool CanUseRadio(Character sender, out WifiComponent radio)
{
msg.Write((byte)ServerNetObject.CHAT_MESSAGE);
msg.Write(NetStateID);
msg.Write((byte)Type);
msg.Write(Text);
msg.Write(SenderName);
msg.Write(Sender != null && c.InGame);
if (Sender != null && c.InGame)
{
msg.Write(Sender.ID);
}
radio = null;
if (sender == null) { return false; }
var senderItem = sender.Inventory.Items.FirstOrDefault(i => i?.GetComponent<WifiComponent>() != null);
if (senderItem == null) { return false; }
radio = senderItem.GetComponent<WifiComponent>();
return sender.HasEquippedItem(senderItem) && radio.CanTransmit();
}
}
}
@@ -5,30 +5,12 @@ using System.Linq;
namespace Barotrauma.Networking
{
class Client
partial class Client : IDisposable
{
public string Name;
public byte ID;
public ulong SteamID;
private float karma = 1.0f;
public float Karma
{
get
{
if (GameMain.Server == null) return 1.0f;
if (!GameMain.Server.KarmaEnabled) return 1.0f;
return karma;
}
set
{
if (GameMain.Server == null) return;
if (!GameMain.Server.KarmaEnabled) return;
karma = Math.Min(Math.Max(value,0.0f),1.0f);
}
}
public byte TeamID = 0;
public Character.TeamType TeamID;
private Character character;
public Character Character
@@ -36,58 +18,59 @@ namespace Barotrauma.Networking
get { return character; }
set
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
GameMain.NetworkMember.LastClientListUpdateID++;
}
else
{
if (value!=null)
{
DebugConsole.NewMessage(value.Name, Microsoft.Xna.Framework.Color.Yellow);
}
}
character = value;
if (character != null) HasSpawned = true;
if (character != null)
{
HasSpawned = true;
#if CLIENT
GameMain.GameSession?.CrewManager?.SetPlayerVoiceIconState(this, muted, mutedLocally);
#endif
}
}
}
public CharacterInfo CharacterInfo;
public NetConnection Connection { get; set; }
public bool InGame;
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.LastClientListUpdateID++;
}
}
}
public VoipQueue VoipQueue
{
get;
private set;
}
public bool InGame;
public bool HasSpawned; //has the client spawned as a character during the current round
public UInt16 LastRecvGeneralUpdate = 0;
public UInt16 LastSentChatMsgID = 0; //last msg this client said
public UInt16 LastRecvChatMsgID = 0; //last msg this client knows about
public UInt16 LastSentEntityEventID = 0;
public UInt16 LastRecvEntityEventID = 0;
public UInt16 LastRecvCampaignUpdate = 0;
public UInt16 LastRecvCampaignSave = 0;
public readonly List<ChatMessage> ChatMsgQueue = new List<ChatMessage>();
public UInt16 LastChatMsgQueueID;
//latest chat messages sent by this client
public readonly List<string> LastSentChatMessages = new List<string>();
public float ChatSpamSpeed;
public float ChatSpamTimer;
public int ChatSpamCount;
public float KickAFKTimer;
public double MidRoundSyncTimeOut;
public bool NeedsMidRoundSync;
//how many unique events the client missed before joining the server
public UInt16 UnreceivedEntityEventCount;
public UInt16 FirstNewEventID;
private List<Client> kickVoters;
//when was a specific entity event last sent to the client
// key = event id, value = NetTime.Now when sending
public readonly Dictionary<UInt16, float> EntityEventLastSent = new Dictionary<UInt16, float>();
public readonly Queue<Entity> PendingPositionUpdates = new Queue<Entity>();
public bool ReadyToStart;
public List<JobPrefab> JobPreferences;
public JobPrefab AssignedJob;
public float DeleteDisconnectedTimer;
public HashSet<string> GivenAchievements = new HashSet<string>();
public HashSet<string> GivenAchievements = new HashSet<string>();
@@ -98,34 +81,21 @@ namespace Barotrauma.Networking
private set;
}
public bool SpectateOnly;
private object[] votes;
public void InitClientSync()
{
LastSentChatMsgID = 0;
LastRecvChatMsgID = ChatMessage.LastID;
LastRecvGeneralUpdate = 0;
LastRecvEntityEventID = 0;
UnreceivedEntityEventCount = 0;
NeedsMidRoundSync = false;
}
public int KickVoteCount
{
get { return kickVoters.Count; }
}
public Client(NetPeer server, string name, byte ID)
/*public Client(NetPeer server, string name, byte ID)
: this(name, ID)
{
}
}*/
partial void InitProjSpecific();
partial void DisposeProjSpecific();
public Client(string name, byte ID)
{
this.Name = name;
@@ -136,56 +106,7 @@ namespace Barotrauma.Networking
votes = new object[Enum.GetNames(typeof(VoteType)).Length];
JobPreferences = new List<JobPrefab>(JobPrefab.List.GetRange(0, Math.Min(JobPrefab.List.Count, 3)));
}
public static bool IsValidName(string name, GameServer server)
{
char[] disallowedChars = new char[] { ';', ',', '<', '>', '/', '\\', '[', ']', '"', '?' };
if (name.Any(c => disallowedChars.Contains(c))) return false;
foreach (char character in name)
{
if (!server.AllowedClientNameChars.Any(charRange => (int)character >= charRange.First && (int)character <= charRange.Second)) return false;
}
return true;
}
public static string SanitizeName(string name)
{
name = name.Trim();
if (name.Length > 20)
{
name = name.Substring(0, 20);
}
string rName = "";
for (int i = 0; i < name.Length; i++)
{
rName += name[i] < 32 ? '?' : name[i];
}
return rName;
}
public void SetPermissions(ClientPermissions permissions, List<DebugConsole.Command> permittedConsoleCommands)
{
this.Permissions = permissions;
this.PermittedConsoleCommands = new List<DebugConsole.Command>(permittedConsoleCommands);
}
public void GivePermission(ClientPermissions permission)
{
if (!this.Permissions.HasFlag(permission)) this.Permissions |= permission;
}
public void RemovePermission(ClientPermissions permission)
{
if (this.Permissions.HasFlag(permission)) this.Permissions &= ~permission;
}
public bool HasPermission(ClientPermissions permission)
{
return this.Permissions.HasFlag(permission);
InitProjSpecific();
}
public T GetVote<T>(VoteType voteType)
@@ -235,6 +156,60 @@ namespace Barotrauma.Networking
client.kickVoters.RemoveAll(voter => !connectedClients.Contains(voter));
}
}
public void WritePermissions(NetBuffer msg)
{
msg.Write(ID);
msg.Write((UInt16)Permissions);
if (HasPermission(ClientPermissions.ConsoleCommands))
{
msg.Write((UInt16)PermittedConsoleCommands.Count);
foreach (DebugConsole.Command command in PermittedConsoleCommands)
{
msg.Write(command.names[0]);
}
}
}
public static void ReadPermissions(NetBuffer inc, out ClientPermissions permissions, out List<DebugConsole.Command> permittedCommands)
{
UInt16 permissionsInt = inc.ReadUInt16();
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++)
{
string commandName = inc.ReadString();
var consoleCommand = DebugConsole.Commands.Find(c => c.names.Contains(commandName));
if (consoleCommand != null)
{
permittedCommands.Add(consoleCommand);
}
}
}
}
public void ReadPermissions(NetIncomingMessage inc)
{
ClientPermissions permissions = ClientPermissions.None;
List<DebugConsole.Command> permittedCommands = new List<DebugConsole.Command>();
ReadPermissions(inc, out permissions, out permittedCommands);
SetPermissions(permissions, permittedCommands);
}
public void Dispose()
{
DisposeProjSpecific();
}
}
}
@@ -8,16 +8,19 @@ namespace Barotrauma.Networking
[Flags]
enum ClientPermissions
{
None = 0,
EndRound = 1,
Kick = 2,
Ban = 4,
Unban = 8,
SelectSub = 16,
SelectMode = 32,
ManageCampaign = 64,
ConsoleCommands = 128,
ServerLog = 256
None = 0x0,
ManageRound = 0x1,
Kick = 0x2,
Ban = 0x4,
Unban = 0x8,
SelectSub = 0x10,
SelectMode = 0x20,
ManageCampaign = 0x40,
ConsoleCommands = 0x80,
ServerLog = 0x100,
ManageSettings = 0x200,
ManagePermissions = 0x400,
All = 0x7ff
}
class PermissionPreset
@@ -1,5 +1,6 @@
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
namespace Barotrauma
@@ -26,14 +27,14 @@ namespace Barotrauma
public ItemSpawnInfo(ItemPrefab prefab, Vector2 worldPosition, float? condition = null)
{
Prefab = prefab;
Prefab = prefab ?? throw new ArgumentException("ItemSpawnInfo prefab cannot be null.");
Position = worldPosition;
Condition = condition ?? prefab.Health;
}
public ItemSpawnInfo(ItemPrefab prefab, Vector2 position, Submarine sub, float? condition = null)
{
Prefab = prefab;
Prefab = prefab ?? throw new ArgumentException("ItemSpawnInfo prefab cannot be null.");
Position = position;
Submarine = sub;
Condition = condition ?? prefab.Health;
@@ -41,13 +42,17 @@ namespace Barotrauma
public ItemSpawnInfo(ItemPrefab prefab, Inventory inventory, float? condition = null)
{
Prefab = prefab;
Prefab = prefab ?? throw new ArgumentException("ItemSpawnInfo prefab cannot be null.");
Inventory = inventory;
Condition = condition ?? prefab.Health;
}
public Entity Spawn()
{
if (Prefab == null)
{
return null;
}
Item spawnedItem = null;
if (Inventory != null)
{
@@ -87,37 +92,57 @@ namespace Barotrauma
public void AddToSpawnQueue(ItemPrefab itemPrefab, Vector2 worldPosition, float? condition = null)
{
if (GameMain.Client != null) return;
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
if (itemPrefab == null)
{
string errorMsg = "Attempted to add a null item to entity spawn queue.\n" + Environment.StackTrace;
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue3:ItemPrefabNull", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
return;
}
spawnQueue.Enqueue(new ItemSpawnInfo(itemPrefab, worldPosition, condition));
}
public void AddToSpawnQueue(ItemPrefab itemPrefab, Vector2 position, Submarine sub, float? condition = null)
{
if (GameMain.Client != null) return;
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
if (itemPrefab == null)
{
string errorMsg = "Attempted to add a null item to entity spawn queue.\n" + Environment.StackTrace;
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue3:ItemPrefabNull", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
return;
}
spawnQueue.Enqueue(new ItemSpawnInfo(itemPrefab, position, sub, condition));
}
public void AddToSpawnQueue(ItemPrefab itemPrefab, Inventory inventory, float? condition = null)
{
if (GameMain.Client != null) return;
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
if (itemPrefab == null)
{
string errorMsg = "Attempted to add a null item to entity spawn queue.\n" + Environment.StackTrace;
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue3:ItemPrefabNull", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
return;
}
spawnQueue.Enqueue(new ItemSpawnInfo(itemPrefab, inventory, condition));
}
public void AddToRemoveQueue(Entity entity)
{
if (GameMain.Client != null) return;
if (removeQueue.Contains(entity) || entity.Removed) return;
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
if (removeQueue.Contains(entity) || entity.Removed || entity == null) { return; }
if (entity is Character)
{
Character character = entity as Character;
#if SERVER
if (GameMain.Server != null)
{
Client client = GameMain.Server.ConnectedClients.Find(c => c.Character == character);
if (client != null) GameMain.Server.SetClientCharacter(client, null);
}
#endif
}
removeQueue.Enqueue(entity);
@@ -125,8 +150,8 @@ namespace Barotrauma
public void AddToRemoveQueue(Item item)
{
if (GameMain.Client != null) return;
if (removeQueue.Contains(item) || item.Removed) return;
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
if (removeQueue.Contains(item) || item.Removed) { return; }
removeQueue.Enqueue(item);
if (item.ContainedItems == null) return;
@@ -136,18 +161,9 @@ namespace Barotrauma
}
}
public void CreateNetworkEvent(Entity entity, bool remove)
{
if (GameMain.Server != null && entity != null)
{
GameMain.Server.CreateEntityEvent(this, new object[] { new SpawnOrRemove(entity, remove) });
}
}
public void Update()
{
if (GameMain.Client != null) return;
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
while (spawnQueue.Count > 0)
{
var entitySpawnInfo = spawnQueue.Dequeue();
@@ -155,7 +171,9 @@ namespace Barotrauma
var spawnedEntity = entitySpawnInfo.Spawn();
if (spawnedEntity != null)
{
#if SERVER
CreateNetworkEvent(spawnedEntity, false);
#endif
if (spawnedEntity is Item)
{
((Item)spawnedEntity).Condition = ((ItemSpawnInfo)entitySpawnInfo).Condition;
@@ -167,10 +185,12 @@ namespace Barotrauma
{
var removedEntity = removeQueue.Dequeue();
#if SERVER
if (GameMain.Server != null)
{
CreateNetworkEvent(removedEntity, true);
}
#endif
removedEntity.Remove();
}
@@ -181,33 +201,5 @@ namespace Barotrauma
removeQueue.Clear();
spawnQueue.Clear();
}
public void ServerWrite(Lidgren.Network.NetBuffer message, Client client, object[] extraData = null)
{
if (GameMain.Server == null) return;
SpawnOrRemove entities = (SpawnOrRemove)extraData[0];
message.Write(entities.Remove);
if (entities.Remove)
{
message.Write(entities.Entity.ID);
}
else
{
if (entities.Entity is Item)
{
message.Write((byte)SpawnableType.Item);
((Item)entities.Entity).WriteSpawnData(message);
}
else if (entities.Entity is Character)
{
message.Write((byte)SpawnableType.Character);
DebugConsole.NewMessage("WRITING CHARACTER DATA: " + (entities.Entity).ToString() + " (ID: " + entities.Entity.ID + ")", Color.Cyan);
((Character)entities.Entity).WriteSpawnData(message);
}
}
}
}
}
@@ -1,293 +0,0 @@
using Lidgren.Network;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace Barotrauma.Networking
{
enum FileTransferStatus
{
NotStarted, Sending, Receiving, Finished, Canceled, Error
}
enum FileTransferMessageType
{
Unknown, Initiate, Data, Cancel
}
enum FileTransferType
{
Submarine, CampaignSave
}
class FileSender
{
public class FileTransferOut
{
private byte[] data;
private DateTime startingTime;
private NetConnection connection;
public FileTransferStatus Status;
public string FileName
{
get;
private set;
}
public string FilePath
{
get;
private set;
}
public FileTransferType FileType
{
get;
private set;
}
public float Progress
{
get { return SentOffset / (float)Data.Length; }
}
public float WaitTimer
{
get;
set;
}
public byte[] Data
{
get { return data; }
}
public int SentOffset
{
get;
set;
}
public NetConnection Connection
{
get { return connection; }
}
public int SequenceChannel;
public FileTransferOut(NetConnection recipient, FileTransferType fileType, string filePath)
{
connection = recipient;
FileType = fileType;
FilePath = filePath;
FileName = Path.GetFileName(filePath);
Status = FileTransferStatus.NotStarted;
startingTime = DateTime.Now;
data = File.ReadAllBytes(filePath);
}
}
const int MaxTransferCount = 16;
const int MaxTransferCountPerRecipient = 5;
public static TimeSpan MaxTransferDuration = new TimeSpan(0, 2, 0);
public delegate void FileTransferDelegate(FileTransferOut fileStreamReceiver);
public FileTransferDelegate OnStarted;
public FileTransferDelegate OnEnded;
private List<FileTransferOut> activeTransfers;
private int chunkLen;
private NetPeer peer;
public List<FileTransferOut> ActiveTransfers
{
get { return activeTransfers; }
}
public FileSender(NetworkMember networkMember)
{
peer = networkMember.NetPeer;
chunkLen = peer.Configuration.MaximumTransmissionUnit - 100;
activeTransfers = new List<FileTransferOut>();
}
public FileTransferOut StartTransfer(NetConnection recipient, FileTransferType fileType, string filePath)
{
if (activeTransfers.Count >= MaxTransferCount)
{
return null;
}
if (activeTransfers.Count(t => t.Connection == recipient) > MaxTransferCountPerRecipient)
{
return null;
}
if (!File.Exists(filePath))
{
DebugConsole.ThrowError("Failed to initiate file transfer (file \""+filePath+"\" not found.");
return null;
}
FileTransferOut transfer = null;
try
{
transfer = new FileTransferOut(recipient, fileType, filePath);
transfer.SequenceChannel = 1;
while (activeTransfers.Any(t => t.Connection == recipient && t.SequenceChannel == transfer.SequenceChannel))
{
transfer.SequenceChannel++;
}
activeTransfers.Add(transfer);
}
catch (Exception e)
{
DebugConsole.ThrowError("Failed to initiate file transfer", e);
return null;
}
OnStarted(transfer);
return transfer;
}
public void Update(float deltaTime)
{
activeTransfers.RemoveAll(t => t.Connection.Status != NetConnectionStatus.Connected);
var endedTransfers = activeTransfers.FindAll(t =>
t.Connection.Status != NetConnectionStatus.Connected ||
t.Status == FileTransferStatus.Finished ||
t.Status == FileTransferStatus.Canceled ||
t.Status == FileTransferStatus.Error);
foreach (FileTransferOut transfer in endedTransfers)
{
activeTransfers.Remove(transfer);
OnEnded(transfer);
}
foreach (FileTransferOut transfer in activeTransfers)
{
transfer.WaitTimer -= deltaTime;
if (transfer.WaitTimer > 0.0f) continue;
if (!transfer.Connection.CanSendImmediately(NetDeliveryMethod.ReliableOrdered, 1)) continue;
transfer.WaitTimer = transfer.Connection.AverageRoundtripTime;
// send another part of the file
long remaining = transfer.Data.Length - transfer.SentOffset;
int sendByteCount = (remaining > chunkLen ? chunkLen : (int)remaining);
NetOutgoingMessage message;
//first message; send length, chunk length, file name etc
if (transfer.SentOffset == 0)
{
message = peer.CreateMessage();
message.Write((byte)ServerPacketHeader.FILE_TRANSFER);
message.Write((byte)FileTransferMessageType.Initiate);
message.Write((byte)transfer.FileType);
message.Write((ushort)chunkLen);
message.Write((ulong)transfer.Data.Length);
message.Write(transfer.FileName);
GameMain.Server.CompressOutgoingMessage(message);
transfer.Connection.SendMessage(message, NetDeliveryMethod.ReliableOrdered, transfer.SequenceChannel);
transfer.Status = FileTransferStatus.Sending;
if (GameSettings.VerboseLogging)
{
DebugConsole.Log("Sending file transfer initiation message: ");
DebugConsole.Log(" File: " + transfer.FileName);
DebugConsole.Log(" Size: " + transfer.Data.Length);
DebugConsole.Log(" Sequence channel: " + transfer.SequenceChannel);
}
}
message = peer.CreateMessage(1 + 1 + sendByteCount);
message.Write((byte)ServerPacketHeader.FILE_TRANSFER);
message.Write((byte)FileTransferMessageType.Data);
byte[] sendBytes = new byte[sendByteCount];
Array.Copy(transfer.Data, transfer.SentOffset, sendBytes, 0, sendByteCount);
message.Write(sendBytes);
GameMain.Server.CompressOutgoingMessage(message);
transfer.Connection.SendMessage(message, NetDeliveryMethod.ReliableOrdered, transfer.SequenceChannel);
transfer.SentOffset += sendByteCount;
if (GameSettings.VerboseLogging)
{
DebugConsole.Log("Sending " + sendByteCount + " bytes of the file " + transfer.FileName + " (" + transfer.SentOffset + "/" + transfer.Data.Length + " sent)");
}
if (remaining - sendByteCount <= 0)
{
transfer.Status = FileTransferStatus.Finished;
}
}
}
public void CancelTransfer(FileTransferOut transfer)
{
transfer.Status = FileTransferStatus.Canceled;
activeTransfers.Remove(transfer);
OnEnded(transfer);
GameMain.Server.SendCancelTransferMsg(transfer);
}
public void ReadFileRequest(NetIncomingMessage inc)
{
byte messageType = inc.ReadByte();
if (messageType == (byte)FileTransferMessageType.Cancel)
{
byte sequenceChannel = inc.ReadByte();
var matchingTransfer = activeTransfers.Find(t => t.Connection == inc.SenderConnection && t.SequenceChannel == sequenceChannel);
if (matchingTransfer != null) CancelTransfer(matchingTransfer);
return;
}
byte fileType = inc.ReadByte();
switch (fileType)
{
case (byte)FileTransferType.Submarine:
string fileName = inc.ReadString();
string fileHash = inc.ReadString();
var requestedSubmarine = Submarine.SavedSubmarines.FirstOrDefault(s => s.Name == fileName && s.MD5Hash.Hash == fileHash);
if (requestedSubmarine != null)
{
StartTransfer(inc.SenderConnection, FileTransferType.Submarine, requestedSubmarine.FilePath);
}
break;
case (byte)FileTransferType.CampaignSave:
if (GameMain.GameSession != null)
{
StartTransfer(inc.SenderConnection, FileTransferType.CampaignSave, GameMain.GameSession.SavePath);
}
break;
}
}
}
}
@@ -0,0 +1,17 @@
namespace Barotrauma.Networking
{
enum FileTransferStatus
{
NotStarted, Sending, Receiving, Finished, Canceled, Error
}
enum FileTransferMessageType
{
Unknown, Initiate, Data, Cancel
}
enum FileTransferType
{
Submarine, CampaignSave
}
}
File diff suppressed because it is too large Load Diff
@@ -1,462 +0,0 @@
using Lidgren.Network;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Barotrauma.Networking
{
class UnauthenticatedClient
{
public readonly NetConnection Connection;
public readonly ulong SteamID;
public Facepunch.Steamworks.ServerAuth.Status? SteamAuthStatus = null;
public readonly int Nonce;
public int FailedAttempts;
public float AuthTimer;
public UnauthenticatedClient(NetConnection connection, int nonce, ulong steamID = 0)
{
Connection = connection;
SteamID = steamID;
Nonce = nonce;
AuthTimer = 10.0f;
FailedAttempts = 0;
}
}
partial class GameServer : NetworkMember, ISerializableEntity
{
List<UnauthenticatedClient> unauthenticatedClients = new List<UnauthenticatedClient>();
private void ReadClientSteamAuthRequest(NetIncomingMessage inc, out ulong clientSteamID)
{
clientSteamID = 0;
if (!Steam.SteamManager.USE_STEAM)
{
//not using steam, handle auth normally
HandleClientAuthRequest(inc.SenderConnection, 0);
return;
}
clientSteamID = inc.ReadUInt64();
int authTicketLength = inc.ReadInt32();
inc.ReadBytes(authTicketLength, out byte[] authTicketData);
DebugConsole.Log("Received a Steam auth request");
DebugConsole.Log(" Steam ID: "+ clientSteamID);
DebugConsole.Log(" Auth ticket length: " + authTicketLength);
DebugConsole.Log(" Auth ticket data: " +
((authTicketData == null) ? "null" : ToolBox.LimitString(string.Concat(authTicketData.Select(b => b.ToString("X2"))), 16)));
if (banList.IsBanned("", clientSteamID))
{
return;
}
ulong steamID = clientSteamID;
if (unauthenticatedClients.Any(uc => uc.Connection == inc.SenderConnection))
{
var steamAuthedClient = unauthenticatedClients.Find(uc =>
uc.Connection == inc.SenderConnection &&
uc.SteamID == steamID &&
uc.SteamAuthStatus == Facepunch.Steamworks.ServerAuth.Status.OK);
if (steamAuthedClient != null)
{
DebugConsole.Log("Client already authenticated, sending AUTH_RESPONSE again...");
HandleClientAuthRequest(inc.SenderConnection, steamID);
}
DebugConsole.Log("Steam authentication already pending...");
return;
}
if (authTicketData == null)
{
DebugConsole.Log("Invalid request");
return;
}
unauthenticatedClients.RemoveAll(uc => uc.Connection == inc.SenderConnection);
var unauthClient = new UnauthenticatedClient(inc.SenderConnection, 0, clientSteamID)
{
AuthTimer = 20
};
unauthenticatedClients.Add(unauthClient);
if (!Steam.SteamManager.StartAuthSession(authTicketData, clientSteamID))
{
unauthenticatedClients.Remove(unauthClient);
if (GameMain.Config.RequireSteamAuthentication)
{
unauthClient.Connection.Disconnect(DisconnectReason.SteamAuthenticationFailed.ToString());
Log("Disconnected unauthenticated client (Steam ID: " + steamID + "). Steam authentication failed.", ServerLog.MessageType.ServerMessage);
}
else
{
DebugConsole.Log("Steam authentication failed, skipping to basic auth...");
HandleClientAuthRequest(inc.SenderConnection);
return;
}
}
return;
}
public void OnAuthChange(ulong steamID, ulong ownerID, Facepunch.Steamworks.ServerAuth.Status status)
{
DebugConsole.Log("************ OnAuthChange");
DebugConsole.Log(" Steam ID: " + steamID);
DebugConsole.Log(" Owner ID: " + ownerID);
DebugConsole.Log(" Status: " + status);
UnauthenticatedClient unauthClient = unauthenticatedClients.Find(uc => uc.SteamID == ownerID);
if (unauthClient != null)
{
unauthClient.SteamAuthStatus = status;
switch (status)
{
case Facepunch.Steamworks.ServerAuth.Status.OK:
////steam authentication done, check password next
Log("Successfully authenticated client via Steam (Steam ID: " + steamID + ").", ServerLog.MessageType.ServerMessage);
HandleClientAuthRequest(unauthClient.Connection, unauthClient.SteamID);
break;
default:
unauthenticatedClients.Remove(unauthClient);
if (GameMain.Config.RequireSteamAuthentication)
{
Log("Disconnected unauthenticated client (Steam ID: " + steamID + "). Steam authentication failed, (" + status + ").", ServerLog.MessageType.ServerMessage);
unauthClient.Connection.Disconnect(DisconnectReason.SteamAuthenticationFailed.ToString() + "; (" + status.ToString() + ")");
}
else
{
DebugConsole.Log("Steam authentication failed (" + status.ToString() + "), skipping to basic auth...");
HandleClientAuthRequest(unauthClient.Connection);
return;
}
break;
}
return;
}
else
{
DebugConsole.Log(" No unauthenticated clients found with the Steam ID " + steamID);
}
//kick connected client if status becomes invalid (e.g. VAC banned, not connected to steam)
if (status != Facepunch.Steamworks.ServerAuth.Status.OK && GameMain.Config.RequireSteamAuthentication)
{
var connectedClient = connectedClients.Find(c => c.SteamID == ownerID);
if (connectedClient != null)
{
Log("Disconnecting client " + connectedClient.Name + " (Steam ID: " + steamID + "). Steam authentication no longer valid (" + status + ").", ServerLog.MessageType.ServerMessage);
KickClient(connectedClient, TextManager.Get("DisconnectMessage.SteamAuthNoLongerValid").Replace("[status]", status.ToString()));
}
}
}
private void HandleClientAuthRequest(NetConnection connection, ulong steamID = 0)
{
DebugConsole.Log("HandleClientAuthRequest (steamID " + steamID + ")");
if (GameMain.Config.RequireSteamAuthentication && steamID == 0)
{
connection.Disconnect(DisconnectReason.SteamAuthenticationRequired.ToString());
return;
}
//client wants to know if server requires password
if (ConnectedClients.Find(c => c.Connection == connection) != null)
{
//this client has already been authenticated
return;
}
UnauthenticatedClient unauthClient = unauthenticatedClients.Find(uc => uc.Connection == connection);
if (unauthClient == null)
{
DebugConsole.Log("Unauthed client, generating a nonce...");
//new client, generate nonce and add to unauth queue
if (ConnectedClients.Count >= maxPlayers)
{
//server is full, can't allow new connection
connection.Disconnect(DisconnectReason.ServerFull.ToString());
if (steamID > 0) { Steam.SteamManager.StopAuthSession(steamID); }
return;
}
int nonce = CryptoRandom.Instance.Next();
unauthClient = new UnauthenticatedClient(connection, nonce, steamID);
unauthenticatedClients.Add(unauthClient);
}
unauthClient.AuthTimer = 10.0f;
//if the client is already in the queue, getting another unauth request means that our response was lost; resend
NetOutgoingMessage nonceMsg = server.CreateMessage();
nonceMsg.Write((byte)ServerPacketHeader.AUTH_RESPONSE);
if (string.IsNullOrEmpty(password))
{
nonceMsg.Write(false); //false = no password
}
else
{
nonceMsg.Write(true); //true = password
nonceMsg.Write((Int32)unauthClient.Nonce); //here's nonce, encrypt with this
}
CompressOutgoingMessage(nonceMsg);
DebugConsole.Log("Sending auth response...");
server.SendMessage(nonceMsg, connection, NetDeliveryMethod.Unreliable);
}
private void ClientInitRequest(NetIncomingMessage inc)
{
DebugConsole.Log("Received client init request");
if (ConnectedClients.Find(c => c.Connection == inc.SenderConnection) != null)
{
//this client was already authenticated
//another init request means they didn't get any update packets yet
DebugConsole.Log("Client already connected, ignoring...");
return;
}
UnauthenticatedClient unauthClient = unauthenticatedClients.Find(uc => uc.Connection == inc.SenderConnection);
if (unauthClient == null)
{
//client did not ask for nonce first, can't authorize
inc.SenderConnection.Disconnect(DisconnectReason.AuthenticationRequired.ToString());
if (unauthClient.SteamID > 0) { Steam.SteamManager.StopAuthSession(unauthClient.SteamID); }
return;
}
if (!string.IsNullOrEmpty(password))
{
//decrypt message and compare password
string saltedPw = password;
saltedPw = saltedPw + Convert.ToString(unauthClient.Nonce);
saltedPw = Encoding.UTF8.GetString(NetUtility.ComputeSHAHash(Encoding.UTF8.GetBytes(saltedPw)));
string clPw = inc.ReadString();
if (clPw != saltedPw)
{
unauthClient.FailedAttempts++;
if (unauthClient.FailedAttempts > 3)
{
//disconnect and ban after too many failed attempts
banList.BanPlayer("Unnamed", unauthClient.Connection.RemoteEndPoint.Address.ToString(), TextManager.Get("DisconnectMessage.TooManyFailedLogins"), duration: null);
DisconnectUnauthClient(inc, unauthClient, DisconnectReason.TooManyFailedLogins, "");
Log(inc.SenderConnection.RemoteEndPoint.Address.ToString() + " has been banned from the server (too many wrong passwords)", ServerLog.MessageType.Error);
DebugConsole.NewMessage(inc.SenderConnection.RemoteEndPoint.Address.ToString() + " has been banned from the server (too many wrong passwords)", Color.Red);
return;
}
else
{
//not disconnecting the player here, because they'll still use the same connection and nonce if they try logging in again
NetOutgoingMessage reject = server.CreateMessage();
reject.Write((byte)ServerPacketHeader.AUTH_FAILURE);
reject.Write("Wrong password! You have " + Convert.ToString(4 - unauthClient.FailedAttempts) + " more attempts before you're banned from the server.");
Log(inc.SenderConnection.RemoteEndPoint.Address.ToString() + " failed to join the server (incorrect password)", ServerLog.MessageType.Error);
DebugConsole.NewMessage(inc.SenderConnection.RemoteEndPoint.Address.ToString() + " failed to join the server (incorrect password)", Color.Red);
CompressOutgoingMessage(reject);
server.SendMessage(reject, unauthClient.Connection, NetDeliveryMethod.Unreliable);
unauthClient.AuthTimer = 10.0f;
return;
}
}
}
string clVersion = inc.ReadString();
UInt16 contentPackageCount = inc.ReadUInt16();
List<string> contentPackageNames = new List<string>();
List<string> contentPackageHashes = new List<string>();
for (int i = 0; i < contentPackageCount; i++)
{
string packageName = inc.ReadString();
string packageHash = inc.ReadString();
contentPackageNames.Add(packageName);
contentPackageHashes.Add(packageHash);
if (contentPackageCount == 0)
{
DebugConsole.Log("Client is using content package " +
(packageName ?? "null") + " (" + (packageHash ?? "null" + ")"));
}
}
if (contentPackageCount == 0)
{
DebugConsole.Log("Client did not list any content packages.");
}
string clName = Client.SanitizeName(inc.ReadString());
if (string.IsNullOrWhiteSpace(clName))
{
DisconnectUnauthClient(inc, unauthClient, DisconnectReason.NoName, "");
Log(inc.SenderConnection.RemoteEndPoint.Address.ToString() + " couldn't join the server (no name given)", ServerLog.MessageType.Error);
DebugConsole.NewMessage(inc.SenderConnection.RemoteEndPoint.Address.ToString() + " couldn't join the server (no name given)", Color.Red);
return;
}
if (clVersion != GameMain.Version.ToString())
{
DisconnectUnauthClient(inc, unauthClient, DisconnectReason.InvalidVersion,
TextManager.Get("DisconnectMessage.InvalidVersion").Replace("[version]", GameMain.Version.ToString()).Replace("[clientversion]", clVersion));
Log(clName + " (" + inc.SenderConnection.RemoteEndPoint.Address.ToString() + ") couldn't join the server (wrong game version)", ServerLog.MessageType.Error);
DebugConsole.NewMessage(clName + " (" + inc.SenderConnection.RemoteEndPoint.Address.ToString() + ") couldn't join the server (wrong game version)", Color.Red);
return;
}
//check if the client is missing any of the content packages the server requires
List<ContentPackage> missingPackages = new List<ContentPackage>();
foreach (ContentPackage contentPackage in GameMain.SelectedPackages)
{
if (!contentPackage.HasMultiplayerIncompatibleContent) continue;
bool packageFound = false;
for (int i = 0; i < contentPackageCount; i++)
{
if (contentPackageNames[i] == contentPackage.Name && contentPackageHashes[i] == contentPackage.MD5hash.Hash)
{
packageFound = true;
break;
}
}
if (!packageFound) missingPackages.Add(contentPackage);
}
if (missingPackages.Count == 1)
{
DisconnectUnauthClient(inc, unauthClient, DisconnectReason.MissingContentPackage, TextManager.Get("DisconnectMessage.MissingContentPackage").Replace("[missingcontentpackage]", GetPackageStr(missingPackages[0])));
Log(clName + " (" + inc.SenderConnection.RemoteEndPoint.Address.ToString() + ") couldn't join the server (missing content package " + GetPackageStr(missingPackages[0]) + ")", ServerLog.MessageType.Error);
return;
}
else if (missingPackages.Count > 1)
{
List<string> packageStrs = new List<string>();
missingPackages.ForEach(cp => packageStrs.Add(GetPackageStr(cp)));
DisconnectUnauthClient(inc, unauthClient, DisconnectReason.MissingContentPackage, TextManager.Get("DisconnectMessage.MissingContentPackages").Replace("[missingcontentpackages]", string.Join(", ", packageStrs)));
Log(clName + " (" + inc.SenderConnection.RemoteEndPoint.Address.ToString() + ") couldn't join the server (missing content packages " + string.Join(", ", packageStrs) + ")", ServerLog.MessageType.Error);
return;
}
string GetPackageStr(ContentPackage contentPackage)
{
return "\"" + contentPackage.Name + "\" (hash " + contentPackage.MD5hash.ShortHash + ")";
}
//check if the client is using any contentpackages that are not compatible with the server
List<Pair<string, string>> incompatiblePackages = new List<Pair<string, string>>();
for (int i = 0; i < contentPackageNames.Count; i++)
{
if (!GameMain.Config.SelectedContentPackages.Any(cp => cp.Name == contentPackageNames[i] && cp.MD5hash.Hash == contentPackageHashes[i]))
{
incompatiblePackages.Add(new Pair<string, string>(contentPackageNames[i], contentPackageHashes[i]));
}
}
if (incompatiblePackages.Count == 1)
{
DisconnectUnauthClient(inc, unauthClient, DisconnectReason.IncompatibleContentPackage,
TextManager.Get("DisconnectMessage.IncompatibleContentPackage").Replace("[incompatiblecontentpackage]", GetPackageStr2(incompatiblePackages[0])));
Log(clName + " (" + inc.SenderConnection.RemoteEndPoint.Address.ToString() + ") couldn't join the server (incompatible content package " + GetPackageStr2(incompatiblePackages[0]) + ")", ServerLog.MessageType.Error);
return;
}
else if (incompatiblePackages.Count > 1)
{
List<string> packageStrs = new List<string>();
incompatiblePackages.ForEach(cp => packageStrs.Add(GetPackageStr2(cp)));
DisconnectUnauthClient(inc, unauthClient, DisconnectReason.IncompatibleContentPackage,
TextManager.Get("DisconnectMessage.IncompatibleContentPackages").Replace("[incompatiblecontentpackages]", string.Join(", ", packageStrs)));
Log(clName + " (" + inc.SenderConnection.RemoteEndPoint.Address.ToString() + ") couldn't join the server (incompatible content packages " + string.Join(", ", packageStrs) + ")", ServerLog.MessageType.Error);
return;
}
string GetPackageStr2(Pair<string, string> nameAndHash)
{
return "\"" + nameAndHash.First + "\" (hash " + Md5Hash.GetShortHash(nameAndHash.Second) + ")";
}
if (!whitelist.IsWhiteListed(clName, inc.SenderConnection.RemoteEndPoint.Address.ToString()))
{
DisconnectUnauthClient(inc, unauthClient, DisconnectReason.NotOnWhitelist, "");
Log(clName + " (" + inc.SenderConnection.RemoteEndPoint.Address.ToString() + ") couldn't join the server (not in whitelist)", ServerLog.MessageType.Error);
DebugConsole.NewMessage(clName + " (" + inc.SenderConnection.RemoteEndPoint.Address.ToString() + ") couldn't join the server (not in whitelist)", Color.Red);
return;
}
if (!Client.IsValidName(clName, this))
{
DisconnectUnauthClient(inc, unauthClient, DisconnectReason.InvalidName, "");
Log(clName + " (" + inc.SenderConnection.RemoteEndPoint.Address.ToString() + ") couldn't join the server (invalid name)", ServerLog.MessageType.Error);
DebugConsole.NewMessage(clName + " (" + inc.SenderConnection.RemoteEndPoint.Address.ToString() + ") couldn't join the server (invalid name)", Color.Red);
return;
}
if (Homoglyphs.Compare(clName.ToLower(),Name.ToLower()))
{
DisconnectUnauthClient(inc, unauthClient, DisconnectReason.NameTaken, "");
Log(clName + " (" + inc.SenderConnection.RemoteEndPoint.Address.ToString() + ") couldn't join the server (name taken by the server)", ServerLog.MessageType.Error);
DebugConsole.NewMessage(clName + " (" + inc.SenderConnection.RemoteEndPoint.Address.ToString() + ") couldn't join the server (name taken by the server)", Color.Red);
return;
}
Client nameTaken = ConnectedClients.Find(c => Homoglyphs.Compare(c.Name.ToLower(), clName.ToLower()));
if (nameTaken != null)
{
if (nameTaken.Connection.RemoteEndPoint.Address.ToString() == inc.SenderEndPoint.Address.ToString())
{
//both name and IP address match, replace this player's connection
nameTaken.Connection.Disconnect(DisconnectReason.SessionTaken.ToString());
nameTaken.Connection = unauthClient.Connection;
nameTaken.InitClientSync(); //reinitialize sync ids because this is a new connection
unauthenticatedClients.Remove(unauthClient);
unauthClient = null;
return;
}
else
{
//can't authorize this client
DisconnectUnauthClient(inc, unauthClient, DisconnectReason.NameTaken, "");
Log(clName + " (" + inc.SenderConnection.RemoteEndPoint.Address.ToString() + ") couldn't join the server (name already taken)", ServerLog.MessageType.Error);
DebugConsole.NewMessage(clName + " (" + inc.SenderConnection.RemoteEndPoint.Address.ToString() + ") couldn't join the server (name already taken)", Color.Red);
return;
}
}
//new client
Client newClient = new Client(clName, GetNewClientID());
newClient.InitClientSync();
newClient.Connection = unauthClient.Connection;
newClient.SteamID = unauthClient.SteamID;
unauthenticatedClients.Remove(unauthClient);
unauthClient = null;
ConnectedClients.Add(newClient);
#if CLIENT
GameMain.NetLobbyScreen.AddPlayer(newClient.Name);
#endif
GameMain.Server.SendChatMessage(clName + " has joined the server.", ChatMessageType.Server, null);
var savedPermissions = clientPermissions.Find(cp =>
cp.SteamID > 0 ?
cp.SteamID == newClient.SteamID :
cp.IP == newClient.Connection.RemoteEndPoint.Address.ToString());
if (savedPermissions != null)
{
newClient.SetPermissions(savedPermissions.Permissions, savedPermissions.PermittedCommands);
}
else
{
newClient.SetPermissions(ClientPermissions.None, new List<DebugConsole.Command>());
}
}
private void DisconnectUnauthClient(NetIncomingMessage inc, UnauthenticatedClient unauthClient, DisconnectReason reason, string message)
{
inc.SenderConnection.Disconnect(reason.ToString() + "; " + message);
if (unauthClient.SteamID > 0) { Steam.SteamManager.StopAuthSession(unauthClient.SteamID); }
if (unauthClient != null)
{
unauthenticatedClients.Remove(unauthClient);
}
}
}
}
@@ -1,715 +0,0 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Linq;
namespace Barotrauma.Networking
{
enum SelectionMode
{
Manual = 0, Random = 1, Vote = 2
}
enum YesNoMaybe
{
No = 0, Maybe = 1, Yes = 2
}
enum BotSpawnMode
{
Normal, Fill
}
partial class GameServer : NetworkMember, ISerializableEntity
{
private class SavedClientPermission
{
public readonly string IP;
public readonly ulong SteamID;
public readonly string Name;
public List<DebugConsole.Command> PermittedCommands;
public ClientPermissions Permissions;
public SavedClientPermission(string name, string ip, ClientPermissions permissions, List<DebugConsole.Command> permittedCommands)
{
this.Name = name;
this.IP = ip;
this.Permissions = permissions;
this.PermittedCommands = permittedCommands;
}
public SavedClientPermission(string name, ulong steamID, ClientPermissions permissions, List<DebugConsole.Command> permittedCommands)
{
this.Name = name;
this.SteamID = steamID;
this.Permissions = permissions;
this.PermittedCommands = permittedCommands;
}
}
public const string SettingsFile = "serversettings.xml";
public static readonly string PermissionPresetFile = "Data" + Path.DirectorySeparatorChar + "permissionpresets.xml";
public static readonly string ClientPermissionsFile = "Data" + Path.DirectorySeparatorChar + "clientpermissions.xml";
public Dictionary<string, SerializableProperty> SerializableProperties
{
get;
private set;
}
public Dictionary<ItemPrefab, int> extraCargo;
public bool ShowNetStats;
private TimeSpan refreshMasterInterval = new TimeSpan(0, 0, 30);
private TimeSpan sparseUpdateInterval = new TimeSpan(0, 0, 0, 3);
private SelectionMode subSelectionMode, modeSelectionMode;
private float selectedLevelDifficulty;
private bool registeredToMaster;
private WhiteList whitelist;
private BanList banList;
private string password;
public float AutoRestartTimer;
private bool autoRestart;
private bool isPublic;
private int maxPlayers;
private List<SavedClientPermission> clientPermissions = new List<SavedClientPermission>();
[Serialize(true, true)]
public bool RandomizeSeed
{
get;
set;
}
[Serialize(300.0f, true)]
public float RespawnInterval
{
get;
private set;
}
[Serialize(180.0f, true)]
public float MaxTransportTime
{
get;
private set;
}
[Serialize(0.2f, true)]
public float MinRespawnRatio
{
get;
private set;
}
[Serialize(60.0f, true)]
public float AutoRestartInterval
{
get;
set;
}
[Serialize(false, true)]
public bool StartWhenClientsReady
{
get;
private set;
}
[Serialize(0.8f, true)]
public float StartWhenClientsReadyRatio
{
get;
private set;
}
[Serialize(true, true)]
public bool AllowSpectating
{
get;
private set;
}
[Serialize(true, true)]
public bool EndRoundAtLevelEnd
{
get;
private set;
}
[Serialize(true, true)]
public bool SaveServerLogs
{
get;
private set;
}
[Serialize(true, true)]
public bool AllowRagdollButton
{
get;
private set;
}
[Serialize(true, true)]
public bool AllowFileTransfers
{
get;
private set;
}
[Serialize(800, true)]
private int LinesPerLogFile
{
get
{
return ServerLog.LinesPerFile;
}
set
{
ServerLog.LinesPerFile = value;
}
}
public bool AutoRestart
{
get { return autoRestart; }
set
{
autoRestart = value;
AutoRestartTimer = autoRestart ? AutoRestartInterval : 0.0f;
}
}
[Serialize(true, true)]
public bool AllowRespawn
{
get;
set;
}
[Serialize(0, true)]
public int BotCount
{
get;
set;
}
[Serialize(16, true)]
public int MaxBotCount
{
get;
set;
}
public BotSpawnMode BotSpawnMode
{
get;
set;
}
public float SelectedLevelDifficulty
{
get { return selectedLevelDifficulty; }
set { selectedLevelDifficulty = MathHelper.Clamp(value, 0.0f, 100.0f); }
}
[Serialize(true, true)]
public bool AllowDisguises
{
get;
set;
}
public YesNoMaybe TraitorsEnabled
{
get;
set;
}
public SelectionMode SubSelectionMode
{
get { return subSelectionMode; }
}
public SelectionMode ModeSelectionMode
{
get { return modeSelectionMode; }
}
public BanList BanList
{
get { return banList; }
}
[Serialize(true, true)]
public bool AllowVoteKick
{
get;
private set;
}
[Serialize(0.6f, true)]
public float EndVoteRequiredRatio
{
get;
private set;
}
[Serialize(0.6f, true)]
public float KickVoteRequiredRatio
{
get;
private set;
}
[Serialize(30.0f, true)]
public float KillDisconnectedTime
{
get;
private set;
}
[Serialize(120.0f, true)]
public float KickAFKTime
{
get;
private set;
}
[Serialize(true, true)]
public bool TraitorUseRatio
{
get;
private set;
}
[Serialize(0.2f, true)]
public float TraitorRatio
{
get;
private set;
}
[Serialize(false, true)]
public bool KarmaEnabled
{
get;
set;
}
[Serialize("sandbox", true)]
public string GameModeIdentifier
{
get;
set;
}
[Serialize("Random", true)]
public string MissionType
{
get;
set;
}
public int MaxPlayers
{
get { return maxPlayers; }
}
public List<MissionType> AllowedRandomMissionTypes
{
get;
set;
}
[Serialize(60f, true)]
public float AutoBanTime
{
get;
private set;
}
[Serialize(360f, true)]
public float MaxAutoBanTime
{
get;
private set;
}
/// <summary>
/// A list of int pairs that represent the ranges of UTF-16 codes allowed in client names
/// </summary>
public List<Pair<int, int>> AllowedClientNameChars
{
get;
private set;
} = new List<Pair<int, int>>();
private void SaveSettings()
{
XDocument doc = new XDocument(new XElement("serversettings"));
SerializableProperty.SerializeProperties(this, doc.Root, true);
doc.Root.SetAttributeValue("name", name);
doc.Root.SetAttributeValue("public", isPublic);
doc.Root.SetAttributeValue("port", NetPeerConfiguration.Port);
if (Steam.SteamManager.USE_STEAM) doc.Root.SetAttributeValue("queryport", QueryPort);
doc.Root.SetAttributeValue("maxplayers", maxPlayers);
doc.Root.SetAttributeValue("enableupnp", NetPeerConfiguration.EnableUPnP);
doc.Root.SetAttributeValue("autorestart", autoRestart);
doc.Root.SetAttributeValue("SubSelection", subSelectionMode.ToString());
doc.Root.SetAttributeValue("ModeSelection", modeSelectionMode.ToString());
doc.Root.SetAttributeValue("LevelDifficulty", ((int)selectedLevelDifficulty).ToString());
doc.Root.SetAttributeValue("TraitorsEnabled", TraitorsEnabled.ToString());
/*doc.Root.SetAttributeValue("BotCount", BotCount);
doc.Root.SetAttributeValue("MaxBotCount", MaxBotCount);*/
doc.Root.SetAttributeValue("BotSpawnMode", BotSpawnMode.ToString());
doc.Root.SetAttributeValue("AllowedRandomMissionTypes", string.Join(",", AllowedRandomMissionTypes));
doc.Root.SetAttributeValue("AllowedClientNameChars", string.Join(",", AllowedClientNameChars.Select(c => c.First + "-" + c.Second)));
#if SERVER
doc.Root.SetAttributeValue("password", password);
#endif
if (GameMain.NetLobbyScreen != null
#if CLIENT
&& GameMain.NetLobbyScreen.ServerMessage != null
#endif
)
{
doc.Root.SetAttributeValue("ServerMessage", GameMain.NetLobbyScreen.ServerMessageText);
}
XmlWriterSettings settings = new XmlWriterSettings();
settings.Indent = true;
settings.NewLineOnAttributes = true;
using (var writer = XmlWriter.Create(SettingsFile, settings))
{
doc.Save(writer);
}
}
private void LoadSettings()
{
XDocument doc = null;
if (File.Exists(SettingsFile))
{
doc = XMLExtensions.TryLoadXml(SettingsFile);
}
if (doc == null || doc.Root == null)
{
doc = new XDocument(new XElement("serversettings"));
}
SerializableProperties = SerializableProperty.DeserializeProperties(this, doc.Root);
AutoRestart = doc.Root.GetAttributeBool("autorestart", false);
#if CLIENT
if (autoRestart)
{
GameMain.NetLobbyScreen.SetAutoRestart(autoRestart, AutoRestartInterval);
}
#endif
subSelectionMode = SelectionMode.Manual;
Enum.TryParse(doc.Root.GetAttributeString("SubSelection", "Manual"), out subSelectionMode);
Voting.AllowSubVoting = subSelectionMode == SelectionMode.Vote;
modeSelectionMode = SelectionMode.Manual;
Enum.TryParse(doc.Root.GetAttributeString("ModeSelection", "Manual"), out modeSelectionMode);
Voting.AllowModeVoting = modeSelectionMode == SelectionMode.Vote;
selectedLevelDifficulty = doc.Root.GetAttributeFloat("LevelDifficulty", 20.0f);
GameMain.NetLobbyScreen.SetLevelDifficulty(selectedLevelDifficulty);
var traitorsEnabled = TraitorsEnabled;
Enum.TryParse(doc.Root.GetAttributeString("TraitorsEnabled", "No"), out traitorsEnabled);
TraitorsEnabled = traitorsEnabled;
GameMain.NetLobbyScreen.SetTraitorsEnabled(traitorsEnabled);
var botSpawnMode = BotSpawnMode.Fill;
Enum.TryParse(doc.Root.GetAttributeString("BotSpawnMode", "Fill"), out botSpawnMode);
BotSpawnMode = botSpawnMode;
//"65-90", "97-122", "48-59" = upper and lower case english alphabet and numbers
string[] allowedClientNameCharsStr = doc.Root.GetAttributeStringArray("AllowedClientNameChars", new string[] { "32-33", "65-90", "97-122", "48-59" });
foreach (string allowedClientNameCharRange in allowedClientNameCharsStr)
{
string[] splitRange = allowedClientNameCharRange.Split('-');
if (splitRange.Length == 0 || splitRange.Length > 2)
{
DebugConsole.ThrowError("Error in server settings - "+ allowedClientNameCharRange+" is not a valid range for characters allowed in client names.");
continue;
}
int min = -1;
if (!int.TryParse(splitRange[0], out min))
{
DebugConsole.ThrowError("Error in server settings - " + allowedClientNameCharRange + " is not a valid range for characters allowed in client names.");
continue;
}
int max = min;
if (splitRange.Length == 2)
{
if (!int.TryParse(splitRange[1], out max))
{
DebugConsole.ThrowError("Error in server settings - " + allowedClientNameCharRange + " is not a valid range for characters allowed in client names.");
continue;
}
}
if (min > -1 && max > -1) AllowedClientNameChars.Add(new Pair<int, int>(min, max));
}
AllowedRandomMissionTypes = new List<MissionType>();
string[] allowedMissionTypeNames = doc.Root.GetAttributeStringArray(
"AllowedRandomMissionTypes", Enum.GetValues(typeof(MissionType)).Cast<MissionType>().Select(m => m.ToString()).ToArray());
foreach (string missionTypeName in allowedMissionTypeNames)
{
if (Enum.TryParse(missionTypeName, out MissionType missionType))
{
if (missionType == Barotrauma.MissionType.None) continue;
AllowedRandomMissionTypes.Add(missionType);
}
}
if (GameMain.NetLobbyScreen != null
#if CLIENT
&& GameMain.NetLobbyScreen.ServerMessage != null
#endif
)
{
#if SERVER
GameMain.NetLobbyScreen.ServerName = doc.Root.GetAttributeString("name", "");
GameMain.NetLobbyScreen.SelectedModeIdentifier = GameModeIdentifier;
GameMain.NetLobbyScreen.MissionTypeName = MissionType;
#endif
GameMain.NetLobbyScreen.ServerMessageText = doc.Root.GetAttributeString("ServerMessage", "");
}
GameMain.NetLobbyScreen.SetBotSpawnMode(BotSpawnMode);
GameMain.NetLobbyScreen.SetBotCount(BotCount);
#if CLIENT
showLogButton.Visible = SaveServerLogs;
#endif
List<string> monsterNames = GameMain.Instance.GetFilesOfType(ContentType.Character).ToList();
for (int i = 0; i < monsterNames.Count; i++)
{
monsterNames[i] = Path.GetFileName(Path.GetDirectoryName(monsterNames[i]));
}
monsterEnabled = new Dictionary<string, bool>();
foreach (string s in monsterNames)
{
if (!monsterEnabled.ContainsKey(s)) monsterEnabled.Add(s, true);
}
extraCargo = new Dictionary<ItemPrefab, int>();
AutoBanTime = doc.Root.GetAttributeFloat("autobantime", 60);
MaxAutoBanTime = doc.Root.GetAttributeFloat("maxautobantime", 360);
}
public void LoadClientPermissions()
{
clientPermissions.Clear();
if (!File.Exists(ClientPermissionsFile))
{
if (File.Exists("Data/clientpermissions.txt"))
{
LoadClientPermissionsOld("Data/clientpermissions.txt");
}
return;
}
XDocument doc = XMLExtensions.TryLoadXml(ClientPermissionsFile);
foreach (XElement clientElement in doc.Root.Elements())
{
string clientName = clientElement.GetAttributeString("name", "");
string clientIP = clientElement.GetAttributeString("ip", "");
string steamIdStr = clientElement.GetAttributeString("steamid", "");
if (string.IsNullOrWhiteSpace(clientName))
{
DebugConsole.ThrowError("Error in " + ClientPermissionsFile + " - all clients must have a name and an IP address.");
continue;
}
if (string.IsNullOrWhiteSpace(clientIP) && string.IsNullOrWhiteSpace(steamIdStr))
{
DebugConsole.ThrowError("Error in " + ClientPermissionsFile + " - all clients must have an IP address or a Steam ID.");
continue;
}
string permissionsStr = clientElement.GetAttributeString("permissions", "");
ClientPermissions permissions = ClientPermissions.None;
if (permissionsStr.ToLowerInvariant() == "all")
{
foreach (ClientPermissions permission in Enum.GetValues(typeof(ClientPermissions)))
{
permissions |= permission;
}
}
else if (!Enum.TryParse(permissionsStr, out permissions))
{
DebugConsole.ThrowError("Error in " + ClientPermissionsFile + " - \"" + permissionsStr + "\" is not a valid client permission.");
continue;
}
List<DebugConsole.Command> permittedCommands = new List<DebugConsole.Command>();
if (permissions.HasFlag(ClientPermissions.ConsoleCommands))
{
foreach (XElement commandElement in clientElement.Elements())
{
if (commandElement.Name.ToString().ToLowerInvariant() != "command") continue;
string commandName = commandElement.GetAttributeString("name", "");
DebugConsole.Command command = DebugConsole.FindCommand(commandName);
if (command == null)
{
DebugConsole.ThrowError("Error in " + ClientPermissionsFile + " - \"" + commandName + "\" is not a valid console command.");
continue;
}
permittedCommands.Add(command);
}
}
if (!string.IsNullOrEmpty(steamIdStr))
{
if (ulong.TryParse(steamIdStr, out ulong steamID))
{
clientPermissions.Add(new SavedClientPermission(clientName, steamID, permissions, permittedCommands));
}
else
{
DebugConsole.ThrowError("Error in " + ClientPermissionsFile + " - \"" + steamIdStr + "\" is not a valid Steam ID.");
continue;
}
}
else
{
clientPermissions.Add(new SavedClientPermission(clientName, clientIP, permissions, permittedCommands));
}
}
}
/// <summary>
/// Method for loading old .txt client permission files to provide backwards compatibility
/// </summary>
private void LoadClientPermissionsOld(string file)
{
if (!File.Exists(file)) return;
string[] lines;
try
{
lines = File.ReadAllLines(file);
}
catch (Exception e)
{
DebugConsole.ThrowError("Failed to open client permission file " + ClientPermissionsFile, e);
return;
}
clientPermissions.Clear();
foreach (string line in lines)
{
string[] separatedLine = line.Split('|');
if (separatedLine.Length < 3) continue;
string name = string.Join("|", separatedLine.Take(separatedLine.Length - 2));
string ip = separatedLine[separatedLine.Length - 2];
ClientPermissions permissions = ClientPermissions.None;
if (Enum.TryParse(separatedLine.Last(), out permissions))
{
clientPermissions.Add(new SavedClientPermission(name, ip, permissions, new List<DebugConsole.Command>()));
}
}
}
public void SaveClientPermissions()
{
//delete old client permission file
if (File.Exists("Data/clientpermissions.txt"))
{
File.Delete("Data/clientpermissions.txt");
}
Log("Saving client permissions", ServerLog.MessageType.ServerMessage);
XDocument doc = new XDocument(new XElement("ClientPermissions"));
foreach (SavedClientPermission clientPermission in clientPermissions)
{
XElement clientElement = new XElement("Client",
new XAttribute("name", clientPermission.Name),
new XAttribute("permissions", clientPermission.Permissions.ToString()));
if (clientPermission.SteamID > 0)
{
clientElement.Add(new XAttribute("steamid", clientPermission.SteamID));
}
else
{
clientElement.Add(new XAttribute("ip", clientPermission.IP));
}
if (clientPermission.Permissions.HasFlag(ClientPermissions.ConsoleCommands))
{
foreach (DebugConsole.Command command in clientPermission.PermittedCommands)
{
clientElement.Add(new XElement("command", new XAttribute("name", command.names[0])));
}
}
doc.Root.Add(clientElement);
}
try
{
XmlWriterSettings settings = new XmlWriterSettings();
settings.Indent = true;
settings.NewLineOnAttributes = true;
using (var writer = XmlWriter.Create(ClientPermissionsFile, settings))
{
doc.Save(writer);
}
}
catch (Exception e)
{
DebugConsole.ThrowError("Saving client permissions to " + ClientPermissionsFile + " failed", e);
}
}
}
}
@@ -12,7 +12,9 @@ namespace Barotrauma.Networking
#if CLIENT
void ClientWrite(NetBuffer msg, object[] extraData = null);
#endif
#if SERVER
void ServerRead(ClientNetObject type, NetBuffer msg, Client c);
#endif
}
/// <summary>
@@ -20,7 +22,9 @@ namespace Barotrauma.Networking
/// </summary>
interface IServerSerializable : INetSerializable
{
#if SERVER
void ServerWrite(NetBuffer msg, Client c, object[] extraData = null);
#endif
#if CLIENT
void ClientRead(ServerNetObject type, NetBuffer msg, float sendingTime);
#endif
@@ -1,4 +1,7 @@
namespace Barotrauma.Networking
using Microsoft.Xna.Framework;
using System;
namespace Barotrauma.Networking
{
static class NetConfig
{
@@ -18,10 +21,55 @@
public const float EnableCharacterDist = 20000.0f;
public const float EnableCharacterDistSqr = EnableCharacterDist * EnableCharacterDist;
public const float MaxPhysicsBodyVelocity = 64.0f;
public const float MaxPhysicsBodyAngularVelocity = 16.0f;
public const float MaxHealthUpdateInterval = 2.0f;
public const float MaxHealthUpdateIntervalDead = 10.0f;
public const float HighPrioCharacterPositionUpdateDistance = 1000.0f;
public const float LowPrioCharacterPositionUpdateDistance = 10000.0f;
public const float HighPrioCharacterPositionUpdateInterval = 0.0f;
public const float LowPrioCharacterPositionUpdateInterval = 1.0f;
//how much the physics body of an item has to move until the server
//send a position update to clients (in sim units)
public const float ItemPosUpdateDistance = 2.0f;
public const float DeleteDisconnectedTime = 10.0f;
public const float DeleteDisconnectedTime = 10.0f;
/// <summary>
/// Interpolates the positional error of a physics body towards zero.
/// </summary>
public static Vector2 InterpolateSimPositionError(Vector2 simPositionError, float? smoothingFactor = null)
{
float lengthSqr = simPositionError.LengthSquared();
//correct immediately if the error is very large
if (lengthSqr > 100.0f) { return Vector2.Zero; }
float positionSmoothingFactor = smoothingFactor ?? MathHelper.Lerp(0.95f, 0.8f, MathHelper.Clamp(lengthSqr, 0.0f, 1.0f));
return simPositionError *= positionSmoothingFactor;
}
/// <summary>
/// Interpolates the rotational error of a physics body towards zero.
/// </summary>
public static float InterpolateRotationError(float rotationError)
{
//correct immediately if the error is very large
if (rotationError > MathHelper.TwoPi) { return 0.0f; }
float rotationSmoothingFactor = MathHelper.Lerp(0.95f, 0.8f, Math.Min(Math.Abs(rotationError), 1.0f));
return rotationError *= rotationSmoothingFactor;
}
/// <summary>
/// Interpolates the cursor position error towards zero.
/// </summary>
public static Vector2 InterpolateCursorPositionError(Vector2 cursorPositionError)
{
float lengthSqr = cursorPositionError.LengthSquared();
//correct immediately if the error is very large
if (lengthSqr > 1000.0f) { return Vector2.Zero; }
return cursorPositionError *= 0.7f;
}
}
}
@@ -62,38 +62,4 @@ namespace Barotrauma.Networking
return Data == other.Data;
}
}
class ServerEntityEvent : NetEntityEvent
{
private IServerSerializable serializable;
public bool Sent;
#if DEBUG
public string StackTrace;
#endif
private double createTime;
public double CreateTime
{
get { return createTime; }
}
public ServerEntityEvent(IServerSerializable entity, UInt16 id)
: base(entity, id)
{
serializable = entity;
createTime = Timing.TotalTime;
#if DEBUG
StackTrace = Environment.StackTrace.ToString();
#endif
}
public void Write(NetBuffer msg, Client recipient)
{
serializable.ServerWrite(msg, recipient, Data);
}
}
}
@@ -1,434 +0,0 @@
using Lidgren.Network;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma.Networking
{
class ServerEntityEventManager : NetEntityEventManager
{
private List<ServerEntityEvent> events;
//list of unique events (i.e. !IsDuplicate) created during the round
//used for syncing clients who join mid-round
private List<ServerEntityEvent> uniqueEvents;
private UInt16 lastSentToAll;
public List<ServerEntityEvent> Events
{
get { return events; }
}
public List<ServerEntityEvent> UniqueEvents
{
get { return uniqueEvents; }
}
private class BufferedEvent
{
public readonly Client Sender;
public readonly UInt16 CharacterStateID;
public readonly NetBuffer Data;
public readonly Character Character;
public readonly IClientSerializable TargetEntity;
public bool IsProcessed;
public BufferedEvent(Client sender, Character senderCharacter, UInt16 characterStateID, IClientSerializable targetEntity, NetBuffer data)
{
this.Sender = sender;
this.Character = senderCharacter;
this.CharacterStateID = characterStateID;
this.TargetEntity = targetEntity;
this.Data = data;
}
}
private List<BufferedEvent> bufferedEvents;
private UInt16 ID;
private GameServer server;
public ServerEntityEventManager(GameServer server)
{
events = new List<ServerEntityEvent>();
this.server = server;
bufferedEvents = new List<BufferedEvent>();
uniqueEvents = new List<ServerEntityEvent>();
}
public void CreateEvent(IServerSerializable entity, object[] extraData = null)
{
if (entity == null || !(entity is Entity))
{
DebugConsole.ThrowError("Can't create an entity event for " + entity + "!");
return;
}
if (((Entity)entity).Removed && !(entity is Level))
{
DebugConsole.ThrowError("Can't create an entity event for " + entity + " - the entity has been removed.\n"+Environment.StackTrace);
return;
}
if (((Entity)entity).IdFreed)
{
DebugConsole.ThrowError("Can't create an entity event for " + entity + " - the ID of the entity has been freed.\n"+Environment.StackTrace);
return;
}
var newEvent = new ServerEntityEvent(entity, (UInt16)(ID + 1));
if (extraData != null) newEvent.SetData(extraData);
//remove events that have been sent to all clients, they are redundant now
//keep at least one event in the list (lastSentToAll == e.ID) so we can use it to keep track of the latest ID
events.RemoveAll(e => NetIdUtils.IdMoreRecent(lastSentToAll, e.ID));
if (server.ConnectedClients.Count(c => c.InGame) == 0 && events.Count > 1)
{
events.RemoveRange(0, events.Count - 1);
}
for (int i = events.Count - 1; i >= 0; i--)
{
//we already have an identical event that's waiting to be sent
// -> no need to add a new one
if (events[i].IsDuplicate(newEvent) && !events[i].Sent) return;
}
ID++;
events.Add(newEvent);
if (!uniqueEvents.Any(e => e.IsDuplicate(newEvent)))
{
//create a copy of the event and give it a new ID
var uniqueEvent = new ServerEntityEvent(entity, (UInt16)(uniqueEvents.Count + 1));
uniqueEvent.SetData(extraData);
uniqueEvents.Add(uniqueEvent);
}
}
public void Update(List<Client> clients)
{
foreach (BufferedEvent bufferedEvent in bufferedEvents)
{
if (bufferedEvent.Character == null || bufferedEvent.Character.IsDead)
{
bufferedEvent.IsProcessed = true;
continue;
}
//delay reading the events until the inputs for the corresponding frame have been processed
//UNLESS the character is unconscious, in which case we'll read the messages immediately (because further inputs will be ignored)
//atm the "give in" command is the only thing unconscious characters can do, other types of events are ignored
if (!bufferedEvent.Character.IsUnconscious &&
NetIdUtils.IdMoreRecent(bufferedEvent.CharacterStateID, bufferedEvent.Character.LastProcessedID))
{
continue;
}
try
{
ReadEvent(bufferedEvent.Data, bufferedEvent.TargetEntity, bufferedEvent.Sender);
}
catch (Exception e)
{
string entityName = bufferedEvent.TargetEntity == null ? "null" : bufferedEvent.TargetEntity.ToString();
if (GameSettings.VerboseLogging)
{
DebugConsole.ThrowError("Failed to read server event for entity \"" + entityName + "\"!", e);
}
GameAnalyticsManager.AddErrorEventOnce("ServerEntityEventManager.Read:ReadFailed" + entityName,
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
"Failed to read server event for entity \"" + entityName + "\"!\n" + e.StackTrace);
}
bufferedEvent.IsProcessed = true;
}
var inGameClients = clients.FindAll(c => c.InGame && !c.NeedsMidRoundSync);
if (inGameClients.Count > 0)
{
lastSentToAll = inGameClients[0].LastRecvEntityEventID;
inGameClients.ForEach(c => { if (NetIdUtils.IdMoreRecent(lastSentToAll, c.LastRecvEntityEventID)) lastSentToAll = c.LastRecvEntityEventID; });
ServerEntityEvent firstEventToResend = events.Find(e => e.ID == (ushort)(lastSentToAll + 1));
if (firstEventToResend != null && (Timing.TotalTime - firstEventToResend.CreateTime) > 10.0f)
{
//it's been 10 seconds since this event was created
//kick everyone that hasn't received it yet, this is way too old
List<Client> toKick = inGameClients.FindAll(c => NetIdUtils.IdMoreRecent((UInt16)(lastSentToAll + 1), c.LastRecvEntityEventID));
toKick.ForEach(c =>
{
DebugConsole.NewMessage(c.Name + " was kicked due to excessive desync (expected old event " + c.LastRecvEntityEventID.ToString() + ")", Microsoft.Xna.Framework.Color.Red);
GameServer.Log("Disconnecting client " + c.Name + " due to excessive desync (expected old event " + c.LastRecvEntityEventID.ToString() + ").", ServerLog.MessageType.ServerMessage);
server.DisconnectClient(c, "", "You have been disconnected because of excessive desync");
}
);
}
if (events.Count > 0)
{
//the client is waiting for an event that we don't have anymore
//(the ID they're expecting is smaller than the ID of the first event in our list)
List<Client> toKick = inGameClients.FindAll(c => NetIdUtils.IdMoreRecent(events[0].ID, (UInt16)(c.LastRecvEntityEventID + 1)));
toKick.ForEach(c =>
{
DebugConsole.NewMessage(c.Name + " was kicked due to excessive desync (expected " + c.LastRecvEntityEventID.ToString() + ", last available is " + events[0].ID.ToString() + ")", Microsoft.Xna.Framework.Color.Red);
GameServer.Log("Disconnecting client " + c.Name + " due to excessive desync (expected " + c.LastRecvEntityEventID.ToString() + ", last available is " + events[0].ID.ToString() + ")", ServerLog.MessageType.ServerMessage);
server.DisconnectClient(c, "", "You have been disconnected because of excessive desync");
});
}
}
var timedOutClients = clients.FindAll(c => c.InGame && c.NeedsMidRoundSync && Timing.TotalTime > c.MidRoundSyncTimeOut);
foreach (Client timedOutClient in timedOutClients)
{
GameServer.Log("Disconnecting client " + timedOutClient.Name + ". Syncing the client with the server took too long.", ServerLog.MessageType.ServerMessage);
GameMain.Server.DisconnectClient(timedOutClient, "", "You have been disconnected because syncing your client with the server took too long.");
}
bufferedEvents.RemoveAll(b => b.IsProcessed);
}
private void BufferEvent(BufferedEvent bufferedEvent)
{
if (bufferedEvents.Count > 512)
{
//should normally never happen
//a client could potentially spam events with a much higher character state ID
//than the state of their character and/or stop sending character inputs,
//so we'll drop some events to make sure no-one blows up our buffer
bufferedEvents.RemoveRange(0, 256);
}
bufferedEvents.Add(bufferedEvent);
}
/// <summary>
/// Writes all the events that the client hasn't received yet into the outgoing message
/// </summary>
public void Write(Client client, NetOutgoingMessage msg)
{
List<NetEntityEvent> eventsToSync = null;
if (client.NeedsMidRoundSync)
{
eventsToSync = GetEventsToSync(client, uniqueEvents);
}
else
{
eventsToSync = GetEventsToSync(client, events);
}
if (eventsToSync.Count == 0) return;
//too many events for one packet
if (eventsToSync.Count > MaxEventsPerWrite)
{
eventsToSync.RemoveRange(MaxEventsPerWrite, eventsToSync.Count - MaxEventsPerWrite);
}
foreach (NetEntityEvent entityEvent in eventsToSync)
{
(entityEvent as ServerEntityEvent).Sent = true;
client.EntityEventLastSent[entityEvent.ID] = (float)NetTime.Now;
}
if (client.NeedsMidRoundSync)
{
msg.Write((byte)ServerNetObject.ENTITY_EVENT_INITIAL);
//how many (unique) events the clients had missed before joining
client.UnreceivedEntityEventCount = (UInt16)uniqueEvents.Count;
//ID of the first event sent after the client joined
//(after the client has been synced they'll switch their lastReceivedID
//to the one before this, and the eventmanagers will start to function "normally")
client.FirstNewEventID = events.Count == 0 ? (UInt16)0 : events[events.Count - 1].ID;
msg.Write(client.UnreceivedEntityEventCount);
msg.Write(client.FirstNewEventID);
//DebugConsole.NewMessage(eventsToSync[0].ID.ToString(), Microsoft.Xna.Framework.Color.Yellow);
Write(msg, eventsToSync, client);
}
else
{
msg.Write((byte)ServerNetObject.ENTITY_EVENT);
Write(msg, eventsToSync, client);
}
}
/// <summary>
/// Returns a list of events that should be sent to the client from the eventList
/// </summary>
/// <param name="client"></param>
/// <param name="eventList"></param>
/// <returns></returns>
private List<NetEntityEvent> GetEventsToSync(Client client, List<ServerEntityEvent> eventList)
{
List<NetEntityEvent> eventsToSync = new List<NetEntityEvent>();
if (eventList.Count == 0) return eventsToSync;
//find the index of the first event the client hasn't received
int startIndex = eventList.Count;
while (startIndex > 0 &&
NetIdUtils.IdMoreRecent(eventList[startIndex - 1].ID,client.LastRecvEntityEventID))
{
startIndex--;
}
for (int i = startIndex; i < eventList.Count; i++)
{
//find the first event that hasn't been sent in 1.5 * roundtriptime or at all
float lastSent = 0;
client.EntityEventLastSent.TryGetValue(eventList[i].ID, out lastSent);
if (lastSent > NetTime.Now - client.Connection.AverageRoundtripTime * 1.5f)
{
continue;
}
eventsToSync.AddRange(eventList.GetRange(i, eventList.Count - i));
break;
}
return eventsToSync;
}
public void InitClientMidRoundSync(Client client)
{
//no need for midround syncing if no events have been created,
//or if the first created unique event is still in the event list
if (uniqueEvents.Count == 0 || (events.Count > 0 && events[0].ID == uniqueEvents[0].ID))
{
client.UnreceivedEntityEventCount = 0;
client.FirstNewEventID = 0;
client.NeedsMidRoundSync = false;
}
else
{
double midRoundSyncTimeOut = uniqueEvents.Count / MaxEventsPerWrite * server.UpdateInterval.TotalSeconds;
midRoundSyncTimeOut = Math.Max(10.0f, midRoundSyncTimeOut * 2.0f);
client.UnreceivedEntityEventCount = (UInt16)uniqueEvents.Count;
client.FirstNewEventID = 0;
client.NeedsMidRoundSync = true;
client.MidRoundSyncTimeOut = Timing.TotalTime + midRoundSyncTimeOut;
}
}
/// <summary>
/// Read the events from the message, ignoring ones we've already received
/// </summary>
public void Read(NetIncomingMessage msg, Client sender = null)
{
UInt16 firstEventID = msg.ReadUInt16();
int eventCount = msg.ReadByte();
for (int i = 0; i < eventCount; i++)
{
UInt16 thisEventID = (UInt16)(firstEventID + (UInt16)i);
UInt16 entityID = msg.ReadUInt16();
if (entityID == Entity.NullEntityID)
{
msg.ReadPadBits();
if (thisEventID == (UInt16)(sender.LastSentEntityEventID + 1)) sender.LastSentEntityEventID++;
continue;
}
byte msgLength = msg.ReadByte();
IClientSerializable entity = Entity.FindEntityByID(entityID) as IClientSerializable;
//skip the event if we've already received it
if (thisEventID != (UInt16)(sender.LastSentEntityEventID + 1))
{
if (GameSettings.VerboseLogging)
{
DebugConsole.NewMessage("received msg " + thisEventID, Microsoft.Xna.Framework.Color.Red);
}
msg.Position += msgLength * 8;
}
else if (entity == null)
{
//entity not found -> consider the even read and skip over it
//(can happen, for example, when a client uses a medical item repeatedly
//and creates an event for it before receiving the event about it being removed)
if (GameSettings.VerboseLogging)
{
DebugConsole.NewMessage(
"received msg " + thisEventID + ", entity " + entityID + " not found",
Microsoft.Xna.Framework.Color.Orange);
}
sender.LastSentEntityEventID++;
msg.Position += msgLength * 8;
}
else
{
if (GameSettings.VerboseLogging)
{
DebugConsole.NewMessage("received msg " + thisEventID, Microsoft.Xna.Framework.Color.Green);
}
UInt16 characterStateID = msg.ReadUInt16();
NetBuffer buffer = new NetBuffer();
buffer.Write(msg.ReadBytes(msgLength - 2));
BufferEvent(new BufferedEvent(sender, sender.Character, characterStateID, entity, buffer));
sender.LastSentEntityEventID++;
}
msg.ReadPadBits();
}
}
protected override void WriteEvent(NetBuffer buffer, NetEntityEvent entityEvent, Client recipient = null)
{
var serverEvent = entityEvent as ServerEntityEvent;
if (serverEvent == null) return;
serverEvent.Write(buffer, recipient);
}
protected void ReadEvent(NetBuffer buffer, INetSerializable entity, Client sender = null)
{
var clientEntity = entity as IClientSerializable;
if (clientEntity == null) return;
clientEntity.ServerRead(ClientNetObject.ENTITY_STATE, buffer, sender);
}
public void Clear()
{
ID = 0;
events.Clear();
bufferedEvents.Clear();
lastSentToAll = 0;
uniqueEvents.Clear();
foreach (Client c in server.ConnectedClients)
{
c.EntityEventLastSent.Clear();
c.LastRecvEntityEventID = 0;
c.LastSentEntityEventID = 0;
}
}
}
}
@@ -15,7 +15,13 @@ namespace Barotrauma.Networking
UPDATE_LOBBY, //update state in lobby
UPDATE_INGAME, //update state ingame
SERVER_SETTINGS, //change server settings
CAMPAIGN_SETUP_INFO,
FILE_REQUEST, //request a (submarine) file from the server
VOICE,
RESPONSE_STARTGAME, //tell the server whether you're ready to start
SERVER_COMMAND, //tell the server to end a round or kick/ban someone (special permissions required)
@@ -47,9 +53,14 @@ namespace Barotrauma.Networking
PERMISSIONS, //tell the client which special permissions they have (if any)
ACHIEVEMENT, //give the client a steam achievement
CHEATS_ENABLED, //tell the clients whether cheats are on or off
CAMPAIGN_SETUP_INFO,
FILE_TRANSFER,
VOICE,
QUERY_STARTGAME, //ask the clients whether they're ready to start
STARTGAME, //start a new round
ENDGAME
@@ -60,6 +71,7 @@ namespace Barotrauma.Networking
SYNC_IDS,
CHAT_MESSAGE,
VOTE,
CLIENT_LIST,
ENTITY_POSITION,
ENTITY_EVENT,
ENTITY_EVENT_INITIAL,
@@ -98,6 +110,24 @@ namespace Barotrauma.Networking
abstract partial class NetworkMember
{
public UInt16 LastClientListUpdateID
{
get;
set;
}
public virtual bool IsServer
{
get { return false; }
}
public virtual bool IsClient
{
get { return false; }
}
public abstract void CreateEntityEvent(INetSerializable entity, object[] extraData = null);
#if DEBUG
public Dictionary<string, long> messageCount = new Dictionary<string, long>();
#endif
@@ -110,6 +140,8 @@ namespace Barotrauma.Networking
protected string name;
protected ServerSettings serverSettings;
protected TimeSpan updateInterval;
protected DateTime updateTimer;
@@ -117,17 +149,25 @@ namespace Barotrauma.Networking
protected bool gameStarted;
public Dictionary<string, bool> monsterEnabled;
protected RespawnManager respawnManager;
public Voting Voting;
public bool ShowNetStats;
public int Port
{
get;
set;
}
public int TickRate
{
get { return serverSettings.TickRate; }
set
{
serverSettings.TickRate = MathHelper.Clamp(value, 1, 60);
updateInterval = new TimeSpan(0, 0, 0, 0, MathHelper.Clamp(1000 / serverSettings.TickRate, 1, 500));
}
}
public string Name
{
@@ -154,25 +194,17 @@ namespace Barotrauma.Networking
get { return respawnManager; }
}
public ServerLog ServerLog
public ServerSettings ServerSettings
{
get;
protected set;
get { return serverSettings; }
}
public NetPeerConfiguration NetPeerConfiguration
{
get;
protected set;
}
public NetworkMember()
{
InitProjSpecific();
Voting = new Voting();
}
public bool CanUseRadio(Character sender)
{
if (sender == null) return false;
@@ -190,19 +222,14 @@ namespace Barotrauma.Networking
AddChatMessage(ChatMessage.Create(senderName, message, type, senderCharacter));
}
public void AddChatMessage(ChatMessage message)
public virtual void AddChatMessage(ChatMessage message)
{
GameServer.Log(message.TextWithSender, ServerLog.MessageType.Chat);
if (string.IsNullOrEmpty(message.Text)) { return; }
if (message.Sender != null && !message.Sender.IsDead)
{
message.Sender.ShowSpeechBubble(2.0f, ChatMessage.MessageColor[(int)message.Type]);
}
#if CLIENT
GameMain.NetLobbyScreen.NewChatMessage(message);
chatBox.AddMessage(message);
#endif
}
public virtual void KickPlayer(string kickedName, string reason) { }
@@ -211,12 +238,7 @@ namespace Barotrauma.Networking
public virtual void UnbanPlayer(string playerName, string playerIP) { }
public virtual void Update(float deltaTime)
{
#if CLIENT
UpdateHUD(deltaTime);
#endif
}
public virtual void Update(float deltaTime) { }
public virtual void Disconnect() { }
}
@@ -20,7 +20,7 @@ namespace Barotrauma.Networking
public OrderChatMessage(Order order, string orderOption, Entity targetEntity, Character targetCharacter, Character sender)
: this(order, orderOption,
order.GetChatMessage(targetCharacter?.Name, sender?.CurrentHull?.RoomName, orderOption),
order.GetChatMessage(targetCharacter?.Name, sender?.CurrentHull?.RoomName, givingOrderToSelf: targetCharacter == sender, orderOption: orderOption),
targetEntity, targetCharacter, sender)
{
}
@@ -33,24 +33,5 @@ namespace Barotrauma.Networking
TargetCharacter = targetCharacter;
TargetEntity = targetEntity;
}
public override void ServerWrite(NetOutgoingMessage msg, Client c)
{
msg.Write((byte)ServerNetObject.CHAT_MESSAGE);
msg.Write(NetStateID);
msg.Write((byte)ChatMessageType.Order);
msg.Write(SenderName);
msg.Write(Sender != null && c.InGame);
if (Sender != null && c.InGame)
{
msg.Write(Sender.ID);
}
msg.Write((byte)Order.PrefabList.IndexOf(Order.Prefab));
msg.Write(TargetCharacter == null ? (UInt16)0 : TargetCharacter.ID);
msg.Write(TargetEntity == null ? (UInt16)0 : TargetEntity.ID);
msg.Write((byte)Array.IndexOf(Order.Prefab.Options, OrderOption));
}
}
}
@@ -8,8 +8,8 @@ using System.Linq;
namespace Barotrauma.Networking
{
class RespawnManager : Entity, IServerSerializable
{
partial class RespawnManager : Entity, IServerSerializable
{
public enum State
{
Waiting,
@@ -116,60 +116,15 @@ namespace Barotrauma.Networking
respawnShuttle = null;
}
#if SERVER
if (networkMember is GameServer server)
{
respawnTimer = server.RespawnInterval;
maxTransportTime = server.MaxTransportTime;
}
respawnTimer = server.ServerSettings.RespawnInterval;
maxTransportTime = server.ServerSettings.MaxTransportTime;
}
#endif
}
private List<Client> GetClientsToRespawn()
{
return networkMember.ConnectedClients.FindAll(c =>
c.InGame &&
(!c.SpectateOnly || !((GameServer)networkMember).AllowSpectating) &&
(c.Character == null || c.Character.IsDead));
}
private List<CharacterInfo> GetBotsToRespawn()
{
GameServer server = networkMember as GameServer;
if (server.BotSpawnMode == BotSpawnMode.Normal)
{
return Character.CharacterList
.FindAll(c => c.TeamID == 1 && c.AIController != null && c.Info != null && c.IsDead)
.Select(c => c.Info)
.ToList();
}
int currPlayerCount = server.ConnectedClients.Count(c => c.InGame && (!c.SpectateOnly || !server.AllowSpectating));
if (server.CharacterInfo != null) currPlayerCount++;
var existingBots = Character.CharacterList
.FindAll(c => c.TeamID == 1 && c.AIController != null && c.Info != null);
int requiredBots = server.BotCount - currPlayerCount;
requiredBots -= existingBots.Count(b => !b.IsDead);
List<CharacterInfo> botsToRespawn = new List<CharacterInfo>();
for (int i = 0; i < requiredBots; i++)
{
CharacterInfo botToRespawn = existingBots.Find(b => b.IsDead)?.Info;
if (botToRespawn == null)
{
botToRespawn = new CharacterInfo(Character.HumanConfigFile);
}
else
{
existingBots.Remove(botToRespawn.Character);
}
botsToRespawn.Add(botToRespawn);
}
return botsToRespawn;
}
public void Update(float deltaTime)
{
if (respawnShuttle == null)
@@ -194,61 +149,8 @@ namespace Barotrauma.Networking
}
}
private void UpdateWaiting(float deltaTime)
{
var server = networkMember as GameServer;
if (server == null)
{
if (CountdownStarted)
{
respawnTimer = Math.Max(0.0f, respawnTimer - deltaTime);
}
return;
}
int characterToRespawnCount = GetClientsToRespawn().Count;
int totalCharacterCount = server.ConnectedClients.Count;
if (server.Character != null)
{
totalCharacterCount++;
if (server.Character.IsDead) characterToRespawnCount++;
}
bool startCountdown = (float)characterToRespawnCount >= Math.Max((float)totalCharacterCount * server.MinRespawnRatio, 1.0f);
if (startCountdown)
{
if (!CountdownStarted)
{
CountdownStarted = true;
server.CreateEntityEvent(this);
}
}
else
{
CountdownStarted = false;
}
if (!CountdownStarted) return;
respawnTimer -= deltaTime;
if (respawnTimer <= 0.0f)
{
respawnTimer = server.RespawnInterval;
DispatchShuttle();
}
if (respawnShuttle == null) return;
respawnShuttle.Velocity = Vector2.Zero;
if (shuttleSteering != null)
{
shuttleSteering.AutoPilot = false;
shuttleSteering.MaintainPos = false;
}
}
partial void UpdateWaiting(float deltaTime);
private void UpdateTransporting(float deltaTime)
{
//infinite transport time -> shuttle wont return
@@ -256,14 +158,17 @@ namespace Barotrauma.Networking
shuttleTransportTimer -= deltaTime;
#if CLIENT
GameClient nClient = networkMember as GameClient;
if (shuttleTransportTimer + deltaTime > 15.0f && shuttleTransportTimer <= 15.0f &&
networkMember.Character != null &&
networkMember.Character.Submarine == respawnShuttle)
nClient.Character != null &&
nClient.Character.Submarine == respawnShuttle)
{
networkMember.AddChatMessage("The shuttle will automatically return back to the outpost. Please leave the shuttle immediately.", ChatMessageType.Server);
nClient.AddChatMessage("ServerMessage.ShuttleLeaving", ChatMessageType.Server);
}
#endif
#if SERVER
var server = networkMember as GameServer;
if (server == null) return;
@@ -281,10 +186,11 @@ namespace Barotrauma.Networking
server.CreateEntityEvent(this);
CountdownStarted = false;
maxTransportTime = server.MaxTransportTime;
maxTransportTime = server.ServerSettings.MaxTransportTime;
shuttleReturnTimer = maxTransportTime;
shuttleTransportTimer = maxTransportTime;
}
#endif
}
private void UpdateReturning(float deltaTime)
@@ -311,13 +217,15 @@ namespace Barotrauma.Networking
shuttleSteering.SetDestinationLevelStart();
}
foreach (Door door in shuttleDoors)
{
if (door.IsOpen) door.SetState(false,false,true);
}
#if SERVER
var server = networkMember as GameServer;
if (server == null) return;
foreach (Door door in shuttleDoors)
{
if (door.IsOpen) door.TrySetState(false, false, true);
}
var shuttleGaps = Gap.GapList.FindAll(g => g.Submarine == respawnShuttle && g.ConnectedWall != null);
shuttleGaps.ForEach(g => Spawner.AddToRemoveQueue(g));
@@ -350,46 +258,15 @@ namespace Barotrauma.Networking
GameServer.Log("The respawn shuttle has left.", ServerLog.MessageType.Spawning);
server.CreateEntityEvent(this);
respawnTimer = server.RespawnInterval;
respawnTimer = server.ServerSettings.RespawnInterval;
CountdownStarted = false;
}
#endif
}
}
private void DispatchShuttle()
{
var server = networkMember as GameServer;
if (server == null) return;
if (respawnShuttle != null)
{
state = State.Transporting;
server.CreateEntityEvent(this);
ResetShuttle();
if (shuttleSteering != null)
{
shuttleSteering.TargetVelocity = Vector2.Zero;
}
GameServer.Log("Dispatching the respawn shuttle.", ServerLog.MessageType.Spawning);
RespawnCharacters();
CoroutineManager.StopCoroutines("forcepos");
CoroutineManager.StartCoroutine(ForceShuttleToPos(Level.Loaded.StartPosition - Vector2.UnitY * Level.ShaftHeight, 100.0f), "forcepos");
}
else
{
state = State.Waiting;
GameServer.Log("Respawning everyone in main sub.", ServerLog.MessageType.Spawning);
server.CreateEntityEvent(this);
RespawnCharacters();
}
}
partial void DispatchShuttle();
private IEnumerable<object> ForceShuttleToPos(Vector2 position, float speed)
{
if (respawnShuttle == null)
@@ -468,8 +345,10 @@ namespace Barotrauma.Networking
foreach (Character c in Character.CharacterList)
{
if (c.Submarine != respawnShuttle) continue;
#if CLIENT
if (Character.Controlled == c) Character.Controlled = null;
#endif
c.Kill(CauseOfDeathType.Unknown, null, true);
c.Enabled = false;
@@ -492,133 +371,10 @@ namespace Barotrauma.Networking
}
partial void RespawnCharactersProjSpecific();
public void RespawnCharacters()
{
var server = networkMember as GameServer;
if (server == null) return;
var respawnSub = respawnShuttle ?? Submarine.MainSub;
var clients = GetClientsToRespawn();
foreach (Client c in clients)
{
//all characters are in Team 1 in game modes/missions with only one team.
//if at some point we add a game mode with multiple teams where respawning is possible, this needs to be reworked
c.TeamID = 1;
if (c.CharacterInfo == null) c.CharacterInfo = new CharacterInfo(Character.HumanConfigFile, c.Name);
}
List<CharacterInfo> characterInfos = clients.Select(c => c.CharacterInfo).ToList();
var botsToSpawn = GetBotsToRespawn();
characterInfos.AddRange(botsToSpawn);
if (server.Character != null && server.Character.IsDead)
{
characterInfos.Add(server.CharacterInfo);
}
server.AssignJobs(clients, server.Character != null && server.Character.IsDead);
foreach (Client c in clients)
{
c.CharacterInfo.Job = new Job(c.AssignedJob);
}
//the spawnpoints where the characters will spawn
var shuttleSpawnPoints = WayPoint.SelectCrewSpawnPoints(characterInfos, respawnSub);
//the spawnpoints where they would spawn if they were spawned inside the main sub
//(in order to give them appropriate ID card tags)
var mainSubSpawnPoints = WayPoint.SelectCrewSpawnPoints(characterInfos, Submarine.MainSub);
ItemPrefab divingSuitPrefab = MapEntityPrefab.Find(null, "divingsuit") as ItemPrefab;
ItemPrefab oxyPrefab = MapEntityPrefab.Find(null, "oxygentank") as ItemPrefab;
ItemPrefab scooterPrefab = MapEntityPrefab.Find(null, "underwaterscooter") as ItemPrefab;
ItemPrefab batteryPrefab = MapEntityPrefab.Find(null, "batterycell") as ItemPrefab;
var cargoSp = WayPoint.WayPointList.Find(wp => wp.Submarine == respawnSub && wp.SpawnType == SpawnType.Cargo);
for (int i = 0; i < characterInfos.Count; i++)
{
bool myCharacter = false;
#if CLIENT
myCharacter = i >= clients.Count + botsToSpawn.Count;
#endif
bool bot = i >= clients.Count && !myCharacter;
var character = Character.Create(characterInfos[i], shuttleSpawnPoints[i].WorldPosition, characterInfos[i].Name, !myCharacter && !bot, bot);
character.TeamID = 1;
#if CLIENT
GameMain.GameSession.CrewManager.AddCharacter(character);
if (myCharacter)
{
server.Character = character;
Character.Controlled = character;
GameMain.LightManager.LosEnabled = true;
GameServer.Log(string.Format("Respawning {0} (host) as {1}", character.Name, characterInfos[i].Job.Name), ServerLog.MessageType.Spawning);
}
#endif
if (!myCharacter)
{
if (bot)
{
GameServer.Log(string.Format("Respawning bot {0} as {1}", character.Info.Name, characterInfos[i].Job.Name), ServerLog.MessageType.Spawning);
}
else
{
clients[i].Character = character;
character.OwnerClientIP = clients[i].Connection.RemoteEndPoint.Address.ToString();
character.OwnerClientName = clients[i].Name;
GameServer.Log(string.Format("Respawning {0} ({1}) as {2}", clients[i].Name, clients[i].Connection?.RemoteEndPoint?.Address, characterInfos[i].Job.Name), ServerLog.MessageType.Spawning);
}
}
if (divingSuitPrefab != null && oxyPrefab != null && respawnShuttle != null)
{
Vector2 pos = cargoSp == null ? character.Position : cargoSp.Position;
if (divingSuitPrefab != null && oxyPrefab != null)
{
var divingSuit = new Item(divingSuitPrefab, pos, respawnSub);
Spawner.CreateNetworkEvent(divingSuit, false);
respawnItems.Add(divingSuit);
var oxyTank = new Item(oxyPrefab, pos, respawnSub);
Spawner.CreateNetworkEvent(oxyTank, false);
divingSuit.Combine(oxyTank);
respawnItems.Add(oxyTank);
}
if (scooterPrefab != null && batteryPrefab != null)
{
var scooter = new Item(scooterPrefab, pos, respawnSub);
Spawner.CreateNetworkEvent(scooter, false);
var battery = new Item(batteryPrefab, pos, respawnSub);
Spawner.CreateNetworkEvent(battery, false);
scooter.Combine(battery);
respawnItems.Add(scooter);
respawnItems.Add(battery);
}
}
//give the character the items they would've gotten if they had spawned in the main sub
character.GiveJobItems(mainSubSpawnPoints[i]);
//add the ID card tags they should've gotten when spawning in the shuttle
foreach (Item item in character.Inventory.Items)
{
if (item == null || item.Prefab.Name != "ID Card") continue;
foreach (string s in shuttleSpawnPoints[i].IdCardTags)
{
item.AddTag(s);
}
if (!string.IsNullOrWhiteSpace(shuttleSpawnPoints[i].IdCardDesc))
item.Description = shuttleSpawnPoints[i].IdCardDesc;
}
}
RespawnCharactersProjSpecific();
}
public void ServerWrite(NetBuffer msg, Client c, object[] extraData = null)
@@ -46,13 +46,13 @@ namespace Barotrauma.Networking
private readonly string[] messageTypeName =
{
"Chat message",
"Item interaction",
"Inventory usage",
"Attack & death",
"ChatMessage",
"ItemInteraction",
"InventoryUsage",
"AttackDeath",
"Spawning",
"Server message",
"Console usage",
"ServerMessage",
"ConsoleUsage",
"Error"
};
@@ -64,7 +64,6 @@ namespace Barotrauma.Networking
private int unsavedLineCount;
private string msgFilter;
private bool[] msgTypeHidden = new bool[Enum.GetValues(typeof(MessageType)).Length];
public int LinesPerFile
@@ -86,7 +85,7 @@ namespace Barotrauma.Networking
//string logLine = "[" + DateTime.Now.ToLongTimeString() + "] " + line;
#if SERVER
DebugConsole.NewMessage(line, Color.White); //TODO: REMOVE
DebugConsole.NewMessage(line, messageColor[(int)messageType]); //TODO: REMOVE
#endif
var newText = new LogMessage(line, messageType);
@@ -0,0 +1,704 @@
using Lidgren.Network;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Xml;
using System.Xml.Linq;
namespace Barotrauma.Networking
{
enum SelectionMode
{
Manual = 0, Random = 1, Vote = 2
}
enum YesNoMaybe
{
No = 0, Maybe = 1, Yes = 2
}
enum BotSpawnMode
{
Normal, Fill
}
partial class ServerSettings : ISerializableEntity
{
[Flags]
public enum NetFlags : byte
{
Name = 0x1,
Message = 0x2,
Properties = 0x4,
Misc = 0x8,
LevelSeed = 0x10
}
public static readonly string PermissionPresetFile = "Data" + Path.DirectorySeparatorChar + "permissionpresets.xml";
public string Name
{
get { return "ServerSettings"; }
}
public class SavedClientPermission
{
public readonly string IP;
public readonly ulong SteamID;
public readonly string Name;
public List<DebugConsole.Command> PermittedCommands;
public ClientPermissions Permissions;
public SavedClientPermission(string name, IPAddress ip, ClientPermissions permissions, List<DebugConsole.Command> permittedCommands)
{
this.Name = name;
this.IP = ip.IsIPv4MappedToIPv6 ? ip.MapToIPv4().ToString() : ip.ToString();
this.Permissions = permissions;
this.PermittedCommands = permittedCommands;
}
public SavedClientPermission(string name, string ip, ClientPermissions permissions, List<DebugConsole.Command> permittedCommands)
{
this.Name = name;
this.IP = ip;
this.Permissions = permissions;
this.PermittedCommands = permittedCommands;
}
public SavedClientPermission(string name, ulong steamID, ClientPermissions permissions, List<DebugConsole.Command> permittedCommands)
{
this.Name = name;
this.SteamID = steamID;
this.Permissions = permissions;
this.PermittedCommands = permittedCommands;
}
}
partial class NetPropertyData
{
private SerializableProperty property;
private string typeString;
private ServerSettings serverSettings;
public string Name
{
get { return property.Name; }
}
public object Value
{
get { return property.GetValue(serverSettings); }
}
public NetPropertyData(ServerSettings serverSettings, SerializableProperty property, string typeString)
{
this.property = property;
this.typeString = typeString;
this.serverSettings = serverSettings;
}
public void Read(NetBuffer msg)
{
long oldPos = msg.Position;
UInt32 size = msg.ReadVariableUInt32();
float x; float y; float z; float w;
byte r; byte g; byte b; byte a;
int ix; int iy; int width; int height;
switch (typeString)
{
case "float":
if (size != 4) break;
property.SetValue(serverSettings, msg.ReadFloat());
return;
case "vector2":
if (size != 8) break;
x = msg.ReadFloat();
y = msg.ReadFloat();
property.SetValue(serverSettings, new Vector2(x, y));
return;
case "vector3":
if (size != 12) break;
x = msg.ReadFloat();
y = msg.ReadFloat();
z = msg.ReadFloat();
property.SetValue(serverSettings, new Vector3(x, y, z));
return;
case "vector4":
if (size != 16) break;
x = msg.ReadFloat();
y = msg.ReadFloat();
z = msg.ReadFloat();
w = msg.ReadFloat();
property.SetValue(serverSettings, new Vector4(x, y, z, w));
return;
case "color":
if (size != 4) break;
r = msg.ReadByte();
g = msg.ReadByte();
b = msg.ReadByte();
a = msg.ReadByte();
property.SetValue(serverSettings, new Color(r, g, b, a));
return;
case "rectangle":
if (size != 16) break;
ix = msg.ReadInt32();
iy = msg.ReadInt32();
width = msg.ReadInt32();
height = msg.ReadInt32();
property.SetValue(serverSettings, new Rectangle(ix, iy, width, height));
return;
default:
msg.Position = oldPos; //reset position to properly read the string
string incVal = msg.ReadString();
property.TrySetValue(serverSettings, incVal);
return;
}
//size didn't match: skip this
msg.Position += 8 * size;
}
public void Write(NetBuffer msg, object overrideValue = null)
{
if (overrideValue == null) overrideValue = property.GetValue(serverSettings);
switch (typeString)
{
case "float":
msg.WriteVariableUInt32(4);
msg.Write((float)overrideValue);
break;
case "vector2":
msg.WriteVariableUInt32(8);
msg.Write(((Vector2)overrideValue).X);
msg.Write(((Vector2)overrideValue).Y);
break;
case "vector3":
msg.WriteVariableUInt32(12);
msg.Write(((Vector3)overrideValue).X);
msg.Write(((Vector3)overrideValue).Y);
msg.Write(((Vector3)overrideValue).Z);
break;
case "vector4":
msg.WriteVariableUInt32(16);
msg.Write(((Vector4)overrideValue).X);
msg.Write(((Vector4)overrideValue).Y);
msg.Write(((Vector4)overrideValue).Z);
msg.Write(((Vector4)overrideValue).W);
break;
case "color":
msg.WriteVariableUInt32(4);
msg.Write(((Color)overrideValue).R);
msg.Write(((Color)overrideValue).G);
msg.Write(((Color)overrideValue).B);
msg.Write(((Color)overrideValue).A);
break;
case "rectangle":
msg.WriteVariableUInt32(16);
msg.Write(((Rectangle)overrideValue).X);
msg.Write(((Rectangle)overrideValue).Y);
msg.Write(((Rectangle)overrideValue).Width);
msg.Write(((Rectangle)overrideValue).Height);
break;
default:
string strVal = overrideValue.ToString();
msg.Write(strVal);
break;
}
}
};
public Dictionary<string, SerializableProperty> SerializableProperties
{
get;
private set;
}
Dictionary<UInt32,NetPropertyData> netProperties;
partial void InitProjSpecific();
public ServerSettings(string serverName, int port, int queryPort, int maxPlayers, bool isPublic, bool enableUPnP)
{
ServerLog = new ServerLog(serverName);
Voting = new Voting();
Whitelist = new WhiteList();
BanList = new BanList();
ExtraCargo = new Dictionary<ItemPrefab, int>();
PermissionPreset.LoadAll(PermissionPresetFile);
InitProjSpecific();
ServerName = serverName;
Port = port;
QueryPort = queryPort;
//EnableUPnP = enableUPnP;
this.maxPlayers = maxPlayers;
this.isPublic = isPublic;
netProperties = new Dictionary<UInt32, NetPropertyData>();
using (MD5 md5 = MD5.Create())
{
var saveProperties = SerializableProperty.GetProperties<Serialize>(this);
foreach (var property in saveProperties)
{
object value = property.GetValue(this);
if (value == null) continue;
string typeName = SerializableProperty.GetSupportedTypeName(value.GetType());
if (typeName != null || property.PropertyType.IsEnum)
{
NetPropertyData netPropertyData = new NetPropertyData(this, property, typeName);
UInt32 key = ToolBox.StringToUInt32Hash(property.Name, md5);
if (netProperties.ContainsKey(key)) throw new Exception("Hashing collision in ServerSettings.netProperties: " + netProperties[key] + " has same key as " + property.Name + " (" + key.ToString() + ")");
netProperties.Add(key, netPropertyData);
}
}
}
}
public string ServerName;
public string ServerMessageText;
public int Port;
public int QueryPort;
public ServerLog ServerLog;
public Voting Voting;
public Dictionary<string, bool> MonsterEnabled { get; private set; }
public Dictionary<ItemPrefab, int> ExtraCargo { get; private set; }
private TimeSpan sparseUpdateInterval = new TimeSpan(0, 0, 0, 3);
private float selectedLevelDifficulty;
private string password;
public float AutoRestartTimer;
private bool autoRestart;
public bool isPublic;
private int maxPlayers;
public List<SavedClientPermission> ClientPermissions { get; private set; } = new List<SavedClientPermission>();
public WhiteList Whitelist { get; private set; }
[Serialize(20, true)]
public int TickRate
{
get;
set;
}
[Serialize(true, true)]
public bool RandomizeSeed
{
get;
set;
}
[Serialize(300.0f, true)]
public float RespawnInterval
{
get;
private set;
}
[Serialize(180.0f, true)]
public float MaxTransportTime
{
get;
private set;
}
[Serialize(0.2f, true)]
public float MinRespawnRatio
{
get;
private set;
}
[Serialize(60.0f, true)]
public float AutoRestartInterval
{
get;
set;
}
[Serialize(false, true)]
public bool StartWhenClientsReady
{
get;
private set;
}
[Serialize(0.8f, true)]
public float StartWhenClientsReadyRatio
{
get;
private set;
}
[Serialize(true, true)]
public bool AllowSpectating
{
get;
private set;
}
[Serialize(true, true)]
public bool EndRoundAtLevelEnd
{
get;
private set;
}
[Serialize(true, true)]
public bool SaveServerLogs
{
get;
private set;
}
[Serialize(true, true)]
public bool AllowRagdollButton
{
get;
private set;
}
[Serialize(true, true)]
public bool AllowFileTransfers
{
get;
private set;
}
[Serialize(true, true)]
public bool VoiceChatEnabled
{
get;
set;
}
[Serialize(800, true)]
private int LinesPerLogFile
{
get
{
return ServerLog.LinesPerFile;
}
set
{
ServerLog.LinesPerFile = value;
}
}
public bool AutoRestart
{
get { return autoRestart; }
set
{
autoRestart = value;
AutoRestartTimer = autoRestart ? AutoRestartInterval : 0.0f;
}
}
public bool HasPassword
{
get { return !string.IsNullOrEmpty(password); }
}
[Serialize(true, true)]
public bool AllowVoteKick
{
get
{
return Voting.AllowVoteKick;
}
set
{
Voting.AllowVoteKick = value;
}
}
[Serialize(true, true)]
public bool AllowEndVoting
{
get
{
return Voting.AllowEndVoting;
}
set
{
Voting.AllowEndVoting = value;
}
}
[Serialize(true, true)]
public bool AllowRespawn
{
get;
set;
}
[Serialize(0, true)]
public int BotCount
{
get;
set;
}
[Serialize(16, true)]
public int MaxBotCount
{
get;
set;
}
public BotSpawnMode BotSpawnMode
{
get;
set;
}
public float SelectedLevelDifficulty
{
get { return selectedLevelDifficulty; }
set { selectedLevelDifficulty = MathHelper.Clamp(value, 0.0f, 100.0f); }
}
[Serialize(true, true)]
public bool AllowDisguises
{
get;
set;
}
public YesNoMaybe TraitorsEnabled
{
get;
set;
}
[Serialize(SelectionMode.Manual, true)]
public SelectionMode SubSelectionMode { get; private set; }
[Serialize(SelectionMode.Manual, true)]
public SelectionMode ModeSelectionMode { get; private set; }
public BanList BanList { get; private set; }
[Serialize(0.6f, true)]
public float EndVoteRequiredRatio
{
get;
private set;
}
[Serialize(0.6f, true)]
public float KickVoteRequiredRatio
{
get;
private set;
}
[Serialize(30.0f, true)]
public float KillDisconnectedTime
{
get;
private set;
}
[Serialize(120.0f, true)]
public float KickAFKTime
{
get;
private set;
}
[Serialize(true, true)]
public bool TraitorUseRatio
{
get;
private set;
}
[Serialize(0.2f, true)]
public float TraitorRatio
{
get;
private set;
}
[Serialize(false, true)]
public bool KarmaEnabled
{
get;
set;
}
[Serialize("sandbox", true)]
public string GameModeIdentifier
{
get;
set;
}
[Serialize("Random", true)]
public string MissionType
{
get;
set;
}
public int MaxPlayers
{
get { return maxPlayers; }
}
public List<MissionType> AllowedRandomMissionTypes
{
get;
set;
}
[Serialize(60f, true)]
public float AutoBanTime
{
get;
private set;
}
[Serialize(360f, true)]
public float MaxAutoBanTime
{
get;
private set;
}
public void SetPassword(string password)
{
this.password = Encoding.UTF8.GetString(NetUtility.ComputeSHAHash(Encoding.UTF8.GetBytes(password)));
}
public bool IsPasswordCorrect(string input, int nonce)
{
if (!HasPassword) return true;
string saltedPw = password;
saltedPw = saltedPw + Convert.ToString(nonce);
saltedPw = Encoding.UTF8.GetString(NetUtility.ComputeSHAHash(Encoding.UTF8.GetBytes(saltedPw)));
return input == saltedPw;
}
/// <summary>
/// A list of int pairs that represent the ranges of UTF-16 codes allowed in client names
/// </summary>
public List<Pair<int, int>> AllowedClientNameChars
{
get;
private set;
} = new List<Pair<int, int>>();
public void ReadMonsterEnabled(NetBuffer inc)
{
//monster spawn settings
if (MonsterEnabled == null)
{
List<string> monsterNames1 = GameMain.Instance.GetFilesOfType(ContentType.Character).ToList();
for (int i = 0; i < monsterNames1.Count; i++)
{
monsterNames1[i] = Path.GetFileName(Path.GetDirectoryName(monsterNames1[i]));
}
MonsterEnabled = new Dictionary<string, bool>();
foreach (string s in monsterNames1)
{
if (!MonsterEnabled.ContainsKey(s)) MonsterEnabled.Add(s, true);
}
}
List<string> monsterNames = MonsterEnabled.Keys.ToList();
foreach (string s in monsterNames)
{
MonsterEnabled[s] = inc.ReadBoolean();
}
inc.ReadPadBits();
}
public void WriteMonsterEnabled(NetBuffer msg, Dictionary<string, bool> monsterEnabled = null)
{
//monster spawn settings
if (monsterEnabled == null) monsterEnabled = MonsterEnabled;
List<string> monsterNames = monsterEnabled.Keys.ToList();
foreach (string s in monsterNames)
{
msg.Write(monsterEnabled[s]);
}
msg.WritePadBits();
}
public bool ReadExtraCargo(NetBuffer msg)
{
bool changed = false;
UInt32 count = msg.ReadUInt32();
if (ExtraCargo == null || count != ExtraCargo.Count) changed = true;
Dictionary<ItemPrefab, int> extraCargo = new Dictionary<ItemPrefab, int>();
for (int i = 0; i < count; i++)
{
string prefabName = msg.ReadString();
byte amount = msg.ReadByte();
ItemPrefab ip = MapEntityPrefab.List.Find(p => p is ItemPrefab && p.Name.Equals(prefabName, StringComparison.InvariantCulture)) as ItemPrefab;
if (ip != null && amount > 0)
{
if (changed || !ExtraCargo.ContainsKey(ip) || ExtraCargo[ip] != amount) changed = true;
extraCargo.Add(ip, amount);
}
}
if (changed) ExtraCargo = extraCargo;
return changed;
}
public void WriteExtraCargo(NetBuffer msg)
{
if (ExtraCargo == null)
{
msg.Write((UInt32)0);
return;
}
msg.Write((UInt32)ExtraCargo.Count);
foreach (KeyValuePair<ItemPrefab, int> kvp in ExtraCargo)
{
msg.Write(kvp.Key.Name); msg.Write((byte)kvp.Value);
}
}
}
}
@@ -1,17 +1,11 @@
using Barotrauma.Networking;
using Facepunch.Steamworks;
using Microsoft.Xna.Framework;
using RestSharp.Extensions.MonoHttp;
using Facepunch.Steamworks;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Xml.Linq;
namespace Barotrauma.Steam
{
class SteamManager
partial class SteamManager
{
#if DEBUG
public static bool USE_STEAM
@@ -72,165 +66,10 @@ namespace Barotrauma.Steam
if (!USE_STEAM) return;
instance = new SteamManager();
}
private SteamManager()
{
#if SERVER
return;
#endif
try
{
client = new Facepunch.Steamworks.Client(AppID);
isInitialized = client.IsSubscribed && client.IsValid;
if (isInitialized)
{
DebugConsole.Log("Logged in as " + client.Username + " (SteamID " + client.SteamId + ")");
}
}
catch (DllNotFoundException e)
{
isInitialized = false;
#if CLIENT
new Barotrauma.GUIMessageBox(TextManager.Get("Error"), TextManager.Get("SteamDllNotFound"));
#else
DebugConsole.ThrowError("Initializing Steam client failed (steam_api64.dll not found).", e);
#endif
}
catch (Exception e)
{
isInitialized = false;
#if CLIENT
new Barotrauma.GUIMessageBox(TextManager.Get("Error"), TextManager.Get("SteamClientInitFailed"));
#else
DebugConsole.ThrowError("Initializing Steam client failed.", e);
#endif
}
if (!isInitialized)
{
try
{
Facepunch.Steamworks.Client.Instance.Dispose();
}
catch (Exception e)
{
if (GameSettings.VerboseLogging) DebugConsole.ThrowError("Disposing Steam client failed.", e);
}
}
}
#region Server
public static bool CreateServer(Networking.GameServer server, bool isPublic)
{
#if !SERVER
if (instance == null || !instance.isInitialized)
{
return false;
}
#endif
ServerInit options = new ServerInit("Barotrauma", "Barotrauma")
{
GamePort = (ushort)server.Port,
QueryPort = (ushort)server.QueryPort
};
instance.server = new Server(AppID, options, isPublic);
if (!instance.server.IsValid)
{
instance.server.Dispose();
instance.server = null;
DebugConsole.ThrowError("Initializing Steam server failed.");
return false;
}
#if SERVER
instance.isInitialized = true;
#endif
RefreshServerDetails(server);
instance.server.Auth.OnAuthChange = server.OnAuthChange;
Instance.server.LogOnAnonymous();
return true;
}
public static bool RefreshServerDetails(Networking.GameServer server)
{
if (instance == null || !instance.isInitialized)
{
return false;
}
// These server state variables may be changed at any time. Note that there is no lnoger a mechanism
// to send the player count. The player count is maintained by steam and you should use the player
// creation/authentication functions to maintain your player count.
instance.server.ServerName = server.Name;
instance.server.MaxPlayers = server.MaxPlayers;
instance.server.Passworded = server.HasPassword;
Instance.server.SetKey("message", GameMain.NetLobbyScreen.ServerMessageText);
Instance.server.SetKey("version", GameMain.Version.ToString());
Instance.server.SetKey("contentpackage", string.Join(",", GameMain.Config.SelectedContentPackages.Select(cp => cp.Name)));
Instance.server.SetKey("contentpackagehash", string.Join(",", GameMain.Config.SelectedContentPackages.Select(cp => cp.MD5hash.Hash)));
Instance.server.SetKey("contentpackageurl", string.Join(",", GameMain.Config.SelectedContentPackages.Select(cp => cp.SteamWorkshopUrl ?? "")));
Instance.server.SetKey("usingwhitelist", (server.WhiteList != null && server.WhiteList.Enabled).ToString());
Instance.server.SetKey("modeselectionmode", server.ModeSelectionMode.ToString());
Instance.server.SetKey("subselectionmode", server.SubSelectionMode.ToString());
Instance.server.SetKey("allowspectating", server.AllowSpectating.ToString());
Instance.server.SetKey("allowrespawn", server.AllowRespawn.ToString());
Instance.server.SetKey("traitors", server.TraitorsEnabled.ToString());
Instance.server.SetKey("gamestarted", server.GameStarted.ToString());
Instance.server.SetKey("gamemode", server.GameModeIdentifier);
#if SERVER
instance.server.DedicatedServer = true;
#endif
return true;
}
public static bool StartAuthSession(byte[] authTicketData, ulong clientSteamID)
{
if (instance == null || !instance.isInitialized || instance.server == null) return false;
DebugConsole.Log("SteamManager authenticating Steam client " + clientSteamID);
if (!instance.server.Auth.StartSession(authTicketData, clientSteamID))
{
DebugConsole.Log("Authentication failed");
return false;
}
return true;
}
public static void StopAuthSession(ulong clientSteamID)
{
if (instance == null || !instance.isInitialized || instance.server == null) return;
DebugConsole.Log("SteamManager ending auth session with Steam client " + clientSteamID);
instance.server.Auth.EndSession(clientSteamID);
}
public static bool CloseServer()
{
if (instance == null || !instance.isInitialized || instance.server == null) return false;
instance.server.Dispose();
instance.server = null;
return true;
}
#endregion
public static bool UnlockAchievement(string achievementName)
{
if (instance == null || !instance.isInitialized)
if (instance == null || !instance.isInitialized || instance.client == null)
{
return false;
}
@@ -280,770 +119,11 @@ namespace Barotrauma.Steam
}
return success;
}
#if CLIENT
public static ulong GetSteamID()
{
if (instance == null || !instance.isInitialized)
{
return 0;
}
return instance.client.SteamId;
}
public static ulong GetWorkshopItemIDFromUrl(string url)
{
try
{
Uri uri = new Uri(url);
string idStr = HttpUtility.ParseQueryString(uri.Query).Get("id");
if (ulong.TryParse(idStr, out ulong id))
{
return id;
}
}
catch (Exception e)
{
DebugConsole.ThrowError("Failed to get Workshop item ID from the url \""+url+"\"!", e);
}
return 0;
}
#region Connecting to servers
public static bool GetServers(Action<Networking.ServerInfo> onServerFound, Action<Networking.ServerInfo> onServerRulesReceived, Action onFinished)
{
if (instance == null || !instance.isInitialized)
{
return false;
}
var filter = new ServerList.Filter
{
{ "appid", AppID.ToString() },
{ "gamedir", "Barotrauma" },
{ "secure", "1" }
};
//include unresponsive servers in the server list
//the response is queried using the server's query port, not the game port,
//so it may be possible to play on the server even if it doesn't respond to server list queries
var query = instance.client.ServerList.Internet(filter);
query.OnUpdate += () => { UpdateServerQuery(query, onServerFound, onServerRulesReceived, includeUnresponsive: true); };
query.OnFinished = onFinished;
var localQuery = instance.client.ServerList.Local(filter);
localQuery.OnUpdate += () => { UpdateServerQuery(localQuery, onServerFound, onServerRulesReceived, includeUnresponsive: true); };
localQuery.OnFinished = onFinished;
return true;
}
private static void UpdateServerQuery(ServerList.Request query, Action<Networking.ServerInfo> onServerFound, Action<Networking.ServerInfo> onServerRulesReceived, bool includeUnresponsive)
{
IEnumerable<ServerList.Server> servers = includeUnresponsive ?
new List<ServerList.Server>(query.Responded).Concat(query.Unresponsive) :
query.Responded;
foreach (ServerList.Server s in servers)
{
if (!ValidateServerInfo(s)) { continue; }
bool responded = query.Responded.Contains(s);
if (responded)
{
DebugConsole.Log(s.Name + " responded to server query.");
}
else
{
DebugConsole.Log(s.Name + " did not respond to server query.");
}
var serverInfo = new ServerInfo()
{
ServerName = s.Name,
Port = s.ConnectionPort.ToString(),
IP = s.Address.ToString(),
PlayerCount = s.Players,
MaxPlayers = s.MaxPlayers,
HasPassword = s.Passworded,
RespondedToSteamQuery = responded
};
serverInfo.PingChecked = true;
serverInfo.Ping = s.Ping;
if (responded)
{
s.FetchRules();
}
s.OnReceivedRules += (bool rulesReceived) =>
{
if (!rulesReceived || s.Rules == null) { return; }
if (s.Rules.ContainsKey("message")) serverInfo.ServerMessage = s.Rules["message"];
if (s.Rules.ContainsKey("version")) serverInfo.GameVersion = s.Rules["version"];
if (s.Rules.ContainsKey("contentpackage")) serverInfo.ContentPackageNames.AddRange(s.Rules["contentpackage"].Split(','));
if (s.Rules.ContainsKey("contentpackagehash")) serverInfo.ContentPackageHashes.AddRange(s.Rules["contentpackagehash"].Split(','));
if (s.Rules.ContainsKey("contentpackageurl")) serverInfo.ContentPackageWorkshopUrls.AddRange(s.Rules["contentpackageurl"].Split(','));
if (s.Rules.ContainsKey("usingwhitelist")) serverInfo.UsingWhiteList = s.Rules["usingwhitelist"] == "True";
if (s.Rules.ContainsKey("modeselectionmode"))
{
if (Enum.TryParse(s.Rules["modeselectionmode"], out SelectionMode selectionMode)) serverInfo.ModeSelectionMode = selectionMode;
}
if (s.Rules.ContainsKey("subselectionmode"))
{
if (Enum.TryParse(s.Rules["subselectionmode"], out SelectionMode selectionMode)) serverInfo.SubSelectionMode = selectionMode;
}
if (s.Rules.ContainsKey("allowspectating")) serverInfo.AllowSpectating = s.Rules["allowspectating"] == "True";
if (s.Rules.ContainsKey("allowrespawn")) serverInfo.AllowRespawn = s.Rules["allowrespawn"] == "True";
if (s.Rules.ContainsKey("traitors"))
{
if (Enum.TryParse(s.Rules["traitors"], out YesNoMaybe traitorsEnabled)) serverInfo.TraitorsEnabled = traitorsEnabled;
}
if (serverInfo.ContentPackageNames.Count != serverInfo.ContentPackageHashes.Count ||
serverInfo.ContentPackageHashes.Count != serverInfo.ContentPackageWorkshopUrls.Count)
{
//invalid contentpackage info
serverInfo.ContentPackageNames.Clear();
serverInfo.ContentPackageHashes.Clear();
}
onServerRulesReceived(serverInfo);
};
onServerFound(serverInfo);
}
query.Responded.Clear();
}
private static bool ValidateServerInfo(ServerList.Server server)
{
if (string.IsNullOrEmpty(server.Name)) { return false; }
if (server.Address == null) { return false; }
return true;
}
public static Auth.Ticket GetAuthSessionTicket()
{
if (instance == null || !instance.isInitialized)
{
return null;
}
return instance.client.Auth.GetAuthSessionTicket();
}
#endregion
#region Workshop
public const string WorkshopItemStagingFolder = "NewWorkshopItem";
public const string WorkshopItemPreviewImageFolder = "Workshop";
public const string PreviewImageName = "PreviewImage.png";
private const string MetadataFileName = "filelist.xml";
private const string DefaultPreviewImagePath = "Content/DefaultWorkshopPreviewImage.png";
private Sprite defaultPreviewImage;
public Sprite DefaultPreviewImage
{
get
{
if (defaultPreviewImage == null)
{
defaultPreviewImage = new Sprite(DefaultPreviewImagePath, sourceRectangle: null);
}
return defaultPreviewImage;
}
}
public static void GetSubscribedWorkshopItems(Action<IList<Workshop.Item>> onItemsFound, List<string> requireTags = null)
{
if (instance == null || !instance.isInitialized) return;
var query = instance.client.Workshop.CreateQuery();
query.Order = Workshop.Order.RankedByTrend;
query.UserId = instance.client.SteamId;
query.UserQueryType = Workshop.UserQueryType.Subscribed;
query.UploaderAppId = AppID;
if (requireTags != null) query.RequireTags = requireTags;
query.Run();
query.OnResult += (Workshop.Query q) =>
{
onItemsFound?.Invoke(q.Items);
};
}
public static void GetPopularWorkshopItems(Action<IList<Workshop.Item>> onItemsFound, int amount, List<string> requireTags = null)
{
if (instance == null || !instance.isInitialized) return;
var query = instance.client.Workshop.CreateQuery();
query.Order = Workshop.Order.RankedByTrend;
query.RankedByTrendDays = 30;
query.UploaderAppId = AppID;
if (requireTags != null) query.RequireTags = requireTags;
query.Run();
query.OnResult += (Workshop.Query q) =>
{
//count the number of each unique tag
foreach (var item in q.Items)
{
foreach (string tag in item.Tags)
{
if (string.IsNullOrEmpty(tag)) { continue; }
string caseInvariantTag = tag.ToLowerInvariant();
if (!instance.tagCommonness.ContainsKey(caseInvariantTag))
{
instance.tagCommonness[caseInvariantTag] = 1;
}
else
{
instance.tagCommonness[caseInvariantTag]++;
}
}
}
//populate the popularTags list with tags sorted by commonness
instance.popularTags.Clear();
foreach (KeyValuePair<string, int> tagCommonness in instance.tagCommonness)
{
int i = 0;
while (i < instance.popularTags.Count &&
instance.tagCommonness[instance.popularTags[i]] > tagCommonness.Value)
{
i++;
}
instance.popularTags.Insert(i, tagCommonness.Key);
}
var nonSubscribedItems = q.Items.Where(it => !it.Subscribed && !it.Installed);
if (nonSubscribedItems.Count() > amount)
{
nonSubscribedItems = nonSubscribedItems.Take(amount);
}
onItemsFound?.Invoke(nonSubscribedItems.ToList());
};
}
public static void GetPublishedWorkshopItems(Action<IList<Workshop.Item>> onItemsFound, List<string> requireTags = null)
{
if (instance == null || !instance.isInitialized) return;
var query = instance.client.Workshop.CreateQuery();
query.Order = Workshop.Order.RankedByPublicationDate;
query.UserId = instance.client.SteamId;
query.UserQueryType = Workshop.UserQueryType.Published;
query.UploaderAppId = AppID;
if (requireTags != null) query.RequireTags = requireTags;
query.Run();
query.OnResult += (Workshop.Query q) =>
{
onItemsFound?.Invoke(q.Items);
};
}
public static void SubscribeToWorkshopItem(string itemUrl)
{
if (instance == null || !instance.isInitialized) return;
ulong id = GetWorkshopItemIDFromUrl(itemUrl);
if (id == 0) { return; }
var item = instance.client.Workshop.GetItem(id);
if (item == null)
{
DebugConsole.ThrowError("Failed to find a Steam Workshop item with the ID " + id + ".");
return;
}
item.Subscribe();
item.Download();
}
public static void SaveToWorkshop(Submarine sub)
{
if (instance == null || !instance.isInitialized) return;
Workshop.Editor item;
ContentPackage contentPackage;
try
{
CreateWorkshopItemStaging(
new List<ContentFile>() { new ContentFile(sub.FilePath, ContentType.None) },
out item, out contentPackage);
}
catch (Exception e)
{
DebugConsole.ThrowError("Creating the workshop item failed.", e);
return;
}
item.Description = sub.Description;
item.Title = sub.Name;
item.Tags.Add("Submarine");
string subPreviewPath = Path.GetFullPath(Path.Combine(item.Folder, PreviewImageName));
#if CLIENT
try
{
using (Stream s = File.Create(subPreviewPath))
{
sub.PreviewImage.Texture.SaveAsPng(s, (int)sub.PreviewImage.size.X, (int)sub.PreviewImage.size.Y);
item.PreviewImage = subPreviewPath;
}
}
catch (Exception e)
{
DebugConsole.ThrowError("Saving submarine preview image failed.", e);
item.PreviewImage = null;
}
#endif
StartPublishItem(contentPackage, item);
}
/// <summary>
/// Creates a new folder, copies the specified files there and creates a metadata file with install instructions.
/// </summary>
public static void CreateWorkshopItemStaging(List<ContentFile> contentFiles, out Workshop.Editor itemEditor, out ContentPackage contentPackage)
{
var stagingFolder = new DirectoryInfo(WorkshopItemStagingFolder);
if (stagingFolder.Exists)
{
SaveUtil.ClearFolder(stagingFolder.FullName);
}
else
{
stagingFolder.Create();
}
Directory.CreateDirectory(Path.Combine(WorkshopItemStagingFolder, "Submarines"));
Directory.CreateDirectory(Path.Combine(WorkshopItemStagingFolder, "Mods"));
Directory.CreateDirectory(Path.Combine(WorkshopItemStagingFolder, "Mods", "ModName"));
itemEditor = instance.client.Workshop.CreateItem(Workshop.ItemType.Community);
itemEditor.Visibility = Workshop.Editor.VisibilityType.Public;
itemEditor.WorkshopUploadAppId = AppID;
itemEditor.Folder = stagingFolder.FullName;
string previewImagePath = Path.GetFullPath(Path.Combine(itemEditor.Folder, PreviewImageName));
File.Copy("Content/DefaultWorkshopPreviewImage.png", previewImagePath);
//copy content files to the staging folder
List<string> copiedFilePaths = new List<string>();
foreach (ContentFile file in contentFiles)
{
string relativePath = UpdaterUtil.GetRelativePath(Path.GetFullPath(file.Path), Environment.CurrentDirectory);
string destinationPath = Path.Combine(stagingFolder.FullName, relativePath);
//make sure the directory exists
Directory.CreateDirectory(Path.GetDirectoryName(destinationPath));
File.Copy(file.Path, destinationPath);
copiedFilePaths.Add(destinationPath);
}
System.Diagnostics.Debug.Assert(copiedFilePaths.Count == contentFiles.Count);
//create a new content package and include the copied files in it
contentPackage = ContentPackage.CreatePackage("ContentPackage", Path.Combine(itemEditor.Folder, MetadataFileName), false);
for (int i = 0; i < copiedFilePaths.Count; i++)
{
contentPackage.AddFile(copiedFilePaths[i], contentFiles[i].Type);
}
contentPackage.Save(Path.Combine(stagingFolder.FullName, MetadataFileName));
}
/// <summary>
/// Creates a copy of the specified workshop item in the staging folder and an editor that can be used to edit and update the item
/// </summary>
public static void CreateWorkshopItemStaging(Workshop.Item existingItem, out Workshop.Editor itemEditor, out ContentPackage contentPackage)
{
if (!existingItem.Installed)
{
itemEditor = null;
contentPackage = null;
DebugConsole.ThrowError("Cannot edit the workshop item \"" + existingItem.Title + "\" because it has not been installed.");
return;
}
var stagingFolder = new DirectoryInfo(WorkshopItemStagingFolder);
if (stagingFolder.Exists)
{
SaveUtil.ClearFolder(stagingFolder.FullName);
}
else
{
stagingFolder.Create();
}
itemEditor = instance.client.Workshop.EditItem(existingItem.Id);
itemEditor.Visibility = Workshop.Editor.VisibilityType.Public;
itemEditor.Title = existingItem.Title;
itemEditor.Tags = existingItem.Tags.ToList();
itemEditor.Description = existingItem.Description;
itemEditor.WorkshopUploadAppId = AppID;
itemEditor.Folder = stagingFolder.FullName;
string previewImagePath = Path.GetFullPath(Path.Combine(itemEditor.Folder, PreviewImageName));
itemEditor.PreviewImage = previewImagePath;
using (WebClient client = new WebClient())
{
if (File.Exists(previewImagePath))
{
File.Delete(previewImagePath);
}
client.DownloadFile(new Uri(existingItem.PreviewImageUrl), previewImagePath);
}
ContentPackage tempContentPackage = new ContentPackage(Path.Combine(existingItem.Directory.FullName, MetadataFileName));
if (File.Exists(tempContentPackage.Path))
{
string newContentPackagePath = Path.Combine(WorkshopItemStagingFolder, MetadataFileName);
File.Copy(tempContentPackage.Path, newContentPackagePath, overwrite: true);
contentPackage = new ContentPackage(newContentPackagePath);
foreach (ContentFile contentFile in tempContentPackage.Files)
{
string sourceFile = Path.Combine(existingItem.Directory.FullName, contentFile.Path);
if (!File.Exists(sourceFile)) { continue; }
//make sure the destination directory exists
string destinationPath = Path.Combine(WorkshopItemStagingFolder, contentFile.Path);
Directory.CreateDirectory(Path.GetDirectoryName(destinationPath));
File.Copy(sourceFile, destinationPath, overwrite: true);
contentPackage.AddFile(contentFile.Path, contentFile.Type);
}
}
else
{
contentPackage = ContentPackage.CreatePackage(existingItem.Title, Path.Combine(WorkshopItemStagingFolder, MetadataFileName), false);
contentPackage.Save(contentPackage.Path);
}
}
public static void StartPublishItem(ContentPackage contentPackage, Workshop.Editor item)
{
if (instance == null || !instance.isInitialized) return;
if (string.IsNullOrEmpty(item.Title))
{
DebugConsole.ThrowError("Cannot publish workshop item - title not set.");
return;
}
if (string.IsNullOrEmpty(item.Folder))
{
DebugConsole.ThrowError("Cannot publish workshop item \"" + item.Title + "\" - folder not set.");
return;
}
contentPackage.Name = item.Title;
contentPackage.GameVersion = GameMain.Version;
contentPackage.Save(Path.Combine(WorkshopItemStagingFolder, MetadataFileName));
if (File.Exists(PreviewImageName)) File.Delete(PreviewImageName);
//move the preview image out of the staging folder, it does not need to be included in the folder sent to Workshop
File.Move(Path.GetFullPath(Path.Combine(item.Folder, PreviewImageName)), PreviewImageName);
item.PreviewImage = Path.GetFullPath(PreviewImageName);
CoroutineManager.StartCoroutine(PublishItem(item));
}
private static IEnumerable<object> PublishItem(Workshop.Editor item)
{
if (instance == null || !instance.isInitialized)
{
yield return CoroutineStatus.Success;
}
item.Publish();
while (item.Publishing)
{
yield return CoroutineStatus.Running;
}
if (string.IsNullOrEmpty(item.Error))
{
DebugConsole.NewMessage("Published workshop item " + item.Title + " successfully.", Microsoft.Xna.Framework.Color.LightGreen);
}
else
{
DebugConsole.ThrowError("Publishing workshop item " + item.Title + " failed. " + item.Error);
}
SaveUtil.ClearFolder(WorkshopItemStagingFolder);
Directory.Delete(WorkshopItemStagingFolder);
File.Delete(PreviewImageName);
yield return CoroutineStatus.Success;
}
/// <summary>
/// Enables a workshop item by moving it to the game folder.
/// </summary>
public static bool EnableWorkShopItem(Workshop.Item item, bool allowFileOverwrite, out string errorMsg)
{
if (!item.Installed)
{
errorMsg = TextManager.Get("WorkshopErrorInstallRequiredToEnable").Replace("[itemname]", item.Title);
DebugConsole.NewMessage(errorMsg, Microsoft.Xna.Framework.Color.Red);
return false;
}
ContentPackage contentPackage = new ContentPackage(Path.Combine(item.Directory.FullName, MetadataFileName));
string newContentPackagePath = GetWorkshopItemContentPackagePath(contentPackage);
var allPackageFiles = Directory.GetFiles(item.Directory.FullName, "*", SearchOption.AllDirectories);
List<string> nonContentFiles = new List<string>();
foreach (string file in allPackageFiles)
{
if (file == MetadataFileName) { continue; }
string relativePath = UpdaterUtil.GetRelativePath(file, item.Directory.FullName);
string fullPath = Path.GetFullPath(relativePath);
if (contentPackage.Files.Any(f => { string fp = Path.GetFullPath(f.Path); return fp == fullPath; })) { continue; }
if (ContentPackage.IsModFilePathAllowed(relativePath))
{
nonContentFiles.Add(relativePath);
}
}
if (!allowFileOverwrite)
{
if (File.Exists(newContentPackagePath))
{
errorMsg = TextManager.Get("WorkshopErrorOverwriteOnEnable")
.Replace("[itemname]", item.Title)
.Replace("[filename]", newContentPackagePath);
DebugConsole.NewMessage(errorMsg, Microsoft.Xna.Framework.Color.Red);
return false;
}
foreach (ContentFile contentFile in contentPackage.Files)
{
string sourceFile = Path.Combine(item.Directory.FullName, contentFile.Path);
if (File.Exists(sourceFile) && File.Exists(contentFile.Path))
{
errorMsg = TextManager.Get("WorkshopErrorOverwriteOnEnable")
.Replace("[itemname]", item.Title)
.Replace("[filename]", contentFile.Path);
DebugConsole.NewMessage(errorMsg, Microsoft.Xna.Framework.Color.Red);
return false;
}
}
}
try
{
//we only need to create a new content package for the item if it contains content with a type other than None or Submarine
//e.g. items that are just a sub file are just copied to the game folder
if (contentPackage.Files.Any(f => f.Type != ContentType.None && f.Type != ContentType.Submarine))
{
File.Copy(contentPackage.Path, newContentPackagePath);
}
foreach (ContentFile contentFile in contentPackage.Files)
{
string sourceFile = Path.Combine(item.Directory.FullName, contentFile.Path);
if (!File.Exists(sourceFile)) { continue; }
if (!ContentPackage.IsModFilePathAllowed(contentFile))
{
DebugConsole.ThrowError(TextManager.Get("WorkshopErrorIllegalPathOnEnable").Replace("[filename]", contentFile.Path));
continue;
}
//make sure the destination directory exists
Directory.CreateDirectory(Path.GetDirectoryName(contentFile.Path));
File.Copy(sourceFile, contentFile.Path, overwrite: true);
}
foreach (string nonContentFile in nonContentFiles)
{
string sourceFile = Path.Combine(item.Directory.FullName, nonContentFile);
if (!File.Exists(sourceFile)) { continue; }
if (!ContentPackage.IsModFilePathAllowed(nonContentFile))
{
DebugConsole.ThrowError(TextManager.Get("WorkshopErrorIllegalPathOnEnable").Replace("[filename]", nonContentFile));
continue;
}
Directory.CreateDirectory(Path.GetDirectoryName(nonContentFile));
File.Copy(sourceFile, nonContentFile, overwrite: true);
}
}
catch (Exception e)
{
errorMsg = TextManager.Get("WorkshopErrorEnableFailed").Replace("[itemname]", item.Title) + " " + e.Message;
DebugConsole.NewMessage(errorMsg, Microsoft.Xna.Framework.Color.Red);
return false;
}
var newPackage = new ContentPackage(contentPackage.Path, newContentPackagePath)
{
SteamWorkshopUrl = item.Url
};
newPackage.Save(newContentPackagePath);
ContentPackage.List.Add(newPackage);
if (newPackage.CorePackage)
{
//if enabling a core package, disable all other core packages
GameMain.Config.SelectedContentPackages.RemoveWhere(cp => cp.CorePackage);
}
GameMain.Config.SelectedContentPackages.Add(newPackage);
GameMain.Config.Save();
if (item.Tags.Contains("Submarine") || newPackage.Files.Any(f => f.Type == ContentType.Submarine))
{
Submarine.RefreshSavedSubs();
}
errorMsg = "";
return true;
}
/// <summary>
/// Disables a workshop item by removing the files from the game folder.
/// </summary>
public static bool DisableWorkShopItem(Workshop.Item item, out string errorMsg)
{
if (!item.Installed)
{
errorMsg = "Cannot disable workshop item \"" + item.Title + "\" because it has not been installed.";
DebugConsole.NewMessage(errorMsg, Microsoft.Xna.Framework.Color.Red);
return false;
}
ContentPackage contentPackage = new ContentPackage(Path.Combine(item.Directory.FullName, MetadataFileName));
string installedContentPackagePath = GetWorkshopItemContentPackagePath(contentPackage);
var allPackageFiles = Directory.GetFiles(item.Directory.FullName, "*", SearchOption.AllDirectories);
List<string> nonContentFiles = new List<string>();
foreach (string file in allPackageFiles)
{
if (file == MetadataFileName) { continue; }
string relativePath = UpdaterUtil.GetRelativePath(file, item.Directory.FullName);
string fullPath = Path.GetFullPath(relativePath);
if (contentPackage.Files.Any(f => { string fp = Path.GetFullPath(f.Path); return fp == fullPath; })) { continue; }
if (ContentPackage.IsModFilePathAllowed(relativePath))
{
nonContentFiles.Add(relativePath);
}
}
if (File.Exists(installedContentPackagePath)) { File.Delete(installedContentPackagePath); }
bool wasSub = item.Tags.Contains("Submarine") || contentPackage.Files.Any(f => f.Type == ContentType.Submarine);
HashSet<string> directories = new HashSet<string>();
try
{
foreach (ContentFile contentFile in contentPackage.Files)
{
if (!ContentPackage.IsModFilePathAllowed(contentFile))
{
//Workshop items are not allowed to add or modify files in the Content or Data folders;
continue;
}
if (!File.Exists(contentFile.Path)) { continue; }
File.Delete(contentFile.Path);
directories.Add(Path.GetDirectoryName(contentFile.Path));
}
foreach (string nonContentFile in nonContentFiles)
{
if (!ContentPackage.IsModFilePathAllowed(nonContentFile))
{
//Workshop items are not allowed to add or modify files in the Content or Data folders;
continue;
}
if (!File.Exists(nonContentFile)) { continue; }
File.Delete(nonContentFile);
directories.Add(Path.GetDirectoryName(nonContentFile));
}
foreach (string directory in directories)
{
if (string.IsNullOrWhiteSpace(directory) || !Directory.Exists(directory)) { continue; }
if (Directory.GetFiles(directory, "*", SearchOption.AllDirectories).Count() == 0)
{
Directory.Delete(directory, recursive: true);
}
}
ContentPackage.List.RemoveAll(cp => System.IO.Path.GetFullPath(cp.Path) == System.IO.Path.GetFullPath(installedContentPackagePath));
GameMain.Config.SelectedContentPackages.RemoveWhere(cp => !ContentPackage.List.Contains(cp));
GameMain.Config.Save();
}
catch (Exception e)
{
errorMsg = "Disabling the workshop item \"" + item.Title + "\" failed. "+e.Message;
DebugConsole.NewMessage(errorMsg, Microsoft.Xna.Framework.Color.Red);
return false;
}
if (wasSub)
{
Submarine.RefreshSavedSubs();
}
errorMsg = "";
return true;
}
/// <summary>
/// Is the item compatible with this version of Barotrauma. Returns null if compatibility couldn't be determined (item not installed)
/// </summary>
public static bool? CheckWorkshopItemCompatibility(Workshop.Item item)
{
if (!item.Installed) { return null; }
string metaDataPath = Path.Combine(item.Directory.FullName, MetadataFileName);
if (!File.Exists(metaDataPath))
{
throw new FileNotFoundException("Metadata file for the Workshop item \"" + item.Title + "\" not found. The file may be corrupted.");
}
ContentPackage contentPackage = new ContentPackage(metaDataPath);
return contentPackage.IsCompatible();
}
public static bool CheckWorkshopItemEnabled(Workshop.Item item)
{
if (!item.Installed) return false;
string metaDataPath = Path.Combine(item.Directory.FullName, MetadataFileName);
if (!File.Exists(metaDataPath))
{
throw new FileNotFoundException("Metadata file for the Workshop item \"" + item.Title + "\" not found. The file may be corrupted.");
}
ContentPackage contentPackage = new ContentPackage(metaDataPath);
//make sure the contentpackage file is present
//(unless the package only contains submarine files, in which case we don't need a content package)
if (contentPackage.Files.Any(f => f.Type != ContentType.Submarine) &&
!File.Exists(GetWorkshopItemContentPackagePath(contentPackage)) &&
!ContentPackage.List.Any(cp => cp.Name == contentPackage.Name))
{
return false;
}
foreach (ContentFile contentFile in contentPackage.Files)
{
if (!File.Exists(contentFile.Path)) return false;
}
return true;
}
public static string GetWorkshopItemContentPackagePath(ContentPackage contentPackage)
{
string fileName = contentPackage.Name + ".xml";
string invalidChars = new string(Path.GetInvalidFileNameChars()) + new string(Path.GetInvalidPathChars());
foreach (char c in invalidChars) fileName = fileName.Replace(c.ToString(), "");
return Path.Combine("Data", "ContentPackages", fileName);
}
#endregion
#endif
public static void Update(float deltaTime)
{
if (instance == null || !instance.isInitialized) return;
if (instance == null || !instance.isInitialized) { return; }
instance.client?.Update();
instance.server?.Update();
@@ -1052,6 +132,8 @@ namespace Barotrauma.Steam
public static void ShutDown()
{
if (instance == null) { return; }
instance.client?.Dispose();
instance.client = null;
instance.server?.Dispose();
@@ -0,0 +1,14 @@
using Lidgren.Network;
using System;
using System.Collections.Generic;
using System.Text;
namespace Barotrauma.Networking
{
static partial class VoipConfig
{
public const int MAX_COMPRESSED_SIZE = 120; //amount of bytes we expect each 60ms of audio to fit in
public static readonly TimeSpan SEND_INTERVAL = new TimeSpan(0,0,0,0,120);
}
}
@@ -0,0 +1,161 @@
using Lidgren.Network;
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework;
namespace Barotrauma.Networking
{
public class VoipQueue : IDisposable
{
public const int BUFFER_COUNT = 8;
protected int[] bufferLengths;
protected byte[][] buffers;
protected int newestBufferInd;
protected bool firstRead;
public int EnqueuedTotalLength
{
get
{
int enqueuedTotalLength = 0;
for (int i = 0; i < BUFFER_COUNT; i++)
{
enqueuedTotalLength += bufferLengths[i];
}
return enqueuedTotalLength;
}
}
public byte[] BufferToQueue
{
get;
protected set;
}
public virtual byte QueueID
{
get;
protected set;
}
public UInt16 LatestBufferID
{
get;
protected set;
}
public bool CanSend
{
get;
protected set;
}
public bool CanReceive
{
get;
protected set;
}
public DateTime LastReadTime
{
get;
private set;
}
public VoipQueue(byte id, bool canSend, bool canReceive)
{
BufferToQueue = new byte[VoipConfig.MAX_COMPRESSED_SIZE];
newestBufferInd = BUFFER_COUNT - 1;
bufferLengths = new int[BUFFER_COUNT];
buffers = new byte[BUFFER_COUNT][];
for (int i = 0; i < BUFFER_COUNT; i++)
{
buffers[i] = new byte[VoipConfig.MAX_COMPRESSED_SIZE];
}
QueueID = id;
CanSend = canSend;
CanReceive = canReceive;
LatestBufferID = BUFFER_COUNT-1;
firstRead = true;
LastReadTime = DateTime.Now;
}
public void EnqueueBuffer(int length)
{
if (length > byte.MaxValue) return;
newestBufferInd = (newestBufferInd + 1) % BUFFER_COUNT;
int enqueuedTotalLength = EnqueuedTotalLength;
bufferLengths[newestBufferInd] = length;
BufferToQueue.CopyTo(buffers[newestBufferInd], 0);
if ((enqueuedTotalLength+length)>0) LatestBufferID++;
}
public void RetrieveBuffer(int id,out int outSize,out byte[] outBuf)
{
lock (buffers)
{
if (id >= LatestBufferID - (BUFFER_COUNT - 1) && id <= LatestBufferID)
{
int index = (newestBufferInd - (LatestBufferID - id)); if (index < 0) index += BUFFER_COUNT;
outSize = bufferLengths[index];
outBuf = buffers[index];
return;
}
}
outSize = -1;
outBuf = null;
}
public virtual void Write(NetBuffer msg)
{
if (!CanSend) throw new Exception("Called Write on a VoipQueue not set up for sending");
msg.Write((UInt16)LatestBufferID);
for (int i = 0; i < BUFFER_COUNT; i++)
{
int index = (newestBufferInd + i + 1) % BUFFER_COUNT;
msg.Write((byte)bufferLengths[index]);
msg.Write(buffers[index], 0, bufferLengths[index]);
}
}
public virtual bool Read(NetBuffer msg)
{
if (!CanReceive) throw new Exception("Called Read on a VoipQueue not set up for receiving");
UInt16 incLatestBufferID = msg.ReadUInt16();
if (firstRead || NetIdUtils.IdMoreRecent(incLatestBufferID,LatestBufferID))
{
firstRead = false;
for (int i = 0; i < BUFFER_COUNT; i++)
{
bufferLengths[i] = msg.ReadByte();
msg.ReadBytes(buffers[i], 0, bufferLengths[i]);
}
newestBufferInd = BUFFER_COUNT - 1;
LatestBufferID = incLatestBufferID;
LastReadTime = DateTime.Now;
return true;
}
else
{
for (int i = 0; i < BUFFER_COUNT; i++)
{
byte len = msg.ReadByte();
msg.Position += len * 8;
}
return false;
}
}
public virtual void Dispose() { }
}
}
@@ -72,128 +72,5 @@ namespace Barotrauma
UpdateVoteTexts(connectedClients, VoteType.Sub);
#endif
}
public void ServerRead(NetIncomingMessage inc, Client sender)
{
if (GameMain.Server == null || sender == null) return;
byte voteTypeByte = inc.ReadByte();
VoteType voteType = VoteType.Unknown;
try
{
voteType = (VoteType)voteTypeByte;
}
catch (Exception e)
{
DebugConsole.ThrowError("Failed to cast vote type \"" + voteTypeByte + "\"", e);
return;
}
switch (voteType)
{
case VoteType.Sub:
string subName = inc.ReadString();
Submarine sub = Submarine.SavedSubmarines.FirstOrDefault(s => s.Name == subName);
sender.SetVote(voteType, sub);
#if CLIENT
UpdateVoteTexts(GameMain.Server.ConnectedClients, voteType);
#endif
break;
case VoteType.Mode:
string modeIdentifier = inc.ReadString();
GameModePreset mode = GameModePreset.List.Find(gm => gm.Identifier == modeIdentifier);
if (!mode.Votable) break;
sender.SetVote(voteType, mode);
#if CLIENT
UpdateVoteTexts(GameMain.Server.ConnectedClients, voteType);
#endif
break;
case VoteType.EndRound:
if (!sender.HasSpawned) return;
sender.SetVote(voteType, inc.ReadBoolean());
GameMain.NetworkMember.EndVoteCount = GameMain.Server.ConnectedClients.Count(c => c.HasSpawned && c.GetVote<bool>(VoteType.EndRound));
GameMain.NetworkMember.EndVoteMax = GameMain.Server.ConnectedClients.Count(c => c.HasSpawned);
break;
case VoteType.Kick:
byte kickedClientID = inc.ReadByte();
Client kicked = GameMain.Server.ConnectedClients.Find(c => c.ID == kickedClientID);
if (kicked != null && !kicked.HasKickVoteFrom(sender))
{
kicked.AddKickVote(sender);
Client.UpdateKickVotes(GameMain.Server.ConnectedClients);
GameMain.Server.SendChatMessage(sender.Name + " has voted to kick " + kicked.Name, ChatMessageType.Server, null);
}
break;
case VoteType.StartRound:
bool ready = inc.ReadBoolean();
if (ready != sender.GetVote<bool>(VoteType.StartRound))
{
sender.SetVote(VoteType.StartRound, ready);
GameServer.Log(sender.Name + (ready ? " is ready to start the game." : " is not ready to start the game."), ServerLog.MessageType.ServerMessage);
#if CLIENT
UpdateVoteTexts(GameMain.Server.ConnectedClients, voteType);
#endif
}
break;
}
inc.ReadPadBits();
GameMain.Server.UpdateVoteStatus();
}
public void ServerWrite(NetBuffer msg)
{
if (GameMain.Server == null) return;
msg.Write(allowSubVoting);
if (allowSubVoting)
{
List<Pair<object, int>> voteList = GetVoteList(VoteType.Sub, GameMain.Server.ConnectedClients);
msg.Write((byte)voteList.Count);
foreach (Pair<object, int> vote in voteList)
{
msg.Write((byte)vote.Second);
msg.Write(((Submarine)vote.First).Name);
}
}
msg.Write(AllowModeVoting);
if (allowModeVoting)
{
List<Pair<object, int>> voteList = GetVoteList(VoteType.Mode, GameMain.Server.ConnectedClients);
msg.Write((byte)voteList.Count);
foreach (Pair<object, int> vote in voteList)
{
msg.Write((byte)vote.Second);
msg.Write(((GameModePreset)vote.First).Identifier);
}
}
msg.Write(AllowEndVoting);
if (AllowEndVoting)
{
msg.Write((byte)GameMain.Server.ConnectedClients.Count(v => v.GetVote<bool>(VoteType.EndRound)));
msg.Write((byte)GameMain.Server.ConnectedClients.Count);
}
msg.Write(AllowVoteKick);
var readyClients = GameMain.Server.ConnectedClients.FindAll(c => c.GetVote<bool>(VoteType.StartRound));
msg.Write((byte)readyClients.Count);
foreach (Client c in readyClients)
{
msg.Write(c.ID);
}
msg.WritePadBits();
}
}
}
@@ -5,16 +5,12 @@ using System.Linq;
namespace Barotrauma.Networking
{
class WhiteListedPlayer
partial class WhiteListedPlayer
{
public string Name;
public string IP;
public WhiteListedPlayer(string name,string ip)
{
Name = name;
IP = ip;
}
public UInt16 UniqueIdentifier;
}
partial class WhiteList
@@ -29,107 +25,13 @@ namespace Barotrauma.Networking
public bool Enabled;
partial void InitProjSpecific();
public WhiteList()
{
Enabled = false;
whitelistedPlayers = new List<WhiteListedPlayer>();
if (File.Exists(SavePath))
{
string[] lines;
try
{
lines = File.ReadAllLines(SavePath);
}
catch (Exception e)
{
DebugConsole.ThrowError("Failed to open whitelist in " + SavePath, e);
return;
}
foreach (string line in lines)
{
if (line[0] == '#')
{
string lineval = line.Substring(1, line.Length - 1);
int intVal = 0;
Int32.TryParse(lineval, out intVal);
if (lineval.ToLower() == "true" || intVal != 0)
{
Enabled = true;
}
else
{
Enabled = false;
}
}
else
{
string[] separatedLine = line.Split(',');
if (separatedLine.Length < 2) continue;
string name = String.Join(",", separatedLine.Take(separatedLine.Length - 1));
string ip = separatedLine.Last();
whitelistedPlayers.Add(new WhiteListedPlayer(name, ip));
}
}
}
}
public void Save()
{
GameServer.Log("Saving whitelist", ServerLog.MessageType.ServerMessage);
List<string> lines = new List<string>();
if (Enabled)
{
lines.Add("#true");
}
else
{
lines.Add("#false");
}
foreach (WhiteListedPlayer wlp in whitelistedPlayers)
{
lines.Add(wlp.Name + "," + wlp.IP);
}
try
{
File.WriteAllLines(SavePath, lines);
}
catch (Exception e)
{
DebugConsole.ThrowError("Saving the whitelist to " + SavePath + " failed", e);
}
}
public bool IsWhiteListed(string name, string ip)
{
if (!Enabled) return true;
WhiteListedPlayer wlp = whitelistedPlayers.Find(p => p.Name == name);
if (wlp == null) return false;
if (wlp.IP != ip && !string.IsNullOrWhiteSpace(wlp.IP)) return false;
return true;
}
private void RemoveFromWhiteList(WhiteListedPlayer wlp)
{
DebugConsole.Log("Removing " + wlp.Name + " from whitelist");
GameServer.Log("Removing " + wlp.Name + " from whitelist", ServerLog.MessageType.ServerMessage);
whitelistedPlayers.Remove(wlp);
Save();
}
private void AddToWhiteList(string name,string ip)
{
if (string.IsNullOrWhiteSpace(name)) return;
if (whitelistedPlayers.Any(x => x.Name.ToLower() == name.ToLower() && x.IP == ip)) return;
whitelistedPlayers.Add(new WhiteListedPlayer(name, ip));
Save();
InitProjSpecific();
}
}
}