Renamed project folders from Subsurface to Barotrauma
This commit is contained in:
@@ -0,0 +1,211 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
class BanList
|
||||
{
|
||||
const string SavePath = "Data/bannedplayers.txt";
|
||||
|
||||
private List<BannedPlayer> bannedPlayers;
|
||||
|
||||
private GUIComponent banFrame;
|
||||
|
||||
public GUIComponent BanFrame
|
||||
{
|
||||
get { return banFrame; }
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
public GUIComponent CreateBanFrame(GUIComponent parent)
|
||||
{
|
||||
banFrame = new GUIListBox(new Rectangle(0, 0, 0, 0), "", parent);
|
||||
|
||||
foreach (BannedPlayer bannedPlayer in bannedPlayers)
|
||||
{
|
||||
GUITextBlock textBlock = new GUITextBlock(
|
||||
new Rectangle(0, 0, 0, 25),
|
||||
bannedPlayer.IP + " (" + bannedPlayer.Name + ")",
|
||||
"",
|
||||
Alignment.Left, Alignment.Left, banFrame);
|
||||
textBlock.Padding = new Vector4(10.0f, 10.0f, 0.0f, 0.0f);
|
||||
textBlock.UserData = banFrame;
|
||||
|
||||
var removeButton = new GUIButton(new Rectangle(0, 0, 100, 20), "Remove", Alignment.Right | Alignment.CenterY, "", textBlock);
|
||||
removeButton.UserData = bannedPlayer;
|
||||
removeButton.OnClicked = RemoveBan;
|
||||
if (bannedPlayer.IP.IndexOf(".x") <= -1)
|
||||
{
|
||||
var rangeBanButton = new GUIButton(new Rectangle(-100, 0, 100, 20), "Ban range", Alignment.Right | Alignment.CenterY, "", textBlock);
|
||||
rangeBanButton.UserData = bannedPlayer;
|
||||
rangeBanButton.OnClicked = RangeBan;
|
||||
}
|
||||
}
|
||||
|
||||
return banFrame;
|
||||
}
|
||||
|
||||
private bool RemoveBan(GUIButton button, object obj)
|
||||
{
|
||||
BannedPlayer banned = obj as BannedPlayer;
|
||||
if (banned == null) return false;
|
||||
|
||||
DebugConsole.Log("Removing ban from " + banned.Name);
|
||||
GameServer.Log("Removing ban from " + banned.Name, ServerLog.MessageType.ServerMessage);
|
||||
|
||||
bannedPlayers.Remove(banned);
|
||||
|
||||
Save();
|
||||
|
||||
if (banFrame != null)
|
||||
{
|
||||
banFrame.Parent.RemoveChild(banFrame);
|
||||
CreateBanFrame(banFrame.Parent);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
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 bool RangeBan(GUIButton button, object obj)
|
||||
{
|
||||
BannedPlayer banned = obj as BannedPlayer;
|
||||
if (banned == null) return false;
|
||||
|
||||
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();
|
||||
|
||||
if (banFrame != null)
|
||||
{
|
||||
banFrame.Parent.RemoveChild(banFrame);
|
||||
CreateBanFrame(banFrame.Parent);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool CloseFrame(GUIButton button, object obj)
|
||||
{
|
||||
banFrame = null;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
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,246 @@
|
||||
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 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,210 @@
|
||||
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;
|
||||
|
||||
public bool ReadyToStart;
|
||||
|
||||
private object[] votes;
|
||||
|
||||
public List<JobPrefab> jobPreferences;
|
||||
public JobPrefab assignedJob;
|
||||
|
||||
public float deleteDisconnectedTimer;
|
||||
|
||||
public ClientPermissions Permissions = ClientPermissions.None;
|
||||
|
||||
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,224 @@
|
||||
using Barotrauma.Networking;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
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;
|
||||
|
||||
removeQueue.Enqueue(entity);
|
||||
}
|
||||
|
||||
public void AddToRemoveQueue(Item item)
|
||||
{
|
||||
if (GameMain.Client != null) 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);
|
||||
((Character)entities.Entity).WriteSpawnData(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void ClientRead(ServerNetObject type, Lidgren.Network.NetBuffer message, float sendingTime)
|
||||
{
|
||||
if (GameMain.Server != null) return;
|
||||
|
||||
bool remove = message.ReadBoolean();
|
||||
|
||||
if (remove)
|
||||
{
|
||||
ushort entityId = message.ReadUInt16();
|
||||
|
||||
var entity = FindEntityByID(entityId);
|
||||
if (entity != null)
|
||||
{
|
||||
entity.Remove();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (message.ReadByte())
|
||||
{
|
||||
case (byte)SpawnableType.Item:
|
||||
Item.ReadSpawnData(message, true);
|
||||
break;
|
||||
case (byte)SpawnableType.Character:
|
||||
Character.ReadSpawnData(message, true);
|
||||
break;
|
||||
default:
|
||||
DebugConsole.ThrowError("Received invalid entity spawn message (unknown spawnable type)");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,409 @@
|
||||
using Lidgren.Network;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Xml;
|
||||
|
||||
namespace Barotrauma.Networking
|
||||
{
|
||||
class FileReceiver
|
||||
{
|
||||
public class FileTransferIn : IDisposable
|
||||
{
|
||||
public string FileName
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public string FilePath
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public ulong FileSize
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public ulong Received
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public FileTransferType FileType
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public FileTransferStatus Status
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public float BytesPerSecond
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public float Progress
|
||||
{
|
||||
get { return Received / (float)FileSize; }
|
||||
}
|
||||
|
||||
public FileStream WriteStream
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public int TimeStarted
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public NetConnection Connection
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public int SequenceChannel;
|
||||
|
||||
public FileTransferIn(NetConnection connection, string filePath, FileTransferType fileType)
|
||||
{
|
||||
FilePath = filePath;
|
||||
FileName = Path.GetFileName(FilePath);
|
||||
FileType = fileType;
|
||||
|
||||
Connection = connection;
|
||||
|
||||
WriteStream = new FileStream(FilePath, FileMode.Create, FileAccess.Write, FileShare.None);
|
||||
TimeStarted = Environment.TickCount;
|
||||
|
||||
Status = FileTransferStatus.NotStarted;
|
||||
}
|
||||
|
||||
public void ReadBytes(NetIncomingMessage inc)
|
||||
{
|
||||
byte[] all = inc.ReadBytes(inc.LengthBytes - inc.PositionInBytes);
|
||||
Received += (ulong)all.Length;
|
||||
WriteStream.Write(all, 0, all.Length);
|
||||
|
||||
int passed = Environment.TickCount - TimeStarted;
|
||||
float psec = passed / 1000.0f;
|
||||
|
||||
if (GameSettings.VerboseLogging)
|
||||
{
|
||||
DebugConsole.Log("Received "+all.Length+" bytes of the file "+FileName+" ("+Received+"/"+FileSize+" received)");
|
||||
}
|
||||
|
||||
BytesPerSecond = Received / psec;
|
||||
|
||||
Status = Received >= FileSize ? FileTransferStatus.Finished : FileTransferStatus.Receiving;
|
||||
}
|
||||
|
||||
private bool disposed = false;
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (disposed) return;
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
if (WriteStream != null)
|
||||
{
|
||||
WriteStream.Flush();
|
||||
WriteStream.Close();
|
||||
WriteStream.Dispose();
|
||||
WriteStream = null;
|
||||
}
|
||||
}
|
||||
disposed = true;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
}
|
||||
}
|
||||
|
||||
const int MaxFileSize = 1000000;
|
||||
|
||||
public delegate void OnFinishedDelegate(FileTransferIn fileStreamReceiver);
|
||||
public OnFinishedDelegate OnFinished;
|
||||
|
||||
private List<FileTransferIn> activeTransfers;
|
||||
|
||||
private string downloadFolder;
|
||||
|
||||
public List<FileTransferIn> ActiveTransfers
|
||||
{
|
||||
get { return activeTransfers; }
|
||||
}
|
||||
|
||||
public FileReceiver(string downloadFolder)
|
||||
{
|
||||
if (GameMain.Server != null)
|
||||
{
|
||||
throw new InvalidOperationException("Creating a file receiver is not allowed when a server is running.");
|
||||
}
|
||||
|
||||
activeTransfers = new List<FileTransferIn>();
|
||||
|
||||
this.downloadFolder = downloadFolder;
|
||||
}
|
||||
|
||||
public void ReadMessage(NetIncomingMessage inc)
|
||||
{
|
||||
if (GameMain.Server != null)
|
||||
{
|
||||
throw new InvalidOperationException("Receiving files when a server is running is not allowed");
|
||||
}
|
||||
|
||||
System.Diagnostics.Debug.Assert(!activeTransfers.Any(t =>
|
||||
t.Status == FileTransferStatus.Error ||
|
||||
t.Status == FileTransferStatus.Canceled ||
|
||||
t.Status == FileTransferStatus.Finished), "List of active file transfers contains entires that should have been removed");
|
||||
|
||||
byte transferMessageType = inc.ReadByte();
|
||||
switch (transferMessageType)
|
||||
{
|
||||
case (byte)FileTransferMessageType.Initiate:
|
||||
var existingTransfer = activeTransfers.Find(t => t.SequenceChannel == inc.SequenceChannel);
|
||||
if (existingTransfer != null)
|
||||
{
|
||||
GameMain.Client.CancelFileTransfer(inc.SequenceChannel);
|
||||
DebugConsole.ThrowError("File transfer error: file transfer initiated on a sequence channel that's already in use");
|
||||
return;
|
||||
}
|
||||
|
||||
byte fileType = inc.ReadByte();
|
||||
ushort chunkLen = inc.ReadUInt16();
|
||||
ulong fileSize = inc.ReadUInt64();
|
||||
string fileName = inc.ReadString();
|
||||
|
||||
string errorMsg;
|
||||
if (!ValidateInitialData(fileType, fileName, fileSize, out errorMsg))
|
||||
{
|
||||
GameMain.Client.CancelFileTransfer(inc.SequenceChannel);
|
||||
DebugConsole.ThrowError("File transfer failed (" + errorMsg + ")");
|
||||
return;
|
||||
}
|
||||
|
||||
if (GameSettings.VerboseLogging)
|
||||
{
|
||||
DebugConsole.Log("Received file transfer initiation message: ");
|
||||
DebugConsole.Log(" File: "+fileName);
|
||||
DebugConsole.Log(" Size: " + fileSize);
|
||||
DebugConsole.Log(" Sequence channel: " + inc.SequenceChannel);
|
||||
}
|
||||
|
||||
if (!Directory.Exists(downloadFolder))
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(downloadFolder);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugConsole.ThrowError("Could not start a file transfer: failed to create the folder \""+downloadFolder+"\".", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var newTransfer = new FileTransferIn(inc.SenderConnection, Path.Combine(downloadFolder, fileName), (FileTransferType)fileType);
|
||||
newTransfer.SequenceChannel = inc.SequenceChannel;
|
||||
newTransfer.Status = FileTransferStatus.Receiving;
|
||||
newTransfer.FileSize = fileSize;
|
||||
|
||||
activeTransfers.Add(newTransfer);
|
||||
|
||||
break;
|
||||
case (byte)FileTransferMessageType.Data:
|
||||
var activeTransfer = activeTransfers.Find(t => t.Connection == inc.SenderConnection && t.SequenceChannel == inc.SequenceChannel);
|
||||
if (activeTransfer == null)
|
||||
{
|
||||
GameMain.Client.CancelFileTransfer(inc.SequenceChannel);
|
||||
DebugConsole.ThrowError("File transfer error: received data without a transfer initiation message");
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeTransfer.Received + (ulong)(inc.LengthBytes-inc.PositionInBytes) > activeTransfer.FileSize)
|
||||
{
|
||||
GameMain.Client.CancelFileTransfer(inc.SequenceChannel);
|
||||
DebugConsole.ThrowError("File transfer error: Received more data than expected");
|
||||
activeTransfer.Status = FileTransferStatus.Error;
|
||||
StopTransfer(activeTransfer);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
activeTransfer.ReadBytes(inc);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
GameMain.Client.CancelFileTransfer(inc.SequenceChannel);
|
||||
DebugConsole.ThrowError("File transfer error: "+e.Message);
|
||||
activeTransfer.Status = FileTransferStatus.Error;
|
||||
StopTransfer(activeTransfer, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeTransfer.Status == FileTransferStatus.Finished)
|
||||
{
|
||||
activeTransfer.Dispose();
|
||||
|
||||
string errorMessage = "";
|
||||
if (ValidateReceivedData(activeTransfer, out errorMessage))
|
||||
{
|
||||
OnFinished(activeTransfer);
|
||||
StopTransfer(activeTransfer);
|
||||
}
|
||||
else
|
||||
{
|
||||
new GUIMessageBox("File transfer aborted", errorMessage);
|
||||
|
||||
activeTransfer.Status = FileTransferStatus.Error;
|
||||
StopTransfer(activeTransfer, true);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case (byte)FileTransferMessageType.Cancel:
|
||||
byte sequenceChannel = inc.ReadByte();
|
||||
var matchingTransfer = activeTransfers.Find(t => t.Connection == inc.SenderConnection && t.SequenceChannel == sequenceChannel);
|
||||
if (matchingTransfer != null)
|
||||
{
|
||||
new GUIMessageBox("File transfer cancelled", "The server has cancelled the transfer of the file \"" + matchingTransfer.FileName + "\".");
|
||||
StopTransfer(matchingTransfer);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private bool ValidateInitialData(byte type, string fileName, ulong fileSize, out string errorMessage)
|
||||
{
|
||||
errorMessage = "";
|
||||
|
||||
if (fileSize > MaxFileSize)
|
||||
{
|
||||
errorMessage = "File too large (" + MathUtils.GetBytesReadable((long)fileSize) + ")";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Enum.IsDefined(typeof(FileTransferType), (int)type))
|
||||
{
|
||||
errorMessage = "Unknown file type";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(fileName) ||
|
||||
fileName.IndexOfAny(Path.GetInvalidFileNameChars()) > -1)
|
||||
{
|
||||
errorMessage = "Illegal characters in file name ''" + fileName + "''";
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case (byte)FileTransferType.Submarine:
|
||||
if (Path.GetExtension(fileName) != ".sub")
|
||||
{
|
||||
errorMessage = "Wrong file extension ''" + Path.GetExtension(fileName) + "''! (Expected .sub)";
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool ValidateReceivedData(FileTransferIn fileTransfer, out string ErrorMessage)
|
||||
{
|
||||
ErrorMessage = "";
|
||||
switch (fileTransfer.FileType)
|
||||
{
|
||||
case FileTransferType.Submarine:
|
||||
Stream stream = null;
|
||||
|
||||
try
|
||||
{
|
||||
stream = SaveUtil.DecompressFiletoStream(fileTransfer.FilePath);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ErrorMessage = "Loading received submarine ''" + fileTransfer.FileName + "'' failed! {" + e.Message + "}";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (stream == null)
|
||||
{
|
||||
ErrorMessage = "Decompressing received submarine file''" + fileTransfer.FilePath + "'' failed!";
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
stream.Position = 0;
|
||||
|
||||
XmlReaderSettings settings = new XmlReaderSettings();
|
||||
settings.DtdProcessing = DtdProcessing.Prohibit;
|
||||
settings.IgnoreProcessingInstructions = true;
|
||||
|
||||
using (var reader = XmlReader.Create(stream, settings))
|
||||
{
|
||||
while (reader.Read());
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
stream.Close();
|
||||
stream.Dispose();
|
||||
|
||||
ErrorMessage = "Parsing file ''" + fileTransfer.FilePath + "'' failed! The file may not be a valid submarine file.";
|
||||
return false;
|
||||
}
|
||||
|
||||
stream.Close();
|
||||
stream.Dispose();
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void StopTransfer(FileTransferIn transfer, bool deleteFile = false)
|
||||
{
|
||||
if (transfer.Status != FileTransferStatus.Finished &&
|
||||
transfer.Status != FileTransferStatus.Error)
|
||||
{
|
||||
transfer.Status = FileTransferStatus.Canceled;
|
||||
}
|
||||
|
||||
if (activeTransfers.Contains(transfer)) activeTransfers.Remove(transfer);
|
||||
transfer.Dispose();
|
||||
|
||||
if (deleteFile && File.Exists(transfer.FilePath))
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(transfer.FilePath);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugConsole.ThrowError("Failed to delete file \""+transfer.FilePath+"\" ("+e.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
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,226 @@
|
||||
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.");
|
||||
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.");
|
||||
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);
|
||||
return;
|
||||
}
|
||||
|
||||
if (clVersion != GameMain.Version.ToString())
|
||||
{
|
||||
DisconnectUnauthClient(inc, unauthClient, "Version " + GameMain.Version + " required to connect to the server (Your version: " + clVersion + ")");
|
||||
Log(clName + " couldn't join the server (wrong game version)", ServerLog.MessageType.Error);
|
||||
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 + " couldn't join the server (wrong content package name)", ServerLog.MessageType.Error);
|
||||
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 + " couldn't join the server (wrong content package hash)", ServerLog.MessageType.Error);
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
//new client
|
||||
Client newClient = new Client(clName, GetNewClientID());
|
||||
newClient.InitClientSync();
|
||||
newClient.Connection = unauthClient.Connection;
|
||||
unauthenticatedClients.Remove(unauthClient);
|
||||
unauthClient = null;
|
||||
ConnectedClients.Add(newClient);
|
||||
|
||||
GameMain.NetLobbyScreen.AddPlayer(newClient.name);
|
||||
|
||||
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,868 @@
|
||||
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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private GUIFrame settingsFrame;
|
||||
private GUIFrame[] settingsTabs;
|
||||
private int settingsTabIndex;
|
||||
|
||||
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 && GameMain.NetLobbyScreen.ServerMessage != null)
|
||||
{
|
||||
doc.Root.SetAttributeValue("ServerMessage", GameMain.NetLobbyScreen.ServerMessage.Text);
|
||||
}
|
||||
|
||||
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 && GameMain.NetLobbyScreen.ServerMessage != null)
|
||||
{
|
||||
GameMain.NetLobbyScreen.ServerMessage.Text = ToolBox.GetAttributeString(doc.Root, "ServerMessage", "");
|
||||
}
|
||||
|
||||
showLogButton.Visible = SaveServerLogs;
|
||||
|
||||
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>();
|
||||
}
|
||||
|
||||
private void CreateSettingsFrame()
|
||||
{
|
||||
settingsFrame = new GUIFrame(new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight), Color.Black * 0.5f, null);
|
||||
|
||||
GUIFrame innerFrame = new GUIFrame(new Rectangle(0, 0, 400, 430), null, Alignment.Center, "", settingsFrame);
|
||||
innerFrame.Padding = new Vector4(20.0f, 20.0f, 20.0f, 20.0f);
|
||||
|
||||
new GUITextBlock(new Rectangle(0, -5, 0, 20), "Settings", "", innerFrame, GUI.LargeFont);
|
||||
|
||||
string[] tabNames = { "Rounds", "Server", "Banlist", "Whitelist" };
|
||||
settingsTabs = new GUIFrame[tabNames.Length];
|
||||
for (int i = 0; i < tabNames.Length; i++)
|
||||
{
|
||||
settingsTabs[i] = new GUIFrame(new Rectangle(0, 15, 0, innerFrame.Rect.Height - 120), null, Alignment.Center, "InnerFrame", innerFrame);
|
||||
settingsTabs[i].Padding = new Vector4(40.0f, 20.0f, 40.0f, 40.0f);
|
||||
|
||||
var tabButton = new GUIButton(new Rectangle(85 * i, 35, 80, 20), tabNames[i], "", innerFrame);
|
||||
tabButton.UserData = i;
|
||||
tabButton.OnClicked = SelectSettingsTab;
|
||||
}
|
||||
|
||||
settingsTabs[2].Padding = Vector4.Zero;
|
||||
|
||||
SelectSettingsTab(null, 0);
|
||||
|
||||
var closeButton = new GUIButton(new Rectangle(10, 0, 100, 20), "Close", Alignment.BottomRight, "", innerFrame);
|
||||
closeButton.OnClicked = ToggleSettingsFrame;
|
||||
|
||||
//--------------------------------------------------------------------------------
|
||||
// game settings
|
||||
//--------------------------------------------------------------------------------
|
||||
|
||||
int y = 0;
|
||||
|
||||
settingsTabs[0].Padding = new Vector4(40.0f, 5.0f, 40.0f, 40.0f);
|
||||
|
||||
new GUITextBlock(new Rectangle(0, y, 100, 20), "Submarine selection:", "", settingsTabs[0]);
|
||||
var selectionFrame = new GUIFrame(new Rectangle(0, y + 20, 300, 20), null, settingsTabs[0]);
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
var selectionTick = new GUITickBox(new Rectangle(i * 100, 0, 20, 20), ((SelectionMode)i).ToString(), Alignment.Left, selectionFrame);
|
||||
selectionTick.Selected = i == (int)subSelectionMode;
|
||||
selectionTick.OnSelected = SwitchSubSelection;
|
||||
selectionTick.UserData = (SelectionMode)i;
|
||||
}
|
||||
|
||||
y += 45;
|
||||
|
||||
new GUITextBlock(new Rectangle(0, y, 100, 20), "Mode selection:", "", settingsTabs[0]);
|
||||
selectionFrame = new GUIFrame(new Rectangle(0, y + 20, 300, 20), null, settingsTabs[0]);
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
var selectionTick = new GUITickBox(new Rectangle(i * 100, 0, 20, 20), ((SelectionMode)i).ToString(), Alignment.Left, selectionFrame);
|
||||
selectionTick.Selected = i == (int)modeSelectionMode;
|
||||
selectionTick.OnSelected = SwitchModeSelection;
|
||||
selectionTick.UserData = (SelectionMode)i;
|
||||
}
|
||||
|
||||
y += 60;
|
||||
|
||||
var endBox = new GUITickBox(new Rectangle(0, y, 20, 20), "End round when destination reached", Alignment.Left, settingsTabs[0]);
|
||||
endBox.Selected = EndRoundAtLevelEnd;
|
||||
endBox.OnSelected = (GUITickBox) => { EndRoundAtLevelEnd = GUITickBox.Selected; return true; };
|
||||
|
||||
y += 25;
|
||||
|
||||
var endVoteBox = new GUITickBox(new Rectangle(0, y, 20, 20), "End round by voting", Alignment.Left, settingsTabs[0]);
|
||||
endVoteBox.Selected = Voting.AllowEndVoting;
|
||||
endVoteBox.OnSelected = (GUITickBox) =>
|
||||
{
|
||||
Voting.AllowEndVoting = !Voting.AllowEndVoting;
|
||||
GameMain.Server.UpdateVoteStatus();
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
var votesRequiredText = new GUITextBlock(new Rectangle(20, y + 15, 20, 20), "Votes required: 50 %", "", settingsTabs[0], GUI.SmallFont);
|
||||
|
||||
var votesRequiredSlider = new GUIScrollBar(new Rectangle(150, y + 22, 100, 15), "", 0.1f, settingsTabs[0]);
|
||||
votesRequiredSlider.UserData = votesRequiredText;
|
||||
votesRequiredSlider.Step = 0.2f;
|
||||
votesRequiredSlider.BarScroll = (EndVoteRequiredRatio - 0.5f) * 2.0f;
|
||||
votesRequiredSlider.OnMoved = (GUIScrollBar scrollBar, float barScroll) =>
|
||||
{
|
||||
GUITextBlock voteText = scrollBar.UserData as GUITextBlock;
|
||||
|
||||
EndVoteRequiredRatio = barScroll / 2.0f + 0.5f;
|
||||
voteText.Text = "Votes required: " + (int)MathUtils.Round(EndVoteRequiredRatio * 100.0f, 10.0f) + " %";
|
||||
return true;
|
||||
};
|
||||
votesRequiredSlider.OnMoved(votesRequiredSlider, votesRequiredSlider.BarScroll);
|
||||
|
||||
y += 35;
|
||||
|
||||
var respawnBox = new GUITickBox(new Rectangle(0, y, 20, 20), "Allow respawning", Alignment.Left, settingsTabs[0]);
|
||||
respawnBox.Selected = AllowRespawn;
|
||||
respawnBox.OnSelected = (GUITickBox) =>
|
||||
{
|
||||
AllowRespawn = !AllowRespawn;
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
var respawnIntervalText = new GUITextBlock(new Rectangle(20, y + 13, 20, 20), "Respawn interval", "", settingsTabs[0], GUI.SmallFont);
|
||||
|
||||
var respawnIntervalSlider = new GUIScrollBar(new Rectangle(150, y + 20, 100, 15), "", 0.1f, settingsTabs[0]);
|
||||
respawnIntervalSlider.UserData = respawnIntervalText;
|
||||
respawnIntervalSlider.Step = 0.05f;
|
||||
respawnIntervalSlider.BarScroll = RespawnInterval / 600.0f;
|
||||
respawnIntervalSlider.OnMoved = (GUIScrollBar scrollBar, float barScroll) =>
|
||||
{
|
||||
GUITextBlock text = scrollBar.UserData as GUITextBlock;
|
||||
|
||||
RespawnInterval = Math.Max(barScroll * 600.0f, 10.0f);
|
||||
text.Text = "Interval: " + ToolBox.SecondsToReadableTime(RespawnInterval);
|
||||
return true;
|
||||
};
|
||||
respawnIntervalSlider.OnMoved(respawnIntervalSlider, respawnIntervalSlider.BarScroll);
|
||||
|
||||
y += 35;
|
||||
|
||||
var minRespawnText = new GUITextBlock(new Rectangle(0, y, 200, 20), "Minimum players to respawn", "", settingsTabs[0]);
|
||||
minRespawnText.ToolTip = "What percentage of players has to be dead/spectating until a respawn shuttle is dispatched";
|
||||
|
||||
var minRespawnSlider = new GUIScrollBar(new Rectangle(150, y + 20, 100, 15), "", 0.1f, settingsTabs[0]);
|
||||
minRespawnSlider.ToolTip = minRespawnText.ToolTip;
|
||||
minRespawnSlider.UserData = minRespawnText;
|
||||
minRespawnSlider.Step = 0.1f;
|
||||
minRespawnSlider.BarScroll = MinRespawnRatio;
|
||||
minRespawnSlider.OnMoved = (GUIScrollBar scrollBar, float barScroll) =>
|
||||
{
|
||||
GUITextBlock txt = scrollBar.UserData as GUITextBlock;
|
||||
|
||||
MinRespawnRatio = barScroll;
|
||||
txt.Text = "Minimum players to respawn: " + (int)MathUtils.Round(MinRespawnRatio * 100.0f, 10.0f) + " %";
|
||||
return true;
|
||||
};
|
||||
minRespawnSlider.OnMoved(minRespawnSlider, MinRespawnRatio);
|
||||
|
||||
y += 30;
|
||||
|
||||
var respawnDurationText = new GUITextBlock(new Rectangle(0, y, 200, 20), "Duration of respawn transport", "", settingsTabs[0]);
|
||||
respawnDurationText.ToolTip = "The amount of time respawned players have to navigate the respawn shuttle to the main submarine. " +
|
||||
"After the duration expires, the shuttle will automatically head back out of the level.";
|
||||
|
||||
var respawnDurationSlider = new GUIScrollBar(new Rectangle(150, y + 20, 100, 15), "", 0.1f, settingsTabs[0]);
|
||||
respawnDurationSlider.ToolTip = minRespawnText.ToolTip;
|
||||
respawnDurationSlider.UserData = respawnDurationText;
|
||||
respawnDurationSlider.Step = 0.1f;
|
||||
respawnDurationSlider.BarScroll = MaxTransportTime <= 0.0f ? 1.0f : (MaxTransportTime - 60.0f) / 600.0f;
|
||||
respawnDurationSlider.OnMoved = (GUIScrollBar scrollBar, float barScroll) =>
|
||||
{
|
||||
GUITextBlock txt = scrollBar.UserData as GUITextBlock;
|
||||
|
||||
if (barScroll == 1.0f)
|
||||
{
|
||||
MaxTransportTime = 0;
|
||||
txt.Text = "Duration of respawn transport: unlimited";
|
||||
}
|
||||
else
|
||||
{
|
||||
MaxTransportTime = barScroll * 600.0f + 60.0f;
|
||||
txt.Text = "Duration of respawn transport: " + ToolBox.SecondsToReadableTime(MaxTransportTime);
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
respawnDurationSlider.OnMoved(respawnDurationSlider, respawnDurationSlider.BarScroll);
|
||||
|
||||
y += 35;
|
||||
|
||||
var monsterButton = new GUIButton(new Rectangle(0, y, 130, 20), "Monster Spawns", "", settingsTabs[0]);
|
||||
monsterButton.Enabled = !GameStarted;
|
||||
var monsterFrame = new GUIListBox(new Rectangle(-290, 60, 280, 250), "", settingsTabs[0]);
|
||||
monsterFrame.Visible = false;
|
||||
monsterButton.UserData = monsterFrame;
|
||||
monsterButton.OnClicked = (button, obj) =>
|
||||
{
|
||||
if (gameStarted)
|
||||
{
|
||||
((GUIComponent)obj).Visible = false;
|
||||
button.Enabled = false;
|
||||
return true;
|
||||
}
|
||||
((GUIComponent)obj).Visible = !((GUIComponent)obj).Visible;
|
||||
return true;
|
||||
};
|
||||
List<string> monsterNames = monsterEnabled.Keys.ToList();
|
||||
foreach (string s in monsterNames)
|
||||
{
|
||||
GUITextBlock textBlock = new GUITextBlock(
|
||||
new Rectangle(0, 0, 260, 25),
|
||||
s,
|
||||
"",
|
||||
Alignment.Left, Alignment.Left, monsterFrame);
|
||||
textBlock.Padding = new Vector4(35.0f, 3.0f, 0.0f, 0.0f);
|
||||
textBlock.UserData = monsterFrame;
|
||||
textBlock.CanBeFocused = false;
|
||||
|
||||
var monsterEnabledBox = new GUITickBox(new Rectangle(-25, 0, 20, 20), "", Alignment.Left, textBlock);
|
||||
monsterEnabledBox.Selected = monsterEnabled[s];
|
||||
monsterEnabledBox.OnSelected = (GUITickBox) =>
|
||||
{
|
||||
if (gameStarted)
|
||||
{
|
||||
monsterFrame.Visible = false;
|
||||
monsterButton.Enabled = false;
|
||||
return true;
|
||||
}
|
||||
monsterEnabled[s] = !monsterEnabled[s];
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
var cargoButton = new GUIButton(new Rectangle(160, y, 130, 20), "Additional Cargo", "", settingsTabs[0]);
|
||||
cargoButton.Enabled = !GameStarted;
|
||||
|
||||
var cargoFrame = new GUIListBox(new Rectangle(300, 60, 280, 250), "", settingsTabs[0]);
|
||||
cargoFrame.Visible = false;
|
||||
cargoButton.UserData = cargoFrame;
|
||||
cargoButton.OnClicked = (button, obj) =>
|
||||
{
|
||||
if (gameStarted)
|
||||
{
|
||||
((GUIComponent)obj).Visible = false;
|
||||
button.Enabled = false;
|
||||
return true;
|
||||
}
|
||||
((GUIComponent)obj).Visible = !((GUIComponent)obj).Visible;
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
foreach (MapEntityPrefab pf in MapEntityPrefab.list)
|
||||
{
|
||||
if (!(pf is ItemPrefab) || (pf.Price <= 0.0f && !pf.tags.Contains("smallitem"))) continue;
|
||||
|
||||
GUITextBlock textBlock = new GUITextBlock(
|
||||
new Rectangle(0, 0, 260, 25),
|
||||
pf.Name, "",
|
||||
Alignment.Left, Alignment.CenterLeft, cargoFrame, false, GUI.SmallFont);
|
||||
textBlock.Padding = new Vector4(40.0f, 3.0f, 0.0f, 0.0f);
|
||||
textBlock.UserData = cargoFrame;
|
||||
textBlock.CanBeFocused = false;
|
||||
|
||||
if (pf.sprite != null)
|
||||
{
|
||||
float scale = Math.Min(Math.Min(30.0f / pf.sprite.SourceRect.Width, 30.0f / pf.sprite.SourceRect.Height), 1.0f);
|
||||
GUIImage img = new GUIImage(new Rectangle(-20-(int)(pf.sprite.SourceRect.Width*scale*0.5f), 12-(int)(pf.sprite.SourceRect.Height*scale*0.5f), 40, 40), pf.sprite, Alignment.Left, textBlock);
|
||||
img.Color = pf.SpriteColor;
|
||||
img.Scale = scale;
|
||||
}
|
||||
|
||||
int cargoVal = 0;
|
||||
extraCargo.TryGetValue(pf.Name, out cargoVal);
|
||||
var countText = new GUITextBlock(
|
||||
new Rectangle(160, 0, 55, 25),
|
||||
cargoVal.ToString(),
|
||||
"",
|
||||
Alignment.Left, Alignment.Center, textBlock);
|
||||
|
||||
var incButton = new GUIButton(new Rectangle(200, 5, 15, 15), ">", "", textBlock);
|
||||
incButton.UserData = countText;
|
||||
incButton.OnClicked = (button, obj) =>
|
||||
{
|
||||
int val;
|
||||
if (extraCargo.TryGetValue(pf.Name, out val))
|
||||
{
|
||||
extraCargo[pf.Name]++; val = extraCargo[pf.Name];
|
||||
}
|
||||
else
|
||||
{
|
||||
extraCargo.Add(pf.Name,1); val = 1;
|
||||
}
|
||||
((GUITextBlock)obj).Text = val.ToString();
|
||||
((GUITextBlock)obj).SetTextPos();
|
||||
return true;
|
||||
};
|
||||
|
||||
var decButton = new GUIButton(new Rectangle(160, 5, 15, 15), "<", "", textBlock);
|
||||
decButton.UserData = countText;
|
||||
decButton.OnClicked = (button, obj) =>
|
||||
{
|
||||
int val;
|
||||
if (extraCargo.TryGetValue(pf.Name, out val))
|
||||
{
|
||||
extraCargo[pf.Name]--;
|
||||
val = extraCargo[pf.Name];
|
||||
if (val <= 0)
|
||||
{
|
||||
extraCargo.Remove(pf.Name);
|
||||
val = 0;
|
||||
}
|
||||
((GUITextBlock)obj).Text = val.ToString();
|
||||
((GUITextBlock)obj).SetTextPos();
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
//--------------------------------------------------------------------------------
|
||||
// server settings
|
||||
//--------------------------------------------------------------------------------
|
||||
|
||||
y = 0;
|
||||
|
||||
|
||||
var startIntervalText = new GUITextBlock(new Rectangle(-10, y, 100, 20), "Autorestart delay", "", settingsTabs[1]);
|
||||
var startIntervalSlider = new GUIScrollBar(new Rectangle(10, y + 22, 100, 15), "", 0.1f, settingsTabs[1]);
|
||||
startIntervalSlider.UserData = startIntervalText;
|
||||
startIntervalSlider.Step = 0.05f;
|
||||
startIntervalSlider.BarScroll = AutoRestartInterval / 300.0f;
|
||||
startIntervalSlider.OnMoved = (GUIScrollBar scrollBar, float barScroll) =>
|
||||
{
|
||||
GUITextBlock text = scrollBar.UserData as GUITextBlock;
|
||||
|
||||
AutoRestartInterval = Math.Max(barScroll * 300.0f, 10.0f);
|
||||
|
||||
text.Text = "Autorestart delay: " + ToolBox.SecondsToReadableTime(AutoRestartInterval);
|
||||
return true;
|
||||
};
|
||||
startIntervalSlider.OnMoved(startIntervalSlider, startIntervalSlider.BarScroll);
|
||||
|
||||
y += 45;
|
||||
|
||||
var allowSpecBox = new GUITickBox(new Rectangle(0, y, 20, 20), "Allow spectating", Alignment.Left, settingsTabs[1]);
|
||||
allowSpecBox.Selected = AllowSpectating;
|
||||
allowSpecBox.OnSelected = (GUITickBox) =>
|
||||
{
|
||||
AllowSpectating = GUITickBox.Selected;
|
||||
return true;
|
||||
};
|
||||
|
||||
y += 40;
|
||||
|
||||
var voteKickBox = new GUITickBox(new Rectangle(0, y, 20, 20), "Allow vote kicking", Alignment.Left, settingsTabs[1]);
|
||||
voteKickBox.Selected = Voting.AllowVoteKick;
|
||||
voteKickBox.OnSelected = (GUITickBox) =>
|
||||
{
|
||||
Voting.AllowVoteKick = !Voting.AllowVoteKick;
|
||||
GameMain.Server.UpdateVoteStatus();
|
||||
return true;
|
||||
};
|
||||
|
||||
var kickVotesRequiredText = new GUITextBlock(new Rectangle(20, y + 20, 20, 20), "Votes required: 50 %", "", settingsTabs[1], GUI.SmallFont);
|
||||
|
||||
var kickVoteSlider = new GUIScrollBar(new Rectangle(150, y + 22, 100, 15), "", 0.1f, settingsTabs[1]);
|
||||
kickVoteSlider.UserData = kickVotesRequiredText;
|
||||
kickVoteSlider.Step = 0.2f;
|
||||
kickVoteSlider.BarScroll = (KickVoteRequiredRatio - 0.5f) * 2.0f;
|
||||
kickVoteSlider.OnMoved = (GUIScrollBar scrollBar, float barScroll) =>
|
||||
{
|
||||
GUITextBlock voteText = scrollBar.UserData as GUITextBlock;
|
||||
|
||||
KickVoteRequiredRatio = barScroll / 2.0f + 0.5f;
|
||||
voteText.Text = "Votes required: " + (int)MathUtils.Round(KickVoteRequiredRatio * 100.0f, 10.0f) + " %";
|
||||
return true;
|
||||
};
|
||||
kickVoteSlider.OnMoved(kickVoteSlider, kickVoteSlider.BarScroll);
|
||||
|
||||
y += 45;
|
||||
|
||||
var shareSubsBox = new GUITickBox(new Rectangle(0, y, 20, 20), "Share submarine files with players", Alignment.Left, settingsTabs[1]);
|
||||
shareSubsBox.Selected = AllowFileTransfers;
|
||||
shareSubsBox.OnSelected = (GUITickBox) =>
|
||||
{
|
||||
AllowFileTransfers = GUITickBox.Selected;
|
||||
return true;
|
||||
};
|
||||
|
||||
y += 40;
|
||||
|
||||
var randomizeLevelBox = new GUITickBox(new Rectangle(0, y, 20, 20), "Randomize level seed between rounds", Alignment.Left, settingsTabs[1]);
|
||||
randomizeLevelBox.Selected = RandomizeSeed;
|
||||
randomizeLevelBox.OnSelected = (GUITickBox) =>
|
||||
{
|
||||
RandomizeSeed = GUITickBox.Selected;
|
||||
return true;
|
||||
};
|
||||
|
||||
y += 40;
|
||||
|
||||
var saveLogsBox = new GUITickBox(new Rectangle(0, y, 20, 20), "Save server logs", Alignment.Left, settingsTabs[1]);
|
||||
saveLogsBox.Selected = SaveServerLogs;
|
||||
saveLogsBox.OnSelected = (GUITickBox) =>
|
||||
{
|
||||
SaveServerLogs = GUITickBox.Selected;
|
||||
showLogButton.Visible = SaveServerLogs;
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
//--------------------------------------------------------------------------------
|
||||
// banlist
|
||||
//--------------------------------------------------------------------------------
|
||||
|
||||
|
||||
banList.CreateBanFrame(settingsTabs[2]);
|
||||
|
||||
//--------------------------------------------------------------------------------
|
||||
// whitelist
|
||||
//--------------------------------------------------------------------------------
|
||||
|
||||
whitelist.CreateWhiteListFrame(settingsTabs[3]);
|
||||
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
private bool SwitchSubSelection(GUITickBox tickBox)
|
||||
{
|
||||
subSelectionMode = (SelectionMode)tickBox.UserData;
|
||||
|
||||
foreach (GUIComponent otherTickBox in tickBox.Parent.children)
|
||||
{
|
||||
if (otherTickBox == tickBox) continue;
|
||||
((GUITickBox)otherTickBox).Selected = false;
|
||||
}
|
||||
|
||||
Voting.AllowSubVoting = subSelectionMode == SelectionMode.Vote;
|
||||
|
||||
if (subSelectionMode == SelectionMode.Random)
|
||||
{
|
||||
GameMain.NetLobbyScreen.SubList.Select(Rand.Range(0, GameMain.NetLobbyScreen.SubList.CountChildren));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool SelectSettingsTab(GUIButton button, object obj)
|
||||
{
|
||||
settingsTabIndex = (int)obj;
|
||||
|
||||
for (int i = 0; i < settingsTabs.Length; i++ )
|
||||
{
|
||||
settingsTabs[i].Visible = i == settingsTabIndex;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool SwitchModeSelection(GUITickBox tickBox)
|
||||
{
|
||||
modeSelectionMode = (SelectionMode)tickBox.UserData;
|
||||
|
||||
foreach (GUIComponent otherTickBox in tickBox.Parent.children)
|
||||
{
|
||||
if (otherTickBox == tickBox) continue;
|
||||
((GUITickBox)otherTickBox).Selected = false;
|
||||
}
|
||||
|
||||
Voting.AllowModeVoting = modeSelectionMode == SelectionMode.Vote;
|
||||
|
||||
if (modeSelectionMode == SelectionMode.Random)
|
||||
{
|
||||
GameMain.NetLobbyScreen.ModeList.Select(Rand.Range(0, GameMain.NetLobbyScreen.ModeList.CountChildren));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
public bool ToggleSettingsFrame(GUIButton button, object obj)
|
||||
{
|
||||
if (settingsFrame==null)
|
||||
{
|
||||
CreateSettingsFrame();
|
||||
}
|
||||
else
|
||||
{
|
||||
settingsFrame = null;
|
||||
SaveSettings();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void ManagePlayersFrame(GUIFrame infoFrame)
|
||||
{
|
||||
GUIListBox cList = new GUIListBox(new Rectangle(0, 0, 0, 300), Color.White * 0.7f, "", infoFrame);
|
||||
cList.Padding = new Vector4(10.0f, 10.0f, 10.0f, 10.0f);
|
||||
//crewList.OnSelected = SelectCrewCharacter;
|
||||
|
||||
foreach (Client c in ConnectedClients)
|
||||
{
|
||||
GUIFrame frame = new GUIFrame(new Rectangle(0, 0, 0, 40), Color.Transparent, null, cList);
|
||||
frame.Padding = new Vector4(5.0f, 5.0f, 5.0f, 5.0f);
|
||||
frame.Color = (c.inGame && c.Character!=null && !c.Character.IsDead) ? Color.Gold * 0.2f : Color.Transparent;
|
||||
frame.HoverColor = Color.LightGray * 0.5f;
|
||||
frame.SelectedColor = Color.Gold * 0.5f;
|
||||
|
||||
GUITextBlock textBlock = new GUITextBlock(
|
||||
new Rectangle(40, 0, 0, 25),
|
||||
c.name + " (" + c.Connection.RemoteEndPoint.Address.ToString() + ")",
|
||||
Color.Transparent, Color.White,
|
||||
Alignment.Left, Alignment.Left,
|
||||
null, frame);
|
||||
|
||||
var banButton = new GUIButton(new Rectangle(-110, 0, 100, 20), "Ban", Alignment.Right | Alignment.CenterY, "", frame);
|
||||
banButton.UserData = c.name;
|
||||
banButton.OnClicked = GameMain.NetLobbyScreen.BanPlayer;
|
||||
|
||||
var rangebanButton = new GUIButton(new Rectangle(-220, 0, 100, 20), "Ban range", Alignment.Right | Alignment.CenterY, "", frame);
|
||||
rangebanButton.UserData = c.name;
|
||||
rangebanButton.OnClicked = GameMain.NetLobbyScreen.BanPlayerRange;
|
||||
|
||||
var kickButton = new GUIButton(new Rectangle(0, 0, 100, 20), "Kick", Alignment.Right | Alignment.CenterY, "", frame);
|
||||
kickButton.UserData = c.name;
|
||||
kickButton.OnClicked = GameMain.NetLobbyScreen.KickPlayer;
|
||||
|
||||
textBlock.Padding = new Vector4(5.0f, 0.0f, 5.0f, 0.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
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
|
||||
{
|
||||
void ClientWrite(NetBuffer msg, object[] extraData = null);
|
||||
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);
|
||||
void ClientRead(ServerNetObject type, NetBuffer msg, float sendingTime);
|
||||
}
|
||||
}
|
||||
@@ -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,227 @@
|
||||
using Lidgren.Network;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Barotrauma.Networking
|
||||
{
|
||||
class ClientEntityEventManager : NetEntityEventManager
|
||||
{
|
||||
private List<ClientEntityEvent> events;
|
||||
|
||||
private UInt16 ID;
|
||||
|
||||
private GameClient thisClient;
|
||||
|
||||
//when was a specific entity event last sent to the client
|
||||
// key = event id, value = NetTime.Now when sending
|
||||
public Dictionary<UInt16, float> eventLastSent;
|
||||
|
||||
public UInt16 LastReceivedID
|
||||
{
|
||||
get { return lastReceivedID; }
|
||||
}
|
||||
|
||||
private UInt16 lastReceivedID;
|
||||
|
||||
public ClientEntityEventManager(GameClient client)
|
||||
{
|
||||
events = new List<ClientEntityEvent>();
|
||||
eventLastSent = new Dictionary<UInt16, float>();
|
||||
|
||||
thisClient = client;
|
||||
}
|
||||
|
||||
public void CreateEvent(IClientSerializable entity, object[] extraData = null)
|
||||
{
|
||||
if (GameMain.Client == null || GameMain.Client.Character == null) return;
|
||||
|
||||
if (!(entity is Entity))
|
||||
{
|
||||
DebugConsole.ThrowError("Can't create an entity event for " + entity + "!");
|
||||
return;
|
||||
}
|
||||
|
||||
ID++;
|
||||
var newEvent = new ClientEntityEvent(entity, ID);
|
||||
newEvent.CharacterStateID = GameMain.Client.Character.LastNetworkUpdateID;
|
||||
if (extraData != null) newEvent.SetData(extraData);
|
||||
|
||||
events.Add(newEvent);
|
||||
}
|
||||
|
||||
public void Write(NetOutgoingMessage msg, NetConnection serverConnection)
|
||||
{
|
||||
if (events.Count == 0 || serverConnection == null) return;
|
||||
|
||||
List<NetEntityEvent> eventsToSync = new List<NetEntityEvent>();
|
||||
|
||||
//find the index of the first event the server hasn't received
|
||||
int startIndex = events.Count;
|
||||
while (startIndex > 0 &&
|
||||
NetIdUtils.IdMoreRecent(events[startIndex-1].ID,thisClient.LastSentEntityEventID))
|
||||
{
|
||||
startIndex--;
|
||||
}
|
||||
|
||||
for (int i = startIndex; i < events.Count; i++)
|
||||
{
|
||||
//find the first event that hasn't been sent in 1.5 * roundtriptime or at all
|
||||
float lastSent = 0;
|
||||
eventLastSent.TryGetValue(events[i].ID, out lastSent);
|
||||
|
||||
if (lastSent > NetTime.Now - serverConnection.AverageRoundtripTime * 1.5f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
eventsToSync.AddRange(events.GetRange(i, events.Count - i));
|
||||
break;
|
||||
}
|
||||
if (eventsToSync.Count == 0) return;
|
||||
|
||||
//too many events for one packet
|
||||
if (eventsToSync.Count > MaxEventsPerWrite)
|
||||
{
|
||||
eventsToSync.RemoveRange(MaxEventsPerWrite, eventsToSync.Count - MaxEventsPerWrite);
|
||||
}
|
||||
if (eventsToSync.Count == 0) return;
|
||||
|
||||
foreach (NetEntityEvent entityEvent in eventsToSync)
|
||||
{
|
||||
eventLastSent[entityEvent.ID] = (float)NetTime.Now;
|
||||
}
|
||||
|
||||
msg.Write((byte)ClientNetObject.ENTITY_STATE);
|
||||
Write(msg, eventsToSync);
|
||||
}
|
||||
|
||||
private UInt16? firstNewID;
|
||||
|
||||
/// <summary>
|
||||
/// Read the events from the message, ignoring ones we've already received
|
||||
/// </summary>
|
||||
public void Read(ServerNetObject type, NetIncomingMessage msg, float sendingTime)
|
||||
{
|
||||
UInt16 unreceivedEntityEventCount = 0;
|
||||
|
||||
if (type == ServerNetObject.ENTITY_EVENT_INITIAL)
|
||||
{
|
||||
unreceivedEntityEventCount = msg.ReadUInt16();
|
||||
firstNewID = msg.ReadUInt16();
|
||||
|
||||
if (GameSettings.VerboseLogging)
|
||||
{
|
||||
DebugConsole.NewMessage(
|
||||
"received midround syncing msg, unreceived: " + unreceivedEntityEventCount +
|
||||
", first new ID: " + firstNewID, Microsoft.Xna.Framework.Color.Yellow);
|
||||
}
|
||||
}
|
||||
else if (firstNewID != null)
|
||||
{
|
||||
if (GameSettings.VerboseLogging)
|
||||
{
|
||||
DebugConsole.NewMessage("midround syncing complete, switching to ID " + (UInt16) (firstNewID - 1),
|
||||
Microsoft.Xna.Framework.Color.Yellow);
|
||||
}
|
||||
|
||||
lastReceivedID = (UInt16)(firstNewID - 1);
|
||||
firstNewID = 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 && thisEventID == (UInt16)(lastReceivedID + 1))
|
||||
{
|
||||
msg.ReadPadBits();
|
||||
lastReceivedID++;
|
||||
continue;
|
||||
}
|
||||
|
||||
byte msgLength = msg.ReadByte();
|
||||
|
||||
IServerSerializable entity = Entity.FindEntityByID(entityID) as IServerSerializable;
|
||||
|
||||
//skip the event if we've already received it or if the entity isn't found
|
||||
if (thisEventID != (UInt16)(lastReceivedID + 1) || entity == null)
|
||||
{
|
||||
if (GameSettings.VerboseLogging)
|
||||
{
|
||||
if (thisEventID != (UInt16) (lastReceivedID + 1))
|
||||
{
|
||||
DebugConsole.NewMessage(
|
||||
"received msg " + thisEventID + " (waiting for " + (lastReceivedID + 1) + ")",
|
||||
thisEventID < lastReceivedID + 1
|
||||
? Microsoft.Xna.Framework.Color.Yellow
|
||||
: 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
|
||||
{
|
||||
long msgPosition = msg.Position;
|
||||
if (GameSettings.VerboseLogging)
|
||||
{
|
||||
DebugConsole.NewMessage("received msg " + thisEventID + " (" + entity.ToString() + ")",
|
||||
Microsoft.Xna.Framework.Color.Green);
|
||||
}
|
||||
lastReceivedID++;
|
||||
try
|
||||
{
|
||||
ReadEvent(msg, entity, sendingTime);
|
||||
}
|
||||
|
||||
catch (Exception e)
|
||||
{
|
||||
if (GameSettings.VerboseLogging)
|
||||
{
|
||||
DebugConsole.ThrowError("Failed to read event for entity \"" + entity.ToString() + "!", e);
|
||||
}
|
||||
msg.Position = msgPosition + msgLength * 8;
|
||||
}
|
||||
}
|
||||
msg.ReadPadBits();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void WriteEvent(NetBuffer buffer, NetEntityEvent entityEvent, Client recipient = null)
|
||||
{
|
||||
var clientEvent = entityEvent as ClientEntityEvent;
|
||||
if (clientEvent == null) return;
|
||||
|
||||
clientEvent.Write(buffer);
|
||||
}
|
||||
|
||||
protected void ReadEvent(NetIncomingMessage buffer, IServerSerializable entity, float sendingTime)
|
||||
{
|
||||
entity.ClientRead(ServerNetObject.ENTITY_EVENT, buffer, sendingTime);
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
ID = 0;
|
||||
|
||||
lastReceivedID = 0;
|
||||
|
||||
firstNewID = null;
|
||||
|
||||
events.Clear();
|
||||
eventLastSent.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
using Lidgren.Network;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Barotrauma.Networking
|
||||
{
|
||||
abstract class NetEntityEvent
|
||||
{
|
||||
public enum Type
|
||||
{
|
||||
ComponentState,
|
||||
InventoryState,
|
||||
Status,
|
||||
Repair,
|
||||
ApplyStatusEffect,
|
||||
ChangeProperty
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
private double createTime;
|
||||
public double CreateTime
|
||||
{
|
||||
get { return createTime; }
|
||||
}
|
||||
|
||||
public ServerEntityEvent(IServerSerializable entity, UInt16 id)
|
||||
: base(entity, id)
|
||||
{
|
||||
serializable = entity;
|
||||
|
||||
createTime = Timing.TotalTime;
|
||||
}
|
||||
|
||||
public void Write(NetBuffer msg, Client recipient)
|
||||
{
|
||||
serializable.ServerWrite(msg, recipient, Data);
|
||||
}
|
||||
}
|
||||
|
||||
class ClientEntityEvent : NetEntityEvent
|
||||
{
|
||||
private IClientSerializable serializable;
|
||||
|
||||
public UInt16 CharacterStateID;
|
||||
|
||||
public ClientEntityEvent(IClientSerializable entity, UInt16 id)
|
||||
: base(entity, id)
|
||||
{
|
||||
serializable = entity;
|
||||
}
|
||||
|
||||
public void Write(NetBuffer msg)
|
||||
{
|
||||
msg.Write(CharacterStateID);
|
||||
serializable.ClientWrite(msg, Data);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
using Lidgren.Network;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Barotrauma.Networking
|
||||
{
|
||||
abstract class NetEntityEventManager
|
||||
{
|
||||
public const int MaxEventBufferLength = 1024;
|
||||
public const int MaxEventsPerWrite = 64;
|
||||
|
||||
//public UInt16 LastReceivedEntityEventID
|
||||
//{
|
||||
// get { return lastReceivedEntityEventID; }
|
||||
//}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
msg.Write(eventsToSync[0].ID);
|
||||
msg.Write((byte)eventsToSync.Count);
|
||||
|
||||
foreach (NetEntityEvent e in eventsToSync)
|
||||
{
|
||||
//write into a temporary buffer so we can write the length before the actual data
|
||||
NetBuffer tempBuffer = new NetBuffer();
|
||||
try
|
||||
{
|
||||
WriteEvent(tempBuffer, e, recipient);
|
||||
}
|
||||
|
||||
catch (Exception exception)
|
||||
{
|
||||
DebugConsole.ThrowError("Failed to write an event for the entity \""+e.Entity+"\"", exception);
|
||||
continue;
|
||||
}
|
||||
|
||||
Debug.Assert(
|
||||
tempBuffer.LengthBytes < 128,
|
||||
"Maximum EntityEvent size exceeded when serializing \""+e.Entity+"\"!");
|
||||
|
||||
//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
|
||||
msg.Write((UInt16)0);
|
||||
msg.WritePadBits();
|
||||
}
|
||||
else
|
||||
{
|
||||
msg.Write((UInt16)e.Entity.ID);
|
||||
msg.Write((byte)tempBuffer.LengthBytes);
|
||||
msg.Write(tempBuffer);
|
||||
msg.WritePadBits();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void WriteEvent(NetBuffer buffer, NetEntityEvent entityEvent, Client recipient = null)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,399 @@
|
||||
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 => 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 => 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);
|
||||
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,177 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using Microsoft.Xna.Framework.Graphics;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Barotrauma.Networking
|
||||
{
|
||||
class NetStats
|
||||
{
|
||||
public enum NetStatType
|
||||
{
|
||||
SentBytes = 0,
|
||||
ReceivedBytes = 1,
|
||||
ResentMessages = 2
|
||||
}
|
||||
|
||||
private Graph[] graphs;
|
||||
|
||||
private float[] totalValue;
|
||||
private float[] lastValue;
|
||||
|
||||
const float UpdateInterval = 0.1f;
|
||||
float updateTimer;
|
||||
|
||||
public NetStats()
|
||||
{
|
||||
graphs = new Graph[3];
|
||||
|
||||
totalValue = new float[3];
|
||||
lastValue = new float[3];
|
||||
for (int i = 0; i < 3; i++ )
|
||||
{
|
||||
graphs[i] = new Graph();
|
||||
}
|
||||
}
|
||||
|
||||
public void AddValue(NetStatType statType, float value)
|
||||
{
|
||||
float valueChange = value - lastValue[(int)statType];
|
||||
|
||||
totalValue[(int)statType] += valueChange;
|
||||
|
||||
lastValue[(int)statType] = value;
|
||||
}
|
||||
|
||||
public void Update(float deltaTime)
|
||||
{
|
||||
updateTimer -= deltaTime;
|
||||
|
||||
if (updateTimer > 0.0f) return;
|
||||
|
||||
for (int i = 0; i<3; i++)
|
||||
{
|
||||
|
||||
graphs[i].Update(totalValue[i] / UpdateInterval);
|
||||
totalValue[i] = 0.0f;
|
||||
}
|
||||
|
||||
updateTimer = UpdateInterval;
|
||||
}
|
||||
|
||||
public void Draw(SpriteBatch spriteBatch, Rectangle rect, GameServer server)
|
||||
{
|
||||
GUI.DrawRectangle(spriteBatch, rect, Color.Black*0.4f, true);
|
||||
|
||||
graphs[(int)NetStatType.ReceivedBytes].Draw(spriteBatch, rect, null, 0.0f, Color.Cyan);
|
||||
|
||||
graphs[(int)NetStatType.SentBytes].Draw(spriteBatch, rect, null, 0.0f, Color.Orange);
|
||||
|
||||
graphs[(int)NetStatType.ResentMessages].Draw(spriteBatch, rect, null, 0.0f, Color.Red);
|
||||
|
||||
GUI.SmallFont.DrawString(spriteBatch,
|
||||
"Peak received: " + MathUtils.GetBytesReadable((int)graphs[(int)NetStatType.ReceivedBytes].LargestValue()) + "/s " +
|
||||
"Avg received: " + MathUtils.GetBytesReadable((int)graphs[(int)NetStatType.ReceivedBytes].Average()) + "/s",
|
||||
new Vector2(rect.X + 10, rect.Y + 10), Color.Cyan);
|
||||
|
||||
|
||||
GUI.SmallFont.DrawString(spriteBatch, "Peak sent: " + MathUtils.GetBytesReadable((int)graphs[(int)NetStatType.SentBytes].LargestValue()) + "/s " +
|
||||
"Avg sent: " + MathUtils.GetBytesReadable((int)graphs[(int)NetStatType.SentBytes].Average()) + "/s",
|
||||
new Vector2(rect.X + 10, rect.Y + 30), Color.Orange);
|
||||
|
||||
GUI.SmallFont.DrawString(spriteBatch, "Peak resent: " + graphs[(int)NetStatType.ResentMessages].LargestValue() + " messages/s",
|
||||
new Vector2(rect.X + 10, rect.Y + 50), Color.Red);
|
||||
#if DEBUG
|
||||
int y = 10;
|
||||
|
||||
foreach (KeyValuePair<string, long> msgBytesSent in server.messageCount.OrderBy(key => -key.Value))
|
||||
{
|
||||
GUI.SmallFont.DrawString(spriteBatch, msgBytesSent.Key + ": " + MathUtils.GetBytesReadable(msgBytesSent.Value),
|
||||
new Vector2(rect.Right - 200, rect.Y + y), Color.Red);
|
||||
|
||||
y += 15;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
class Graph
|
||||
{
|
||||
public const int ArraySize = 100;
|
||||
|
||||
private float[] values;
|
||||
|
||||
public Graph()
|
||||
{
|
||||
values = new float[ArraySize];
|
||||
}
|
||||
|
||||
public float LargestValue()
|
||||
{
|
||||
float maxValue = 0.0f;
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
if (values[i] > maxValue) maxValue = values[i];
|
||||
}
|
||||
return maxValue;
|
||||
}
|
||||
|
||||
public float Average()
|
||||
{
|
||||
return values.Length == 0 ? 0.0f : values.Average();
|
||||
}
|
||||
|
||||
public void Update(float newValue)
|
||||
{
|
||||
for (int i = values.Length-1; i > 0; i--)
|
||||
{
|
||||
values[i] = values[i - 1];
|
||||
}
|
||||
values[0] = newValue;
|
||||
}
|
||||
|
||||
public void Draw(SpriteBatch spriteBatch, Rectangle rect, float? maxVal, float xOffset, Color color)
|
||||
{
|
||||
float graphMaxVal = 1.0f;
|
||||
if (maxVal == null)
|
||||
{
|
||||
graphMaxVal = LargestValue();
|
||||
}
|
||||
else if (maxVal > 0.0f)
|
||||
{
|
||||
graphMaxVal = (float)maxVal;
|
||||
}
|
||||
|
||||
GUI.DrawRectangle(spriteBatch, rect, Color.White);
|
||||
|
||||
if (values.Length == 0) return;
|
||||
|
||||
float lineWidth = (float)rect.Width / (float)(values.Length - 2);
|
||||
float yScale = (float)rect.Height / graphMaxVal;
|
||||
|
||||
Vector2 prevPoint = new Vector2(rect.Right, rect.Bottom - (values[1] + (values[0] - values[1]) * xOffset) * yScale);
|
||||
|
||||
float currX = rect.Right - ((xOffset - 1.0f) * lineWidth);
|
||||
|
||||
for (int i = 1; i < values.Length - 1; i++)
|
||||
{
|
||||
currX -= lineWidth;
|
||||
|
||||
Vector2 newPoint = new Vector2(currX, rect.Bottom - values[i] * yScale);
|
||||
|
||||
GUI.DrawLine(spriteBatch, prevPoint, newPoint - new Vector2(1.0f, 0), color);
|
||||
|
||||
prevPoint = newPoint;
|
||||
}
|
||||
|
||||
Vector2 lastPoint = new Vector2(rect.X,
|
||||
rect.Bottom - (values[values.Length - 1] + (values[values.Length - 2] - values[values.Length - 1]) * xOffset) * yScale);
|
||||
|
||||
GUI.DrawLine(spriteBatch, prevPoint, lastPoint, color);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,374 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Microsoft.Xna.Framework;
|
||||
using Microsoft.Xna.Framework.Graphics;
|
||||
using Microsoft.Xna.Framework.Input;
|
||||
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 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;
|
||||
|
||||
protected GUIFrame inGameHUD;
|
||||
protected GUIListBox chatBox;
|
||||
protected GUITextBox chatMsgBox;
|
||||
|
||||
public int EndVoteCount, EndVoteMax;
|
||||
//private GUITextBlock endVoteText;
|
||||
|
||||
public int Port;
|
||||
|
||||
protected bool gameStarted;
|
||||
|
||||
protected Character myCharacter;
|
||||
protected CharacterInfo characterInfo;
|
||||
|
||||
public Dictionary<string, bool> monsterEnabled;
|
||||
|
||||
protected RespawnManager respawnManager;
|
||||
|
||||
public Voting Voting;
|
||||
|
||||
public Character Character
|
||||
{
|
||||
get { return myCharacter; }
|
||||
set { myCharacter = value; }
|
||||
}
|
||||
|
||||
public CharacterInfo CharacterInfo
|
||||
{
|
||||
get { return characterInfo; }
|
||||
set { characterInfo = value; }
|
||||
}
|
||||
|
||||
public string Name
|
||||
{
|
||||
get { return name; }
|
||||
set
|
||||
{
|
||||
if (string.IsNullOrEmpty(name)) return;
|
||||
name = value;
|
||||
}
|
||||
}
|
||||
|
||||
public bool GameStarted
|
||||
{
|
||||
get { return gameStarted; }
|
||||
}
|
||||
|
||||
public GUIFrame InGameHUD
|
||||
{
|
||||
get { return inGameHUD; }
|
||||
}
|
||||
|
||||
public virtual List<Client> ConnectedClients
|
||||
{
|
||||
get { return null; }
|
||||
}
|
||||
|
||||
public NetworkMember()
|
||||
{
|
||||
inGameHUD = new GUIFrame(new Rectangle(0,0,0,0), null, null);
|
||||
inGameHUD.CanBeFocused = false;
|
||||
|
||||
int width = (int)MathHelper.Clamp(GameMain.GraphicsWidth * 0.35f, 350, 500);
|
||||
int height = (int)MathHelper.Clamp(GameMain.GraphicsHeight * 0.15f, 100, 200);
|
||||
chatBox = new GUIListBox(new Rectangle(
|
||||
GameMain.GraphicsWidth - 20 - width,
|
||||
GameMain.GraphicsHeight - 40 - 25 - height,
|
||||
width, height),
|
||||
Color.White * 0.5f, "", inGameHUD);
|
||||
chatBox.Padding = Vector4.Zero;
|
||||
|
||||
chatMsgBox = new GUITextBox(
|
||||
new Rectangle(chatBox.Rect.X, chatBox.Rect.Y + chatBox.Rect.Height + 20, chatBox.Rect.Width, 25),
|
||||
Color.White * 0.5f, Color.Black, Alignment.TopLeft, Alignment.Left, "", inGameHUD);
|
||||
chatMsgBox.Font = GUI.SmallFont;
|
||||
chatMsgBox.MaxTextLength = ChatMessage.MaxLength;
|
||||
chatMsgBox.Padding = Vector4.Zero;
|
||||
chatMsgBox.OnEnterPressed = EnterChatMessage;
|
||||
chatMsgBox.OnTextChanged = TypingChatMessage;
|
||||
|
||||
Voting = new Voting();
|
||||
}
|
||||
|
||||
public bool TypingChatMessage(GUITextBox textBox, string text)
|
||||
{
|
||||
string tempStr;
|
||||
string command = ChatMessage.GetChatMessageCommand(text, out tempStr);
|
||||
switch (command)
|
||||
{
|
||||
case "r":
|
||||
case "radio":
|
||||
textBox.TextColor = ChatMessage.MessageColor[(int)ChatMessageType.Radio];
|
||||
break;
|
||||
case "d":
|
||||
case "dead":
|
||||
textBox.TextColor = ChatMessage.MessageColor[(int)ChatMessageType.Dead];
|
||||
break;
|
||||
default:
|
||||
textBox.TextColor = ChatMessage.MessageColor[(int)ChatMessageType.Default];
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
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 bool EnterChatMessage(GUITextBox textBox, string message)
|
||||
{
|
||||
textBox.TextColor = ChatMessage.MessageColor[(int)ChatMessageType.Default];
|
||||
|
||||
if (string.IsNullOrWhiteSpace(message)) return false;
|
||||
|
||||
if (this == GameMain.Server)
|
||||
{
|
||||
GameMain.Server.SendChatMessage(message, null, null);
|
||||
}
|
||||
else if (this == GameMain.Client)
|
||||
{
|
||||
GameMain.Client.SendChatMessage(message);
|
||||
}
|
||||
|
||||
if (textBox == chatMsgBox) textBox.Deselect();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
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]);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
public virtual void KickPlayer(string kickedName, bool ban, bool range = false) { }
|
||||
|
||||
public virtual void AddToGUIUpdateList()
|
||||
{
|
||||
if (gameStarted && Screen.Selected == GameMain.GameScreen)
|
||||
{
|
||||
inGameHUD.AddToGUIUpdateList();
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void Update(float deltaTime)
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void Draw(Microsoft.Xna.Framework.Graphics.SpriteBatch spriteBatch)
|
||||
{
|
||||
if (!gameStarted || Screen.Selected != GameMain.GameScreen) return;
|
||||
|
||||
GameMain.GameSession.CrewManager.Draw(spriteBatch);
|
||||
|
||||
inGameHUD.Draw(spriteBatch);
|
||||
|
||||
if (EndVoteCount > 0)
|
||||
{
|
||||
if (GameMain.NetworkMember.myCharacter == null)
|
||||
{
|
||||
GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - 180.0f, 40),
|
||||
"Votes to end the round (y/n): " + EndVoteCount + "/" + (EndVoteMax - EndVoteCount), Color.White, null, 0, GUI.SmallFont);
|
||||
}
|
||||
else
|
||||
{
|
||||
GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - 140.0f, 40),
|
||||
"Votes (y/n): " + EndVoteCount + "/" + (EndVoteMax - EndVoteCount), Color.White, null, 0, GUI.SmallFont);
|
||||
}
|
||||
}
|
||||
|
||||
if (respawnManager != null)
|
||||
{
|
||||
string respawnInfo = "";
|
||||
|
||||
if (respawnManager.CurrentState == RespawnManager.State.Waiting &&
|
||||
respawnManager.CountdownStarted)
|
||||
{
|
||||
respawnInfo = respawnManager.RespawnTimer <= 0.0f ? "" : "Respawn Shuttle dispatching in " + ToolBox.SecondsToReadableTime(respawnManager.RespawnTimer);
|
||||
|
||||
}
|
||||
else if (respawnManager.CurrentState == RespawnManager.State.Transporting)
|
||||
{
|
||||
respawnInfo = respawnManager.TransportTimer <= 0.0f ? "" : "Shuttle leaving in " + ToolBox.SecondsToReadableTime(respawnManager.TransportTimer);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(respawnInfo))
|
||||
{
|
||||
GUI.DrawString(spriteBatch,
|
||||
new Vector2(120.0f, 10),
|
||||
respawnInfo, Color.White, null, 0, GUI.SmallFont);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public virtual bool SelectCrewCharacter(Character character, GUIComponent crewFrame)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public virtual void Disconnect() { }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,529 @@
|
||||
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 = i >= clients.Count;
|
||||
|
||||
var character = Character.Create(characterInfos[i], shuttleSpawnPoints[i].WorldPosition, !myCharacter, false);
|
||||
|
||||
character.TeamID = 1;
|
||||
|
||||
if (myCharacter)
|
||||
{
|
||||
server.Character = character;
|
||||
Character.Controlled = character;
|
||||
GameMain.LightManager.LosEnabled = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
clients[i].Character = character;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
character.GiveJobItems(mainSubSpawnPoints[i]);
|
||||
GameMain.GameSession.CrewManager.characters.Add(character);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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,262 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma.Networking
|
||||
{
|
||||
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;
|
||||
|
||||
public GUIFrame LogFrame;
|
||||
|
||||
private GUIListBox listBox;
|
||||
|
||||
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;
|
||||
|
||||
var newText = new LogMessage(line, messageType);
|
||||
|
||||
lines.Enqueue(newText);
|
||||
|
||||
if (LogFrame != null)
|
||||
{
|
||||
AddLine(newText);
|
||||
|
||||
listBox.UpdateScrollBarSize();
|
||||
}
|
||||
|
||||
unsavedLineCount++;
|
||||
|
||||
if (unsavedLineCount >= LinesPerFile)
|
||||
{
|
||||
Save();
|
||||
unsavedLineCount = 0;
|
||||
}
|
||||
|
||||
while (lines.Count > LinesPerFile)
|
||||
{
|
||||
lines.Dequeue();
|
||||
}
|
||||
|
||||
while (listBox != null && listBox.children.Count > LinesPerFile)
|
||||
{
|
||||
listBox.RemoveChild(listBox.children[0]);
|
||||
}
|
||||
}
|
||||
|
||||
public void CreateLogFrame()
|
||||
{
|
||||
LogFrame = new GUIFrame(new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight), Color.Black * 0.5f);
|
||||
|
||||
GUIFrame innerFrame = new GUIFrame(new Rectangle(0, 0, 600, 420), null, Alignment.Center, "", LogFrame);
|
||||
innerFrame.Padding = new Vector4(10.0f, 20.0f, 10.0f, 20.0f);
|
||||
|
||||
new GUITextBlock(new Rectangle(-200, 0, 100, 15), "Filter", "", Alignment.TopRight, Alignment.CenterRight, innerFrame, false, GUI.SmallFont);
|
||||
|
||||
GUITextBox searchBox = new GUITextBox(new Rectangle(-20, 0, 180, 15), Alignment.TopRight, "", innerFrame);
|
||||
searchBox.Font = GUI.SmallFont;
|
||||
searchBox.OnTextChanged = (textBox, text) =>
|
||||
{
|
||||
msgFilter = text;
|
||||
FilterMessages();
|
||||
return true;
|
||||
};
|
||||
GUIComponent.KeyboardDispatcher.Subscriber = searchBox;
|
||||
|
||||
var clearButton = new GUIButton(new Rectangle(0, 0, 15, 15), "x", Alignment.TopRight, "", innerFrame);
|
||||
clearButton.OnClicked = ClearFilter;
|
||||
clearButton.UserData = searchBox;
|
||||
|
||||
listBox = new GUIListBox(new Rectangle(0, 30, 450, 340), "", Alignment.TopRight, innerFrame);
|
||||
|
||||
int y = 30;
|
||||
foreach (MessageType msgType in Enum.GetValues(typeof(MessageType)))
|
||||
{
|
||||
var tickBox = new GUITickBox(new Rectangle(0, y, 20, 20), messageTypeName[(int)msgType], Alignment.TopLeft, GUI.SmallFont, innerFrame);
|
||||
tickBox.Selected = true;
|
||||
tickBox.TextColor = messageColor[(int)msgType];
|
||||
|
||||
tickBox.OnSelected += (GUITickBox tb) =>
|
||||
{
|
||||
msgTypeHidden[(int)msgType] = !tb.Selected;
|
||||
FilterMessages();
|
||||
return true;
|
||||
};
|
||||
|
||||
y += 20;
|
||||
}
|
||||
|
||||
var currLines = lines.ToList();
|
||||
|
||||
foreach (LogMessage line in currLines)
|
||||
{
|
||||
AddLine(line);
|
||||
}
|
||||
|
||||
listBox.UpdateScrollBarSize();
|
||||
|
||||
if (listBox.BarScroll == 0.0f || listBox.BarScroll == 1.0f) listBox.BarScroll = 1.0f;
|
||||
|
||||
GUIButton closeButton = new GUIButton(new Rectangle(-100, 10, 100, 15), "Close", Alignment.BottomRight, "", innerFrame);
|
||||
closeButton.OnClicked = (button, userData) =>
|
||||
{
|
||||
LogFrame = null;
|
||||
return true;
|
||||
};
|
||||
|
||||
msgFilter = "";
|
||||
}
|
||||
|
||||
private void AddLine(LogMessage line)
|
||||
{
|
||||
float prevSize = listBox.BarSize;
|
||||
|
||||
var textBlock = new GUITextBlock(new Rectangle(0, 0, 0, 0), line.Text, "", Alignment.TopLeft, Alignment.TopLeft, listBox, true, GUI.SmallFont);
|
||||
textBlock.Rect = new Rectangle(textBlock.Rect.X, textBlock.Rect.Y, textBlock.Rect.Width, Math.Max(13, textBlock.Rect.Height));
|
||||
textBlock.TextColor = messageColor[(int)line.Type];
|
||||
textBlock.CanBeFocused = false;
|
||||
textBlock.UserData = line;
|
||||
|
||||
if ((prevSize == 1.0f && listBox.BarScroll == 0.0f) || (prevSize < 1.0f && listBox.BarScroll == 1.0f)) listBox.BarScroll = 1.0f;
|
||||
}
|
||||
|
||||
private bool FilterMessages()
|
||||
{
|
||||
string filter = msgFilter == null ? "" : msgFilter.ToLower();
|
||||
|
||||
foreach (GUIComponent child in listBox.children)
|
||||
{
|
||||
var textBlock = child as GUITextBlock;
|
||||
if (textBlock == null) continue;
|
||||
|
||||
child.Visible = true;
|
||||
|
||||
if (msgTypeHidden[(int)((LogMessage)child.UserData).Type])
|
||||
{
|
||||
child.Visible = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
textBlock.Visible = string.IsNullOrEmpty(filter) || textBlock.Text.ToLower().Contains(filter);
|
||||
}
|
||||
|
||||
listBox.BarScroll = 0.0f;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool ClearFilter(GUIComponent button, object obj)
|
||||
{
|
||||
var searchBox = button.UserData as GUITextBox;
|
||||
if (searchBox != null) searchBox.Text = "";
|
||||
|
||||
msgFilter = "";
|
||||
FilterMessages();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
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,330 @@
|
||||
using Barotrauma.Networking;
|
||||
using Lidgren.Network;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class Voting
|
||||
{
|
||||
private bool allowSubVoting, allowModeVoting;
|
||||
|
||||
public bool AllowVoteKick = true;
|
||||
|
||||
public bool AllowEndVoting = true;
|
||||
|
||||
public bool AllowSubVoting
|
||||
{
|
||||
get { return allowSubVoting; }
|
||||
set
|
||||
{
|
||||
if (value == allowSubVoting) return;
|
||||
allowSubVoting = value;
|
||||
GameMain.NetLobbyScreen.SubList.Enabled = value || GameMain.Server != null;
|
||||
GameMain.NetLobbyScreen.InfoFrame.FindChild("subvotes").Visible = value;
|
||||
|
||||
if (GameMain.Server != null)
|
||||
{
|
||||
UpdateVoteTexts(GameMain.Server.ConnectedClients, VoteType.Sub);
|
||||
GameMain.Server.UpdateVoteStatus();
|
||||
}
|
||||
else
|
||||
{
|
||||
GameMain.NetLobbyScreen.SubList.Deselect();
|
||||
}
|
||||
}
|
||||
}
|
||||
public bool AllowModeVoting
|
||||
{
|
||||
get { return allowModeVoting; }
|
||||
set
|
||||
{
|
||||
if (value == allowModeVoting) return;
|
||||
allowModeVoting = value;
|
||||
GameMain.NetLobbyScreen.ModeList.Enabled = value || GameMain.Server != null;
|
||||
GameMain.NetLobbyScreen.InfoFrame.FindChild("modevotes").Visible = value;
|
||||
if (GameMain.Server != null)
|
||||
{
|
||||
UpdateVoteTexts(GameMain.Server.ConnectedClients, VoteType.Mode);
|
||||
GameMain.Server.UpdateVoteStatus();
|
||||
}
|
||||
else
|
||||
{
|
||||
GameMain.NetLobbyScreen.ModeList.Deselect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateVoteTexts(List<Client> clients, VoteType voteType)
|
||||
{
|
||||
GUIListBox listBox = (voteType == VoteType.Sub) ?
|
||||
GameMain.NetLobbyScreen.SubList : GameMain.NetLobbyScreen.ModeList;
|
||||
|
||||
foreach (GUIComponent comp in listBox.children)
|
||||
{
|
||||
GUITextBlock voteText = comp.FindChild("votes") as GUITextBlock;
|
||||
if (voteText != null) comp.RemoveChild(voteText);
|
||||
}
|
||||
|
||||
List<Pair<object, int>> voteList = GetVoteList(voteType, clients);
|
||||
foreach (Pair<object, int> votable in voteList)
|
||||
{
|
||||
SetVoteText(listBox, votable.First, votable.Second);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetVoteText(GUIListBox listBox, object userData, int votes)
|
||||
{
|
||||
if (userData == null) return;
|
||||
foreach (GUIComponent comp in listBox.children)
|
||||
{
|
||||
if (comp.UserData != userData) continue;
|
||||
GUITextBlock voteText = comp.FindChild("votes") as GUITextBlock;
|
||||
if (voteText == null)
|
||||
{
|
||||
voteText = new GUITextBlock(new Rectangle(0, 0, 30, 0), "", "", Alignment.Right, Alignment.Right, comp);
|
||||
voteText.UserData = "votes";
|
||||
}
|
||||
|
||||
voteText.Text = votes == 0 ? "" : votes.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
UpdateVoteTexts(connectedClients, VoteType.Mode);
|
||||
UpdateVoteTexts(connectedClients, VoteType.Sub);
|
||||
}
|
||||
|
||||
public void ClientWrite(NetBuffer msg, VoteType voteType, object data)
|
||||
{
|
||||
if (GameMain.Server != null) return;
|
||||
|
||||
msg.Write((byte)voteType);
|
||||
|
||||
switch (voteType)
|
||||
{
|
||||
case VoteType.Sub:
|
||||
Submarine sub = data as Submarine;
|
||||
if (sub == null) return;
|
||||
|
||||
msg.Write(sub.Name);
|
||||
break;
|
||||
case VoteType.Mode:
|
||||
GameModePreset gameMode = data as GameModePreset;
|
||||
if (gameMode == null) return;
|
||||
|
||||
msg.Write(gameMode.Name);
|
||||
break;
|
||||
case VoteType.EndRound:
|
||||
if (!(data is bool)) return;
|
||||
|
||||
msg.Write((bool)data);
|
||||
break;
|
||||
case VoteType.Kick:
|
||||
Client votedClient = data as Client;
|
||||
if (votedClient == null) return;
|
||||
|
||||
msg.Write(votedClient.ID);
|
||||
break;
|
||||
}
|
||||
|
||||
msg.WritePadBits();
|
||||
}
|
||||
|
||||
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);
|
||||
UpdateVoteTexts(GameMain.Server.ConnectedClients, voteType);
|
||||
break;
|
||||
|
||||
case VoteType.Mode:
|
||||
string modeName = inc.ReadString();
|
||||
GameModePreset mode = GameModePreset.list.Find(gm => gm.Name == modeName);
|
||||
sender.SetVote(voteType, mode);
|
||||
UpdateVoteTexts(GameMain.Server.ConnectedClients, voteType);
|
||||
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();
|
||||
}
|
||||
|
||||
public void ClientRead(NetIncomingMessage inc)
|
||||
{
|
||||
if (GameMain.Server != null) return;
|
||||
|
||||
AllowSubVoting = inc.ReadBoolean();
|
||||
if (allowSubVoting)
|
||||
{
|
||||
foreach (Submarine sub in Submarine.SavedSubmarines)
|
||||
{
|
||||
SetVoteText(GameMain.NetLobbyScreen.SubList, sub, 0);
|
||||
}
|
||||
int votableCount = inc.ReadByte();
|
||||
for (int i = 0; i < votableCount; i++)
|
||||
{
|
||||
int votes = inc.ReadByte();
|
||||
string subName = inc.ReadString();
|
||||
Submarine sub = Submarine.SavedSubmarines.Find(sm => sm.Name == subName);
|
||||
SetVoteText(GameMain.NetLobbyScreen.SubList, sub, votes);
|
||||
}
|
||||
}
|
||||
AllowModeVoting = inc.ReadBoolean();
|
||||
if (allowModeVoting)
|
||||
{
|
||||
int votableCount = inc.ReadByte();
|
||||
for (int i = 0; i < votableCount; i++)
|
||||
{
|
||||
int votes = inc.ReadByte();
|
||||
string modeName = inc.ReadString();
|
||||
GameModePreset mode = GameModePreset.list.Find(m => m.Name == modeName);
|
||||
SetVoteText(GameMain.NetLobbyScreen.ModeList, mode, votes);
|
||||
}
|
||||
}
|
||||
AllowEndVoting = inc.ReadBoolean();
|
||||
if (AllowEndVoting)
|
||||
{
|
||||
GameMain.NetworkMember.EndVoteCount = inc.ReadByte();
|
||||
GameMain.NetworkMember.EndVoteMax = inc.ReadByte();
|
||||
}
|
||||
AllowVoteKick = inc.ReadBoolean();
|
||||
|
||||
inc.ReadPadBits();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
class WhiteList
|
||||
{
|
||||
const string SavePath = "Data/whitelist.txt";
|
||||
|
||||
private List<WhiteListedPlayer> whitelistedPlayers;
|
||||
public List<WhiteListedPlayer> WhiteListedPlayers
|
||||
{
|
||||
get { return whitelistedPlayers; }
|
||||
}
|
||||
|
||||
private GUIComponent whitelistFrame;
|
||||
|
||||
private GUITextBox nameBox;
|
||||
private GUITextBox ipBox;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public GUIComponent CreateWhiteListFrame(GUIComponent parent)
|
||||
{
|
||||
if (whitelistFrame!=null)
|
||||
{
|
||||
whitelistFrame.Parent.ClearChildren();
|
||||
whitelistFrame = null;
|
||||
}
|
||||
|
||||
parent.Padding = new Vector4(10.0f, 10.0f, 10.0f, 10.0f);
|
||||
|
||||
var enabledTick = new GUITickBox(new Rectangle(0, 0, 20, 20), "Enabled", Alignment.TopLeft, parent);
|
||||
enabledTick.Selected = Enabled;
|
||||
enabledTick.OnSelected = (GUITickBox box) =>
|
||||
{
|
||||
Enabled = !Enabled;
|
||||
|
||||
if (Enabled)
|
||||
{
|
||||
foreach (Client c in GameMain.Server.ConnectedClients)
|
||||
{
|
||||
if (!IsWhiteListed(c.name,c.Connection.RemoteEndPoint.Address.ToString()))
|
||||
{
|
||||
whitelistedPlayers.Add(new WhiteListedPlayer(c.name, c.Connection.RemoteEndPoint.Address.ToString()));
|
||||
if (whitelistFrame != null) CreateWhiteListFrame(whitelistFrame.Parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Save();
|
||||
return true;
|
||||
};
|
||||
|
||||
new GUITextBlock(new Rectangle(0, -35, 90, 20), "Name:", "", Alignment.BottomLeft, Alignment.CenterLeft, parent, false, GUI.Font);
|
||||
nameBox = new GUITextBox(new Rectangle(100, -35, 170, 20), Alignment.BottomLeft, "", parent);
|
||||
nameBox.Font = GUI.Font;
|
||||
|
||||
new GUITextBlock(new Rectangle(0, 0, 90, 20), "IP Address:", "", Alignment.BottomLeft, Alignment.CenterLeft, parent, false, GUI.Font);
|
||||
ipBox = new GUITextBox(new Rectangle(100, 0, 170, 20), Alignment.BottomLeft, "", parent);
|
||||
ipBox.Font = GUI.Font;
|
||||
|
||||
var addnewButton = new GUIButton(new Rectangle(0, 35, 150, 20), "Add to whitelist", Alignment.BottomLeft, "", parent);
|
||||
addnewButton.OnClicked = AddToWhiteList;
|
||||
|
||||
whitelistFrame = new GUIListBox(new Rectangle(0, 30, 0, parent.Rect.Height-110), "", parent);
|
||||
|
||||
foreach (WhiteListedPlayer wlp in whitelistedPlayers)
|
||||
{
|
||||
string blockText = wlp.Name;
|
||||
if (!string.IsNullOrWhiteSpace(wlp.IP)) blockText += " (" + wlp.IP + ")";
|
||||
GUITextBlock textBlock = new GUITextBlock(
|
||||
new Rectangle(0, 0, 0, 25),
|
||||
blockText,
|
||||
"",
|
||||
Alignment.Left, Alignment.Left, whitelistFrame);
|
||||
textBlock.Padding = new Vector4(10.0f, 10.0f, 0.0f, 0.0f);
|
||||
textBlock.UserData = wlp;
|
||||
|
||||
var removeButton = new GUIButton(new Rectangle(0, 0, 100, 20), "Remove", Alignment.Right | Alignment.CenterY, "", textBlock);
|
||||
removeButton.UserData = wlp;
|
||||
removeButton.OnClicked = RemoveFromWhiteList;
|
||||
}
|
||||
|
||||
return parent;
|
||||
}
|
||||
|
||||
private bool RemoveFromWhiteList(GUIButton button, object obj)
|
||||
{
|
||||
WhiteListedPlayer wlp = obj as WhiteListedPlayer;
|
||||
if (wlp == null) return false;
|
||||
|
||||
DebugConsole.Log("Removing " + wlp.Name + " from whitelist");
|
||||
GameServer.Log("Removing " + wlp.Name + " from whitelist", ServerLog.MessageType.ServerMessage);
|
||||
|
||||
whitelistedPlayers.Remove(wlp);
|
||||
Save();
|
||||
|
||||
if (whitelistFrame != null)
|
||||
{
|
||||
whitelistFrame.Parent.ClearChildren();
|
||||
CreateWhiteListFrame(whitelistFrame.Parent);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool AddToWhiteList(GUIButton button, object obj)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(nameBox.Text)) return false;
|
||||
if (whitelistedPlayers.Any(x => x.Name.ToLower() == nameBox.Text.ToLower() && x.IP == ipBox.Text)) return false;
|
||||
whitelistedPlayers.Add(new WhiteListedPlayer(nameBox.Text,ipBox.Text));
|
||||
Save();
|
||||
|
||||
if (whitelistFrame != null)
|
||||
{
|
||||
CreateWhiteListFrame(whitelistFrame.Parent);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
using Lidgren.Network;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Xml;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma.Networking
|
||||
{
|
||||
class FileStreamReceiver : IDisposable
|
||||
{
|
||||
const int MaxFileSize = 1000000;
|
||||
|
||||
public delegate void OnFinished(FileStreamReceiver fileStreamReceiver);
|
||||
private OnFinished onFinished;
|
||||
|
||||
private NetClient client;
|
||||
private ulong length;
|
||||
private ulong received;
|
||||
private FileStream writeStream;
|
||||
private int timeStarted;
|
||||
|
||||
private string downloadFolder;
|
||||
|
||||
private FileTransferMessageType fileType;
|
||||
|
||||
public string FileName
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public string FilePath
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public ulong FileSize
|
||||
{
|
||||
get { return length; }
|
||||
}
|
||||
|
||||
public ulong Received
|
||||
{
|
||||
get { return received; }
|
||||
}
|
||||
|
||||
public FileTransferMessageType FileType
|
||||
{
|
||||
get { return fileType; }
|
||||
}
|
||||
|
||||
public FileTransferStatus Status
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public string ErrorMessage
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public float BytesPerSecond
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public float Progress
|
||||
{
|
||||
get { return (float)received / (float)length; }
|
||||
}
|
||||
|
||||
public FileStreamReceiver(NetClient client, string filePath, FileTransferMessageType fileType, OnFinished onFinished)
|
||||
{
|
||||
this.client = client;
|
||||
|
||||
this.downloadFolder = filePath;
|
||||
this.fileType = fileType;
|
||||
|
||||
this.onFinished = onFinished;
|
||||
|
||||
Status = FileTransferStatus.NotStarted;
|
||||
}
|
||||
|
||||
public void ReadMessage(NetIncomingMessage inc)
|
||||
{
|
||||
try
|
||||
{
|
||||
TryReadMessage(inc);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ErrorMessage = "Error while receiving file ''"+FileName+"'' {"+e.Message+"}";
|
||||
DeleteFile();
|
||||
|
||||
if (onFinished != null) onFinished(this);
|
||||
}
|
||||
}
|
||||
|
||||
private bool ValidateInitialData(byte type, string fileName, ulong fileSize)
|
||||
{
|
||||
if (fileSize > MaxFileSize)
|
||||
{
|
||||
ErrorMessage = "File too large (" + MathUtils.GetBytesReadable((long)fileSize) + ")";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (type != (byte)fileType)
|
||||
{
|
||||
ErrorMessage = "Unexpected file type ''" + type + "'' (expected " + fileType + ")";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Regex.Match(fileName, @"^[\w\- ]+[\w\-. ]*$").Success)
|
||||
{
|
||||
ErrorMessage = "Illegal characters in file name ''" + fileName + "''";
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case (byte)FileTransferMessageType.Submarine:
|
||||
if (Path.GetExtension(fileName) != ".sub")
|
||||
{
|
||||
ErrorMessage = "Wrong file extension ''" + Path.GetExtension(fileName) + "''! (Expected .sub)";
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void DeleteFile()
|
||||
{
|
||||
if (FileName == null) return;
|
||||
|
||||
string file = Path.Combine(downloadFolder, FileName);
|
||||
|
||||
if (writeStream!=null)
|
||||
{
|
||||
writeStream.Flush();
|
||||
writeStream.Close();
|
||||
writeStream.Dispose();
|
||||
writeStream = null;
|
||||
}
|
||||
|
||||
Status = FileTransferStatus.Canceled;
|
||||
|
||||
if (File.Exists(file))
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(file);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugConsole.ThrowError("Couldn't delete file ''" + file + "''!", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void TryReadMessage(NetIncomingMessage inc)
|
||||
{
|
||||
if (Status == FileTransferStatus.Error ||
|
||||
Status == FileTransferStatus.Finished ||
|
||||
Status == FileTransferStatus.Canceled) return;
|
||||
|
||||
byte transferMessageType = inc.ReadByte();
|
||||
|
||||
//int chunkLen = inc.LengthBytes;
|
||||
if (length == 0)
|
||||
{
|
||||
if (transferMessageType != (byte)FileTransferMessageType.Initiate) return;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(downloadFolder) && !Directory.Exists(downloadFolder))
|
||||
{
|
||||
Directory.CreateDirectory(downloadFolder);
|
||||
}
|
||||
|
||||
byte fileTypeByte = inc.ReadByte();
|
||||
|
||||
|
||||
length = inc.ReadUInt64();
|
||||
FileName = inc.ReadString();
|
||||
|
||||
if (!ValidateInitialData(fileTypeByte, FileName, length))
|
||||
{
|
||||
Status = FileTransferStatus.Error;
|
||||
DeleteFile();
|
||||
if (onFinished != null) onFinished(this);
|
||||
return;
|
||||
}
|
||||
|
||||
FilePath = Path.Combine(downloadFolder, FileName);
|
||||
|
||||
writeStream = new FileStream(FilePath, FileMode.Create, FileAccess.Write, FileShare.None);
|
||||
timeStarted = Environment.TickCount;
|
||||
|
||||
Status = FileTransferStatus.NotStarted;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (received + (ulong)inc.LengthBytes > length*1.1f)
|
||||
{
|
||||
ErrorMessage = "Receiving more data than expected (> " + MathUtils.GetBytesReadable((long)(received + (ulong)inc.LengthBytes)) + ")";
|
||||
Status = FileTransferStatus.Error;
|
||||
if (onFinished != null) onFinished(this);
|
||||
return;
|
||||
}
|
||||
|
||||
byte[] all = inc.ReadBytes(inc.LengthBytes - inc.PositionInBytes);
|
||||
received += (ulong)all.Length;
|
||||
writeStream.Write(all, 0, all.Length);
|
||||
|
||||
int passed = Environment.TickCount - timeStarted;
|
||||
float psec = passed / 1000.0f;
|
||||
|
||||
BytesPerSecond = received / psec;
|
||||
|
||||
Status = FileTransferStatus.Receiving;
|
||||
|
||||
|
||||
if (received >= length)
|
||||
{
|
||||
writeStream.Flush();
|
||||
writeStream.Close();
|
||||
writeStream.Dispose();
|
||||
writeStream = null;
|
||||
|
||||
Status = IsReceivedFileValid() ? FileTransferStatus.Finished : FileTransferStatus.Error;
|
||||
if (onFinished!=null) onFinished(this);
|
||||
|
||||
if (Status == FileTransferStatus.Error) DeleteFile();
|
||||
Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsReceivedFileValid()
|
||||
{
|
||||
switch (fileType)
|
||||
{
|
||||
case FileTransferMessageType.Submarine:
|
||||
string file = Path.Combine(downloadFolder, FileName);
|
||||
Stream stream = null;
|
||||
|
||||
try
|
||||
{
|
||||
stream = SaveUtil.DecompressFiletoStream(file);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ErrorMessage = "Loading submarine ''" + file + "'' failed! {"+ e.Message + "}";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (stream == null)
|
||||
{
|
||||
ErrorMessage = "Decompressing submarine file''" + file + "'' failed!";
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
stream.Position = 0;
|
||||
|
||||
XmlReaderSettings settings = new XmlReaderSettings();
|
||||
settings.DtdProcessing = DtdProcessing.Prohibit;
|
||||
settings.IgnoreProcessingInstructions = true;
|
||||
|
||||
using (var reader = XmlReader.Create(stream, settings))
|
||||
{
|
||||
while (reader.Read())
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
stream.Close();
|
||||
stream.Dispose();
|
||||
|
||||
ErrorMessage = "Parsing file ''"+file+"'' failed! The file may not be a valid submarine file.";
|
||||
return false;
|
||||
}
|
||||
|
||||
stream.Close();
|
||||
stream.Dispose();
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (writeStream != null)
|
||||
{
|
||||
writeStream.Flush();
|
||||
writeStream.Close();
|
||||
writeStream.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
using Lidgren.Network;
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace Barotrauma.Networking
|
||||
{
|
||||
enum FileTransferStatus
|
||||
{
|
||||
NotStarted, Sending, Receiving, Finished, Error, Canceled
|
||||
}
|
||||
|
||||
enum FileTransferMessageType
|
||||
{
|
||||
Unknown, Initiate, Submarine, Cancel
|
||||
}
|
||||
|
||||
class FileStreamSender : IDisposable
|
||||
{
|
||||
public static TimeSpan MaxTransferDuration = new TimeSpan(0, 2, 0);
|
||||
|
||||
private FileStream inputStream;
|
||||
private int sentOffset;
|
||||
private int chunkLen;
|
||||
private byte[] tempBuffer;
|
||||
private NetConnection connection;
|
||||
|
||||
float waitTimer;
|
||||
|
||||
DateTime startingTime;
|
||||
|
||||
private FileTransferMessageType fileType;
|
||||
|
||||
public FileTransferStatus Status
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public string FileName
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public string FilePath
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public float Progress
|
||||
{
|
||||
get { return inputStream == null ? 0.0f : (float)sentOffset / (float)inputStream.Length; }
|
||||
}
|
||||
|
||||
public int Sent
|
||||
{
|
||||
get { return sentOffset; }
|
||||
}
|
||||
|
||||
public long FileSize
|
||||
{
|
||||
get { return inputStream == null ? 0 : inputStream.Length; }
|
||||
}
|
||||
|
||||
public static FileStreamSender Create(NetConnection conn, string filePath, FileTransferMessageType fileType)
|
||||
{
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
DebugConsole.ThrowError("Sending a file failed. File ''"+filePath+"'' not found.");
|
||||
return null;
|
||||
}
|
||||
|
||||
FileStreamSender sender = null;
|
||||
|
||||
try
|
||||
{
|
||||
sender = new FileStreamSender(conn, filePath, fileType);
|
||||
}
|
||||
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugConsole.ThrowError("Couldn't open file ''"+filePath+"''",e);
|
||||
}
|
||||
|
||||
return sender;
|
||||
}
|
||||
|
||||
private FileStreamSender(NetConnection conn, string filePath, FileTransferMessageType fileType)
|
||||
{
|
||||
connection = conn;
|
||||
inputStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
chunkLen = connection.Peer.Configuration.MaximumTransmissionUnit - 100;
|
||||
tempBuffer = new byte[chunkLen];
|
||||
sentOffset = 0;
|
||||
|
||||
FilePath = filePath;
|
||||
FileName = Path.GetFileName(filePath);
|
||||
|
||||
this.fileType = fileType;
|
||||
|
||||
Status = FileTransferStatus.NotStarted;
|
||||
|
||||
startingTime = DateTime.Now;
|
||||
}
|
||||
|
||||
public void Update(float deltaTime)
|
||||
{
|
||||
if (inputStream == null ||
|
||||
Status == FileTransferStatus.Canceled ||
|
||||
Status == FileTransferStatus.Error ||
|
||||
Status == FileTransferStatus.Finished) return;
|
||||
|
||||
if (DateTime.Now > startingTime + MaxTransferDuration)
|
||||
{
|
||||
CancelTransfer();
|
||||
return;
|
||||
}
|
||||
|
||||
waitTimer -= deltaTime;
|
||||
if (waitTimer > 0.0f) return;
|
||||
|
||||
if (!connection.CanSendImmediately(NetDeliveryMethod.ReliableOrdered, 1)) return;
|
||||
|
||||
// send another part of the file!
|
||||
long remaining = inputStream.Length - sentOffset;
|
||||
int sendBytes = (remaining > chunkLen ? chunkLen : (int)remaining);
|
||||
|
||||
// just assume we can read the whole thing in one Read()
|
||||
inputStream.Read(tempBuffer, 0, sendBytes);
|
||||
|
||||
NetOutgoingMessage message;
|
||||
if (sentOffset == 0)
|
||||
{
|
||||
// first message; send length, chunk length and file name
|
||||
message = connection.Peer.CreateMessage(sendBytes + 8 + 1);
|
||||
message.Write((byte)PacketTypes.FileStream);
|
||||
message.Write((byte)FileTransferMessageType.Initiate);
|
||||
message.Write((byte)fileType);
|
||||
message.Write((ulong)inputStream.Length);
|
||||
message.Write(Path.GetFileName(inputStream.Name));
|
||||
connection.SendMessage(message, NetDeliveryMethod.ReliableOrdered, 1);
|
||||
|
||||
Status = FileTransferStatus.Sending;
|
||||
}
|
||||
|
||||
message = connection.Peer.CreateMessage(sendBytes + 8 + 1);
|
||||
message.Write((byte)PacketTypes.FileStream);
|
||||
message.Write((byte)fileType);
|
||||
message.Write(tempBuffer, 0, sendBytes);
|
||||
|
||||
connection.SendMessage(message, NetDeliveryMethod.ReliableOrdered, 1);
|
||||
sentOffset += sendBytes;
|
||||
|
||||
waitTimer = connection.AverageRoundtripTime;
|
||||
|
||||
//Program.Output("Sent " + m_sentOffset + "/" + m_inputStream.Length + " bytes to " + m_connection);
|
||||
|
||||
if (remaining - sendBytes <= 0)
|
||||
{
|
||||
//Dispose();
|
||||
|
||||
Status = FileTransferStatus.Finished;
|
||||
}
|
||||
}
|
||||
|
||||
public void CancelTransfer()
|
||||
{
|
||||
Status = FileTransferStatus.Canceled;
|
||||
}
|
||||
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
inputStream.Close();
|
||||
inputStream.Dispose();
|
||||
inputStream = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
using System.Collections.Generic;
|
||||
using Lidgren.Network;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma.Networking
|
||||
{
|
||||
enum NetworkEventDeliveryMethod
|
||||
{
|
||||
Unreliable = 0,
|
||||
ReliableChannel = 1,
|
||||
ReliableLidgren = 2
|
||||
}
|
||||
|
||||
enum NetworkEventType
|
||||
{
|
||||
EntityUpdate = 0,
|
||||
ImportantEntityUpdate = 1,
|
||||
|
||||
KillCharacter = 2,
|
||||
SelectCharacter = 3,
|
||||
|
||||
ComponentUpdate = 4,
|
||||
ImportantComponentUpdate = 5,
|
||||
|
||||
PickItem = 6,
|
||||
DropItem = 7,
|
||||
InventoryUpdate = 8,
|
||||
ItemFixed = 9,
|
||||
|
||||
UpdateProperty = 10,
|
||||
WallDamage = 11,
|
||||
|
||||
PhysicsBodyPosition = 12,
|
||||
|
||||
ApplyStatusEffect = 13
|
||||
}
|
||||
|
||||
class NetworkEvent
|
||||
{
|
||||
public static List<NetworkEvent> Events = new List<NetworkEvent>();
|
||||
|
||||
private static NetworkEventDeliveryMethod[] deliveryMethod;
|
||||
private static bool[] overridePrevious;
|
||||
|
||||
static NetworkEvent()
|
||||
{
|
||||
deliveryMethod = new NetworkEventDeliveryMethod[Enum.GetNames(typeof(NetworkEventType)).Length];
|
||||
deliveryMethod[(int)NetworkEventType.ImportantEntityUpdate] = NetworkEventDeliveryMethod.ReliableChannel;
|
||||
deliveryMethod[(int)NetworkEventType.ImportantComponentUpdate] = NetworkEventDeliveryMethod.ReliableChannel;
|
||||
deliveryMethod[(int)NetworkEventType.KillCharacter] = NetworkEventDeliveryMethod.ReliableLidgren;
|
||||
deliveryMethod[(int)NetworkEventType.SelectCharacter] = NetworkEventDeliveryMethod.ReliableChannel;
|
||||
|
||||
deliveryMethod[(int)NetworkEventType.ImportantComponentUpdate] = NetworkEventDeliveryMethod.ReliableChannel;
|
||||
deliveryMethod[(int)NetworkEventType.PickItem] = NetworkEventDeliveryMethod.ReliableChannel;
|
||||
deliveryMethod[(int)NetworkEventType.DropItem] = NetworkEventDeliveryMethod.ReliableChannel;
|
||||
deliveryMethod[(int)NetworkEventType.InventoryUpdate] = NetworkEventDeliveryMethod.ReliableChannel;
|
||||
deliveryMethod[(int)NetworkEventType.ItemFixed] = NetworkEventDeliveryMethod.ReliableLidgren;
|
||||
|
||||
deliveryMethod[(int)NetworkEventType.UpdateProperty] = NetworkEventDeliveryMethod.ReliableChannel;
|
||||
deliveryMethod[(int)NetworkEventType.WallDamage] = NetworkEventDeliveryMethod.ReliableChannel;
|
||||
|
||||
deliveryMethod[(int)NetworkEventType.ApplyStatusEffect] = NetworkEventDeliveryMethod.ReliableLidgren;
|
||||
|
||||
overridePrevious = new bool[deliveryMethod.Length];
|
||||
for (int i = 0; i < overridePrevious.Length; i++ )
|
||||
{
|
||||
overridePrevious[i] = true;
|
||||
}
|
||||
overridePrevious[(int)NetworkEventType.KillCharacter] = false;
|
||||
|
||||
overridePrevious[(int)NetworkEventType.PickItem] = false;
|
||||
overridePrevious[(int)NetworkEventType.DropItem] = false;
|
||||
overridePrevious[(int)NetworkEventType.ItemFixed] = false;
|
||||
overridePrevious[(int)NetworkEventType.ApplyStatusEffect] = false;
|
||||
}
|
||||
|
||||
private readonly ushort id;
|
||||
|
||||
private readonly NetworkEventType eventType;
|
||||
|
||||
private readonly bool isClientEvent;
|
||||
|
||||
private readonly object data;
|
||||
|
||||
public NetConnection SenderConnection;
|
||||
|
||||
//private NetOutgoingMessage message;
|
||||
|
||||
public ushort ID
|
||||
{
|
||||
get { return id; }
|
||||
}
|
||||
|
||||
public bool IsClient
|
||||
{
|
||||
get { return isClientEvent; }
|
||||
}
|
||||
|
||||
public NetworkEventDeliveryMethod DeliveryMethod
|
||||
{
|
||||
get { return deliveryMethod[(int)eventType]; }
|
||||
}
|
||||
|
||||
public NetworkEventType Type
|
||||
{
|
||||
get { return eventType; }
|
||||
}
|
||||
|
||||
public NetworkEvent(ushort id, bool allowClientSend)
|
||||
: this(NetworkEventType.EntityUpdate, id, allowClientSend)
|
||||
{
|
||||
}
|
||||
|
||||
public NetworkEvent(NetworkEventType type, ushort id, bool allowClientSend, object data = null)
|
||||
{
|
||||
if (!allowClientSend && GameMain.Server == null) return;
|
||||
|
||||
eventType = type;
|
||||
|
||||
if (overridePrevious[(int)type])
|
||||
{
|
||||
if (type == NetworkEventType.ComponentUpdate || type == NetworkEventType.ImportantComponentUpdate)
|
||||
{
|
||||
if (Events.Any(e => e.id == id && e.eventType == type && data == e.data)) return;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Events.Any(e => e.id == id && e.eventType == type)) return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
this.id = id;
|
||||
isClientEvent = allowClientSend;
|
||||
|
||||
this.data = data;
|
||||
|
||||
Events.Add(this);
|
||||
}
|
||||
|
||||
public bool FillData(NetBuffer message)
|
||||
{
|
||||
message.Write((byte)eventType);
|
||||
|
||||
Entity e = Entity.FindEntityByID(id);
|
||||
if (e == null) return false;
|
||||
|
||||
message.Write(id);
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
catch (Exception exception)
|
||||
{
|
||||
#if DEBUG
|
||||
DebugConsole.ThrowError("Failed to write network message for entity "+e.ToString(), exception);
|
||||
#endif
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void ReadMessage(NetIncomingMessage message, bool resend=false)
|
||||
{
|
||||
float sendingTime = message.ReadFloat();
|
||||
|
||||
sendingTime = (float)message.SenderConnection.GetLocalTime(sendingTime);
|
||||
|
||||
byte msgCount = message.ReadByte();
|
||||
long currPos = message.PositionInBytes;
|
||||
|
||||
for (int i = 0; i < msgCount; i++)
|
||||
{
|
||||
|
||||
byte msgLength = message.ReadByte();
|
||||
|
||||
try
|
||||
{
|
||||
ReadData(message, sendingTime, resend);
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
}
|
||||
//+1 because msgLength is one additional byte
|
||||
currPos += msgLength + 1;
|
||||
message.Position = currPos * 8;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool ReadData(NetIncomingMessage message, float sendingTime, bool resend=false)
|
||||
{
|
||||
NetworkEventType eventType;
|
||||
ushort id;
|
||||
|
||||
try
|
||||
{
|
||||
eventType = (NetworkEventType)message.ReadByte();
|
||||
id = message.ReadUInt16();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
#if DEBUG
|
||||
DebugConsole.ThrowError("Received invalid network message", exception);
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
Entity e = Entity.FindEntityByID(id);
|
||||
if (e == null)
|
||||
{
|
||||
#if DEBUG
|
||||
DebugConsole.ThrowError("Couldn't find an entity matching the ID ''" + id + "''");
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
//System.Diagnostics.Debug.WriteLine(e.ToString());
|
||||
|
||||
object data;
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
#if DEBUG
|
||||
DebugConsole.ThrowError("Received invalid network message", exception);
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
/*if (resend)
|
||||
{
|
||||
var resendEvent = new NetworkEvent(eventType, id, false, data);
|
||||
resendEvent.SenderConnection = message.SenderConnection;
|
||||
}*/
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,488 @@
|
||||
using Lidgren.Network;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma.Networking.ReliableMessages
|
||||
{
|
||||
|
||||
class ReliableChannel
|
||||
{
|
||||
ReliableSender sender;
|
||||
ReliableReceiver receiver;
|
||||
|
||||
public ReliableChannel(NetPeer host)
|
||||
{
|
||||
sender = new ReliableSender(host);
|
||||
receiver = new ReliableReceiver(host);
|
||||
}
|
||||
|
||||
public ReliableMessage CreateMessage()
|
||||
{
|
||||
return sender.CreateMessage();
|
||||
}
|
||||
|
||||
public void SendMessage(ReliableMessage message, NetConnection receiver)
|
||||
{
|
||||
try
|
||||
{
|
||||
sender.SendMessage(message, receiver);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
#if DEBUG
|
||||
DebugConsole.ThrowError("Sending a reliable message failed", e);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
public void HandleResendRequest(NetIncomingMessage inc)
|
||||
{
|
||||
sender.HandleResendRequest(inc);
|
||||
}
|
||||
|
||||
public void HandleLatestMessageID(NetIncomingMessage inc)
|
||||
{
|
||||
//make sure we've received what's been sent to us, if not, rerequest
|
||||
receiver.HandleLatestMessageID(inc);
|
||||
}
|
||||
|
||||
public bool CheckMessage(NetIncomingMessage inc)
|
||||
{
|
||||
return receiver.CheckMessage(inc);
|
||||
}
|
||||
|
||||
public void Update(float deltaTime)
|
||||
{
|
||||
sender.Update(deltaTime);
|
||||
//update receiver to rerequest missed messages
|
||||
receiver.Update(deltaTime);
|
||||
}
|
||||
|
||||
public static int IdDiff(ushort id1, ushort id2)
|
||||
{
|
||||
if (Math.Abs((int)id1 - (int)id2) > ushort.MaxValue / 2)
|
||||
{
|
||||
return (ushort.MaxValue - Math.Max(id1, id2)) + Math.Min(id1, id2);
|
||||
}
|
||||
|
||||
return Math.Abs(id1 - id2);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal class ReliableSender
|
||||
{
|
||||
private Dictionary<ushort, ReliableMessage> messageBuffer;
|
||||
|
||||
private ushort messageCount;
|
||||
|
||||
private NetPeer sender;
|
||||
|
||||
private NetConnection recipient;
|
||||
|
||||
private float idSendTimer;
|
||||
|
||||
private float idSendInterval;
|
||||
|
||||
public ReliableSender(NetPeer sender)
|
||||
{
|
||||
this.sender = sender;
|
||||
|
||||
messageCount = 1;
|
||||
|
||||
messageBuffer = new Dictionary<ushort, ReliableMessage>();
|
||||
}
|
||||
|
||||
public ReliableMessage CreateMessage()
|
||||
{
|
||||
ushort messageID = (messageCount == ushort.MaxValue) ? (ushort)1 : (ushort)(messageCount + 1);
|
||||
|
||||
NetOutgoingMessage message = sender.CreateMessage();
|
||||
|
||||
var reliableMessage = new ReliableMessage(message, messageID);
|
||||
|
||||
message.Write((byte)PacketTypes.ReliableMessage);
|
||||
|
||||
message.Write(messageID);
|
||||
|
||||
if (messageBuffer.Count > NetConfig.ReliableMessageBufferSize)
|
||||
{
|
||||
int end = messageCount - NetConfig.ReliableMessageBufferSize;
|
||||
int start = end - (messageBuffer.Count - NetConfig.ReliableMessageBufferSize);
|
||||
|
||||
if (start < 0)
|
||||
{
|
||||
int wrappedStart = start + ushort.MaxValue;
|
||||
if (wrappedStart == 0) wrappedStart = ushort.MaxValue;
|
||||
int wrappedEnd = end + ushort.MaxValue;
|
||||
if (wrappedEnd == 0) wrappedEnd = ushort.MaxValue;
|
||||
|
||||
for (ushort i = (ushort)wrappedStart; i <= (ushort)wrappedEnd; i++)
|
||||
{
|
||||
messageBuffer.Remove(i);
|
||||
if (i == ushort.MaxValue) break;
|
||||
Debug.WriteLine("removing message " + i);
|
||||
}
|
||||
}
|
||||
|
||||
for (ushort i = (ushort)Math.Max(start,0); i <= (ushort)Math.Max(end,0); i++)
|
||||
{
|
||||
messageBuffer.Remove(i);
|
||||
if (i == ushort.MaxValue) break;
|
||||
Debug.WriteLine("removing message " + i);
|
||||
}
|
||||
}
|
||||
|
||||
return reliableMessage;
|
||||
}
|
||||
|
||||
public void SendMessage(ReliableMessage message, NetConnection connection)
|
||||
{
|
||||
idSendInterval = 0.0f;
|
||||
idSendTimer = connection.AverageRoundtripTime;
|
||||
|
||||
messageBuffer.Add(message.ID, message);
|
||||
|
||||
Debug.WriteLine("sending reliable massage (id " + message.ID + ")");
|
||||
|
||||
if (messageCount == ushort.MaxValue) messageCount = 0;
|
||||
messageCount++;
|
||||
|
||||
message.SaveInnerMessage();
|
||||
|
||||
sender.SendMessage(message.InnerMessage, connection, NetDeliveryMethod.Unreliable, 0);
|
||||
|
||||
recipient = connection;
|
||||
}
|
||||
|
||||
public void HandleResendRequest(NetIncomingMessage inc)
|
||||
{
|
||||
ushort messageId = inc.ReadUInt16();
|
||||
|
||||
Debug.WriteLine("received resend request for msg id "+messageId);
|
||||
|
||||
ResendMessage(messageId, inc.SenderConnection);
|
||||
}
|
||||
|
||||
private void ResendMessage(ushort messageId, NetConnection connection)
|
||||
{
|
||||
ReliableMessage message;
|
||||
if (!messageBuffer.TryGetValue(messageId, out message)) return;
|
||||
|
||||
Debug.WriteLine("resending " + messageId);
|
||||
|
||||
NetOutgoingMessage resendMessage = sender.CreateMessage();
|
||||
message.RestoreInnerMessage(resendMessage);
|
||||
|
||||
idSendTimer = connection.AverageRoundtripTime;
|
||||
|
||||
sender.SendMessage(resendMessage, connection, NetDeliveryMethod.Unreliable);
|
||||
}
|
||||
|
||||
public void Update(float deltaTime)
|
||||
{
|
||||
if (recipient == null) return;
|
||||
|
||||
idSendTimer -= deltaTime;
|
||||
|
||||
if (idSendTimer > 0.0f) return;
|
||||
|
||||
//Debug.WriteLine("Sending ack message: "+messageCount);
|
||||
|
||||
NetOutgoingMessage message = sender.CreateMessage();
|
||||
message.Write((byte)PacketTypes.LatestMessageID);
|
||||
|
||||
message.Write(messageCount);
|
||||
|
||||
sender.SendMessage(message, recipient, NetDeliveryMethod.Unreliable);
|
||||
|
||||
float roundTripTime = Math.Min(recipient.AverageRoundtripTime, 1.0f);
|
||||
|
||||
idSendTimer = Math.Max(roundTripTime, NetConfig.IdSendInterval + idSendInterval);
|
||||
idSendInterval += 0.1f;
|
||||
}
|
||||
}
|
||||
|
||||
internal class ReliableReceiver
|
||||
{
|
||||
ushort lastMessageID;
|
||||
|
||||
Queue<ushort> missingMessageIds;
|
||||
Dictionary<ushort, MissingMessage> missingMessages;
|
||||
|
||||
private NetPeer receiver;
|
||||
|
||||
private NetConnection recipient;
|
||||
|
||||
public ReliableReceiver(NetPeer receiver)
|
||||
{
|
||||
this.receiver = receiver;
|
||||
|
||||
lastMessageID = 1;
|
||||
|
||||
missingMessages = new Dictionary<ushort,MissingMessage>();
|
||||
missingMessageIds = new Queue<ushort>();
|
||||
}
|
||||
|
||||
public void Update(float deltaTime)
|
||||
{
|
||||
foreach (var message in missingMessages.Where(m => m.Value.ResendRequestsSent > NetConfig.ResendAttempts).ToList())
|
||||
{
|
||||
Debug.WriteLine("Max rerequest attempts reached on message "+message.Value.ID);
|
||||
missingMessages.Remove(message.Key);
|
||||
}
|
||||
|
||||
while (missingMessageIds.Count>NetConfig.ReliableMessageBufferSize)
|
||||
{
|
||||
ushort id = missingMessageIds.Dequeue();
|
||||
|
||||
missingMessages.Remove(id);
|
||||
}
|
||||
|
||||
foreach (KeyValuePair<ushort, MissingMessage> valuePair in missingMessages)
|
||||
{
|
||||
MissingMessage missingMessage = valuePair.Value;
|
||||
|
||||
missingMessage.ResendTimer -= deltaTime;
|
||||
|
||||
if (missingMessage.ResendTimer > 0.0f) continue;
|
||||
|
||||
Debug.WriteLine("rerequest "+missingMessage.ID+" (try #"+missingMessage.ResendRequestsSent+")");
|
||||
|
||||
NetOutgoingMessage resendRequest = receiver.CreateMessage();
|
||||
resendRequest.Write((byte)PacketTypes.ResendRequest);
|
||||
|
||||
resendRequest.Write(missingMessage.ID);
|
||||
|
||||
receiver.SendMessage(resendRequest, recipient,
|
||||
missingMessage.ResendRequestsSent==0 ? NetDeliveryMethod.ReliableUnordered : NetDeliveryMethod.Unreliable);
|
||||
|
||||
float roundTripTime = Math.Min(recipient.AverageRoundtripTime, 1.0f);
|
||||
|
||||
missingMessage.ResendTimer = Math.Max(roundTripTime, NetConfig.RerequestInterval);
|
||||
missingMessage.ResendRequestsSent++;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public bool CheckMessage(NetIncomingMessage message)
|
||||
{
|
||||
recipient = message.SenderConnection;
|
||||
|
||||
ushort id = message.ReadUInt16();
|
||||
|
||||
if (ReliableChannel.IdDiff(lastMessageID, id) > NetConfig.ReliableMessageBufferSize)
|
||||
{
|
||||
Debug.WriteLine("id diff > NetConfig.ReliableMessageBufferSize, resetting reliable channel");
|
||||
lastMessageID = id;
|
||||
return false;
|
||||
}
|
||||
|
||||
Debug.WriteLine("received message ID " + id + " - last id: " + lastMessageID);
|
||||
|
||||
//wrapped around
|
||||
if (Math.Abs((int)lastMessageID - (int)id) > ushort.MaxValue / 2)
|
||||
{
|
||||
//id wrapped around and we missed some messages in between, rerequest them
|
||||
if (lastMessageID>ushort.MaxValue/2 && id>=1)
|
||||
{
|
||||
for (ushort i = (ushort)(Math.Min(lastMessageID, (ushort)(ushort.MaxValue-1)) + 1); i < ushort.MaxValue; i++)
|
||||
{
|
||||
QueueMissingMessage(i);
|
||||
}
|
||||
for (ushort i = 1; i < id; i++)
|
||||
{
|
||||
QueueMissingMessage(i);
|
||||
}
|
||||
|
||||
lastMessageID = id;
|
||||
}
|
||||
//we already wrapped around but the message hasn't, check if it's a duplicate
|
||||
else if (lastMessageID < ushort.MaxValue / 2 && id > ushort.MaxValue / 2 && !missingMessages.ContainsKey(id))
|
||||
{
|
||||
Debug.WriteLine("old already received message, ignore");
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
RemoveMissingMessage(id);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (id>lastMessageID+1)
|
||||
{
|
||||
for (ushort i = (ushort)(lastMessageID+1); i < id; i++ )
|
||||
{
|
||||
QueueMissingMessage(i);
|
||||
}
|
||||
|
||||
}
|
||||
//received an old message and it wasn't marked as missed, lets ignore it
|
||||
else if (id<=lastMessageID && !missingMessages.ContainsKey(id))
|
||||
{
|
||||
Debug.WriteLine("old already received message, ignore");
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
RemoveMissingMessage(id);
|
||||
}
|
||||
|
||||
lastMessageID = Math.Max(lastMessageID, id);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void QueueMissingMessage(ushort id)
|
||||
{
|
||||
//message already marked as missed, continue
|
||||
if (missingMessages.ContainsKey(id)) return;
|
||||
|
||||
Debug.WriteLine("added " + id + " to missed");
|
||||
|
||||
float waitTime = Math.Abs(lastMessageID - id)>1 ? 0.0f : recipient.AverageRoundtripTime*0.5f;
|
||||
|
||||
missingMessages.Add(id, new MissingMessage(id, waitTime));
|
||||
|
||||
missingMessageIds.Enqueue(id);
|
||||
}
|
||||
|
||||
private void RemoveMissingMessage(ushort id)
|
||||
{
|
||||
if (!missingMessages.ContainsKey(id)) return;
|
||||
|
||||
Debug.WriteLine("remove " + id + " from missed");
|
||||
missingMessages.Remove(id);
|
||||
}
|
||||
|
||||
public void HandleLatestMessageID(NetIncomingMessage inc)
|
||||
{
|
||||
ushort messageId = inc.ReadUInt16();
|
||||
|
||||
recipient = inc.SenderConnection;
|
||||
|
||||
//id matches, all good
|
||||
if (messageId == lastMessageID)
|
||||
{
|
||||
//Debug.WriteLine("Received ack message: " + messageId + ", all good");
|
||||
return;
|
||||
}
|
||||
|
||||
if (ReliableChannel.IdDiff(lastMessageID, messageId) > NetConfig.ReliableMessageBufferSize)
|
||||
{
|
||||
Debug.WriteLine("id diff > NetConfig.ReliableMessageBufferSize, resetting reliable channel");
|
||||
lastMessageID = messageId;
|
||||
return;
|
||||
}
|
||||
|
||||
if (messageId < lastMessageID && Math.Abs((int)lastMessageID - (int)messageId) < ushort.MaxValue / 2)
|
||||
{
|
||||
Debug.WriteLine("Received id update message: " + messageId + ": ignoring, already received (" + lastMessageID + ")");
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.WriteLine("Received id update message: " + messageId + ", need to rerequest messages (last id: "+lastMessageID+")");
|
||||
|
||||
if (lastMessageID > ushort.MaxValue / 2 && messageId < short.MaxValue / 2)
|
||||
{
|
||||
for (ushort i = (ushort)Math.Min((int)lastMessageID + 1, ushort.MaxValue); i <= ushort.MaxValue; i++)
|
||||
{
|
||||
if (i == ushort.MaxValue && lastMessageID == ushort.MaxValue) break;
|
||||
QueueMissingMessage(i);
|
||||
if (i == ushort.MaxValue) break;
|
||||
}
|
||||
|
||||
for (ushort i = 1; i <= messageId; i++)
|
||||
{
|
||||
QueueMissingMessage(i);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//we already wrapped around but message hasn't, so it's an old message
|
||||
if (lastMessageID < ushort.MaxValue / 2 && messageId > ushort.MaxValue / 2)
|
||||
{
|
||||
Debug.WriteLine("old already received message, ignore");
|
||||
return;
|
||||
}
|
||||
|
||||
for (ushort i = (ushort)Math.Min((int)lastMessageID+1, ushort.MaxValue); i <= messageId; i++)
|
||||
{
|
||||
QueueMissingMessage(i);
|
||||
if (i == ushort.MaxValue) break;
|
||||
}
|
||||
}
|
||||
|
||||
lastMessageID = messageId;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
internal class MissingMessage
|
||||
{
|
||||
private ushort id;
|
||||
|
||||
public byte ResendRequestsSent;
|
||||
|
||||
public float ResendTimer;
|
||||
|
||||
public ushort ID
|
||||
{
|
||||
get { return id; }
|
||||
}
|
||||
|
||||
public MissingMessage(ushort id)
|
||||
{
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public MissingMessage(ushort id, float resendTimer)
|
||||
{
|
||||
this.id = id;
|
||||
this.ResendTimer = resendTimer;
|
||||
}
|
||||
}
|
||||
|
||||
class ReliableMessage
|
||||
{
|
||||
private NetOutgoingMessage innerMessage;
|
||||
private ushort id;
|
||||
|
||||
private byte[] innerMessageBytes;
|
||||
|
||||
public NetOutgoingMessage InnerMessage
|
||||
{
|
||||
get { return innerMessage; }
|
||||
}
|
||||
|
||||
public ushort ID
|
||||
{
|
||||
get { return id; }
|
||||
}
|
||||
|
||||
|
||||
public ReliableMessage(NetOutgoingMessage message, ushort id)
|
||||
{
|
||||
this.innerMessage = message;
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public void SaveInnerMessage()
|
||||
{
|
||||
innerMessage.WritePadBits();
|
||||
innerMessageBytes = innerMessage.PeekBytes(innerMessage.LengthBytes);
|
||||
//innerMessage = null;
|
||||
}
|
||||
|
||||
public void RestoreInnerMessage(NetOutgoingMessage message)
|
||||
{
|
||||
message.Write(innerMessageBytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user