From 5f07d4b0c9eab526f383bf5aec40c15b93a615c3 Mon Sep 17 00:00:00 2001 From: Regalis Date: Mon, 19 Oct 2015 22:49:38 +0300 Subject: [PATCH] "ReliableMessages", networkevents aren't sent if FillNetworkEvent fails --- Lidgren.Network/Lidgren.Network.csproj | 2 +- Subsurface/Barotrauma.csproj | 1 + Subsurface/Properties/AssemblyInfo.cs | 4 +- Subsurface/Source/Characters/AICharacter.cs | 7 +- Subsurface/Source/Characters/Character.cs | 12 +- Subsurface/Source/DebugConsole.cs | 5 + .../GameSession/GameModes/TutorialMode.cs | 2 + Subsurface/Source/Items/Inventory.cs | 4 +- Subsurface/Source/Items/Item.cs | 6 +- Subsurface/Source/Map/Entity.cs | 5 +- Subsurface/Source/Map/Hull.cs | 4 +- Subsurface/Source/Map/Structure.cs | 6 +- Subsurface/Source/Map/Submarine.cs | 8 +- Subsurface/Source/Networking/GameClient.cs | 39 +- Subsurface/Source/Networking/GameServer.cs | 121 ++++-- Subsurface/Source/Networking/NetStats.cs | 6 +- Subsurface/Source/Networking/NetworkEvent.cs | 10 +- Subsurface/Source/Networking/NetworkMember.cs | 9 +- .../Source/Networking/ReliableSender.cs | 404 ++++++++++++++++++ Subsurface/Source/Screens/ServerListScreen.cs | 4 +- Subsurface/changelog.txt | 21 + Subsurface_Solution.v12.suo | Bin 838144 -> 781824 bytes 22 files changed, 604 insertions(+), 76 deletions(-) create mode 100644 Subsurface/Source/Networking/ReliableSender.cs diff --git a/Lidgren.Network/Lidgren.Network.csproj b/Lidgren.Network/Lidgren.Network.csproj index b5ea57927..80fec2436 100644 --- a/Lidgren.Network/Lidgren.Network.csproj +++ b/Lidgren.Network/Lidgren.Network.csproj @@ -45,7 +45,7 @@ pdbonly true bin\Release\ - TRACE + TRACE;USE_RELEASE_STATISTICS prompt 4 AllRules.ruleset diff --git a/Subsurface/Barotrauma.csproj b/Subsurface/Barotrauma.csproj index 2f4419f25..b825317a0 100644 --- a/Subsurface/Barotrauma.csproj +++ b/Subsurface/Barotrauma.csproj @@ -100,6 +100,7 @@ + diff --git a/Subsurface/Properties/AssemblyInfo.cs b/Subsurface/Properties/AssemblyInfo.cs index 32dd7fc3b..43a9e6164 100644 --- a/Subsurface/Properties/AssemblyInfo.cs +++ b/Subsurface/Properties/AssemblyInfo.cs @@ -31,5 +31,5 @@ using System.Runtime.InteropServices; // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("0.2.0.0")] -[assembly: AssemblyFileVersion("0.2.0.0")] +[assembly: AssemblyVersion("0.2.2.0")] +[assembly: AssemblyFileVersion("0.2.2.0")] diff --git a/Subsurface/Source/Characters/AICharacter.cs b/Subsurface/Source/Characters/AICharacter.cs index 0642b529d..69dd89a79 100644 --- a/Subsurface/Source/Characters/AICharacter.cs +++ b/Subsurface/Source/Characters/AICharacter.cs @@ -83,11 +83,11 @@ namespace Barotrauma return result; } - public override void FillNetworkData(NetworkEventType type, NetOutgoingMessage message, object data) + public override bool FillNetworkData(NetworkEventType type, NetOutgoingMessage message, object data) { if (type == NetworkEventType.KillCharacter) { - return; + return true; } message.Write((float)NetTime.Now); @@ -127,6 +127,9 @@ namespace Barotrauma LargeUpdateTimer = Math.Max(0, LargeUpdateTimer - 1); } + + + return true; } public override void ReadNetworkData(NetworkEventType type, NetIncomingMessage message) diff --git a/Subsurface/Source/Characters/Character.cs b/Subsurface/Source/Characters/Character.cs index 2a557fe23..a36ce84b1 100644 --- a/Subsurface/Source/Characters/Character.cs +++ b/Subsurface/Source/Characters/Character.cs @@ -1084,21 +1084,21 @@ namespace Barotrauma } } - public override void FillNetworkData(NetworkEventType type, NetOutgoingMessage message, object data) + public override bool FillNetworkData(NetworkEventType type, NetOutgoingMessage message, object data) { if (type == NetworkEventType.PickItem) { message.Write((int)data); - return; + return true; } else if (type== NetworkEventType.SelectCharacter) { message.Write((int)data); - return; + return true; } else if (type == NetworkEventType.KillCharacter) { - return; + return true; } var hasInputs = @@ -1160,7 +1160,9 @@ namespace Barotrauma message.Write(AnimController.RefLimb.SimPosition.Y); LargeUpdateTimer = Math.Max(0, LargeUpdateTimer-1); - } + } + + return true; } public override void ReadNetworkData(NetworkEventType type, NetIncomingMessage message) diff --git a/Subsurface/Source/DebugConsole.cs b/Subsurface/Source/DebugConsole.cs index 4ed836b2b..d132e6a6c 100644 --- a/Subsurface/Source/DebugConsole.cs +++ b/Subsurface/Source/DebugConsole.cs @@ -325,6 +325,11 @@ namespace Barotrauma //Ragdoll.DebugDraw = !Ragdoll.DebugDraw; GameMain.DebugDraw = !GameMain.DebugDraw; break; + case "netstats": + if (GameMain.Server == null) return; + + GameMain.Server.ShowNetStats = !GameMain.Server.ShowNetStats; + break; default: NewMessage("Command not found", Color.Red); break; diff --git a/Subsurface/Source/GameSession/GameModes/TutorialMode.cs b/Subsurface/Source/GameSession/GameModes/TutorialMode.cs index 5cadb67c0..6b49c7be9 100644 --- a/Subsurface/Source/GameSession/GameModes/TutorialMode.cs +++ b/Subsurface/Source/GameSession/GameModes/TutorialMode.cs @@ -613,6 +613,8 @@ namespace Barotrauma { do { + if (enemy == null) break; + enemy.Health = 50.0f; enemy.AIController.State = AIController.AiState.None; diff --git a/Subsurface/Source/Items/Inventory.cs b/Subsurface/Source/Items/Inventory.cs index 8cbff3e8f..70df9f8f4 100644 --- a/Subsurface/Source/Items/Inventory.cs +++ b/Subsurface/Source/Items/Inventory.cs @@ -279,12 +279,14 @@ namespace Barotrauma spriteBatch.DrawString(GUI.Font, (int)item.Condition + " %", new Vector2(rect.X + rect.Width / 2, rect.Y + rect.Height / 2), Color.Red); } - public override void FillNetworkData(NetworkEventType type, NetOutgoingMessage message, object data) + public override bool FillNetworkData(NetworkEventType type, NetOutgoingMessage message, object data) { for (int i = 0; i= components.Count) return; + if (componentIndex < 0 || componentIndex >= components.Count) return false; message.Write((byte)componentIndex); components[componentIndex].FillNetworkData(type, message); @@ -1214,6 +1214,8 @@ namespace Barotrauma break; } + + return true; } public override void ReadNetworkData(NetworkEventType type, NetIncomingMessage message) diff --git a/Subsurface/Source/Map/Entity.cs b/Subsurface/Source/Map/Entity.cs index 48d868ce2..835ccdcec 100644 --- a/Subsurface/Source/Map/Entity.cs +++ b/Subsurface/Source/Map/Entity.cs @@ -64,7 +64,10 @@ namespace Barotrauma dictionary.Add(id, this); } - public virtual void FillNetworkData(NetworkEventType type, NetOutgoingMessage message, object data) { } + public virtual bool FillNetworkData(NetworkEventType type, NetOutgoingMessage message, object data) + { + return false; + } public virtual void ReadNetworkData(NetworkEventType type, NetIncomingMessage message) { } /// diff --git a/Subsurface/Source/Map/Hull.cs b/Subsurface/Source/Map/Hull.cs index 36142d8a3..b75ef5671 100644 --- a/Subsurface/Source/Map/Hull.cs +++ b/Subsurface/Source/Map/Hull.cs @@ -471,9 +471,11 @@ namespace Barotrauma h.ID = int.Parse(element.Attribute("ID").Value); } - public override void FillNetworkData(Networking.NetworkEventType type, Lidgren.Network.NetOutgoingMessage message, object data) + public override bool FillNetworkData(Networking.NetworkEventType type, Lidgren.Network.NetOutgoingMessage message, object data) { message.Write(volume); + + return true; } public override void ReadNetworkData(Networking.NetworkEventType type, Lidgren.Network.NetIncomingMessage message) diff --git a/Subsurface/Source/Map/Structure.cs b/Subsurface/Source/Map/Structure.cs index a5d541f40..d8858e61b 100644 --- a/Subsurface/Source/Map/Structure.cs +++ b/Subsurface/Source/Map/Structure.cs @@ -636,7 +636,7 @@ namespace Barotrauma } - public override void FillNetworkData(NetworkEventType type, NetOutgoingMessage message, object data) + public override bool FillNetworkData(NetworkEventType type, NetOutgoingMessage message, object data) { int sectionIndex = 0; byte byteIndex = 0; @@ -648,12 +648,14 @@ namespace Barotrauma } catch { - return; + return false; } message.Write((float)NetTime.Now); message.Write(byteIndex); message.Write(sections[sectionIndex].damage); + + return true; } public override void ReadNetworkData(NetworkEventType type, NetIncomingMessage message) diff --git a/Subsurface/Source/Map/Submarine.cs b/Subsurface/Source/Map/Submarine.cs index 21ed6e856..b52386284 100644 --- a/Subsurface/Source/Map/Submarine.cs +++ b/Subsurface/Source/Map/Submarine.cs @@ -149,7 +149,7 @@ namespace Barotrauma } base.Remove(); - ID = -1; + ID = -5; } //drawing ---------------------------------------------------- @@ -378,14 +378,18 @@ namespace Barotrauma Level.Loaded.Move(-amount); } - public override void FillNetworkData(Networking.NetworkEventType type, NetOutgoingMessage message, object data) + public override bool FillNetworkData(Networking.NetworkEventType type, NetOutgoingMessage message, object data) { + if (subBody == null) return false; + message.Write((float)NetTime.Now); message.Write(Position.X); message.Write(Position.Y); message.Write(Speed.X); message.Write(Speed.Y); + + return true; } public override void ReadNetworkData(Networking.NetworkEventType type, NetIncomingMessage message) diff --git a/Subsurface/Source/Networking/GameClient.cs b/Subsurface/Source/Networking/GameClient.cs index 625a41121..26ef45aa0 100644 --- a/Subsurface/Source/Networking/GameClient.cs +++ b/Subsurface/Source/Networking/GameClient.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using Lidgren.Network; using Microsoft.Xna.Framework; using System.Collections.Generic; +using Barotrauma.Networking.ReliableMessages; namespace Barotrauma.Networking { @@ -12,6 +13,8 @@ namespace Barotrauma.Networking private GUIMessageBox reconnectBox; + private ReliableChannel reliableChannel; + private bool connected; private int myID; @@ -34,6 +37,7 @@ namespace Barotrauma.Networking otherClients = new List(); + } public void ConnectToServer(string hostIP, string password = "") @@ -63,6 +67,7 @@ namespace Barotrauma.Networking #if DEBUG config.SimulatedLoss = 0.1f; config.SimulatedMinimumLatency = 0.3f; + config.SimulatedRandomLatency = 0.5f; #endif config.DisableMessageType(NetIncomingMessageType.DebugMessage | NetIncomingMessageType.WarningMessage | NetIncomingMessageType.Receipt @@ -70,6 +75,7 @@ namespace Barotrauma.Networking // Create new client, with previously created configs client = new NetClient(config); + reliableChannel = new ReliableChannel(client); NetOutgoingMessage outmsg = client.CreateMessage(); client.Start(); @@ -329,6 +335,8 @@ namespace Barotrauma.Networking new NetworkEvent(myCharacter.ID, true); } } + + reliableChannel.Update(deltaTime); foreach (NetworkEvent networkEvent in NetworkEvent.events) { @@ -368,8 +376,17 @@ namespace Barotrauma.Networking while ((inc = client.ReadMessage()) != null) { if (inc.MessageType != NetIncomingMessageType.Data) continue; - - switch (inc.ReadByte()) + + //todo: exception handling + byte packetType = inc.ReadByte(); + + if (packetType == (byte)PacketTypes.ReliableMessage) + { + if (!reliableChannel.CheckMessage(inc)) continue; + packetType = inc.ReadByte(); + } + + switch (packetType) { case (byte)PacketTypes.StartGame: if (gameStarted) continue; @@ -427,6 +444,12 @@ namespace Barotrauma.Networking new GUIMessageBox("You are the Traitor!", "Your secret task is to assassinate " + targetName + "!"); + break; + case (byte)PacketTypes.ResendRequest: + reliableChannel.HandleResendRequest(inc); + break; + case (byte)PacketTypes.Ack: + reliableChannel.HandleAckMessage(inc); break; } } @@ -652,13 +675,13 @@ namespace Barotrauma.Networking //AddChatMessage(message); type = (gameStarted && myCharacter != null && myCharacter.IsDead) ? ChatMessageType.Dead : ChatMessageType.Default; + + ReliableMessage msg = reliableChannel.CreateMessage(); + msg.InnerMessage.Write((byte)PacketTypes.Chatmessage); + msg.InnerMessage.Write((byte)type); + msg.InnerMessage.Write(message); - NetOutgoingMessage msg = client.CreateMessage(); - msg.Write((byte)PacketTypes.Chatmessage); - msg.Write((byte)type); - msg.Write(message); - - client.SendMessage(msg, NetDeliveryMethod.ReliableUnordered); + reliableChannel.SendMessage(msg, client.ServerConnection); } /// diff --git a/Subsurface/Source/Networking/GameServer.cs b/Subsurface/Source/Networking/GameServer.cs index 2a9df35c1..19fb18d3d 100644 --- a/Subsurface/Source/Networking/GameServer.cs +++ b/Subsurface/Source/Networking/GameServer.cs @@ -5,11 +5,13 @@ using System.Diagnostics; using Lidgren.Network; using Microsoft.Xna.Framework; using RestSharp; +using Barotrauma.Networking.ReliableMessages; namespace Barotrauma.Networking { class GameServer : NetworkMember { + public bool ShowNetStats; public List connectedClients = new List(); @@ -64,7 +66,7 @@ namespace Barotrauma.Networking #if DEBUG config.SimulatedLoss = 0.2f; - config.SimulatedMinimumLatency = 0.3f; + config.SimulatedRandomLatency = 0.6f; config.SimulatedDuplicatesChance = 0.05f; config.SimulatedMinimumLatency = 0.1f; #endif @@ -96,9 +98,9 @@ namespace Barotrauma.Networking } catch (Exception e) { - DebugConsole.ThrowError("Couldn't start the server", e); + DebugConsole.ThrowError("Couldn't start the server", e); } - + if (config.EnableUPnP) { @@ -225,7 +227,7 @@ namespace Barotrauma.Networking public override void Update(float deltaTime) { - if (GameMain.DebugDraw) netStats.Update(deltaTime); + if (ShowNetStats) netStats.Update(deltaTime); if (!started) return; @@ -268,8 +270,13 @@ namespace Barotrauma.Networking disconnectedClients.RemoveAt(i); } - NetIncomingMessage inc = server.ReadMessage(); - if (inc != null) + foreach (Client c in connectedClients) + { + c.ReliableChannel.Update(deltaTime); + } + + NetIncomingMessage inc = null; + while ((inc = server.ReadMessage()) != null) { try { @@ -277,7 +284,11 @@ namespace Barotrauma.Networking } catch { +#if DEBUG + DebugConsole.ThrowError("Failed to read incoming message"); +#endif + continue; } } @@ -416,7 +427,26 @@ namespace Barotrauma.Networking break; case NetIncomingMessageType.Data: - switch (inc.ReadByte()) + Client dataSender = connectedClients.Find(c => c.Connection == inc.SenderConnection); + if (dataSender == null) return; + + byte packetType = 0; + try + { + packetType = inc.ReadByte(); + } + catch + { + return; + } + + if (packetType == (byte)PacketTypes.ReliableMessage) + { + if (!dataSender.ReliableChannel.CheckMessage(inc)) return; + packetType = inc.ReadByte(); + } + + switch (packetType) { case (byte)PacketTypes.NetworkEvent: if (!gameStarted) break; @@ -452,6 +482,13 @@ namespace Barotrauma.Networking case (byte)PacketTypes.CharacterInfo: ReadCharacterData(inc); break; + case (byte)PacketTypes.ResendRequest: + + dataSender.ReliableChannel.HandleResendRequest(inc); + break; + case (byte)PacketTypes.Ack: + dataSender.ReliableChannel.HandleAckMessage(inc); + break; } break; case NetIncomingMessageType.WarningMessage: @@ -552,7 +589,7 @@ namespace Barotrauma.Networking userID++; } - Client newClient = new Client(name, userID); + Client newClient = new Client(server, name, userID); newClient.Connection = inc.SenderConnection; newClient.version = version; @@ -581,26 +618,23 @@ namespace Barotrauma.Networking { if (NetworkEvent.events.Count == 0) return; + List recipients = new List(); + foreach (Client c in connectedClients) + { + if (c.character == null) continue; + //if (networkEvent.Type == NetworkEventType.UpdateEntity && + // Vector2.Distance(e.SimPosition, c.character.SimPosition) > NetConfig.UpdateEntityDistance) continue; + + recipients.Add(c.Connection); + } + + if (recipients.Count == 0) return; + foreach (NetworkEvent networkEvent in NetworkEvent.events) { - - List recipients = new List(); - Entity e = Entity.FindEntityByID(networkEvent.ID); if (e == null) continue; - foreach (Client c in connectedClients) - { - if (c.character == null) continue; - //if (networkEvent.Type == NetworkEventType.UpdateEntity && - // Vector2.Distance(e.SimPosition, c.character.SimPosition) > NetConfig.UpdateEntityDistance) continue; - - recipients.Add(c.Connection); - } - - - if (recipients.Count == 0) return; - NetOutgoingMessage message = server.CreateMessage(); message.Write((byte)PacketTypes.NetworkEvent); //if (!networkEvent.IsClient) continue; @@ -872,7 +906,7 @@ namespace Barotrauma.Networking { base.Draw(spriteBatch); - if (!GameMain.DebugDraw) return; + if (!ShowNetStats) return; int width = 200, height = 300; int x = GameMain.GraphicsWidth - width, y = (int)(GameMain.GraphicsHeight*0.3f); @@ -934,27 +968,22 @@ namespace Barotrauma.Networking if (server.Connections.Count == 0) return; - NetOutgoingMessage msg = server.CreateMessage(); - msg.Write((byte)PacketTypes.Chatmessage); - msg.Write((byte)type); - msg.Write(message); + List recipients = new List(); - if (type==ChatMessageType.Dead) + foreach (Client c in connectedClients) { - List recipients = new List(); - foreach (Client c in connectedClients) - { - if (c.character != null && c.character.IsDead) recipients.Add(c.Connection); - } - if (recipients.Count>0) - { - server.SendMessage(msg, recipients, NetDeliveryMethod.ReliableUnordered, 0); - } + if (type!=ChatMessageType.Dead || (c.character != null && c.character.IsDead)) recipients.Add(c); } - else + + foreach (Client c in recipients) { - server.SendMessage(msg, server.Connections, NetDeliveryMethod.ReliableUnordered, 0); - } + ReliableMessage msg = c.ReliableChannel.CreateMessage(); + msg.InnerMessage.Write((byte)PacketTypes.Chatmessage); + msg.InnerMessage.Write((byte)type); + msg.InnerMessage.Write(message); + + c.ReliableChannel.SendMessage(msg, c.Connection); + } } @@ -1177,13 +1206,21 @@ namespace Barotrauma.Networking public List jobPreferences; public JobPrefab assignedJob; + public ReliableChannel ReliableChannel; + public float deleteDisconnectedTimer; + public Client(NetPeer server, string name, int ID) + : this(name, ID) + { + ReliableChannel = new ReliableChannel(server); + } + public Client(string name, int ID) { this.name = name; this.ID = ID; - + jobPreferences = new List(JobPrefab.List.GetRange(0,3)); } } diff --git a/Subsurface/Source/Networking/NetStats.cs b/Subsurface/Source/Networking/NetStats.cs index 038bde271..709796eb6 100644 --- a/Subsurface/Source/Networking/NetStats.cs +++ b/Subsurface/Source/Networking/NetStats.cs @@ -79,13 +79,13 @@ namespace Barotrauma.Networking graphs[(int)NetStatType.ResentMessages].Draw(spriteBatch, rect, null, 0.0f, Color.Red); - spriteBatch.DrawString(GUI.SmallFont, "Max received: "+graphs[(int)NetStatType.ReceivedBytes].LargestValue()+" bytes/s", + spriteBatch.DrawString(GUI.SmallFont, "Peak received: "+graphs[(int)NetStatType.ReceivedBytes].LargestValue()+" bytes/s", new Vector2(rect.X + 10, rect.Y+10), Color.Cyan); - spriteBatch.DrawString(GUI.SmallFont, "Max sent: " + graphs[(int)NetStatType.SentBytes].LargestValue() + " bytes/s", + spriteBatch.DrawString(GUI.SmallFont, "Peak sent: " + graphs[(int)NetStatType.SentBytes].LargestValue() + " bytes/s", new Vector2(rect.X + 10, rect.Y + 30), Color.Orange); - spriteBatch.DrawString(GUI.SmallFont, "Max resent: " + graphs[(int)NetStatType.ResentMessages].LargestValue() + " messages/s", + spriteBatch.DrawString(GUI.SmallFont, "Peak resent: " + graphs[(int)NetStatType.ResentMessages].LargestValue() + " messages/s", new Vector2(rect.X + 10, rect.Y + 50), Color.Red); } } diff --git a/Subsurface/Source/Networking/NetworkEvent.cs b/Subsurface/Source/Networking/NetworkEvent.cs index 021ce3974..0f97eeea5 100644 --- a/Subsurface/Source/Networking/NetworkEvent.cs +++ b/Subsurface/Source/Networking/NetworkEvent.cs @@ -93,7 +93,15 @@ namespace Barotrauma.Networking message.Write(id); - e.FillNetworkData(eventType, message, data); + try + { + if (!e.FillNetworkData(eventType, message, data)) return false; + } + + catch + { + return false; + } return true; } diff --git a/Subsurface/Source/Networking/NetworkMember.cs b/Subsurface/Source/Networking/NetworkMember.cs index bb9d01277..729d14b98 100644 --- a/Subsurface/Source/Networking/NetworkMember.cs +++ b/Subsurface/Source/Networking/NetworkMember.cs @@ -8,6 +8,8 @@ namespace Barotrauma.Networking { enum PacketTypes { + Unknown, + Login, LoggedIn, LogOut, @@ -26,7 +28,12 @@ namespace Barotrauma.Networking NetworkEvent, - Traitor + Traitor, + + ResendRequest, + ReliableMessage, + Ack + } class NetworkMember diff --git a/Subsurface/Source/Networking/ReliableSender.cs b/Subsurface/Source/Networking/ReliableSender.cs new file mode 100644 index 000000000..a16ed7b1d --- /dev/null +++ b/Subsurface/Source/Networking/ReliableSender.cs @@ -0,0 +1,404 @@ +using Lidgren.Network; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; + +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(int lengthBytes = 0) + { + return sender.CreateMessage(); + } + + public void SendMessage(ReliableMessage message, NetConnection receiver) + { + sender.SendMessage(message, receiver); + } + + public void HandleResendRequest(NetIncomingMessage inc) + { + sender.HandleResendRequest(inc); + } + + public void HandleAckMessage(NetIncomingMessage inc) + { + //make sure we've received what's been sent to us, if not, rerequest + receiver.HandleAckMessage(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); + } + + } + + internal class ReliableSender + { + private List messageBuffer; + + private ushort messageCount; + + private NetPeer sender; + + private NetConnection recipient; + + private float ackTimer; + + public ReliableSender(NetPeer sender) + { + this.sender = sender; + + messageBuffer = new List(); + } + + public ReliableMessage CreateMessage() + { + if (messageCount == ushort.MaxValue) messageCount = 0; + messageCount++; + + NetOutgoingMessage message = sender.CreateMessage(); + + var reliableMessage = new ReliableMessage(message, messageCount); + messageBuffer.Add(reliableMessage); + + message.Write((byte)PacketTypes.ReliableMessage); + message.Write(messageCount); + + while (messageBuffer.Count>100) + { + messageBuffer.RemoveAt(0); + } + + return reliableMessage; + + //server.SendMessage(msg, server.Connections, NetDeliveryMethod.Unreliable, 0); + } + + public void SendMessage(ReliableMessage message, NetConnection connection) + { + message.SaveInnerMessage(); + + sender.SendMessage(message.InnerMessage, connection, NetDeliveryMethod.Unreliable, 0); + + recipient = connection; + } + + // NetOutgoingMessage msg = server.CreateMessage(); + //reliableSender.CreateMessage(msg); + //msg.Write((byte)PacketTypes.Chatmessage); + //msg.Write((byte)type); + //msg.Write(message); + + + + 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 = messageBuffer.Find(m => m.ID == messageId); + if (message == null) return; + + Debug.WriteLine("resending " + messageId); + + + NetOutgoingMessage resendMessage = sender.CreateMessage(); + message.RestoreInnerMessage(resendMessage); + + sender.SendMessage(resendMessage, connection, NetDeliveryMethod.Unreliable); + } + + public void Update(float deltaTime) + { + if (recipient == null) return; + + ackTimer -= deltaTime; + + if (ackTimer > 0.0f) return; + + Debug.WriteLine("Sending ack message: "+messageCount); + + NetOutgoingMessage message = sender.CreateMessage(); + message.Write((byte)PacketTypes.Ack); + message.Write(messageCount); + + sender.SendMessage(message, recipient, NetDeliveryMethod.Unreliable); + + ackTimer = Math.Max(recipient.AverageRoundtripTime, 1.0f); + } + } + + internal class ReliableReceiver + { + ushort lastMessageID; + + Dictionary missingMessages; + + private NetPeer receiver; + + private NetConnection recipient; + + public ReliableReceiver(NetPeer receiver) + { + this.receiver = receiver; + + missingMessages = new Dictionary(); + } + + public void Update(float deltaTime) + { + foreach (var message in missingMessages.Where(m => m.Value.ResendRequestsSent>10).ToList()) + { + missingMessages.Remove(message.Key); + } + + foreach (KeyValuePair valuePair in missingMessages) + { + MissingMessage missingMessage = valuePair.Value; + + missingMessage.ResendTimer -= deltaTime; + + if (missingMessage.ResendRequestsSent==0 + || missingMessage.ResendTimer<0.0f) + { + 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, NetDeliveryMethod.Unreliable); + + + missingMessage.ResendTimer = Math.Max(recipient.AverageRoundtripTime, 0.2f); + missingMessage.ResendRequestsSent++; + } + } + + } + + public bool CheckMessage(NetIncomingMessage message) + { + recipient = message.SenderConnection; + + ushort id = message.ReadUInt16(); + + 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 && id>1) + { + for (ushort i = (ushort)(Math.Min(lastMessageID, (ushort)(ushort.MaxValue-1)) + 1); i < ushort.MaxValue; i++) + { + //message already marked as missed, continue + if (missingMessages.ContainsKey((i))) continue; + + Debug.WriteLine("added " + i + " to missed"); + missingMessages.Add(i, new MissingMessage((ushort)i)); + } + for (ushort i = 1; i < id; i++) + { + //message already marked as missed, continue + if (missingMessages.ContainsKey((i))) continue; + + Debug.WriteLine("added " + i + " to missed"); + missingMessages.Add(i, new MissingMessage((ushort)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 + { + if (missingMessages.ContainsKey(id)) + { + Debug.WriteLine("remove " + id + " from missed"); + missingMessages.Remove(id); + } + } + } + else + { + if (id>lastMessageID+1) + { + for (ushort i = (ushort)(lastMessageID+1); i < id; i++ ) + { + //message already marked as missed, continue + if (missingMessages.ContainsKey((i))) continue; + + Debug.WriteLine("added "+i+" to missed"); + missingMessages.Add(i, new MissingMessage((ushort)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 + { + if (missingMessages.ContainsKey(id)) + { + Debug.WriteLine("remove "+id+" from missed"); + missingMessages.Remove(id); + } + } + + lastMessageID = Math.Max(lastMessageID, id); + } + + return true; + } + + public void HandleAckMessage(NetIncomingMessage inc) + { + int messageId = inc.ReadUInt16(); + + recipient = inc.SenderConnection; + + //id matches, all good + if (messageId == lastMessageID) + { + + Debug.WriteLine("Received ack message: " + messageId + ", all good"); + return; + } + + if (lastMessageID > messageId) + { + //shouldn't happen: we have somehow received messages that the other end hasn't sent + Debug.WriteLine("Reliable message error - recipient last sent: " + messageId + " (current count " + lastMessageID + ")"); + return; + } + + Debug.WriteLine("Received ack message: " + messageId + ", need to rerequest messages"); + + 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 (!missingMessages.ContainsKey(i)) missingMessages.Add(i, new MissingMessage(i)); + } + + for (ushort i = 1; i <= messageId; i++) + { + if (!missingMessages.ContainsKey(i)) missingMessages.Add(i, new MissingMessage(i)); + } + } + else + { + for (ushort i = (ushort)Math.Min((int)lastMessageID+1, ushort.MaxValue); i <= messageId; i++) + { + + if (!missingMessages.ContainsKey(i)) missingMessages.Add(i, new MissingMessage(i)); + } + } + + + + // Debug.WriteLine("received recent request for msg id " + messageId); + + //ReliableMessage message = messageBuffer.Find(m => m.ID == messageId); + //if (message == null) return; + + //NetOutgoingMessage resendMessage = sender.CreateMessage(); + //message.RestoreInnerMessage(resendMessage); + + //sender.SendMessage(resendMessage, inc.SenderConnection, NetDeliveryMethod.Unreliable); + } + } + + 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; + } + } + + 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() + { + innerMessageBytes = innerMessage.PeekBytes(innerMessage.LengthBytes); + //innerMessage = null; + } + + public void RestoreInnerMessage(NetOutgoingMessage message) + { + message.Write(innerMessageBytes); + } + } +} diff --git a/Subsurface/Source/Screens/ServerListScreen.cs b/Subsurface/Source/Screens/ServerListScreen.cs index 33e334f9e..379ffcfbb 100644 --- a/Subsurface/Source/Screens/ServerListScreen.cs +++ b/Subsurface/Source/Screens/ServerListScreen.cs @@ -287,8 +287,8 @@ namespace Barotrauma if (serverList.Selected!=null && (serverList.Selected.GetChild("password") as GUITickBox).Selected) { - var msgBox = new GUIMessageBox("Password required", ""); - var passwordBox = new GUITextBox(new Rectangle(0,0,150,20), Alignment.BottomCenter, GUI.Style, msgBox); + var msgBox = new GUIMessageBox("Password required:", ""); + var passwordBox = new GUITextBox(new Rectangle(0,40,150,25), Alignment.TopLeft, GUI.Style, msgBox); passwordBox.UserData = "password"; var okButton = msgBox.GetChild(); diff --git a/Subsurface/changelog.txt b/Subsurface/changelog.txt index 8c9517226..c6d7790bc 100644 --- a/Subsurface/changelog.txt +++ b/Subsurface/changelog.txt @@ -1,3 +1,24 @@ +--------------------------------------------------------------------------------------------------------- +v0.2.2 +--------------------------------------------------------------------------------------------------------- + +Multiplayer: + - network statistics view which can be enabled by opening the debug console (F3) and entering "netstats" + (only works if you're running a server) + - updated to latest version of Lidgren networking library, which may or may not have an effect + on the chat lag issues + +Items: + - fixed some game-crashing bugs related to detaching and attaching items (such as buttons) + - railgun shells can be bought in single player + +Submarine: + - more tools, diving suits and misc supplies in both default subs + +Misc: + - fixed Moloch spawning inside the level in the tutorial + - the launcher shows an error message instead of crashing if it can't connect to the update server + --------------------------------------------------------------------------------------------------------- v0.2.1 --------------------------------------------------------------------------------------------------------- diff --git a/Subsurface_Solution.v12.suo b/Subsurface_Solution.v12.suo index cb424628c26db4e7ae71a361dea2a2f346726f9b..45a8ce9b47313c1891db2ef979b236057db5fe09 100644 GIT binary patch delta 18174 zcmeHu3s_Xu+W)M5nb~{K2qPjAB92G|2ndLZW;WcslowL7G*VEpyd$ESc^S*fqh`qH zyrpSqHkO%@7|T4SBiY?!YMwG9BahnMLu5BI{eEi(6s>dppYJ@+_dNg2^W%NjWv}bI z)_UJ}?QyN_;;QXj)+@8UWHN=BOr{GLFJ1&OF<>6B9{2`OfgZ@y3rGP{fi&O-WE=n} zbjupay`rqXT$&tInelYteB%)7ikF;~?m5yN_I%a%l9gqCgsi`!il#?YC&D#|cO!Qf zpc^34qZ(;3ChBY8&Swt&#o_^b^32yng^aAx0IleIB$1h(TOSq-_H)YPTXRmc_523TPkX z?T+*d2uFg(NTxofHedp64g60-#<*s2@!%7H#KyP?(3HjsqQDEm_?0qOOKseFdz4gB zh=^)>1m6+$IvZclX{1f*`C{pfW{(Q;sAruVT@@X4oR!;hv^d#h>V%4DZ?@FhUC^Qx zd)htIHb`fwh!icjoUqaTz3p916q(*U-t0of-?gH1lAKT#6H+TL_%Djk?@5pp5TO>$ zMF>*R>0cI`4dK#%O>!Wq{|6-Jbr$_pbYxgn^qBv!^i-Ye8fWn67>07?Xm`x0NO$j0 zb5%}Ot(6>x@|644&{R5WD1+UvN~u(8RhH41`}7R5@6f}l2IU=YS>-tY3{%=R7fh=c zgQ_9MANh`DVAX(eM`Z5UDc3Q#bKoM+8?YYB|u7v#aGiEdy!dz-yMiMqWlk8Z6E#=jXm)&uk@z zHnwAV>L#>kt^3~4c#7`9hNznnTuPbo)|<(BK#&W6nqJAtwf{ZSgazp23j1)B74*$^(~}WKsx~Q zfFBXwCjh=L=sDmF@H$Y2xQ9WL=;JunSr10=0|f7**72;p5)PI!;#s8n8n~@A0+Fl} z-5bx^cKseZ!e@y66wvXSXm1tx?co0kD%$id=vv?l#JvKVM{mWmj;&UJy#{0X3!)pq zdvTIn9MQ2V`oaah%8?esw3$e~xA|zhOS_24NOOPRrIWiNOW`pFJ1v(A zZ9qrhWg0PtT^~6O$aWuXBsV=QwN-H0yt0=`7nhfu&_;4bh#fHKhTz%cL{=u^PU;6p)&0HVAJpiaC$ z7W5^+0R9Br1bhn=14OTkWo?pP!mAGegAp|c*aZF<-~ca*8U~t+qP_#2N||>_SyZuF zOXSn~uoW#lxeSXgT)KtNj8T>{?{H@GxFiR6)Ja(D*_iWj?6`$0uU8eWgv#d`WyR@p z`P`{&Q9F+&IfT@>H+FGoT_6h4sP&R47!P4yA4Lq}F5IKUx?{S;xZey-G+su$Sf-ca zZ+2*1@phz$X*yrkKZ->k*aI?Hw3dMkJ%CFYY`c-+0e>+N|K%*m?Yb7{^=a%$4=s@F zE6P&1a~<=wz^;-_*~#n`vAwuAc6PY;g{BxTyu8Z)a%0;%JG2xOpVqtz^#SDK^m7c0 zuZkXog2#NLW$~;^wz~xzL+oX#pSw6Cf{&dj4`KF>``hmvH}X(}eD{3m2b!>&3iDYT zs+}aqczw)9sgu+;G=waXVacYkrWvMjA9h_-((1vl5~uD?U-ZkmyC`!!YfWz+FrwKg zT397T(ZP9^7z#RJM8=Q(Xmj1d2`{~pc=lfD*^C8d(QYxZ;*qi;S50Bh0VBiy`QYv$ z1J6%-^tOJR-r;6jE|vARRZ`8-X z50Q)mZ2*n{VjuAU4+7apy9RONfeK&-uo@VEwB5jWKqA6Dk?&d1{=i0H7cdTZAL%cE z{t0v%=t0nnpi4l#%+%X-6oDghr0t(Oy8QTL>!EXmOVe@zz{4>B(I`jl< zwcr)7eNlf9lHLG+D=--0*FakU3-H!C(EUIc;8}zp109Y$hrs^`{yAU)&%fb^2f#!?$chk?x4>Vd zRcl$tKIg&qMD!-m9l#XeIKrnvGf@e3a_DbdqS zzQ;c0-k9l$cXe%h;gK`f-g)eSaYFJ#X-zkIk$v;MpVw#gdwo~%v~Hhf-?ZkdfRyKS zmv@!7$JSz#j<@psfkC0z9K)13sPNMAc9%Z4-Q(0^sPYYqo##hO8{^%sx)>hgl4_YJ zhhc{rB-QEU`^w0nh8oR5bq{Ef7GYW7p>Mlddra%r!K)lXo)83BCXzmy()dhR5WoWO))?axJ(OI1(a>rzm zqd{J*h|faMe!F88E-^x>_@Ev`{kmCqkgvDR#cra7<5@RKII4y6o9~xiYT^A!mOKsx zf>18iSv>pYxT<2KjIAh(WbaeURdOd!9J2-eo?a*#{m<%!VtoD+dZ84|C};{4&C3em z7Z9~86+%&2bA@m@ax_;6Mf&e6gsp!^Av7(gg+H>HfqTR4(#4g{YJ{Q z%v&QF|4Yi#Um#ikvhw6t!T+C?CsE1-RR14Po z?MQ=?dxSlyn`U9z-veCQ`{?Wu22~xVTgBg8OG8m?`J)oCKv0W{J!(jgEA|ig)bZZ$eyN!+lPIy#3+3J zwz7c_XAH|b96Xm#-ylEAJhF^)QIap{?hR+6wKb5Gb?$WNyyg{$TWH- zTe^m)&z1MH!08w+cVNQrz)P=@mucQDO!CIaA#5&Xc96%=)`@C_{f{o=gB9O56r4X3 zwR7hU<;8w|fNyZhqnI~C)`SWVtw27`yuO!wBZf!mCj)YA=vAk6nRY;)KED7zD?La8+JfpuBXDoX51D zh`YIYd<#O6PlOYp7=y7YQwS+z%Y zDCEdx*xte=2kpycDP|X1@UY1}=h}pqHp=;|daJya*_ZE3G z0@b`A_a@gmA=2yQT3Hpd^$6WEPU*a06&9Wdisx!8N5pxMPQYWpO3rUb^NK*E$!DeZX{y+FM80pf)c z`5~xKB0mDH1)7w|Pr%m!zo$f=M!ZlWKL@?6I6}!p_#5C1@K>N75Q=02=nueI06Slj zX(=LwHu)1Geg?h+egVz{zXBJ4i-52_=+J1j9dEZ${)>KDx9*_FQ_4xh&V~`&&9dmt z^_CEONN(6;caN|Ccui5nM~^)iev}q|A-7Rym`Y89+HtX@Y5*X)69t zHH`rT)zv}>rfFKb?}(?~K^fyf`6f7aLpt<+qj)eru)toW<=5 zjdpMB5XpP@R?JLlZ@OgrZYsS|k<7hO@vaz}lc_x9-6j7k?tp(vJND!#rBY{uX5}ac zv~K8VGMap8xk?F0$yEmVJz3W`Pf_FH;n0Ma<@Xy;X#6sUlc|+Fhz~v>ZwscHQAP?+ zt(E37`vYx%Z2jCbCq`x!{;BJ3u-~b@bL^|(-5yH4_113+ z6K?9%*zO0k1kY}kO^$p`p+u&Jjt{jzf9loBXU)G{*!0U5+YG;L=%0W5YYj#I4-l;g zxUC3{8Pm>WT88kfj#$$OkBto+1c&H*<@)BBLxLcbhO6wMnzdAb@!=hIkg zRw}mkEK2FCrtz8Q<)xPD;mSUyy?{bc#^q_XiLC$;{)bu?s$HndaoK18aOwWrHck!x z?E3WJUpsyGP^36Y+R*c}B!jlTWsM}~C$i70DN@yR<58UG)9KI$mL#5JQ{I!Qrdkc9 z(A$*)UOFF>U~Gmg#cn{i8hv>GtDZi0G!3yfbL;VotrRAe`yB3yItAymQp?yxaem!I z&RUCuPYPFFvT(-`Wh0Z?U0!kOOAzW?`Oh5`i$p%3+=c39>yeaqgJI^k4v_9)_Ux^P zMkF+R_i+q~PwuGg`qF z%+px&qm&v(D|Tz~JU(6dfR)=+gapvgi3%JKq)X2m<22W z+_f!ZAd7dz_82j{A?M~1Uj58RU$y*s>9b<4QIh{jf%`)EQ7>+$g%_krf- z@w|?6*D+a%@vk1p<7Jtl(lY6Ya_yB-DE3v-3UoYh- z6r%A;Tvb2s4(45=NaZ6j99bBSYNt}l%>A&@Rrth4_e@o^*UOYEs#p7?b8Gm|xymoh zS^$1bpm=`q9SK(owX8no6)f*pflmM*@GejThymQ!NGbGVmJ)Eb^!u3YxGc5mEhuj- zzjwZJAG4f6kDTk7`@wPChMMlnm} zT2G#mqjkZX=-K6gHns7Y@*|oJorVR8-1YP3;`Vg+$MsZ)Z4+{ zDcDx(D6lgHdxtU$EFCRU;#QO?q%_Ej{*O7V3ebL`hy42vD}RTZFWi$*`HPiLnB}(< zPcy2F@D*h-`U;G_SaDB??*cl7JlPl&Q5E2ylKB0rrTPBV;N34tUZ<*fmno8WE0f$E zv9Z+cP!t}qTsh4=vy`*yZp`xQ#AYh(Df}L#%$;H=wB;UUn5RU^2rp|ayu6XRL01Bl z_Y;e>t!<1I_8p&HqAX_KiKwU?mhkNBgoxB3gs2;)B=fIiRvJ#f7BdFb&0CR zS3hd5vrzBP6pioRXwH(Uz6zV-?o_q6OmoUDcBxM{T2^i;vF!Tm&bi0+Lv-jfWnd4n z`i9}6HWTjST+{XVPG>0SXvE~gv7d|hKzJx}Wdi*X7XPzp#ddQNxppZ|zU6b}sLYR% zB{qmEPpZAB?jXAKiOZ~UY8VfDO!<=0w#90-r(O->m18Xk`UaUpDdoI!oY%Q6W9+0nt7P$< z)yi&0+fS)Ixof87YxehRCe$2F-D;Fr9{YrHOs0@k>Ncvl*=$gKts27}&siKea^F&V z@m>|mKA9w!x?H3uP~K_vSfB~@FDrKcP*cJ4W;@$YExM|yRK8k4yElq zCSttSek|Gf_`T+HH&XO)HJTTEh&`P*j5i;a_yJOr(a`luIBj`ciQ&DrLLqU79*&@t zw=w8X+@^+5@j&>0cU$#Hp0`G=H2;m#^E>l7531uB|KWXgU$D1QG1HhAEXBO`E^`9B z*FvK5KUYU--fBhSCnC)?Xy#e7gAR7GBvamYi=76|u|!hYJoDXr)I~L)QQ9_Tr+>`& z$sI~0tBy4%GOEAV;^hafGbbzFUFdW}vU#?DY73$)b6WD60C= zK41+m8+FzGhhW8!Q~%x;C?t5Jl{;Em)tb0EGmBHvz14pqL5o5NufwZyhm zvPvzzyq#(}xkIyavxnyKiuWxAjJqDxTnZf=sg2{~HfoWK#d9`ajAHaCtq(b)@vTI| z(^?SRo<5Xl-=)XI8fbsH){WmZ05bHE&CYd z?Y5-xwD&CsnD;n_v*Lt~+8Qhk!4&d>lus2N%}mNIdKURsX=!};Nex0CvkbRrn7Bxu zB54P8IuU6srqK^e;oNarD`3>HS@TfJAkD$6ZQ4yT6`!^w^28ABi0+NTeQ9|QEH__{ zZs(gn(}pWl*++}wQD?LhFnqMdWd9XC+;~>2RlMD_Aa9;#qw)^8uf?Y&$#nQ<{3~9k zMZ6TDpO(qBUYp1#n)NSR)3{@nD5`&6i=|8M7N|oB6ztM-$v!|2Bj-%bpt4vUx<#a( z4+-#z&r$NVx)!vqvlZ+Ttz;h4MP%$|vs>hH*$|{Y``i8CVrHnvYS) zNuwtpma4ywCBL4b>HD-g)xGUx0?+B9zpqo;QA-5X#p_|rN#8p3>uL8m-ONkg)|TT1 zP0!(@ZqTvjPi#~1?g6cik>f23CjN-lfOEgE)`u6|s8_&Iu1n;20@JnpkY;$^z-jE9 zt>fBhz6vdAr5-}-`ss72{BbRT>|Y)hNo5xPlHeIP4zEAPrAFmz6~c_gpt8B z>a;hR)ED(Vt`FnI1NAZ+RX%L(M!Q$zV&bNJy}B(O{1h*`DwIeXIY%$?OW!3$0#n+N z7RF32@}m(#%hU7-o>;FP zVf^qTda_o%PG2t5$p)=a_AdE?P}#MChWhKTSFwt}T(5^IR9_5XF7B&G^FCYjQ0Cd9 z+aV8fG#wZPZK=2?CTduN1`}|@D_R5v*$oF@`Mf?^@@~`3 zgL*9Up2Nmaa}Am|`H;Sn>NBurr%pE_dGs+I9~b$~LIU$fpoxyZ8aN(4w}jEUGbnb4 zVqkZx8G#+4ahJx9?+gYSL6s@nQ3gX(6HT>1fJMOKf=5tAx_29(JOmD zeJbTmQKPtbwhnE%VW~BiyZRf)WcQE^m=t+>PoC$|E0MB8OXtf68VaM(U3!k+r;EwH zPUdBUjH!zEF%((;3*;|usPTSF$~tXHAlFiyG5uESJD?atG~JcmLa6sftXpT10WDj4 z-7<#ChGQbb}Hn%E(F`D|Ad}YcJmOE@K~~jSGx89=Ah>Z(>(IHZ1#o zyGXl`_ENES@#z$=aT%Q=^bUNxE-IBjLOG8*Y6X3 zk%h*+U|cPyknvoqoU1!{@@M*z7Mm{IZAj_}_{)lEVLR;(8az=B<%!?x8}#3kK2&op zcE=x$JZdph4&!MT^#eGF+F>unvJK(*g7!{V*&XhSkQ zfOVCYAak_PgQJOes{hOihtlBSQ<{wBkzcO>3ntE2dCS z3u`FtTxmSUCR6GGEB4H#Mo|1+C}j%XpW#Q7D?cBfuLa_#z@a}*q|xA2MrkNB*{`lQ z!n55RflB?9POLK8vn}+)VQU9Ai`aTOk1Fm{W9`f8zv%nbT_1gtB7HQ--P!mc;0sC*YxWy=b_q zpD>bJQTb+T0WF_ylyYgCaVN8<);#c7?!&X6Su#50*+-wBJc_12%OYuMmC==se{1b# zY;CGE#q+X+(`mcmV)X4&k_l6blE==NJNG_H?aRWcVkC}I-(JIqx-0QBBLAx+J^Kt_ zyP0EV%q$sSIJPK%#VhUvXcr+<@R*^ zRl_cVI`!LYgoWXvxaslT2@aFiSWesOnWA^8rLvRz{sd{EcV9J9MV0O8lYOZClkcrT zq8Z^d{tY9nWrbLI{wKs&4c^9gceF{8Y|43FH0uUWtj$F)zH0QK!oIe?-uDbi8asnh zciW&NB-`)}?%T4J-tTLR4cpmNkjddY0=@%~vz4YpiheU|p_`nMhMD$$W4)$T%OQU6nayz@?1X28MtP<(Q`~{L1Smh?Z~+oUtVrK%|h(LQHtmC7s>V~q!f2~t;^fp z3Pm@KCD8Goue*r%gldw*|GBjae4^xu65+ zkj>hOFDbLt>&!(hoz~PFhT`;`X1WKS&Jz5I!y^u7VX6NRnTk`;;Xhpq{fA5mKDcl+ zPo=>laKeTLTOVS~%u}xMh9!8IPxV~XSKLf z>~%;Hw7tJ<9M>x@7}wC^WNSidvgu}=qGL^y;1-??t_X@oDTbzb$bs}rX_U&Wn{X+; zH_1A}?>}hW3Mfs#!Zwl(H z#^arZqMUo($&yC(7maikMZwX1Xs7L3n)bTQ%DYzE-hsZSSh300$LTN%HmTvbnEDy{ z%Fs^QJKEZsf_K}3c&Aa;(Gs1T1a)}#P8%8QIx2fy3b)QGDw#21+SCbC$4{g6Kg+E- zO|nKXj^c+y2rfciD*9W0fCc!PWgO*o!JaVqj2z)f!ZqpPNNX~qlxQP}f<{7r+tA*c z#g7iOo{)t^)T-V%t3p}z!pPqa$3(?QYXS3w={@PdL3l`RDz+wsUA6;oX?Go_ehjT& zZo$vQ%Cs;_`OXr`k3MAW7e{GdG%4iRtrMuG+&Y{pkHPGE>;>xkM!e3jw0 zBl5EH8`K?}zBuYJFIH0@_w+t!bW51iLNg}k5@R@A?SE4RR-387^p|p@i(di0rb*Gg zS^@sImE^I09hfQ)TU-83bvYR7ayIsuLJ0HSrXl~NqMQTqFZS$MAOT}Lf ze*2q*Tp}$KTnhd&foxL`{O{xc%QkhPTi%!AX=|PoLT9()j2+?=LLMratG5}W|DN{l z>>m`^(KP(*qS0>1OzV@|Jv}!yDXV*Wm!#CxoRp-@+|>eq7`t-@}GH-6L zffHto?K9!By01c!?>*PQGsHC88IosDnG99DPiB|woGz)kNj);UXCKrYdi@)2ig@5m$R*l-=Lt%@2%N%#;k&H9(Ym2 zfuFNQ%vR!+c7oqPk*5B}9taDFtWe~eOYf@bw$3&o%ei94(R^CZo10(q6J6(UZaX`0}%AH#T46tU_ zlCaa7>>t0KcDbsiXn$3?zh@TZ{EO?LnQ3Bc+@db;Oa1;m*`F7D)q4T`=jvF`09zRK zvf6C!&XQ(pZqK==*LDxsNNIa4DfX*%jjN2N4p)A9+6$+oa2QR+uL@h3prHxx+G*DQ zl1j-maT+EL^V+#;g{A7CKQ}jwD242wbm&` zGiDUtRU{GvJ6h*&tSvojr7qDA1TQm%elfoPimqQS&KvQ~Q8poRXMd$|OXkx@UmE=E X51!8%t@_5=$v=8cn+Way#+v>=)`~rd delta 22374 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 rT%v8{H?VLBcD^9t(fk;WGCch<