From 77d3d228106ed6179e6a833f5e57c437156b8389 Mon Sep 17 00:00:00 2001 From: Regalis Date: Thu, 25 Feb 2016 22:54:10 +0200 Subject: [PATCH] Camera twitching fixes, fixed crashing when loading a sub with no hull, fixed server lobby player count only working for 0-15 players, progress on downloading subs from the server --- Subsurface/Source/Camera.cs | 12 +- Subsurface/Source/Map/Submarine.cs | 59 +++---- Subsurface/Source/Map/SubmarineBody.cs | 21 ++- .../Source/Networking/FileStreamReceiver.cs | 123 +++++++++++--- .../Source/Networking/FileStreamSender.cs | 49 ++++-- Subsurface/Source/Networking/GameClient.cs | 146 +++++++++++------ Subsurface/Source/Networking/GameServer.cs | 153 ++++++++++++------ .../Source/Networking/NetBufferExtensions.cs | 16 +- Subsurface/Source/Networking/NetworkEvent.cs | 4 +- Subsurface/Source/Networking/NetworkMember.cs | 25 +-- .../Source/Networking/ReliableSender.cs | 7 +- Subsurface/Source/Screens/NetLobbyScreen.cs | 61 +++++-- Subsurface/Source/Screens/ServerListScreen.cs | 17 +- Subsurface_Solution.v12.suo | Bin 923136 -> 907264 bytes 14 files changed, 464 insertions(+), 229 deletions(-) diff --git a/Subsurface/Source/Camera.cs b/Subsurface/Source/Camera.cs index 12aacd1be..c8b75de5b 100644 --- a/Subsurface/Source/Camera.cs +++ b/Subsurface/Source/Camera.cs @@ -127,11 +127,11 @@ namespace Barotrauma //UpdateTransform(); } - public void UpdateTransform() + public void UpdateTransform(bool interpolate = true) { - Vector2 interpolatedPosition = Physics.Interpolate(prevPosition, position); + Vector2 interpolatedPosition = interpolate ? Physics.Interpolate(prevPosition, position) : position; - float interpolatedZoom = Physics.Interpolate(prevZoom, zoom); + float interpolatedZoom = interpolate ? Physics.Interpolate(prevZoom, zoom) : zoom; worldView.X = (int)(interpolatedPosition.X - worldView.Width / 2.0); worldView.Y = (int)(interpolatedPosition.Y + worldView.Height / 2.0); @@ -149,6 +149,12 @@ namespace Barotrauma viewMatrix; Sound.CameraPos = new Vector3(WorldViewCenter.X, WorldViewCenter.Y, 0.0f); + + if (!interpolate) + { + prevPosition = position; + prevZoom = zoom; + } } public void MoveCamera(float deltaTime) diff --git a/Subsurface/Source/Map/Submarine.cs b/Subsurface/Source/Map/Submarine.cs index d7af79305..dbcc754f9 100644 --- a/Subsurface/Source/Map/Submarine.cs +++ b/Subsurface/Source/Map/Submarine.cs @@ -713,37 +713,42 @@ namespace Barotrauma } } - Vector2 topLeft = new Vector2(Hull.hullList[0].Rect.X, Hull.hullList[0].Rect.Y); - Vector2 bottomRight = new Vector2(Hull.hullList[0].Rect.X, Hull.hullList[0].Rect.Y); - foreach (Hull hull in Hull.hullList) + Vector2 center = Vector2.Zero; + + if (Hull.hullList.Any()) { - if (hull.Rect.X < topLeft.X) topLeft.X = hull.Rect.X; - if (hull.Rect.Y > topLeft.Y) topLeft.Y = hull.Rect.Y; - - if (hull.Rect.Right > bottomRight.X) bottomRight.X = hull.Rect.Right; - if (hull.Rect.Y - hull.Rect.Height < bottomRight.Y) bottomRight.Y = hull.Rect.Y - hull.Rect.Height; - } - - Vector2 center = (topLeft + bottomRight) / 2.0f; - center.X -= center.X % GridSize.X; - center.Y -= center.Y % GridSize.Y; - - foreach (Item item in Item.ItemList) - { - var wire = item.GetComponent(); - if (wire == null) continue; - - for (int i = 0; i < wire.Nodes.Count; i++) + Vector2 topLeft = new Vector2(Hull.hullList[0].Rect.X, Hull.hullList[0].Rect.Y); + Vector2 bottomRight = new Vector2(Hull.hullList[0].Rect.X, Hull.hullList[0].Rect.Y); + foreach (Hull hull in Hull.hullList) { - wire.Nodes[i] -= center; - } - } + if (hull.Rect.X < topLeft.X) topLeft.X = hull.Rect.X; + if (hull.Rect.Y > topLeft.Y) topLeft.Y = hull.Rect.Y; - for (int i = 0; i < MapEntity.mapEntityList.Count; i++) - { - if (MapEntity.mapEntityList[i].Submarine == null) continue; + if (hull.Rect.Right > bottomRight.X) bottomRight.X = hull.Rect.Right; + if (hull.Rect.Y - hull.Rect.Height < bottomRight.Y) bottomRight.Y = hull.Rect.Y - hull.Rect.Height; + } + + center = (topLeft + bottomRight) / 2.0f; + center.X -= center.X % GridSize.X; + center.Y -= center.Y % GridSize.Y; + + foreach (Item item in Item.ItemList) + { + var wire = item.GetComponent(); + if (wire == null) continue; + + for (int i = 0; i < wire.Nodes.Count; i++) + { + wire.Nodes[i] -= center; + } + } + + for (int i = 0; i < MapEntity.mapEntityList.Count; i++) + { + if (MapEntity.mapEntityList[i].Submarine == null) continue; - MapEntity.mapEntityList[i].Move(-center); + MapEntity.mapEntityList[i].Move(-center); + } } subBody = new SubmarineBody(this); diff --git a/Subsurface/Source/Map/SubmarineBody.cs b/Subsurface/Source/Map/SubmarineBody.cs index 7e45546b9..9fb1fcbb9 100644 --- a/Subsurface/Source/Map/SubmarineBody.cs +++ b/Subsurface/Source/Map/SubmarineBody.cs @@ -226,25 +226,32 @@ namespace Barotrauma if (dist > 1000.0f) { Vector2 moveAmount = ConvertUnits.ToSimUnits((Vector2)targetPosition) - body.Position; - Vector2 displayerMoveAmount = ConvertUnits.ToDisplayUnits(moveAmount); + Vector2 displayMoveAmount = ConvertUnits.ToDisplayUnits(moveAmount); body.SetTransform(body.Position + moveAmount, 0.0f); - if (Character.Controlled != null) Character.Controlled.CursorPosition += displayerMoveAmount; + if (Character.Controlled != null) Character.Controlled.CursorPosition += displayMoveAmount; - GameMain.GameScreen.Cam.Position += displayerMoveAmount; - GameMain.GameScreen.Cam.UpdateTransform(); + GameMain.GameScreen.Cam.Position += displayMoveAmount; + if (GameMain.GameScreen.Cam.TargetPos!=Vector2.Zero) GameMain.GameScreen.Cam.TargetPos += displayMoveAmount; + GameMain.GameScreen.Cam.UpdateTransform(false); + + submarine.SetPrevTransform(submarine.Position); + submarine.UpdateTransform(); targetPosition = null; } else if (dist > 50.0f) { Vector2 moveAmount = Vector2.Normalize((Vector2)targetPosition - Position); moveAmount *= ConvertUnits.ToSimUnits(Math.Min(dist, 100.0f)); - Vector2 displayerMoveAmount = ConvertUnits.ToDisplayUnits(moveAmount); + Vector2 displayMoveAmount = ConvertUnits.ToDisplayUnits(moveAmount); body.SetTransform(body.Position + moveAmount * deltaTime, 0.0f); - GameMain.GameScreen.Cam.Position += displayerMoveAmount * deltaTime; - if (Character.Controlled != null) Character.Controlled.CursorPosition += displayerMoveAmount; + GameMain.GameScreen.Cam.Position += displayMoveAmount * deltaTime; + if (GameMain.GameScreen.Cam.TargetPos != Vector2.Zero) GameMain.GameScreen.Cam.TargetPos += displayMoveAmount; + if (Character.Controlled != null) Character.Controlled.CursorPosition += displayMoveAmount; + + //GameMain.GameScreen.Cam.UpdateTransform(false); } else { diff --git a/Subsurface/Source/Networking/FileStreamReceiver.cs b/Subsurface/Source/Networking/FileStreamReceiver.cs index 45cfbe9e1..886d5c8e4 100644 --- a/Subsurface/Source/Networking/FileStreamReceiver.cs +++ b/Subsurface/Source/Networking/FileStreamReceiver.cs @@ -4,13 +4,36 @@ using System.IO; namespace Barotrauma.Networking { - class FileStreamReceiver + class FileStreamReceiver : IDisposable { - private NetClient s_client; - private ulong s_length; - private ulong s_received; - private FileStream s_writeStream; - private int s_timeStarted; + 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 filePath; + + private FileTransferType fileType; + + public string FileName + { + get; + private set; + } + + public ulong FileSize + { + get { return length; } + } + + public ulong Received + { + get { return received; } + } public FileTransferStatus Status { @@ -24,47 +47,97 @@ namespace Barotrauma.Networking private set; } - public FileStreamReceiver(NetClient client) + public float Progress { - s_client = client; + get { return length / (float)received; } + + } + + public FileStreamReceiver(NetClient client, string filePath, FileTransferType fileType, OnFinished onFinished) + { + client = client; + + this.filePath = filePath; + this.fileType = fileType; + + this.onFinished = onFinished; Status = FileTransferStatus.NotStarted; } public void ReadMessage(NetIncomingMessage inc) { - int chunkLen = inc.LengthBytes; - if (s_length == 0) + try { - s_length = inc.ReadUInt64(); - string filename = inc.ReadString(); - s_writeStream = new FileStream(filename, FileMode.Create, FileAccess.Write, FileShare.None); - s_timeStarted = Environment.TickCount; + TryReadMessage(inc); + } + catch (Exception e) + { + DebugConsole.ThrowError("Error while receiving file ''"+FileName+"''", e); + Status = FileTransferStatus.Error; + } + } + + private void TryReadMessage(NetIncomingMessage inc) + { + //int chunkLen = inc.LengthBytes; + if (length == 0) + { + + if (!Directory.Exists(filePath)) + { + Directory.CreateDirectory(filePath); + } + + byte fileTypeByte = inc.ReadByte(); + if (fileTypeByte != (byte)fileType) + { + Status = FileTransferStatus.Error; + return; + } + + length = inc.ReadUInt64(); + FileName = inc.ReadString(); + writeStream = new FileStream(Path.Combine(filePath, FileName), FileMode.Create, FileAccess.Write, FileShare.None); + timeStarted = Environment.TickCount; Status = FileTransferStatus.NotStarted; return; } - byte[] all = inc.ReadBytes(inc.LengthBytes); - s_received += (ulong)all.Length; - s_writeStream.Write(all, 0, all.Length); + byte[] all = inc.ReadBytes(inc.LengthBytes - inc.PositionInBytes); + received += (ulong)all.Length; + writeStream.Write(all, 0, all.Length); - int passed = Environment.TickCount - s_timeStarted; + int passed = Environment.TickCount - timeStarted; float psec = passed / 1000.0f; - BytesPerSecond = s_received / psec; + BytesPerSecond = received / psec; Status = FileTransferStatus.Receiving; - if (s_received >= s_length) + if (received >= length) { - s_writeStream.Flush(); - s_writeStream.Close(); - s_writeStream.Dispose(); - - Status = FileTransferStatus.Finished; + Status = FileTransferStatus.Finished; + if (onFinished!=null) onFinished(this); } } + + public void Dispose() + { + Dispose(true); + + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + writeStream.Flush(); + writeStream.Close(); + writeStream.Dispose(); + } } + + } diff --git a/Subsurface/Source/Networking/FileStreamSender.cs b/Subsurface/Source/Networking/FileStreamSender.cs index 7eaf60a97..24eb282da 100644 --- a/Subsurface/Source/Networking/FileStreamSender.cs +++ b/Subsurface/Source/Networking/FileStreamSender.cs @@ -1,4 +1,5 @@ using Lidgren.Network; +using System; using System.IO; namespace Barotrauma.Networking @@ -8,7 +9,12 @@ namespace Barotrauma.Networking NotStarted, Sending, Receiving, Finished, Error } - class FileStreamSender + enum FileTransferType + { + Unknown, Submarine + } + + class FileStreamSender : IDisposable { private FileStream inputStream; private int sentOffset; @@ -16,6 +22,9 @@ namespace Barotrauma.Networking private byte[] tempBuffer; private NetConnection connection; + + private FileTransferType fileType; + public FileTransferStatus Status { get; @@ -28,8 +37,8 @@ namespace Barotrauma.Networking private set; } - - public static FileStreamSender Create(NetConnection conn, string fileName) + + public static FileStreamSender Create(NetConnection conn, string fileName, FileTransferType fileType) { if (!File.Exists(fileName)) { @@ -37,19 +46,21 @@ namespace Barotrauma.Networking return null; } - return new FileStreamSender(conn, fileName); + return new FileStreamSender(conn, fileName, fileType); } - private FileStreamSender(NetConnection conn, string fileName) + private FileStreamSender(NetConnection conn, string fileName, FileTransferType fileType) { connection = conn; inputStream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read); - chunkLen = connection.Peer.Configuration.MaximumTransmissionUnit - 20; + chunkLen = connection.Peer.Configuration.MaximumTransmissionUnit - 100; tempBuffer = new byte[chunkLen]; sentOffset = 0; FileName = fileName; + this.fileType = fileType; + Status = FileTransferStatus.NotStarted; } @@ -70,7 +81,9 @@ namespace Barotrauma.Networking if (sentOffset == 0) { // first message; send length, chunk length and file name - message = connection.Peer.CreateMessage(sendBytes + 8); + message = connection.Peer.CreateMessage(sendBytes + 8 + 1); + message.Write((byte)PacketTypes.FileStream); + message.Write((byte)fileType); message.Write((ulong)inputStream.Length); message.Write(Path.GetFileName(inputStream.Name)); connection.SendMessage(message, NetDeliveryMethod.ReliableOrdered, 1); @@ -78,7 +91,8 @@ namespace Barotrauma.Networking Status = FileTransferStatus.Sending; } - message = connection.Peer.CreateMessage(sendBytes + 8); + message = connection.Peer.CreateMessage(sendBytes + 8 + 1); + message.Write((byte)PacketTypes.FileStream); message.Write(tempBuffer, 0, sendBytes); connection.SendMessage(message, NetDeliveryMethod.ReliableOrdered, 1); @@ -88,12 +102,25 @@ namespace Barotrauma.Networking if (remaining - sendBytes <= 0) { - inputStream.Close(); - inputStream.Dispose(); - inputStream = null; + //Dispose(); Status = FileTransferStatus.Finished; } } + + + public void Dispose() + { + Dispose(true); + + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + inputStream.Close(); + inputStream.Dispose(); + inputStream = null; + } } } diff --git a/Subsurface/Source/Networking/GameClient.cs b/Subsurface/Source/Networking/GameClient.cs index 63ad27adb..29ca1f730 100644 --- a/Subsurface/Source/Networking/GameClient.cs +++ b/Subsurface/Source/Networking/GameClient.cs @@ -4,6 +4,7 @@ using Microsoft.Xna.Framework; using System.Collections.Generic; using Barotrauma.Networking.ReliableMessages; using FarseerPhysics; +using System.IO; namespace Barotrauma.Networking { @@ -15,6 +16,8 @@ namespace Barotrauma.Networking private ReliableChannel reliableChannel; + private FileStreamReceiver fileStreamReceiver; + private GUITickBox endRoundButton; private bool connected; @@ -99,7 +102,7 @@ namespace Barotrauma.Networking NetOutgoingMessage outmsg = client.CreateMessage(); client.Start(); - outmsg.WriteEnum(PacketTypes.Login); + outmsg.Write((byte)PacketTypes.Login); outmsg.Write(myID); outmsg.Write(password); outmsg.Write(GameMain.Version.ToString()); @@ -221,7 +224,7 @@ namespace Barotrauma.Networking { // All manually sent messages are type of "Data" case NetIncomingMessageType.Data: - byte packetType = (byte)inc.ReadEnum(); + byte packetType = inc.ReadByte(); if (packetType == (byte)PacketTypes.LoggedIn) { myID = inc.ReadByte(); @@ -262,7 +265,7 @@ namespace Barotrauma.Networking CanStart = true; NetOutgoingMessage lobbyUpdateRequest = client.CreateMessage(); - lobbyUpdateRequest.WriteEnum(PacketTypes.RequestNetLobbyUpdate); + lobbyUpdateRequest.Write((byte)PacketTypes.RequestNetLobbyUpdate); client.SendMessage(lobbyUpdateRequest, NetDeliveryMethod.ReliableUnordered); } else if (packetType == (byte)PacketTypes.KickedOut) @@ -445,12 +448,12 @@ namespace Barotrauma.Networking { if (inc.MessageType != NetIncomingMessageType.Data) continue; - byte packetType = (byte)inc.ReadEnum(); + byte packetType = inc.ReadByte(); if (packetType == (byte)PacketTypes.ReliableMessage) { if (!reliableChannel.CheckMessage(inc)) continue; - packetType = (byte)inc.ReadEnum(); + packetType = inc.ReadByte(); } switch (packetType) @@ -474,6 +477,22 @@ namespace Barotrauma.Networking AddChatMessage(otherClient.name + " has joined the server", ChatMessageType.Server); + break; + case (byte)PacketTypes.RequestFile: + new GUIMessageBox("Couldn't the file from the server", "Sharing files has been disabled by the server."); + break; + case (byte)PacketTypes.FileStream: + if (fileStreamReceiver == null) + { + //todo: unexpected file + } + else + { + + fileStreamReceiver.ReadMessage(inc); + } + + break; case (byte)PacketTypes.PlayerLeft: byte leavingID = inc.ReadByte(); @@ -510,7 +529,7 @@ namespace Barotrauma.Networking break; case (byte)PacketTypes.Chatmessage: - ChatMessageType messageType = inc.ReadEnum(); + ChatMessageType messageType = (ChatMessageType)inc.ReadByte(); AddChatMessage(inc.ReadString(), messageType); break; @@ -691,24 +710,59 @@ namespace Barotrauma.Networking spriteBatch.DrawString(GUI.SmallFont, "Sent bytes: " + client.Statistics.SentBytes, new Vector2(x + 10, y + 75), Color.White); spriteBatch.DrawString(GUI.SmallFont, "Sent packets: " + client.Statistics.SentPackets, new Vector2(x + 10, y + 90), Color.White); - + + if (fileStreamReceiver!=null) + { + GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2 - 100, 20), "Downloading "+fileStreamReceiver.FileName, Color.White); + GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2 - 100, 20), + MathUtils.GetBytesReadable((long)fileStreamReceiver.Received)+" / "+MathUtils.GetBytesReadable((long)fileStreamReceiver.FileSize), Color.White); + GUI.DrawProgressBar(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2 - 100, 20), new Vector2(200, 15), fileStreamReceiver.Progress, Color.Green); + } + } + + private void OnFileReceived(FileStreamReceiver receiver) + { + if (receiver.Status == FileTransferStatus.Error) + { + + } + else if (receiver.Status == FileTransferStatus.Finished) + { + new GUIMessageBox("Download finished", "File ''"+receiver.FileName+"'' was downloaded succesfully."); + } + + receiver.Dispose(); + fileStreamReceiver = null; } public override void Disconnect() { NetOutgoingMessage msg = client.CreateMessage(); - msg.WriteEnum(PacketTypes.PlayerLeft); + msg.Write((byte)PacketTypes.PlayerLeft); client.SendMessage(msg, NetDeliveryMethod.ReliableUnordered); client.Shutdown(""); GameMain.NetworkMember = null; } + public void RequestFile(string file, FileTransferType fileType) + { + NetOutgoingMessage msg = client.CreateMessage(); + msg.Write((byte)PacketTypes.RequestFile); + msg.Write((byte)fileType); + + msg.Write(file); + + client.SendMessage(msg, NetDeliveryMethod.ReliableUnordered); + + fileStreamReceiver = new FileStreamReceiver(client, Path.Combine(Submarine.SavePath, "Downloaded"), fileType, OnFileReceived); + } + public void Vote(VoteType voteType, object userData) { NetOutgoingMessage msg = client.CreateMessage(); - msg.WriteEnum(PacketTypes.Vote); - msg.WriteEnum(voteType); + msg.Write((byte)PacketTypes.Vote); + msg.Write((byte)voteType); switch (voteType) { @@ -729,7 +783,7 @@ namespace Barotrauma.Networking public bool SpectateClicked(GUIButton button, object userData) { NetOutgoingMessage msg = client.CreateMessage(); - msg.WriteEnum(PacketTypes.SpectateRequest); + msg.Write((byte)PacketTypes.SpectateRequest); client.SendMessage(msg, NetDeliveryMethod.ReliableUnordered); @@ -760,7 +814,7 @@ namespace Barotrauma.Networking if (characterInfo == null) return; NetOutgoingMessage msg = client.CreateMessage(); - msg.WriteEnum(PacketTypes.CharacterInfo); + msg.Write((byte)PacketTypes.CharacterInfo); msg.Write(characterInfo.Name); msg.Write(characterInfo.Gender == Gender.Male); @@ -837,8 +891,8 @@ namespace Barotrauma.Networking (myCharacter == null || myCharacter.IsDead)) ? ChatMessageType.Dead : ChatMessageType.Default; ReliableMessage msg = reliableChannel.CreateMessage(); - msg.InnerMessage.WriteEnum(PacketTypes.Chatmessage); - msg.InnerMessage.WriteEnum(type); + msg.InnerMessage.Write((byte)PacketTypes.Chatmessage); + msg.InnerMessage.Write((byte)type); msg.InnerMessage.Write(message); reliableChannel.SendMessage(msg, client.ServerConnection); @@ -848,41 +902,41 @@ namespace Barotrauma.Networking /// sends some random data to the server (can be a networkevent or just something completely random) /// use for debugging purposes /// - public void SendRandomData() - { - NetOutgoingMessage msg = client.CreateMessage(); - switch (Rand.Int(5)) - { - case 0: - msg.WriteEnum(PacketTypes.NetworkEvent); - msg.WriteEnum(NetworkEventType.EntityUpdate); - msg.Write(Rand.Int(MapEntity.mapEntityList.Count)); - break; - case 1: - msg.WriteEnum(PacketTypes.NetworkEvent); - msg.Write((byte)Enum.GetNames(typeof(NetworkEventType)).Length); - msg.Write(Rand.Int(MapEntity.mapEntityList.Count)); - break; - case 2: - msg.WriteEnum(PacketTypes.NetworkEvent); - msg.WriteEnum(NetworkEventType.ComponentUpdate); - msg.Write((int)Item.ItemList[Rand.Int(Item.ItemList.Count)].ID); - msg.Write(Rand.Int(8)); - break; - case 3: - msg.Write((byte)Enum.GetNames(typeof(PacketTypes)).Length); - break; - } + //public void SendRandomData() + //{ + // NetOutgoingMessage msg = client.CreateMessage(); + // switch (Rand.Int(5)) + // { + // case 0: + // msg.WriteEnum(PacketTypes.NetworkEvent); + // msg.WriteEnum(NetworkEventType.EntityUpdate); + // msg.Write(Rand.Int(MapEntity.mapEntityList.Count)); + // break; + // case 1: + // msg.WriteEnum(PacketTypes.NetworkEvent); + // msg.Write((byte)Enum.GetNames(typeof(NetworkEventType)).Length); + // msg.Write(Rand.Int(MapEntity.mapEntityList.Count)); + // break; + // case 2: + // msg.WriteEnum(PacketTypes.NetworkEvent); + // msg.WriteEnum(NetworkEventType.ComponentUpdate); + // msg.Write((int)Item.ItemList[Rand.Int(Item.ItemList.Count)].ID); + // msg.Write(Rand.Int(8)); + // break; + // case 3: + // msg.Write((byte)Enum.GetNames(typeof(PacketTypes)).Length); + // break; + // } - int bitCount = Rand.Int(100); - for (int i = 0; i c.Connection == inc.SenderConnection); if (dataSender == null) return; - byte packetType = (byte)inc.ReadEnum(); + byte packetType = inc.ReadByte(); if (packetType == (byte)PacketTypes.ReliableMessage) { if (!dataSender.ReliableChannel.CheckMessage(inc)) return; - packetType = (byte)inc.ReadEnum(); + packetType = inc.ReadByte(); } switch (packetType) @@ -490,7 +504,7 @@ namespace Barotrauma.Networking break; case (byte)PacketTypes.Chatmessage: - ChatMessageType messageType = inc.ReadEnum(); + ChatMessageType messageType = (ChatMessageType)inc.ReadByte(); SendChatMessage(inc.ReadString(), messageType); @@ -500,6 +514,33 @@ namespace Barotrauma.Networking break; case (byte)PacketTypes.CharacterInfo: ReadCharacterData(inc); + break; + case (byte)PacketTypes.RequestFile: + string fileName = inc.ReadString(); + byte fileType = inc.ReadByte(); + + switch (fileType) + { + case (byte)FileTransferType.Submarine: + + var requestedSubmarine = Submarine.SavedSubmarines.Find(s => s.Name == fileName); + + if (requestedSubmarine==null) + { + //todo: ei voi ladata + } + else + { + var fileStreamSender = FileStreamSender.Create(dataSender.Connection, requestedSubmarine.FilePath, FileTransferType.Submarine); + if (fileStreamSender != null) dataSender.FileStreamSender = fileStreamSender; + } + break; + default: + DebugConsole.ThrowError("Unknown file type was requested ("+fileType+")"); + break; + } + + break; case (byte)PacketTypes.ResendRequest: @@ -541,7 +582,7 @@ namespace Barotrauma.Networking private void HandleConnectionApproval(NetIncomingMessage inc) { - if (inc.ReadEnum() != PacketTypes.Login) return; + if ((PacketTypes)inc.ReadByte() != PacketTypes.Login) return; DebugConsole.NewMessage("New player has joined the server", Color.White); @@ -649,6 +690,8 @@ namespace Barotrauma.Networking UpdateCrewFrame(); inc.SenderConnection.Approve(); + + refreshMasterTimer = DateTime.Now; } @@ -886,7 +929,7 @@ namespace Barotrauma.Networking private NetOutgoingMessage CreateStartMessage(int seed, Submarine selectedSub, GameModePreset selectedMode) { NetOutgoingMessage msg = server.CreateMessage(); - msg.WriteEnum(PacketTypes.StartGame); + msg.Write((byte)PacketTypes.StartGame); msg.Write(seed); @@ -910,7 +953,7 @@ namespace Barotrauma.Networking if (myCharacter != null) { - msg.Write(0); + msg.Write((byte)0); WriteCharacterData(msg, myCharacter.Info.Name, myCharacter); } @@ -949,7 +992,7 @@ namespace Barotrauma.Networking if (ConnectedClients.Count > 0) { NetOutgoingMessage msg = server.CreateMessage(); - msg.WriteEnum(PacketTypes.EndGame); + msg.Write((byte)PacketTypes.EndGame); msg.Write(endMessage); if (server.ConnectionsCount > 0) @@ -989,7 +1032,9 @@ namespace Barotrauma.Networking private void DisconnectClient(NetConnection senderConnection, string msg = "", string targetmsg = "") { Client client = ConnectedClients.Find(x => x.Connection == senderConnection); - if (client != null) DisconnectClient(client, msg, targetmsg); + if (client == null) return; + + DisconnectClient(client, msg, targetmsg); } private void DisconnectClient(Client client, string msg = "", string targetmsg = "") @@ -1007,14 +1052,14 @@ namespace Barotrauma.Networking Log(msg, messageColor[(int)ChatMessageType.Server]); NetOutgoingMessage outmsg = server.CreateMessage(); - outmsg.WriteEnum(PacketTypes.KickedOut); + outmsg.Write((byte)PacketTypes.KickedOut); outmsg.Write(targetmsg); server.SendMessage(outmsg, client.Connection, NetDeliveryMethod.ReliableUnordered, 0); ConnectedClients.Remove(client); outmsg = server.CreateMessage(); - outmsg.WriteEnum(PacketTypes.PlayerLeft); + outmsg.Write((byte)PacketTypes.PlayerLeft); outmsg.Write(client.ID); outmsg.Write(msg); @@ -1028,6 +1073,8 @@ namespace Barotrauma.Networking AddChatMessage(msg, ChatMessageType.Server); UpdateCrewFrame(); + + refreshMasterTimer = DateTime.Now; } private void UpdateCrewFrame() @@ -1086,7 +1133,7 @@ namespace Barotrauma.Networking } NetOutgoingMessage msg = server.CreateMessage(); - msg.WriteEnum(PacketTypes.Traitor); + msg.Write((byte)PacketTypes.Traitor); msg.Write(target.Info.Name); if (server.Connections.Count > 0) { @@ -1175,7 +1222,7 @@ namespace Barotrauma.Networking try { NetOutgoingMessage msg = server.CreateMessage(); - msg.WriteEnum(PacketTypes.VoteStatus); + msg.Write((byte)PacketTypes.VoteStatus); Voting.WriteData(msg, ConnectedClients); server.SendMessage(msg, server.Connections, NetDeliveryMethod.ReliableUnordered, 0); @@ -1199,7 +1246,7 @@ namespace Barotrauma.Networking if (server.Connections.Count == 0) return true; NetOutgoingMessage msg = server.CreateMessage(); - msg.WriteEnum(PacketTypes.UpdateNetLobby); + msg.Write((byte)PacketTypes.UpdateNetLobby); GameMain.NetLobbyScreen.WriteData(msg); server.SendMessage(msg, server.Connections, NetDeliveryMethod.ReliableUnordered, 0); @@ -1275,8 +1322,8 @@ namespace Barotrauma.Networking foreach (Client c in recipients) { ReliableMessage msg = c.ReliableChannel.CreateMessage(); - msg.InnerMessage.WriteEnum(PacketTypes.Chatmessage); - msg.InnerMessage.WriteEnum(type); + msg.InnerMessage.Write((byte)PacketTypes.Chatmessage); + msg.InnerMessage.Write((byte)type); msg.InnerMessage.Write(message); c.ReliableChannel.SendMessage(msg, c.Connection); @@ -1343,7 +1390,7 @@ namespace Barotrauma.Networking if (items == null || !items.Any()) return; NetOutgoingMessage message = server.CreateMessage(); - message.WriteEnum(PacketTypes.NewItem); + message.Write((byte)PacketTypes.NewItem); Item.Spawner.FillNetworkData(message, items, inventories); @@ -1460,38 +1507,38 @@ namespace Barotrauma.Networking /// sends some random data to the clients /// use for debugging purposes /// - public void SendRandomData() - { - NetOutgoingMessage msg = server.CreateMessage(); - switch (Rand.Int(5)) - { - case 0: - msg.WriteEnum(PacketTypes.NetworkEvent); - msg.Write(Rand.Int(Enum.GetNames(typeof(NetworkEventType)).Length)); - msg.Write(Rand.Int(MapEntity.mapEntityList.Count)); - break; - case 1: - msg.WriteEnum(PacketTypes.NetworkEvent); - msg.WriteEnum(NetworkEventType.ComponentUpdate); - msg.Write((int)Item.ItemList[Rand.Int(Item.ItemList.Count)].ID); - msg.Write(Rand.Int(8)); - break; - case 2: - msg.Write((byte)Enum.GetNames(typeof(PacketTypes)).Length); - break; - case 3: - msg.Write((byte)PacketTypes.UpdateNetLobby); - break; - } + //public void SendRandomData() + //{ + // NetOutgoingMessage msg = server.CreateMessage(); + // switch (Rand.Int(5)) + // { + // case 0: + // msg.WriteEnum(PacketTypes.NetworkEvent); + // msg.Write(Rand.Int(Enum.GetNames(typeof(NetworkEventType)).Length)); + // msg.Write(Rand.Int(MapEntity.mapEntityList.Count)); + // break; + // case 1: + // msg.WriteEnum(PacketTypes.NetworkEvent); + // msg.WriteEnum(NetworkEventType.ComponentUpdate); + // msg.Write((int)Item.ItemList[Rand.Int(Item.ItemList.Count)].ID); + // msg.Write(Rand.Int(8)); + // break; + // case 2: + // msg.Write((byte)Enum.GetNames(typeof(PacketTypes)).Length); + // break; + // case 3: + // msg.Write((byte)PacketTypes.UpdateNetLobby); + // break; + // } - int bitCount = Rand.Int(100); - for (int i = 0; i < bitCount; i++) - { - msg.Write(Rand.Int(2) == 0); - } - SendMessage(msg, (Rand.Int(2) == 0) ? NetDeliveryMethod.ReliableOrdered : NetDeliveryMethod.Unreliable, null); + // int bitCount = Rand.Int(100); + // for (int i = 0; i < bitCount; i++) + // { + // msg.Write(Rand.Int(2) == 0); + // } + // SendMessage(msg, (Rand.Int(2) == 0) ? NetDeliveryMethod.ReliableOrdered : NetDeliveryMethod.Unreliable, null); - } + //} public override void Disconnect() { @@ -1523,6 +1570,8 @@ namespace Barotrauma.Networking public List jobPreferences; public JobPrefab assignedJob; + public FileStreamSender FileStreamSender; + public bool Spectating; public ReliableChannel ReliableChannel; diff --git a/Subsurface/Source/Networking/NetBufferExtensions.cs b/Subsurface/Source/Networking/NetBufferExtensions.cs index 0bec997fb..b328e20a5 100644 --- a/Subsurface/Source/Networking/NetBufferExtensions.cs +++ b/Subsurface/Source/Networking/NetBufferExtensions.cs @@ -5,14 +5,14 @@ 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 void WriteEnum(this NetBuffer buffer, Enum value) + //{ + // buffer.WriteRangedInteger(0, Enum.GetValues(value.GetType()).Length - 1, Convert.ToInt32(value)); + //} - public static TEnum ReadEnum(this NetBuffer buffer) - { - return (TEnum)(object)buffer.ReadRangedInteger(0, Enum.GetValues(typeof(TEnum)).Length - 1); - } + //public static TEnum ReadEnum(this NetBuffer buffer) + //{ + // return (TEnum)(object)buffer.ReadRangedInteger(0, Enum.GetValues(typeof(TEnum)).Length - 1); + //} } } diff --git a/Subsurface/Source/Networking/NetworkEvent.cs b/Subsurface/Source/Networking/NetworkEvent.cs index 9f98d3c68..563b7ffd8 100644 --- a/Subsurface/Source/Networking/NetworkEvent.cs +++ b/Subsurface/Source/Networking/NetworkEvent.cs @@ -132,7 +132,7 @@ namespace Barotrauma.Networking public bool FillData(NetBuffer message) { - message.WriteEnum(eventType); + message.Write((byte)eventType); Entity e = Entity.FindEntityByID(id); if (e == null) return false; @@ -189,7 +189,7 @@ namespace Barotrauma.Networking try { - eventType = message.ReadEnum(); + eventType = (NetworkEventType)message.ReadByte(); id = message.ReadUInt16(); } catch (Exception exception) diff --git a/Subsurface/Source/Networking/NetworkMember.cs b/Subsurface/Source/Networking/NetworkMember.cs index 72c83b042..7f75d07ed 100644 --- a/Subsurface/Source/Networking/NetworkMember.cs +++ b/Subsurface/Source/Networking/NetworkMember.cs @@ -7,7 +7,7 @@ using Lidgren.Network; namespace Barotrauma.Networking { - enum PacketTypes : int + enum PacketTypes : byte { Unknown, @@ -32,6 +32,8 @@ namespace Barotrauma.Networking Vote, VoteStatus, ResendRequest, ReliableMessage, LatestMessageID, + + RequestFile, FileStream, SpectateRequest } @@ -163,7 +165,7 @@ namespace Barotrauma.Networking if (msgBytes.Count == 0) return null; NetOutgoingMessage message = netPeer.CreateMessage(); - message.WriteEnum(PacketTypes.NetworkEvent); + message.Write((byte)PacketTypes.NetworkEvent); message.Write((float)NetTime.Now); @@ -295,25 +297,6 @@ namespace Barotrauma.Networking } public virtual void Disconnect() { } - - protected byte PlayerCountToByte(int playerCount, int maxPlayers) - { - byte byteVal = (byte)playerCount; - - byteVal |= (byte)((maxPlayers - 1) << 4); - - return byteVal; - } - - public static int ByteToPlayerCount(byte byteVal, out int maxPlayers) - { - maxPlayers = (byteVal >> 4)+1; - - int playerCount = byteVal & (byte)((1 << 4) - 1); - - return playerCount; - } - } } diff --git a/Subsurface/Source/Networking/ReliableSender.cs b/Subsurface/Source/Networking/ReliableSender.cs index b90244522..27059b237 100644 --- a/Subsurface/Source/Networking/ReliableSender.cs +++ b/Subsurface/Source/Networking/ReliableSender.cs @@ -94,7 +94,7 @@ namespace Barotrauma.Networking.ReliableMessages var reliableMessage = new ReliableMessage(message, messageID); - message.WriteEnum(PacketTypes.ReliableMessage); + message.Write((byte)PacketTypes.ReliableMessage); message.Write(messageID); @@ -184,7 +184,7 @@ namespace Barotrauma.Networking.ReliableMessages //Debug.WriteLine("Sending ack message: "+messageCount); NetOutgoingMessage message = sender.CreateMessage(); - message.WriteEnum(PacketTypes.LatestMessageID); + message.Write((byte)PacketTypes.LatestMessageID); message.Write(messageCount); @@ -244,7 +244,7 @@ namespace Barotrauma.Networking.ReliableMessages Debug.WriteLine("rerequest "+missingMessage.ID+" (try #"+missingMessage.ResendRequestsSent+")"); NetOutgoingMessage resendRequest = receiver.CreateMessage(); - resendRequest.WriteEnum(PacketTypes.ResendRequest); + resendRequest.Write((byte)PacketTypes.ResendRequest); resendRequest.Write(missingMessage.ID); @@ -443,6 +443,7 @@ namespace Barotrauma.Networking.ReliableMessages public void SaveInnerMessage() { + innerMessage.WritePadBits(); innerMessageBytes = innerMessage.PeekBytes(innerMessage.LengthBytes); //innerMessage = null; } diff --git a/Subsurface/Source/Screens/NetLobbyScreen.cs b/Subsurface/Source/Screens/NetLobbyScreen.cs index c88ea34af..1cd4d37fe 100644 --- a/Subsurface/Source/Screens/NetLobbyScreen.cs +++ b/Subsurface/Source/Screens/NetLobbyScreen.cs @@ -37,6 +37,10 @@ namespace Barotrauma public bool IsServer; public string ServerName, ServerMessage; + const float NetworkUpdateInterval = 1.0f; + private float networkUpdateTimer; + private bool valueChanged; + private Sprite backgroundSprite; private GUITextBox serverMessage; @@ -327,7 +331,7 @@ namespace Barotrauma modeList.OnSelected = VotableClicked; modeList.OnSelected = SelectMode; subList.OnSelected = VotableClicked; - subList.OnSelected = SelectMap; + subList.OnSelected = SelectSub; traitorProbabilityButtons[0].OnClicked = ToggleTraitorsEnabled; traitorProbabilityButtons[1].OnClicked = ToggleTraitorsEnabled; @@ -484,8 +488,8 @@ namespace Barotrauma if (GameMain.Server == null) return false; GameMain.Server.AutoRestart = tickBox.Selected; - - GameMain.Server.UpdateNetLobby(tickBox); + + valueChanged = true; return true; } @@ -501,8 +505,8 @@ namespace Barotrauma if (index > 2) index = 0; SetTraitorsEnabled((YesNoMaybe)index); - - if (GameMain.Server != null) GameMain.Server.UpdateNetLobby(null); + + valueChanged = true; return true; } @@ -515,9 +519,9 @@ namespace Barotrauma } - private bool SelectMap(GUIComponent component, object obj) + private bool SelectSub(GUIComponent component, object obj) { - if (GameMain.Server != null) GameMain.Server.UpdateNetLobby(obj); + valueChanged = true; //Submarine sub = (Submarine)obj; @@ -558,7 +562,7 @@ namespace Barotrauma { if (GameMain.Server == null) return false; ServerName = text; - GameMain.Server.UpdateNetLobby(null, null); + valueChanged = true; return true; } @@ -567,7 +571,7 @@ namespace Barotrauma { if (GameMain.Server == null) return false; ServerMessage = text; - GameMain.Server.UpdateNetLobby(null, null); + valueChanged = true; return true; } @@ -692,6 +696,18 @@ namespace Barotrauma { if (GameMain.Server.BanList.BanFrame != null) GameMain.Server.BanList.BanFrame.Update((float)deltaTime); } + + if (valueChanged && GameMain.Server != null) + { + networkUpdateTimer -= (float)deltaTime; + if (networkUpdateTimer <= 0.0f) + { + GameMain.Server.UpdateNetLobby(null); + + valueChanged = false; + networkUpdateTimer = NetworkUpdateInterval; + } + } //durationBar.BarScroll = Math.Max(durationBar.BarScroll, 1.0f / 60.0f); } @@ -803,7 +819,7 @@ namespace Barotrauma GameModePreset modePreset = obj as GameModePreset; if (modePreset == null) return false; - if (GameMain.Server != null) GameMain.Server.UpdateNetLobby(obj); + valueChanged = true; return true; } @@ -818,8 +834,8 @@ namespace Barotrauma //textBox.Text = LevelSeed; //textBox.Selected = false; - - if (GameMain.Server != null) GameMain.Server.UpdateNetLobby(null); + + valueChanged = true; return true; } @@ -904,7 +920,18 @@ namespace Barotrauma Submarine sub = Submarine.SavedSubmarines.Find(m => m.Name == subName); if (sub == null) { - new GUIMessageBox("Submarine not found!","The submarine ''" + subName + "'' has been selected by the server. Matching file not found in your map folder."); + var requestFileBox = new GUIMessageBox("Submarine not found!", "The submarine ''" + subName + "'' has been selected by the server. " + +"Matching file not found in your map folder. Do you want to download the file from the server host?", new string[] { "Yes", "No" }); + requestFileBox.Buttons[0].UserData = subName; + requestFileBox.Buttons[0].OnClicked += requestFileBox.Close; + requestFileBox.Buttons[0].OnClicked += (GUIButton button, object userdata) => + { + GameMain.Client.RequestFile(userdata.ToString(), FileTransferType.Submarine); + return true; + }; + requestFileBox.Buttons[1].OnClicked += requestFileBox.Close; + + return false; } else @@ -949,7 +976,7 @@ namespace Barotrauma //msg.Write(AllowSubVoting); //msg.Write(AllowModeVoting); - msg.Write(modeList.SelectedIndex); + msg.Write((byte)modeList.SelectedIndex); //msg.Write(durationBar.BarScroll); msg.Write(LevelSeed); @@ -991,7 +1018,7 @@ namespace Barotrauma //AllowSubVoting = msg.ReadBoolean(); //AllowModeVoting = msg.ReadBoolean(); - modeIndex = msg.ReadInt32(); + modeIndex = msg.ReadByte(); //durationScroll = msg.ReadFloat(); @@ -1001,9 +1028,9 @@ namespace Barotrauma restartTimer = msg.ReadFloat(); int playerCount = msg.ReadByte(); - + playerList.ClearChildren(); - for (int i = 0; i 3) ? arguments[3] : ""; - string playerCountStr = (arguments.Length > 4) ? arguments[4] : ""; + string currPlayersStr = (arguments.Length > 4) ? arguments[4] : ""; + string maxPlayersStr = (arguments.Length > 5) ? arguments[5] : ""; + string hasPassWordStr = (arguments.Length > 5) ? arguments[5] : ""; - var serverFrame = new GUIFrame(new Rectangle(0,0,0,20), (i%2 == 0) ? Color.Transparent : Color.White*0.2f, null, serverList); - serverFrame.UserData = IP+":"+port; + var serverFrame = new GUIFrame(new Rectangle(0, 0, 0, 20), (i % 2 == 0) ? Color.Transparent : Color.White * 0.2f, null, serverList); + serverFrame.UserData = IP + ":" + port; serverFrame.HoverColor = Color.Gold * 0.2f; serverFrame.SelectedColor = Color.Gold * 0.5f; - var passwordBox = new GUITickBox(new Rectangle(columnX[0]/2, 0, 20, 20), "", Alignment.TopLeft, serverFrame); + var passwordBox = new GUITickBox(new Rectangle(columnX[0] / 2, 0, 20, 20), "", Alignment.TopLeft, serverFrame); passwordBox.Selected = hasPassWordStr == "1"; passwordBox.Enabled = false; passwordBox.UserData = "password"; var nameText = new GUITextBlock(new Rectangle(columnX[0], 0, 0, 0), serverName, GUI.Style, serverFrame); - int playerCount, maxPlayers; - playerCount = GameClient.ByteToPlayerCount((byte)int.Parse(playerCountStr), out maxPlayers); + int playerCount = 0, maxPlayers = 1; + int.TryParse(currPlayersStr, out playerCount); + int.TryParse(maxPlayersStr, out maxPlayers); var playerCountText = new GUITextBlock(new Rectangle(columnX[1], 0, 0, 0), playerCount + "/" + maxPlayers, GUI.Style, serverFrame); @@ -208,7 +211,7 @@ namespace Barotrauma if (client == null) yield return CoroutineStatus.Success; - var request = new RestRequest("masterserver.php", Method.GET); + var request = new RestRequest("masterserver2.php", Method.GET); request.AddParameter("gamename", "barotrauma"); // adds to POST or URL querystring based on Method request.AddParameter("action", "listservers"); // adds to POST or URL querystring based on Method diff --git a/Subsurface_Solution.v12.suo b/Subsurface_Solution.v12.suo index c08510be14b570519ce41687ba10f6f0ec458f01..a02bd522ab502f7e976221c4486133cd075df81a 100644 GIT binary patch delta 16517 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