Merge branch 'dedicated-server' (TODO: make sure I didn't break anything)

Conflicts:
	Barotrauma/Barotrauma.csproj
	Barotrauma/BarotraumaShared/Source/Characters/AI/AIController.cs
	Barotrauma/BarotraumaShared/Source/Characters/AI/EnemyAIController.cs
	Barotrauma/BarotraumaShared/Source/Characters/AICharacter.cs
	Barotrauma/BarotraumaShared/Source/Characters/Animation/Ragdoll.cs
	Barotrauma/BarotraumaShared/Source/Characters/Attack.cs
	Barotrauma/BarotraumaShared/Source/Characters/Character.cs
	Barotrauma/BarotraumaShared/Source/Characters/CharacterNetworking.cs
	Barotrauma/BarotraumaShared/Source/Characters/Limb.cs
	Barotrauma/BarotraumaShared/Source/Events/MonsterEvent.cs
	Barotrauma/BarotraumaShared/Source/Map/Explosion.cs
This commit is contained in:
Joonas Rikkonen
2017-06-30 21:02:52 +03:00
1311 changed files with 22015 additions and 91216 deletions
@@ -0,0 +1,147 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace Barotrauma.Networking
{
class BannedPlayer
{
public string Name;
public string IP;
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)
{
this.Name = name;
this.IP = ip;
}
}
partial class BanList
{
const string SavePath = "Data/bannedplayers.txt";
private List<BannedPlayer> bannedPlayers;
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 = String.Join(",", separatedLine.Take(separatedLine.Length - 1));
string ip = separatedLine.Last();
bannedPlayers.Add(new BannedPlayer(name, ip));
}
}
}
public void BanPlayer(string name, string ip)
{
if (bannedPlayers.Any(bp => bp.IP == ip)) return;
DebugConsole.Log("Banned " + name);
bannedPlayers.Add(new BannedPlayer(name, ip));
Save();
}
public bool IsBanned(string IP)
{
return bannedPlayers.Any(bp => bp.CompareTo(IP));
}
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();
}
public string ToRange(string ip)
{
for (int i = ip.Length - 1; i > 0; i--)
{
if (ip[i] == '.')
{
ip = ip.Substring(0, i) + ".x";
break;
}
}
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);
List<string> lines = new List<string>();
foreach (BannedPlayer banned in bannedPlayers)
{
lines.Add(banned.Name + "," + banned.IP);
}
try
{
File.WriteAllLines(SavePath, lines);
}
catch (Exception e)
{
DebugConsole.ThrowError("Saving the list of banned players to " + SavePath + " failed", e);
}
}
}
}
@@ -0,0 +1,274 @@
using Lidgren.Network;
using Microsoft.Xna.Framework;
using System;
using System.Text;
namespace Barotrauma.Networking
{
enum ChatMessageType
{
Default, Error, Dead, Server, Radio, Private
}
class ChatMessage
{
public const int MaxLength = 150;
public const int MaxMessagesPerPacket = 10;
public const float SpeakRange = 2000.0f;
public static Color[] MessageColor =
{
new Color(125, 140, 153), //default
new Color(204, 74, 78), //error
new Color(63, 72, 204), //dead
new Color(157, 225, 160), //server
new Color(238, 208, 0), //radio
new Color(228, 199, 27) //private
};
public readonly string Text;
public ChatMessageType Type;
public readonly Character Sender;
public readonly string SenderName;
public Color Color
{
get { return MessageColor[(int)Type]; }
}
public string TextWithSender
{
get;
private set;
}
public static UInt16 LastID = 0;
public UInt16 NetStateID
{
get;
set;
}
private ChatMessage(string senderName, string text, ChatMessageType type, Character sender)
{
Text = text;
Type = type;
Sender = sender;
SenderName = senderName;
TextWithSender = string.IsNullOrWhiteSpace(senderName) ? text : senderName + ": " + text;
}
public static ChatMessage Create(string senderName, string text, ChatMessageType type, Character sender)
{
return new ChatMessage(senderName, text, type, sender);
}
public static string GetChatMessageCommand(string message, out string messageWithoutCommand)
{
messageWithoutCommand = message;
int separatorIndex = message.IndexOf(";");
if (separatorIndex == -1) return "";
//int colonIndex = message.IndexOf(":");
string command = "";
try
{
command = message.Substring(0, separatorIndex);
command = command.Trim();
}
catch
{
return command;
}
messageWithoutCommand = message.Substring(separatorIndex + 1, message.Length - separatorIndex - 1).TrimStart();
return command;
}
public string ApplyDistanceEffect(Character listener)
{
if (Sender == null) return Text;
return ApplyDistanceEffect(listener, Sender, Text, SpeakRange);
}
public static string ApplyDistanceEffect(Entity listener, Entity Sender, string text, float range)
{
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 *= 2.0f;
if (dist > range) return "";
float garbleAmount = dist / range;
if (garbleAmount < 0.5f) return text;
int startIndex = Math.Max(text.IndexOf(':') + 1, 1);
StringBuilder sb = new StringBuilder(text.Length);
for (int i = 0; i < text.Length; i++)
{
sb.Append((i>startIndex && Rand.Range(0.0f, 1.0f) < garbleAmount) ? '-' : text[i]);
}
return sb.ToString();
}
public void ClientWrite(NetOutgoingMessage msg)
{
msg.Write((byte)ClientNetObject.CHAT_MESSAGE);
msg.Write(NetStateID);
msg.Write(Text);
}
public static void ServerRead(NetIncomingMessage msg, Client c)
{
UInt16 ID = msg.ReadUInt16();
string txt = msg.ReadString();
if (txt == null) txt = "";
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;
for (int i = 0; i < c.lastSentChatMessages.Count; i++)
{
float closeFactor = 1.0f / (c.lastSentChatMessages.Count - i);
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);
}
else
{
ChatMessage denyMsg = ChatMessage.Create("", "You have been blocked by the spam filter. Try again after 10 seconds.", ChatMessageType.Server, null);
c.ChatSpamTimer = 10.0f;
GameMain.Server.SendChatMessage(denyMsg, c);
}
return;
}
c.ChatSpamSpeed += similarity + 0.5f;
if (c.ChatSpamTimer > 0.0f)
{
ChatMessage denyMsg = ChatMessage.Create("", "You have been blocked by the spam filter. Try again after 10 seconds.", ChatMessageType.Server, null);
c.ChatSpamTimer = 10.0f;
GameMain.Server.SendChatMessage(denyMsg, c);
return;
}
GameMain.Server.SendChatMessage(txt, null, c);
}
public int EstimateLengthBytesClient()
{
int length = 1 + //(byte)ServerNetObject.CHAT_MESSAGE
2 + //(UInt16)NetStateID
Encoding.UTF8.GetBytes(Text).Length + 2;
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 void ServerWrite(NetOutgoingMessage msg, Client c)
{
msg.Write((byte)ServerNetObject.CHAT_MESSAGE);
msg.Write(NetStateID);
msg.Write((byte)Type);
msg.Write(Text);
msg.Write(Sender != null && c.inGame);
if (Sender != null && c.inGame)
{
msg.Write(Sender.ID);
}
else
{
msg.Write(SenderName);
}
}
public static void ClientRead(NetIncomingMessage msg)
{
UInt16 ID = msg.ReadUInt16();
ChatMessageType type = (ChatMessageType)msg.ReadByte();
string txt = msg.ReadString();
string senderName = "";
Character senderCharacter = null;
bool hasSenderCharacter = msg.ReadBoolean();
if (hasSenderCharacter)
{
senderCharacter = Entity.FindEntityByID(msg.ReadUInt16()) as Character;
if (senderCharacter != null)
{
senderName = senderCharacter.Name;
}
}
else
{
senderName = msg.ReadString();
}
if (NetIdUtils.IdMoreRecent(ID, LastID))
{
GameMain.Client.AddChatMessage(txt, type, senderName, senderCharacter);
LastID = ID;
}
}
}
}
@@ -0,0 +1,217 @@
using Lidgren.Network;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
namespace Barotrauma.Networking
{
[Flags]
enum ClientPermissions
{
None = 0,
[Description("End round")]
EndRound = 1,
[Description("Kick")]
Kick = 2,
[Description("Ban")]
Ban = 4
}
class Client
{
public string name;
public byte ID;
public byte TeamID = 0;
public Character Character;
public CharacterInfo characterInfo;
public NetConnection Connection { get; set; }
public bool inGame;
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 List<ChatMessage> chatMsgQueue = new List<ChatMessage>();
public UInt16 lastChatMsgQueueID;
//latest chat messages sent by this client
public List<string> lastSentChatMessages = new List<string>();
public float ChatSpamSpeed;
public float ChatSpamTimer;
public int ChatSpamCount;
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 Dictionary<UInt16, float> entityEventLastSent;
private Queue<Entity> pendingPositionUpdates = new Queue<Entity>();
public bool ReadyToStart;
private object[] votes;
public List<JobPrefab> jobPreferences;
public JobPrefab assignedJob;
public float deleteDisconnectedTimer;
public ClientPermissions Permissions = ClientPermissions.None;
public Queue<Entity> PendingPositionUpdates
{
get { return pendingPositionUpdates; }
}
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)
: this(name, ID)
{
}
public Client(string name, byte ID)
{
this.name = name;
this.ID = ID;
kickVoters = new List<Client>();
votes = new object[Enum.GetNames(typeof(VoteType)).Length];
jobPreferences = new List<JobPrefab>(JobPrefab.List.GetRange(0, 3));
entityEventLastSent = new Dictionary<UInt16, float>();
}
public static bool IsValidName(string name)
{
if (name.Contains("\n") || name.Contains("\r\n")) return false;
return (name.All(c =>
c != ';' &&
c != ',' &&
c != '<' &&
c != '/'));
}
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++)
{
if (name[i] < 32)
{
rName += '?';
}
else
{
rName += name[i];
}
}
return rName;
}
public void SetPermissions(ClientPermissions permissions)
{
this.Permissions = permissions;
}
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);
}
public T GetVote<T>(VoteType voteType)
{
return (votes[(int)voteType] is T) ? (T)votes[(int)voteType] : default(T);
}
public void SetVote(VoteType voteType, object value)
{
votes[(int)voteType] = value;
}
public void ResetVotes()
{
for (int i = 0; i < votes.Length; i++)
{
votes[i] = null;
}
}
public void AddKickVote(Client voter)
{
if (!kickVoters.Contains(voter)) kickVoters.Add(voter);
}
public void RemoveKickVote(Client voter)
{
kickVoters.Remove(voter);
}
public bool HasKickVoteFromID(int id)
{
return kickVoters.Any(k => k.ID == id);
}
public static void UpdateKickVotes(List<Client> connectedClients)
{
foreach (Client client in connectedClients)
{
client.kickVoters.RemoveAll(voter => !connectedClients.Contains(voter));
}
}
}
}
@@ -0,0 +1,194 @@
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
partial class EntitySpawner : Entity, IServerSerializable
{
const int MaxEntitiesPerWrite = 10;
private enum SpawnableType { Item, Character };
interface IEntitySpawnInfo
{
Entity Spawn();
}
class ItemSpawnInfo : IEntitySpawnInfo
{
public readonly ItemPrefab Prefab;
public readonly Vector2 Position;
public readonly Inventory Inventory;
public readonly Submarine Submarine;
public ItemSpawnInfo(ItemPrefab prefab, Vector2 worldPosition)
{
Prefab = prefab;
Position = worldPosition;
}
public ItemSpawnInfo(ItemPrefab prefab, Vector2 position, Submarine sub)
{
Prefab = prefab;
Position = position;
Submarine = sub;
}
public ItemSpawnInfo(ItemPrefab prefab, Inventory inventory)
{
Prefab = prefab;
Inventory = inventory;
}
public Entity Spawn()
{
Item spawnedItem = null;
if (Inventory != null)
{
spawnedItem = new Item(Prefab, Vector2.Zero, null);
Inventory.TryPutItem(spawnedItem, spawnedItem.AllowedSlots);
}
else
{
spawnedItem = new Item(Prefab, Position, Submarine);
}
return spawnedItem;
}
}
private readonly Queue<IEntitySpawnInfo> spawnQueue;
private readonly Queue<Entity> removeQueue;
class SpawnOrRemove
{
public readonly Entity Entity;
public readonly bool Remove = false;
public SpawnOrRemove(Entity entity, bool remove)
{
Entity = entity;
Remove = remove;
}
}
public EntitySpawner()
: base(null)
{
spawnQueue = new Queue<IEntitySpawnInfo>();
removeQueue = new Queue<Entity>();
}
public void AddToSpawnQueue(ItemPrefab itemPrefab, Vector2 worldPosition)
{
if (GameMain.Client != null) return;
spawnQueue.Enqueue(new ItemSpawnInfo(itemPrefab, worldPosition));
}
public void AddToSpawnQueue(ItemPrefab itemPrefab, Vector2 position, Submarine sub)
{
if (GameMain.Client != null) return;
spawnQueue.Enqueue(new ItemSpawnInfo(itemPrefab, position, sub));
}
public void AddToSpawnQueue(ItemPrefab itemPrefab, Inventory inventory)
{
if (GameMain.Client != null) return;
spawnQueue.Enqueue(new ItemSpawnInfo(itemPrefab, inventory));
}
public void AddToRemoveQueue(Entity entity)
{
if (GameMain.Client != null) return;
if (removeQueue.Contains(entity) || entity.Removed) return;
removeQueue.Enqueue(entity);
}
public void AddToRemoveQueue(Item item)
{
if (GameMain.Client != null) return;
if (removeQueue.Contains(item) || item.Removed) return;
removeQueue.Enqueue(item);
if (item.ContainedItems == null) return;
foreach (Item containedItem in item.ContainedItems)
{
if (containedItem != null) AddToRemoveQueue(containedItem);
}
}
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;
while (spawnQueue.Count>0)
{
var entitySpawnInfo = spawnQueue.Dequeue();
var spawnedEntity = entitySpawnInfo.Spawn();
if (spawnedEntity != null)
{
CreateNetworkEvent(spawnedEntity, false);
}
}
while (removeQueue.Count > 0)
{
var removedEntity = removeQueue.Dequeue();
if (GameMain.Server != null)
{
CreateNetworkEvent(removedEntity, true);
}
removedEntity.Remove();
}
}
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(),Color.Cyan);
((Character)entities.Entity).WriteSpawnData(message);
}
}
}
}
}
@@ -0,0 +1,286 @@
using Lidgren.Network;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Barotrauma.Networking
{
enum FileTransferStatus
{
NotStarted, Sending, Receiving, Finished, Canceled, Error
}
enum FileTransferMessageType
{
Unknown, Initiate, Data, Cancel
}
enum FileTransferType
{
Submarine
}
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);
}
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);
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);
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.Find(s => s.Name == fileName && s.MD5Hash.Hash == fileHash);
if (requestedSubmarine != null)
{
StartTransfer(inc.SenderConnection, FileTransferType.Submarine, requestedSubmarine.FilePath);
}
break;
}
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,241 @@
using Lidgren.Network;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Barotrauma.Networking
{
class UnauthenticatedClient
{
public NetConnection Connection;
public int Nonce;
public int failedAttempts;
public float AuthTimer;
public UnauthenticatedClient(NetConnection connection, int nonce)
{
Connection = connection;
Nonce = nonce;
AuthTimer = 10.0f;
failedAttempts = 0;
}
}
partial class GameServer : NetworkMember, IPropertyObject
{
List<UnauthenticatedClient> unauthenticatedClients = new List<UnauthenticatedClient>();
private void ClientAuthRequest(NetConnection conn)
{
//client wants to know if server requires password
if (ConnectedClients.Find(c => c.Connection == conn) != null)
{
//this client has already been authenticated
return;
}
UnauthenticatedClient unauthClient = unauthenticatedClients.Find(uc => uc.Connection == conn);
if (unauthClient == null)
{
//new client, generate nonce and add to unauth queue
if (ConnectedClients.Count >= MaxPlayers)
{
//server is full, can't allow new connection
conn.Disconnect("Server full");
return;
}
int nonce = CryptoRandom.Instance.Next();
unauthClient = new UnauthenticatedClient(conn, nonce);
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
}
server.SendMessage(nonceMsg, conn, NetDeliveryMethod.Unreliable);
}
private void ClientInitRequest(NetIncomingMessage inc)
{
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
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("Client did not properly request authentication.");
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());
DisconnectUnauthClient(inc, unauthClient, "Too many failed login attempts. You have been automatically banned from the server.");
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);
server.SendMessage(reject, unauthClient.Connection, NetDeliveryMethod.Unreliable);
unauthClient.AuthTimer = 10.0f;
return;
}
}
}
string clVersion = inc.ReadString();
string clPackageName = inc.ReadString();
string clPackageHash = inc.ReadString();
string clName = Client.SanitizeName(inc.ReadString());
if (string.IsNullOrWhiteSpace(clName))
{
DisconnectUnauthClient(inc, unauthClient, "You need a name.");
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, "Version " + GameMain.Version + " required to connect to the server (Your version: " + 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;
}
if (clPackageName != GameMain.SelectedPackage.Name)
{
DisconnectUnauthClient(inc, unauthClient, "Your content package (" + clPackageName + ") doesn't match the server's version (" + GameMain.SelectedPackage.Name + ")");
Log(clName + " (" + inc.SenderConnection.RemoteEndPoint.Address.ToString() + ") couldn't join the server (wrong content package name)", ServerLog.MessageType.Error);
DebugConsole.NewMessage(clName + " (" + inc.SenderConnection.RemoteEndPoint.Address.ToString() + ") couldn't join the server (wrong content package name)", Color.Red);
return;
}
if (clPackageHash != GameMain.SelectedPackage.MD5hash.Hash)
{
DisconnectUnauthClient(inc, unauthClient, "Your content package (MD5: " + clPackageHash + ") doesn't match the server's version (MD5: " + GameMain.SelectedPackage.MD5hash.Hash + ")");
Log(clName + " (" + inc.SenderConnection.RemoteEndPoint.Address.ToString() + ") couldn't join the server (wrong content package hash)", ServerLog.MessageType.Error);
DebugConsole.NewMessage(clName + " (" + inc.SenderConnection.RemoteEndPoint.Address.ToString() + ") couldn't join the server (wrong content package hash)", Color.Red);
return;
}
if (!whitelist.IsWhiteListed(clName, inc.SenderConnection.RemoteEndPoint.Address.ToString()))
{
DisconnectUnauthClient(inc, unauthClient, "You're not in this server's whitelist.");
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))
{
DisconnectUnauthClient(inc, unauthClient, "Your name contains illegal symbols.");
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 (clName.ToLower() == Name.ToLower())
{
DisconnectUnauthClient(inc, unauthClient, "That name is taken.");
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 => 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("Your session was taken by a new connection on the same IP address.");
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, "That name is taken.");
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;
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.IP == newClient.Connection.RemoteEndPoint.Address.ToString());
if (savedPermissions != null)
{
newClient.SetPermissions(savedPermissions.Permissions);
}
else
{
newClient.SetPermissions(ClientPermissions.None);
}
}
private void DisconnectUnauthClient(NetIncomingMessage inc, UnauthenticatedClient unauthClient, string reason)
{
inc.SenderConnection.Disconnect(reason);
if (unauthClient != null)
{
unauthenticatedClients.Remove(unauthClient);
}
}
}
}
@@ -0,0 +1,360 @@
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
}
partial class GameServer : NetworkMember, IPropertyObject
{
private class SavedClientPermission
{
public readonly string IP;
public readonly string Name;
public ClientPermissions Permissions;
public SavedClientPermission(string name, string ip, ClientPermissions permissions)
{
this.Name = name;
this.IP = ip;
this.Permissions = permissions;
}
}
public const string SettingsFile = "serversettings.xml";
public static readonly string ClientPermissionsFile = "Data" + Path.DirectorySeparatorChar + "clientpermissions.txt";
public Dictionary<string, ObjectProperty> ObjectProperties
{
get;
private set;
}
public Dictionary<string, 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 bool registeredToMaster;
private WhiteList whitelist;
private BanList banList;
private string password;
private string adminAuthPass = "";
public string AdminAuthPass
{
set
{
DebugConsole.NewMessage("Admin auth pass changed!",Color.Yellow);
adminAuthPass = "";
if (value.Length > 0)
{
adminAuthPass = Encoding.UTF8.GetString(Lidgren.Network.NetUtility.ComputeSHAHash(Encoding.UTF8.GetBytes(value)));
}
}
}
public float AutoRestartTimer;
private bool autoRestart;
private List<SavedClientPermission> clientPermissions = new List<SavedClientPermission>();
[HasDefaultValue(true, true)]
public bool RandomizeSeed
{
get;
private set;
}
[HasDefaultValue(300.0f, true)]
public float RespawnInterval
{
get;
private set;
}
[HasDefaultValue(180.0f, true)]
public float MaxTransportTime
{
get;
private set;
}
[HasDefaultValue(0.2f, true)]
public float MinRespawnRatio
{
get;
private set;
}
[HasDefaultValue(60.0f, true)]
public float AutoRestartInterval
{
get;
private set;
}
[HasDefaultValue(true, true)]
public bool AllowSpectating
{
get;
private set;
}
[HasDefaultValue(true, true)]
public bool EndRoundAtLevelEnd
{
get;
private set;
}
[HasDefaultValue(true, true)]
public bool SaveServerLogs
{
get;
private set;
}
[HasDefaultValue(true, true)]
public bool AllowFileTransfers
{
get;
private set;
}
[HasDefaultValue(800, true)]
private int LinesPerLogFile
{
get
{
return log.LinesPerFile;
}
set
{
log.LinesPerFile = value;
}
}
public bool AutoRestart
{
get { return (connectedClients.Count != 0) && autoRestart; }
set
{
autoRestart = value;
AutoRestartTimer = autoRestart ? AutoRestartInterval : 0.0f;
}
}
public YesNoMaybe TraitorsEnabled
{
get;
set;
}
[HasDefaultValue(true, true)]
public bool AllowRespawn
{
get;
set;
}
public SelectionMode SubSelectionMode
{
get { return subSelectionMode; }
}
public SelectionMode ModeSelectionMode
{
get { return modeSelectionMode; }
}
public BanList BanList
{
get { return banList; }
}
[HasDefaultValue(true, true)]
public bool AllowVoteKick
{
get;
private set;
}
[HasDefaultValue(0.6f, true)]
public float EndVoteRequiredRatio
{
get;
private set;
}
[HasDefaultValue(0.6f, true)]
public float KickVoteRequiredRatio
{
get;
private set;
}
private void SaveSettings()
{
XDocument doc = new XDocument(new XElement("serversettings"));
ObjectProperty.SaveProperties(this, doc.Root, true);
doc.Root.SetAttributeValue("SubSelection", subSelectionMode.ToString());
doc.Root.SetAttributeValue("ModeSelection", modeSelectionMode.ToString());
doc.Root.SetAttributeValue("TraitorsEnabled", TraitorsEnabled.ToString());
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 = ToolBox.TryLoadXml(SettingsFile);
}
if (doc == null || doc.Root == null)
{
doc = new XDocument(new XElement("serversettings"));
}
ObjectProperties = ObjectProperty.InitProperties(this, doc.Root);
subSelectionMode = SelectionMode.Manual;
Enum.TryParse<SelectionMode>(ToolBox.GetAttributeString(doc.Root, "SubSelection", "Manual"), out subSelectionMode);
Voting.AllowSubVoting = subSelectionMode == SelectionMode.Vote;
modeSelectionMode = SelectionMode.Manual;
Enum.TryParse<SelectionMode>(ToolBox.GetAttributeString(doc.Root, "ModeSelection", "Manual"), out modeSelectionMode);
Voting.AllowModeVoting = modeSelectionMode == SelectionMode.Vote;
var traitorsEnabled = TraitorsEnabled;
Enum.TryParse<YesNoMaybe>(ToolBox.GetAttributeString(doc.Root, "TraitorsEnabled", "No"), out traitorsEnabled);
TraitorsEnabled = traitorsEnabled;
GameMain.NetLobbyScreen.SetTraitorsEnabled(traitorsEnabled);
if (GameMain.NetLobbyScreen != null
#if CLIENT
&& GameMain.NetLobbyScreen.ServerMessage != null
#endif
)
{
GameMain.NetLobbyScreen.ServerMessageText = ToolBox.GetAttributeString(doc.Root, "ServerMessage", "");
}
#if CLIENT
showLogButton.Visible = SaveServerLogs;
#endif
List<string> monsterNames = Directory.GetDirectories("Content/Characters").ToList();
for (int i = 0; i < monsterNames.Count; i++)
{
monsterNames[i] = monsterNames[i].Replace("Content/Characters", "").Replace("/", "").Replace("\\", "");
}
monsterEnabled = new Dictionary<string, bool>();
foreach (string s in monsterNames)
{
monsterEnabled.Add(s, true);
}
extraCargo = new Dictionary<string, int>();
}
public void LoadClientPermissions()
{
if (!File.Exists(ClientPermissionsFile)) return;
string[] lines;
try
{
lines = File.ReadAllLines(ClientPermissionsFile);
}
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<ClientPermissions>(separatedLine.Last(), out permissions))
{
clientPermissions.Add(new SavedClientPermission(name, ip, permissions));
}
}
}
public void SaveClientPermissions()
{
GameServer.Log("Saving client permissions", ServerLog.MessageType.ServerMessage);
List<string> lines = new List<string>();
foreach (SavedClientPermission clientPermission in clientPermissions)
{
lines.Add(clientPermission.Name + "|" + clientPermission.IP+"|"+clientPermission.Permissions.ToString());
}
try
{
File.WriteAllLines(ClientPermissionsFile, lines);
}
catch (Exception e)
{
DebugConsole.ThrowError("Saving client permissions to " + ClientPermissionsFile + " failed", e);
}
}
}
}
@@ -0,0 +1,29 @@
using System;
using Lidgren.Network;
namespace Barotrauma.Networking
{
interface INetSerializable { }
/// <summary>
/// Interface for entities that the clients can send information of to the server
/// </summary>
interface IClientSerializable : INetSerializable
{
#if CLIENT
void ClientWrite(NetBuffer msg, object[] extraData = null);
#endif
void ServerRead(ClientNetObject type, NetBuffer msg, Client c);
}
/// <summary>
/// Interface for entities that the server can send information of to the clients
/// </summary>
interface IServerSerializable : INetSerializable
{
void ServerWrite(NetBuffer msg, Client c, object[] extraData = null);
#if CLIENT
void ClientRead(ServerNetObject type, NetBuffer msg, float sendingTime);
#endif
}
}
@@ -0,0 +1,15 @@
namespace Barotrauma.Networking
{
static class NetBufferExtensions
{
//public static void WriteEnum(this NetBuffer buffer, Enum value)
//{
// buffer.WriteRangedInteger(0, Enum.GetValues(value.GetType()).Length - 1, Convert.ToInt32(value));
//}
//public static TEnum ReadEnum<TEnum>(this NetBuffer buffer)
//{
// return (TEnum)(object)buffer.ReadRangedInteger(0, Enum.GetValues(typeof(TEnum)).Length - 1);
//}
}
}
@@ -0,0 +1,32 @@
namespace Barotrauma.Networking
{
static class NetConfig
{
public const int DefaultPort = 14242;
//UpdateEntity networkevents aren't sent to clients if they're further than this from the entity
public const float UpdateEntityDistance = 2500.0f;
public const int MaxPlayers = 16;
public static string MasterServerUrl = GameMain.Config.MasterServerUrl;
//if a Character is further than this from the sub, the server will ignore it
//(in display units)
public const float CharacterIgnoreDistance = 20000.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 LargeCharacterUpdateInterval = 5.0f;
public const float DeleteDisconnectedTime = 10.0f;
public const float IdSendInterval = 0.2f;
public const float RerequestInterval = 0.2f;
public const int ReliableMessageBufferSize = 500;
public const int ResendAttempts = 10;
}
}
@@ -0,0 +1,94 @@
using Lidgren.Network;
using System;
namespace Barotrauma.Networking
{
abstract class NetEntityEvent
{
public enum Type
{
ComponentState,
InventoryState,
Status,
Repair,
ApplyStatusEffect,
ChangeProperty,
Control
}
public readonly Entity Entity;
public readonly UInt16 ID;
//arbitrary extra data that will be passed to the Write method of the serializable entity
//(the index of an itemcomponent for example)
protected object[] Data;
protected NetEntityEvent(INetSerializable entity, UInt16 id)
{
this.ID = id;
this.Entity = entity as Entity;
}
public void SetData(object[] data)
{
this.Data = data;
}
public bool IsDuplicate(NetEntityEvent other)
{
if (other.Entity != this.Entity) return false;
if (Data != null && other.Data != null)
{
if (Data.Length == other.Data.Length)
{
for (int i = 0; i<Data.Length; i++)
{
if (!Data[i].Equals(other.Data[i])) return false;
}
return true;
}
else
{
return false;
}
}
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);
}
}
}
@@ -0,0 +1,81 @@
using Lidgren.Network;
using System;
using System.Collections.Generic;
namespace Barotrauma.Networking
{
abstract class NetEntityEventManager
{
public const int MaxEventBufferLength = 1024;
public const int MaxEventsPerWrite = 64;
/// <summary>
/// Write the events to the outgoing message. The recipient parameter is only needed for ServerEntityEventManager
/// </summary>
protected void Write(NetOutgoingMessage msg, List<NetEntityEvent> eventsToSync, Client recipient = null)
{
//write into a temporary buffer so we can write the number of events before the actual data
NetBuffer tempBuffer = new NetBuffer();
int eventCount = 0;
foreach (NetEntityEvent e in eventsToSync)
{
//write into a temporary buffer so we can write the length before the actual data
NetBuffer tempEventBuffer = new NetBuffer();
try
{
WriteEvent(tempEventBuffer, e, recipient);
}
catch (Exception exception)
{
DebugConsole.ThrowError("Failed to write an event for the entity \"" + e.Entity + "\"", exception);
//write an empty event to avoid messing up IDs
//(otherwise the clients might read the next event in the message and think its ID
//is consecutive to the previous one, even though we skipped over this broken event)
tempBuffer.Write((UInt16)0);
tempBuffer.WritePadBits();
eventCount++;
continue;
}
if (msg.LengthBytes + tempBuffer.LengthBytes + tempEventBuffer.LengthBytes > NetPeerConfiguration.kDefaultMTU - 20)
{
//no more room in this packet
break;
}
//the ID has been taken by another entity (the original entity has been removed) -> write an empty event
if (Entity.FindEntityByID(e.Entity.ID) != e.Entity)
{
//technically the clients don't have any use for these, but removing events and shifting the IDs of all
//consecutive ones is so error-prone that I think this is a safer option
tempBuffer.Write((UInt16)0);
tempBuffer.WritePadBits();
}
else
{
tempBuffer.Write((UInt16)e.Entity.ID);
tempBuffer.Write((byte)tempEventBuffer.LengthBytes);
tempBuffer.Write(tempEventBuffer);
tempBuffer.WritePadBits();
}
eventCount++;
}
if (eventCount > 0)
{
msg.Write(eventsToSync[0].ID);
msg.Write((byte)eventCount);
msg.Write(tempBuffer);
}
}
protected virtual void WriteEvent(NetBuffer buffer, NetEntityEvent entityEvent, Client recipient = null)
{
throw new NotImplementedException();
}
}
}
@@ -0,0 +1,409 @@
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;
}
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)
{
if (GameSettings.VerboseLogging)
{
DebugConsole.ThrowError("Failed to read event for entity \"" + bufferedEvent.TargetEntity.ToString() + "\"!", e);
}
}
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);
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);
server.DisconnectClient(c, "", "You have been disconnected because of excessive desync");
}
);
}
}
var timedOutClients = clients.FindAll(c => c.inGame && c.NeedsMidRoundSync && Timing.TotalTime > c.MidRoundSyncTimeOut);
timedOutClients.ForEach(c => GameMain.Server.DisconnectClient(c, "", "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(5.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 == 0)
{
msg.ReadPadBits();
sender.lastSentEntityEventID++;
continue;
}
byte msgLength = msg.ReadByte();
IClientSerializable entity = Entity.FindEntityByID(entityID) as IClientSerializable;
//skip the event if we've already received it or if the entity isn't found
if (thisEventID != (UInt16)(sender.lastSentEntityEventID + 1) || entity == null)
{
if (GameSettings.VerboseLogging)
{
if (thisEventID != (UInt16) (sender.lastSentEntityEventID + 1))
{
DebugConsole.NewMessage("received msg " + thisEventID, Microsoft.Xna.Framework.Color.Red);
}
else if (entity == null)
{
DebugConsole.NewMessage(
"received msg " + thisEventID + ", entity " + entityID + " not found",
Microsoft.Xna.Framework.Color.Red);
}
}
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;
}
}
}
}
@@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Barotrauma.Networking
{
static class NetIdUtils
{
/// <summary>
/// Is newID more recent than oldID
/// </summary>
public static bool IdMoreRecent(ushort newID, ushort oldID)
{
uint id1 = newID;
uint id2 = oldID;
return
(id1 > id2) && (id1 - id2 <= ushort.MaxValue / 2)
||
(id2 > id1) && (id2 - id1 > ushort.MaxValue / 2);
}
public static ushort Clamp(ushort id, ushort min, ushort max)
{
if (IdMoreRecent(min, max))
{
throw new ArgumentException("Min cannot be larger than max");
}
if (!IdMoreRecent(id, min))
{
return min;
}
else if (IdMoreRecent(id, max))
{
return max;
}
return id;
}
#if DEBUG
public static void Test()
{
Debug.Assert(NetIdUtils.IdMoreRecent((ushort)2, (ushort)1));
Debug.Assert(NetIdUtils.IdMoreRecent((ushort)2, (ushort)(ushort.MaxValue - 5)));
Debug.Assert(!NetIdUtils.IdMoreRecent((ushort)ushort.MaxValue, (ushort)5));
Debug.Assert(Clamp((ushort)5, (ushort)1, (ushort)10) == 5);
Debug.Assert(Clamp((ushort)(ushort.MaxValue - 5), (ushort)(ushort.MaxValue - 2), (ushort)3) == (ushort)(ushort.MaxValue - 2));
}
#endif
}
}
@@ -0,0 +1,232 @@
using System;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System.Collections.Generic;
using Lidgren.Network;
using Barotrauma.Items.Components;
namespace Barotrauma.Networking
{
enum ClientPacketHeader
{
REQUEST_AUTH, //ask the server if a password is needed, if so we'll get nonce for encryption
REQUEST_INIT, //ask the server to give you initialization
UPDATE_LOBBY, //update state in lobby
UPDATE_INGAME, //update state ingame
FILE_REQUEST, //request a (submarine) file from the server
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)
}
enum ClientNetObject
{
END_OF_MESSAGE, //self-explanatory
SYNC_IDS, //ids of the last changes the client knows about
CHAT_MESSAGE, //also self-explanatory
VOTE, //you get the idea
CHARACTER_INPUT,
ENTITY_STATE
}
enum ServerPacketHeader
{
AUTH_RESPONSE, //tell the player if they require a password to log in
AUTH_FAILURE, //the server won't authorize player yet, however connection is still alive
UPDATE_LOBBY, //update state in lobby (votes and chat messages)
UPDATE_INGAME, //update state ingame (character input and chat messages)
PERMISSIONS, //tell the client which special permissions they have (if any)
FILE_TRANSFER,
QUERY_STARTGAME, //ask the clients whether they're ready to start
STARTGAME, //start a new round
ENDGAME
}
enum ServerNetObject
{
END_OF_MESSAGE,
SYNC_IDS,
CHAT_MESSAGE,
VOTE,
ENTITY_POSITION,
ENTITY_EVENT,
ENTITY_EVENT_INITIAL
}
enum VoteType
{
Unknown,
Sub,
Mode,
EndRound,
Kick
}
abstract partial class NetworkMember
{
#if DEBUG
public Dictionary<string, long> messageCount = new Dictionary<string, long>();
#endif
public NetPeer netPeer
{
get;
protected set;
}
protected string name;
protected TimeSpan updateInterval;
protected DateTime updateTimer;
public int EndVoteCount, EndVoteMax;
//private GUITextBlock endVoteText;
public int Port;
protected bool gameStarted;
public Dictionary<string, bool> monsterEnabled;
protected RespawnManager respawnManager;
public Voting Voting;
public string Name
{
get { return name; }
set
{
if (string.IsNullOrEmpty(name)) return;
name = value;
}
}
public bool GameStarted
{
get { return gameStarted; }
}
public virtual List<Client> ConnectedClients
{
get { return null; }
}
public NetworkMember()
{
InitProjSpecific();
Voting = new Voting();
}
public bool CanUseRadio(Character sender)
{
if (sender == null) return false;
var radio = sender.Inventory.Items.FirstOrDefault(i => i != null && i.GetComponent<WifiComponent>() != null);
if (radio == null || !sender.HasEquippedItem(radio)) return false;
var radioComponent = radio.GetComponent<WifiComponent>();
if (radioComponent == null) return false;
return radioComponent.HasRequiredContainedItems(false);
}
public void AddChatMessage(string message, ChatMessageType type, string senderName="", Character senderCharacter = null)
{
AddChatMessage(ChatMessage.Create(senderName, message, type, senderCharacter));
}
public void AddChatMessage(ChatMessage message)
{
GameServer.Log(message.TextWithSender, ServerLog.MessageType.Chat);
string displayedText = message.Text;
if (message.Sender != null && !message.Sender.IsDead)
{
message.Sender.ShowSpeechBubble(2.0f, ChatMessage.MessageColor[(int)message.Type]);
}
#if CLIENT
GameMain.NetLobbyScreen.NewChatMessage(message);
while (chatBox.CountChildren > 20)
{
chatBox.RemoveChild(chatBox.children[1]);
}
if (!string.IsNullOrWhiteSpace(message.SenderName))
{
displayedText = message.SenderName + ": " + displayedText;
}
GUITextBlock msg = new GUITextBlock(new Rectangle(0, 0, chatBox.Rect.Width - 40, 0), displayedText,
((chatBox.CountChildren % 2) == 0) ? Color.Transparent : Color.Black * 0.1f, message.Color,
Alignment.Left, Alignment.TopLeft, "", null, true, GUI.SmallFont);
msg.UserData = message.SenderName;
msg.Padding = new Vector4(20.0f, 0, 0, 0);
float prevSize = chatBox.BarSize;
msg.Padding = new Vector4(20, 0, 0, 0);
chatBox.AddChild(msg);
if ((prevSize == 1.0f && chatBox.BarScroll == 0.0f) || (prevSize < 1.0f && chatBox.BarScroll == 1.0f)) chatBox.BarScroll = 1.0f;
GUISoundType soundType = GUISoundType.Message;
if (message.Type == ChatMessageType.Radio)
{
soundType = GUISoundType.RadioMessage;
}
else if (message.Type == ChatMessageType.Dead)
{
soundType = GUISoundType.DeadMessage;
}
GUI.PlayUISound(soundType);
#endif
}
public virtual void KickPlayer(string kickedName, bool ban, bool range = false) { }
public virtual void Update(float deltaTime)
{
#if CLIENT
if (gameStarted && Screen.Selected == GameMain.GameScreen)
{
chatMsgBox.Visible = Character.Controlled == null || Character.Controlled.CanSpeak;
inGameHUD.Update(deltaTime);
GameMain.GameSession.CrewManager.Update(deltaTime);
if (Character.Controlled == null || Character.Controlled.IsDead)
{
GameMain.GameScreen.Cam.TargetPos = Vector2.Zero;
GameMain.LightManager.LosEnabled = false;
}
}
if (PlayerInput.KeyHit(InputType.Chat) && chatMsgBox.Visible)
{
if (chatMsgBox.Selected)
{
chatMsgBox.Text = "";
chatMsgBox.Deselect();
}
else
{
chatMsgBox.Select();
}
}
#endif
}
public virtual void Disconnect() { }
}
}
@@ -0,0 +1,551 @@
using Barotrauma.Items.Components;
using FarseerPhysics;
using Lidgren.Network;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma.Networking
{
class RespawnManager : Entity, IServerSerializable
{
private readonly float respawnInterval;
private float maxTransportTime;
public enum State
{
Waiting,
Transporting,
Returning
}
private NetworkMember networkMember;
private State state;
private Submarine respawnShuttle;
private Steering shuttleSteering;
private List<Door> shuttleDoors;
/// <summary>
/// How long until the shuttle is dispatched with respawned characters
/// </summary>
public float RespawnTimer
{
get { return respawnTimer; }
}
/// <summary>
/// how long until the shuttle starts heading back out of the level
/// </summary>
public float TransportTimer
{
get { return shuttleTransportTimer; }
}
public bool CountdownStarted
{
get;
private set;
}
public State CurrentState
{
get { return state; }
}
private float respawnTimer, shuttleReturnTimer, shuttleTransportTimer;
private float updateReturnTimer;
public RespawnManager(NetworkMember networkMember, Submarine shuttle)
: base(shuttle)
{
this.networkMember = networkMember;
respawnShuttle = new Submarine(shuttle.FilePath, shuttle.MD5Hash.Hash, true);
respawnShuttle.Load(false);
ResetShuttle();
//respawnShuttle.GodMode = true;
shuttleDoors = new List<Door>();
foreach (Item item in Item.ItemList)
{
if (item.Submarine != respawnShuttle) continue;
var steering = item.GetComponent<Steering>();
if (steering != null) shuttleSteering = steering;
var door = item.GetComponent<Door>();
if (door != null) shuttleDoors.Add(door);
//lock all wires to prevent the players from messing up the electronics
var connectionPanel = item.GetComponent<ConnectionPanel>();
if (connectionPanel != null)
{
foreach (Connection connection in connectionPanel.Connections)
{
Array.ForEach(connection.Wires, w => { if (w != null) w.Locked = true; });
}
}
}
var server = networkMember as GameServer;
if (server != null)
{
respawnInterval = server.RespawnInterval;
maxTransportTime = server.MaxTransportTime;
}
respawnTimer = respawnInterval;
}
private List<Client> GetClientsToRespawn()
{
return networkMember.ConnectedClients.FindAll(c => c.inGame && (c.Character == null || c.Character.IsDead));
}
public void Update(float deltaTime)
{
switch (state)
{
case State.Waiting:
UpdateWaiting(deltaTime);
break;
case State.Transporting:
UpdateTransporting(deltaTime);
break;
case State.Returning:
UpdateReturning(deltaTime);
break;
}
}
private void UpdateWaiting(float deltaTime)
{
var server = networkMember as GameServer;
if (server == null)
{
if (CountdownStarted)
{
respawnTimer = Math.Max(0.0f, respawnTimer - deltaTime);
}
return;
}
respawnShuttle.Velocity = Vector2.Zero;
shuttleSteering.AutoPilot = false;
shuttleSteering.MaintainPos = false;
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 = respawnInterval;
DispatchShuttle();
}
}
private void UpdateTransporting(float deltaTime)
{
//infinite transport time -> shuttle wont return
if (maxTransportTime <= 0.0f) return;
shuttleTransportTimer -= deltaTime;
if (shuttleTransportTimer + deltaTime > 15.0f && shuttleTransportTimer <= 15.0f &&
networkMember.Character != null &&
networkMember.Character.Submarine == respawnShuttle)
{
networkMember.AddChatMessage("The shuttle will automatically return back to the outpost. Please leave the shuttle immediately.", ChatMessageType.Server);
}
var server = networkMember as GameServer;
if (server == null) return;
//if there are no living chracters inside, transporting can be stopped immediately
if (!Character.CharacterList.Any(c => c.Submarine == respawnShuttle && !c.IsDead))
{
shuttleTransportTimer = 0.0f;
}
if (shuttleTransportTimer <= 0.0f)
{
GameServer.Log("The respawn shuttle is leaving.", ServerLog.MessageType.ServerMessage);
state = State.Returning;
server.CreateEntityEvent(this);
CountdownStarted = false;
shuttleReturnTimer = maxTransportTime;
shuttleTransportTimer = maxTransportTime;
}
}
private void UpdateReturning(float deltaTime)
{
//if (shuttleReturnTimer == maxTransportTime &&
// networkMember.Character != null &&
// networkMember.Character.Submarine == respawnShuttle)
//{
// networkMember.AddChatMessage("The shuttle will automatically return back to the outpost. Please leave the shuttle immediately.", ChatMessageType.Server);
//}
shuttleReturnTimer -= deltaTime;
updateReturnTimer += deltaTime;
if (updateReturnTimer > 1.0f)
{
updateReturnTimer = 0.0f;
respawnShuttle.PhysicsBody.FarseerBody.IgnoreCollisionWith(Level.Loaded.ShaftBody);
shuttleSteering.SetDestinationLevelStart();
foreach (Door door in shuttleDoors)
{
if (door.IsOpen) door.SetState(false,false,true);
}
var shuttleGaps = Gap.GapList.FindAll(g => g.Submarine == respawnShuttle && g.ConnectedWall != null);
shuttleGaps.ForEach(g => g.Remove());
var dockingPorts = Item.ItemList.FindAll(i => i.Submarine == respawnShuttle && i.GetComponent<DockingPort>() != null);
dockingPorts.ForEach(d => d.GetComponent<DockingPort>().Undock());
var server = networkMember as GameServer;
if (server == null) return;
//shuttle has returned if the path has been traversed or the shuttle is close enough to the exit
if (!CoroutineManager.IsCoroutineRunning("forcepos"))
{
if (shuttleSteering.SteeringPath != null && shuttleSteering.SteeringPath.Finished
|| (respawnShuttle.WorldPosition.Y + respawnShuttle.Borders.Y > Level.Loaded.StartPosition.Y - Level.ShaftHeight &&
Math.Abs(Level.Loaded.StartPosition.X - respawnShuttle.WorldPosition.X) < 1000.0f))
{
CoroutineManager.StopCoroutines("forcepos");
CoroutineManager.StartCoroutine(
ForceShuttleToPos(new Vector2(Level.Loaded.StartPosition.X, Level.Loaded.Size.Y + 1000.0f), 100.0f), "forcepos");
}
}
if (respawnShuttle.WorldPosition.Y > Level.Loaded.Size.Y || shuttleReturnTimer <= 0.0f)
{
CoroutineManager.StopCoroutines("forcepos");
ResetShuttle();
state = State.Waiting;
GameServer.Log("The respawn shuttle has left.", ServerLog.MessageType.ServerMessage);
server.CreateEntityEvent(this);
respawnTimer = respawnInterval;
CountdownStarted = false;
}
}
}
private void DispatchShuttle()
{
var server = networkMember as GameServer;
if (server == null) return;
state = State.Transporting;
server.CreateEntityEvent(this);
ResetShuttle();
shuttleSteering.TargetVelocity = Vector2.Zero;
GameServer.Log("Dispatching the respawn shuttle.", ServerLog.MessageType.ServerMessage);
RespawnCharacters();
CoroutineManager.StopCoroutines("forcepos");
CoroutineManager.StartCoroutine(ForceShuttleToPos(Level.Loaded.StartPosition - Vector2.UnitY * Level.ShaftHeight, 100.0f), "forcepos");
}
private IEnumerable<object> ForceShuttleToPos(Vector2 position, float speed)
{
respawnShuttle.PhysicsBody.FarseerBody.IgnoreCollisionWith(Level.Loaded.ShaftBody);
while (Math.Abs(position.Y - respawnShuttle.WorldPosition.Y) > 100.0f)
{
Vector2 displayVel = Vector2.Normalize(position - respawnShuttle.WorldPosition) * speed;
respawnShuttle.SubBody.Body.LinearVelocity = ConvertUnits.ToSimUnits(displayVel);
yield return CoroutineStatus.Running;
if (respawnShuttle.SubBody == null) yield return CoroutineStatus.Success;
}
respawnShuttle.PhysicsBody.FarseerBody.RestoreCollisionWith(Level.Loaded.ShaftBody);
yield return CoroutineStatus.Success;
}
private void ResetShuttle()
{
shuttleTransportTimer = maxTransportTime;
shuttleReturnTimer = maxTransportTime;
foreach (Item item in Item.ItemList)
{
if (item.Submarine != respawnShuttle) continue;
if (item.body != null && item.body.Enabled && item.ParentInventory == null)
{
Entity.Spawner.AddToRemoveQueue(item);
continue;
}
item.Condition = 100.0f;
var powerContainer = item.GetComponent<PowerContainer>();
if (powerContainer != null)
{
powerContainer.Charge = powerContainer.Capacity;
}
}
foreach (Structure wall in Structure.WallList)
{
if (wall.Submarine != respawnShuttle) continue;
for (int i = 0; i < wall.SectionCount; i++)
{
wall.AddDamage(i, -100000.0f);
}
}
var shuttleGaps = Gap.GapList.FindAll(g => g.Submarine == respawnShuttle && g.ConnectedWall != null);
shuttleGaps.ForEach(g => g.Remove());
foreach (Hull hull in Hull.hullList)
{
if (hull.Submarine != respawnShuttle) continue;
hull.OxygenPercentage = 100.0f;
hull.Volume = 0.0f;
}
foreach (Character c in Character.CharacterList)
{
if (c.Submarine == respawnShuttle)
{
if (Character.Controlled == c) Character.Controlled = null;
c.Enabled = false;
if (c.Inventory != null)
{
foreach (Item item in c.Inventory.Items)
{
if (item == null) continue;
Entity.Spawner.AddToRemoveQueue(item);
}
}
c.Kill(CauseOfDeath.Damage, true);
}
}
respawnShuttle.SetPosition(new Vector2(Level.Loaded.StartPosition.X, Level.Loaded.Size.Y + respawnShuttle.Borders.Height));
respawnShuttle.Velocity = Vector2.Zero;
respawnShuttle.PhysicsBody.FarseerBody.RestoreCollisionWith(Level.Loaded.ShaftBody);
}
public void RespawnCharacters()
{
var server = networkMember as GameServer;
if (server == null) return;
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();
if (server.Character != null && server.Character.IsDead) characterInfos.Add(server.CharacterInfo);
GameServer.Log("Respawning characters: "+string.Join(", ", characterInfos.Select(ci => ci.Name)), ServerLog.MessageType.ServerMessage);
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, respawnShuttle);
//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 = ItemPrefab.list.Find(ip => ip.Name == "Diving Suit") as ItemPrefab;
ItemPrefab oxyPrefab = ItemPrefab.list.Find(ip => ip.Name == "Oxygen Tank") as ItemPrefab;
ItemPrefab scooterPrefab = ItemPrefab.list.Find(ip => ip.Name == "Underwater Scooter") as ItemPrefab;
ItemPrefab batteryPrefab = ItemPrefab.list.Find(ip => ip.Name == "Battery Cell") as ItemPrefab;
var cargoSp = WayPoint.WayPointList.Find(wp => wp.Submarine == respawnShuttle && wp.SpawnType == SpawnType.Cargo);
for (int i = 0; i < characterInfos.Count; i++)
{
bool myCharacter = false;
#if CLIENT
myCharacter = i >= clients.Count;
#endif
var character = Character.Create(characterInfos[i], shuttleSpawnPoints[i].WorldPosition, !myCharacter, false);
character.TeamID = 1;
#if CLIENT
if (myCharacter)
{
server.Character = character;
Character.Controlled = character;
GameMain.LightManager.LosEnabled = true;
}
else
{
#endif
clients[i].Character = character;
#if CLIENT
}
#endif
Vector2 pos = cargoSp == null ? character.Position : cargoSp.Position;
if (divingSuitPrefab != null && oxyPrefab != null)
{
var divingSuit = new Item(divingSuitPrefab, pos, respawnShuttle);
Entity.Spawner.CreateNetworkEvent(divingSuit, false);
var oxyTank = new Item(oxyPrefab, pos, respawnShuttle);
Entity.Spawner.CreateNetworkEvent(oxyTank, false);
divingSuit.Combine(oxyTank);
}
if (scooterPrefab != null && batteryPrefab != null)
{
var scooter = new Item(scooterPrefab, pos, respawnShuttle);
Entity.Spawner.CreateNetworkEvent(scooter, false);
var battery = new Item(batteryPrefab, pos, respawnShuttle);
Entity.Spawner.CreateNetworkEvent(battery, false);
scooter.Combine(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 CLIENT
GameMain.GameSession.CrewManager.characters.Add(character);
#endif
}
}
public void ServerWrite(NetBuffer msg, Client c, object[] extraData = null)
{
msg.WriteRangedInteger(0, Enum.GetNames(typeof(State)).Length, (int)state);
switch (state)
{
case State.Transporting:
msg.Write(TransportTimer);
break;
case State.Waiting:
msg.Write(CountdownStarted);
msg.Write(respawnTimer);
break;
case State.Returning:
break;
}
msg.WritePadBits();
}
public void ClientRead(ServerNetObject type, NetBuffer msg, float sendingTime)
{
var newState = (State)msg.ReadRangedInteger(0, Enum.GetNames(typeof(State)).Length);
switch (newState)
{
case State.Transporting:
maxTransportTime = msg.ReadSingle();
shuttleTransportTimer = maxTransportTime;
CountdownStarted = false;
if (state != newState)
{
CoroutineManager.StopCoroutines("forcepos");
CoroutineManager.StartCoroutine(ForceShuttleToPos(Level.Loaded.StartPosition - Vector2.UnitY * Level.ShaftHeight, 100.0f), "forcepos");
}
break;
case State.Waiting:
CountdownStarted = msg.ReadBoolean();
ResetShuttle();
respawnTimer = msg.ReadSingle();
break;
case State.Returning:
CountdownStarted = false;
break;
}
state = newState;
msg.ReadPadBits();
}
}
}
@@ -0,0 +1,154 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace Barotrauma.Networking
{
partial class ServerLog
{
private struct LogMessage
{
public readonly string Text;
public readonly MessageType Type;
public LogMessage(string text, MessageType type)
{
Text = "[" + DateTime.Now.ToString() + "] " + text;
Type = type;
}
}
public enum MessageType
{
Chat,
ItemInteraction,
Inventory,
Attack,
ServerMessage,
Error
}
private readonly Color[] messageColor =
{
Color.LightBlue,
new Color(255, 142, 0),
new Color(238, 208, 0),
new Color(204, 74, 78),
new Color(157, 225, 160),
Color.Red
};
private readonly string[] messageTypeName =
{
"Chat message",
"Item interaction",
"Inventory usage",
"Attack & death",
"Server message",
"Error"
};
private int linesPerFile = 800;
public const string SavePath = "ServerLogs";
private string serverName;
private readonly Queue<LogMessage> lines;
private int unsavedLineCount;
private string msgFilter;
private bool[] msgTypeHidden = new bool[Enum.GetValues(typeof(MessageType)).Length];
public int LinesPerFile
{
get { return linesPerFile; }
set { linesPerFile = Math.Max(value, 10); }
}
public ServerLog(string serverName)
{
this.serverName = serverName;
lines = new Queue<LogMessage>();
}
public void WriteLine(string line, MessageType messageType)
{
//string logLine = "[" + DateTime.Now.ToLongTimeString() + "] " + line;
#if SERVER
DebugConsole.NewMessage(line, Color.White); //TODO: REMOVE
#endif
var newText = new LogMessage(line, messageType);
lines.Enqueue(newText);
#if CLIENT
if (LogFrame != null)
{
AddLine(newText);
listBox.UpdateScrollBarSize();
}
#endif
unsavedLineCount++;
if (unsavedLineCount >= LinesPerFile)
{
Save();
unsavedLineCount = 0;
}
while (lines.Count > LinesPerFile)
{
lines.Dequeue();
}
#if CLIENT
while (listBox != null && listBox.children.Count > LinesPerFile)
{
listBox.RemoveChild(listBox.children[0]);
}
#endif
}
public void Save()
{
if (!Directory.Exists(SavePath))
{
try
{
Directory.CreateDirectory(SavePath);
}
catch (Exception e)
{
DebugConsole.ThrowError("Failed to create a folder for server logs", e);
return;
}
}
string fileName = serverName+"_"+DateTime.Now.ToShortDateString()+"_"+DateTime.Now.ToShortTimeString()+".txt";
fileName = fileName.Replace(":", "");
fileName = fileName.Replace("../", "");
fileName = fileName.Replace("/", "");
string filePath = Path.Combine(SavePath, fileName);
try
{
File.WriteAllLines(filePath, lines.Select(l => l.Text));
}
catch (Exception e)
{
DebugConsole.ThrowError("Saving the server log to " + filePath + " failed", e);
}
}
}
}
@@ -0,0 +1,180 @@
using Barotrauma.Networking;
using Lidgren.Network;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Barotrauma
{
partial class Voting
{
private bool allowSubVoting, allowModeVoting;
public bool AllowVoteKick = true;
public bool AllowEndVoting = true;
private List<Pair<object, int>> GetVoteList(VoteType voteType, List<Client> voters)
{
List<Pair<object, int>> voteList = new List<Pair<object, int>>();
foreach (Client voter in voters)
{
object vote = voter.GetVote<object>(voteType);
if (vote == null) continue;
var existingVotable = voteList.Find(v => v.First == vote || v.First.Equals(vote));
if (existingVotable == null)
{
voteList.Add(Pair<object, int>.Create(vote, 1));
}
else
{
existingVotable.Second++;
}
}
return voteList;
}
public T HighestVoted<T>(VoteType voteType, List<Client> voters)
{
if (voteType == VoteType.Sub && !AllowSubVoting) return default(T);
if (voteType == VoteType.Mode && !AllowModeVoting) return default(T);
List<Pair<object, int>> voteList = GetVoteList(voteType,voters);
T selected = default(T);
int highestVotes = 0;
foreach (Pair<object, int> votable in voteList)
{
if (selected == null || votable.Second > highestVotes)
{
highestVotes = votable.Second;
selected = (T)votable.First;
}
}
return selected;
}
public void ResetVotes(List<Client> connectedClients)
{
foreach (Client client in connectedClients)
{
client.ResetVotes();
}
GameMain.NetworkMember.EndVoteCount = 0;
GameMain.NetworkMember.EndVoteMax = 0;
#if CLIENT
UpdateVoteTexts(connectedClients, VoteType.Mode);
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.Find(s => s.Name == subName);
sender.SetVote(voteType, sub);
#if CLIENT
UpdateVoteTexts(GameMain.Server.ConnectedClients, voteType);
#endif
break;
case VoteType.Mode:
string modeName = inc.ReadString();
GameModePreset mode = GameModePreset.list.Find(gm => gm.Name == modeName);
sender.SetVote(voteType, mode);
#if CLIENT
UpdateVoteTexts(GameMain.Server.ConnectedClients, voteType);
#endif
break;
case VoteType.EndRound:
if (sender.Character == null) return;
sender.SetVote(voteType, inc.ReadBoolean());
GameMain.NetworkMember.EndVoteCount = GameMain.Server.ConnectedClients.Count(c => c.Character != null && c.GetVote<bool>(VoteType.EndRound));
GameMain.NetworkMember.EndVoteMax = GameMain.Server.ConnectedClients.Count(c => c.Character != null);
break;
case VoteType.Kick:
byte kickedClientID = inc.ReadByte();
Client kicked = GameMain.Server.ConnectedClients.Find(c => c.ID == kickedClientID);
if (kicked != null)
{
kicked.AddKickVote(sender);
Client.UpdateKickVotes(GameMain.Server.ConnectedClients);
GameMain.Server.SendChatMessage(sender.name + " has voted to kick " + kicked.name, ChatMessageType.Server, null);
}
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).Name);
}
}
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);
msg.WritePadBits();
}
}
}
@@ -0,0 +1,137 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace Barotrauma.Networking
{
class WhiteListedPlayer
{
public string Name;
public string IP;
public WhiteListedPlayer(string name,string ip)
{
Name = name;
IP = ip;
}
}
partial class WhiteList
{
const string SavePath = "Data/whitelist.txt";
private List<WhiteListedPlayer> whitelistedPlayers;
public List<WhiteListedPlayer> WhiteListedPlayers
{
get { return whitelistedPlayers; }
}
public bool Enabled;
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();
}
}
}